From 73fe66ed49afa4606ff972bc7b4ef276058b12e0 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Fri, 1 May 2026 12:57:30 -0400 Subject: [PATCH 1/4] feat: per-message thumbs up/down feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds thumbs up/down buttons next to the copy button on every assistant message in the VS Code sidebar, and +/- keybinds in the TUI. UI state is in-memory only — ratings reset on reload / session switch. Persistence can be added later without changing the telemetry contract. Events are sent to PostHog via the existing telemetry pipeline. For Kilo Gateway turns the payload includes session and message IDs so feedback can be correlated against gateway logs; for direct providers those IDs are omitted since we cannot correlate them to upstream data. --- .changeset/message-feedback.md | 6 ++ packages/kilo-telemetry/src/events.ts | 3 + packages/kilo-telemetry/src/telemetry.ts | 16 ++++ .../kilo-ui/src/components/message-part.css | 1 + .../kilo-ui/src/components/message-part.tsx | 54 +++++++++++++ packages/kilo-vscode/src/KiloProvider.ts | 8 +- .../src/services/telemetry/index.ts | 1 + .../src/services/telemetry/types.ts | 3 + .../src/services/telemetry/webview-state.ts | 9 +++ .../tests/unit/feedback-payload.test.ts | 78 +++++++++++++++++++ .../agent-manager/AgentManagerApp.tsx | 17 ++-- packages/kilo-vscode/webview-ui/src/App.tsx | 9 ++- .../src/components/chat/AssistantMessage.tsx | 3 + .../src/components/chat/VscodeSessionTurn.tsx | 23 +++++- .../src/context/feedback-payload.ts | 41 ++++++++++ .../webview-ui/src/context/feedback.tsx | 71 +++++++++++++++++ .../src/types/messages/extension-messages.ts | 6 ++ .../src/cli/cmd/tui/routes/session/index.tsx | 55 +++++++++++++ packages/opencode/src/config/keybinds.ts | 4 + .../test/kilocode/telemetry/feedback.test.ts | 9 +++ packages/ui/src/components/icon.tsx | 6 ++ packages/ui/src/i18n/ar.ts | 5 ++ packages/ui/src/i18n/br.ts | 5 ++ packages/ui/src/i18n/bs.ts | 5 ++ packages/ui/src/i18n/da.ts | 5 ++ packages/ui/src/i18n/de.ts | 5 ++ packages/ui/src/i18n/en.ts | 5 ++ packages/ui/src/i18n/es.ts | 5 ++ packages/ui/src/i18n/fr.ts | 5 ++ packages/ui/src/i18n/ja.ts | 5 ++ packages/ui/src/i18n/ko.ts | 5 ++ packages/ui/src/i18n/nl.ts | 5 ++ packages/ui/src/i18n/no.ts | 5 ++ packages/ui/src/i18n/pl.ts | 5 ++ packages/ui/src/i18n/ru.ts | 5 ++ packages/ui/src/i18n/th.ts | 5 ++ packages/ui/src/i18n/tr.ts | 5 ++ packages/ui/src/i18n/uk.ts | 5 ++ packages/ui/src/i18n/zh.ts | 5 ++ packages/ui/src/i18n/zht.ts | 5 ++ 40 files changed, 502 insertions(+), 16 deletions(-) create mode 100644 .changeset/message-feedback.md create mode 100644 packages/kilo-vscode/src/services/telemetry/webview-state.ts create mode 100644 packages/kilo-vscode/tests/unit/feedback-payload.test.ts create mode 100644 packages/kilo-vscode/webview-ui/src/context/feedback-payload.ts create mode 100644 packages/kilo-vscode/webview-ui/src/context/feedback.tsx create mode 100644 packages/opencode/test/kilocode/telemetry/feedback.test.ts diff --git a/.changeset/message-feedback.md b/.changeset/message-feedback.md new file mode 100644 index 00000000000..98d5c5c2215 --- /dev/null +++ b/.changeset/message-feedback.md @@ -0,0 +1,6 @@ +--- +"kilo-code": minor +"@kilocode/cli": minor +--- + +Rate assistant responses with thumbs up/down. Click the thumbs buttons next to the copy button on any assistant message, or press `+` / `-` in the terminal UI. Only shown when telemetry is enabled; feedback is sent to Kilo to help improve model and prompt quality. diff --git a/packages/kilo-telemetry/src/events.ts b/packages/kilo-telemetry/src/events.ts index d61d4ae06b6..59b489a34ac 100644 --- a/packages/kilo-telemetry/src/events.ts +++ b/packages/kilo-telemetry/src/events.ts @@ -42,6 +42,9 @@ export enum TelemetryEvent { // Config Events TELEMETRY_DISABLED = "Telemetry Disabled", + // Feedback + FEEDBACK_SUBMITTED = "Feedback Submitted", + // Errors ERROR = "Error", } diff --git a/packages/kilo-telemetry/src/telemetry.ts b/packages/kilo-telemetry/src/telemetry.ts index 94a8d946ce8..88b5a887d41 100644 --- a/packages/kilo-telemetry/src/telemetry.ts +++ b/packages/kilo-telemetry/src/telemetry.ts @@ -241,6 +241,22 @@ export namespace Telemetry { track(TelemetryEvent.ERROR, { error, context }) } + // Feedback + export interface FeedbackProperties extends Record { + providerID: string + modelID: string + variant?: string + rating: "up" | "down" | "cleared" + previousRating?: "up" | "down" + sessionID?: string + messageID?: string + parentMessageID?: string + } + + export function trackFeedback(props: FeedbackProperties) { + track(TelemetryEvent.FEEDBACK_SUBMITTED, props) + } + export async function shutdown(): Promise { await Client.shutdown() } diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index e887aaf7770..bac3a669ed0 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -24,6 +24,7 @@ display: flex; align-items: center; justify-content: flex-start; + gap: 2px; margin-top: 2px; [data-component="icon-button"] { diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index ef88e3ec379..3edb43ceed6 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -125,6 +125,12 @@ function DiagnosticsDisplay(props: { diagnostics: Diagnostic[] }): JSX.Element { ) } +export interface MessageFeedbackControls { + enabled?: boolean + rating?: "up" | "down" + onRate?: (rating: "up" | "down" | null) => void +} + export interface MessagePartProps { part: PartType message: MessageType @@ -136,6 +142,7 @@ export interface MessagePartProps { turnDiffSummary?: () => JSX.Element animate?: boolean working?: boolean + feedback?: MessageFeedbackControls } export type PartComponent = Component @@ -971,6 +978,7 @@ export function Part(props: MessagePartProps) { turnDiffSummary={props.turnDiffSummary} animate={props.animate} working={props.working} + feedback={props.feedback} /> ) @@ -1325,6 +1333,52 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyResponse")} /> + + + e.preventDefault()} + onClick={() => { + const next = props.feedback?.rating === "up" ? null : "up" + props.feedback?.onRate?.(next) + }} + aria-pressed={props.feedback?.rating === "up"} + aria-label={i18n.t("ui.message.feedback.helpful")} + /> + + + e.preventDefault()} + onClick={() => { + const next = props.feedback?.rating === "down" ? null : "down" + props.feedback?.onRate?.(next) + }} + aria-pressed={props.feedback?.rating === "down"} + aria-label={i18n.t("ui.message.feedback.notHelpful")} + /> + + diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 3b0ebc44b29..ec471b915a9 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -16,7 +16,7 @@ import type { EditorContext, IndexingStatus } from "./services/cli-backend/types import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController" import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete" import { buildWebviewHtml } from "./utils" -import { TelemetryProxy, type TelemetryPropertiesProvider } from "./services/telemetry" +import { TelemetryProxy, type TelemetryPropertiesProvider, pushTelemetryState } from "./services/telemetry" import { sessionToWebview, indexProvidersById, @@ -346,10 +346,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } // Always push connection state first so the UI can render appropriately. - this.postMessage({ - type: "connectionState", - state: this.connectionState, - }) + this.postMessage({ type: "connectionState", state: this.connectionState }) + pushTelemetryState((m) => this.postMessage(m)) // Re-send ready so the webview can recover after refresh. if (serverInfo) { diff --git a/packages/kilo-vscode/src/services/telemetry/index.ts b/packages/kilo-vscode/src/services/telemetry/index.ts index 2ed56fb0271..856b1afb975 100644 --- a/packages/kilo-vscode/src/services/telemetry/index.ts +++ b/packages/kilo-vscode/src/services/telemetry/index.ts @@ -1,2 +1,3 @@ export { TelemetryEventName, type TelemetryPropertiesProvider } from "./types" export { TelemetryProxy } from "./telemetry-proxy" +export { pushTelemetryState } from "./webview-state" diff --git a/packages/kilo-vscode/src/services/telemetry/types.ts b/packages/kilo-vscode/src/services/telemetry/types.ts index 74c676a2204..5993151d037 100644 --- a/packages/kilo-vscode/src/services/telemetry/types.ts +++ b/packages/kilo-vscode/src/services/telemetry/types.ts @@ -82,6 +82,9 @@ export enum TelemetryEventName { FREE_MODELS_LINK_CLICKED = "Free Models Link Clicked", CREATE_ORGANIZATION_LINK_CLICKED = "Create Organization Link Clicked", GHOST_SERVICE_DISABLED = "Ghost Service Disabled", + + // Feedback + FEEDBACK_SUBMITTED = "Feedback Submitted", } /** diff --git a/packages/kilo-vscode/src/services/telemetry/webview-state.ts b/packages/kilo-vscode/src/services/telemetry/webview-state.ts new file mode 100644 index 00000000000..fd1c028ba2b --- /dev/null +++ b/packages/kilo-vscode/src/services/telemetry/webview-state.ts @@ -0,0 +1,9 @@ +import * as vscode from "vscode" + +/** + * Push the current VS Code telemetry-enabled flag to a webview. Called on + * webview ready / re-sync so the webview can gate feedback UI on the flag. + */ +export function pushTelemetryState(post: (msg: { type: "telemetryState"; enabled: boolean }) => void): void { + post({ type: "telemetryState", enabled: vscode.env.isTelemetryEnabled }) +} diff --git a/packages/kilo-vscode/tests/unit/feedback-payload.test.ts b/packages/kilo-vscode/tests/unit/feedback-payload.test.ts new file mode 100644 index 00000000000..309a066e816 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/feedback-payload.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from "bun:test" +import { buildFeedbackProperties, isKiloGateway } from "../../webview-ui/src/context/feedback-payload" + +const baseInput = { + messageID: "msg_abc", + sessionID: "ses_xyz", + parentMessageID: "msg_parent", + modelID: "claude-sonnet-4-5", + variant: undefined as string | undefined, +} + +describe("isKiloGateway", () => { + it("matches the canonical kilo provider", () => { + expect(isKiloGateway("kilo")).toBe(true) + }) + + it("matches aliased kilo providers", () => { + expect(isKiloGateway("kilo-dev")).toBe(true) + expect(isKiloGateway("kilocloud")).toBe(true) + }) + + it("does not match direct providers", () => { + expect(isKiloGateway("anthropic")).toBe(false) + expect(isKiloGateway("openai")).toBe(false) + expect(isKiloGateway("openrouter")).toBe(false) + }) +}) + +describe("buildFeedbackProperties — non-Kilo providers", () => { + it("includes only provider/model/rating (no session or message IDs)", () => { + const props = buildFeedbackProperties({ ...baseInput, providerID: "anthropic", next: "up" }) + expect(props).toEqual({ + providerID: "anthropic", + modelID: "claude-sonnet-4-5", + rating: "up", + }) + expect(props).not.toHaveProperty("sessionID") + expect(props).not.toHaveProperty("messageID") + expect(props).not.toHaveProperty("parentMessageID") + }) + + it("includes variant when set", () => { + const props = buildFeedbackProperties({ ...baseInput, providerID: "openai", variant: "preview", next: "down" }) + expect(props.variant).toBe("preview") + }) + + it("includes previousRating when provided", () => { + const props = buildFeedbackProperties({ ...baseInput, providerID: "anthropic", next: "down" }, "up") + expect(props.previousRating).toBe("up") + }) + + it("uses 'cleared' when next is null", () => { + const props = buildFeedbackProperties({ ...baseInput, providerID: "anthropic", next: null }, "up") + expect(props.rating).toBe("cleared") + expect(props.previousRating).toBe("up") + }) +}) + +describe("buildFeedbackProperties — Kilo Gateway", () => { + it("includes sessionID, messageID, parentMessageID", () => { + const props = buildFeedbackProperties({ ...baseInput, providerID: "kilo", next: "up" }) + expect(props).toEqual({ + providerID: "kilo", + modelID: "claude-sonnet-4-5", + rating: "up", + sessionID: "ses_xyz", + messageID: "msg_abc", + parentMessageID: "msg_parent", + }) + }) + + it("treats aliased kilo providers the same", () => { + const props = buildFeedbackProperties({ ...baseInput, providerID: "kilo-cloud", next: "up" }) + expect(props.sessionID).toBe("ses_xyz") + expect(props.messageID).toBe("msg_abc") + expect(props.parentMessageID).toBe("msg_parent") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index b5d270f87e2..3dd3c934639 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -77,6 +77,7 @@ import { ProviderProvider } from "../src/context/provider" import { ConfigProvider } from "../src/context/config" import { DisplayProvider } from "../src/context/display" import { NotificationsProvider } from "../src/context/notifications" +import { FeedbackProvider } from "../src/context/feedback" import { SessionProvider, useSession } from "../src/context/session" import { WorktreeModeProvider } from "../src/context/worktree-mode" import { ChatView } from "../src/components/chat" @@ -3170,13 +3171,15 @@ export const AgentManagerApp: Component = () => { - - - - - - - + + + + + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 8665a763fa0..6082f0745b6 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -33,6 +33,7 @@ registerVscodeToolOverrides() import HistoryView from "./components/history/HistoryView" import { MigrationWizard } from "./components/migration" // legacy-migration import { NotificationsProvider } from "./context/notifications" +import { FeedbackProvider } from "./context/feedback" import type { Message as SDKMessage, Part as SDKPart } from "@kilocode/sdk/v2" import "./styles/chat.css" @@ -354,9 +355,11 @@ const App: Component = () => { - - - + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 01d03b0a5b3..f65519ccc52 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -10,6 +10,7 @@ import { Component, For, Show, createMemo } from "solid-js" import { Dynamic } from "solid-js/web" import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part" +import type { MessageFeedbackControls } from "@kilocode/kilo-ui/message-part" import type { AssistantMessage as SDKAssistantMessage, Part as SDKPart, @@ -62,6 +63,7 @@ function matchToolRequest = (props) => { message={props.message as SDKMessage} showAssistantCopyPartID={props.showAssistantCopyPartID} reasoningAutoCollapse={display.reasoningAutoCollapse()} + feedback={props.feedback} animate={ part.type === "tool" && ((part as unknown as ToolPart).state?.status === "pending" || diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx index fa48cb25b29..6a7d32ba6d7 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx @@ -32,6 +32,7 @@ import { ErrorDisplay } from "./ErrorDisplay" import { useServer } from "../../context/server" import { useSession } from "../../context/session" import { useLanguage } from "../../context/language" +import { useFeedback } from "../../context/feedback" import { visibleError } from "../../context/session-errors" import type { ErrorDisplayProps } from "./ErrorDisplay" import type { Message as WebMessage } from "../../types/messages" @@ -68,6 +69,7 @@ export const VscodeSessionTurn: Component = (props) => { const server = useServer() const session = useSession() const language = useLanguage() + const feedback = useFeedback() const emptyParts: SDKPart[] = [] const emptyDiffs: SnapshotFileDiff[] = [] @@ -174,7 +176,26 @@ export const VscodeSessionTurn: Component = (props) => { 0}>
- {(msg) => } + {(amsg) => ( + + feedback.rate({ + messageID: amsg.id, + sessionID: amsg.sessionID, + parentMessageID: amsg.parentID, + providerID: amsg.providerID, + modelID: amsg.modelID, + variant: (amsg as SDKAssistantMessage & { variant?: string }).variant, + next, + }), + }} + /> + )}
diff --git a/packages/kilo-vscode/webview-ui/src/context/feedback-payload.ts b/packages/kilo-vscode/webview-ui/src/context/feedback-payload.ts new file mode 100644 index 00000000000..5cc5e7f6f60 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/feedback-payload.ts @@ -0,0 +1,41 @@ +/** + * Pure helpers for shaping the feedback telemetry payload. + * + * Payload rules: + * - Non-Kilo-Gateway providers: providerID, modelID, variant?, rating, previousRating? only. + * No session or message IDs — they can't be correlated to upstream data. + * - Kilo Gateway providers: add sessionID, messageID, parentMessageID. The + * gateway can join parentMessageID against its `x-kilo-request` header logs. + */ + +export type Rating = "up" | "down" + +export interface RateInput { + messageID: string + sessionID: string + parentMessageID: string + providerID: string + modelID: string + variant?: string + next: Rating | null +} + +export function isKiloGateway(providerID: string): boolean { + return providerID.startsWith("kilo") +} + +export function buildFeedbackProperties(input: RateInput, previousRating?: Rating): Record { + const properties: Record = { + providerID: input.providerID, + modelID: input.modelID, + rating: input.next ?? "cleared", + } + if (input.variant) properties.variant = input.variant + if (previousRating) properties.previousRating = previousRating + if (isKiloGateway(input.providerID)) { + properties.sessionID = input.sessionID + properties.messageID = input.messageID + properties.parentMessageID = input.parentMessageID + } + return properties +} diff --git a/packages/kilo-vscode/webview-ui/src/context/feedback.tsx b/packages/kilo-vscode/webview-ui/src/context/feedback.tsx new file mode 100644 index 00000000000..a52544f04e8 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/feedback.tsx @@ -0,0 +1,71 @@ +/** + * Feedback context + * + * Tracks per-message thumbs up/down ratings (in-memory only) and the VS Code + * telemetry-enabled flag. The context exposes a single `rate()` callback that + * updates local state and fires a telemetry event. + * + * State is not persisted — ratings reset on page reload / session switch. + */ + +import { createContext, useContext, createSignal, onCleanup } from "solid-js" +import type { ParentComponent, Accessor } from "solid-js" +import { useVSCode } from "./vscode" +import type { ExtensionMessage } from "../types/messages" +import { TelemetryEventName } from "../../../src/services/telemetry/types" +import { buildFeedbackProperties, type Rating, type RateInput } from "./feedback-payload" + +export type { Rating, RateInput } from "./feedback-payload" + +interface FeedbackContextValue { + telemetryEnabled: Accessor + getRating: (messageID: string) => Rating | undefined + rate: (input: RateInput) => void +} + +const FeedbackContext = createContext() + +export const FeedbackProvider: ParentComponent = (props) => { + const vscode = useVSCode() + const [telemetryEnabled, setTelemetryEnabled] = createSignal(false) + const [ratings, setRatings] = createSignal>({}) + + const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { + if (message.type !== "telemetryState") return + // Drop stored ratings if the user just revoked consent. + if (telemetryEnabled() && !message.enabled) setRatings({}) + setTelemetryEnabled(message.enabled) + }) + + onCleanup(unsubscribe) + + const getRating = (messageID: string) => ratings()[messageID] + + const rate = (input: RateInput) => { + if (!telemetryEnabled()) return + const prev = ratings()[input.messageID] + + setRatings((current) => { + const updated = { ...current } + if (input.next === null) delete updated[input.messageID] + else updated[input.messageID] = input.next + return updated + }) + + vscode.postMessage({ + type: "telemetry", + event: TelemetryEventName.FEEDBACK_SUBMITTED, + properties: buildFeedbackProperties(input, prev), + }) + } + + const value: FeedbackContextValue = { telemetryEnabled, getRating, rate } + + return {props.children} +} + +export function useFeedback(): FeedbackContextValue { + const context = useContext(FeedbackContext) + if (!context) throw new Error("useFeedback must be used within a FeedbackProvider") + return context +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index d9cb4b1425a..e90444fca98 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -725,6 +725,11 @@ export interface ExtensionDataReadyMessage { type: "extensionDataReady" } +export interface TelemetryStateMessage { + type: "telemetryState" + enabled: boolean +} + // ============================================ // Marketplace Messages // ============================================ @@ -934,4 +939,5 @@ export type ExtensionMessage = | McpStatusLoadedMessage | ClearPendingPromptsMessage | ExtensionDataReadyMessage + | TelemetryStateMessage | RemoteStatusMessage diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index ecd1e8d03d5..a9aa66e4f07 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -86,6 +86,7 @@ import { useTuiConfig } from "../../context/tui-config" import { formatMarkdownTables } from "../../util/markdown" // kilocode_change import { bell } from "@/kilocode/bell" // kilocode_change import { SessionIndexing } from "@/kilocode/components/session-indexing" // kilocode_change +import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change import { getScrollAcceleration } from "../../util/scroll" import { TuiPluginRuntime } from "../../plugin" import { DialogGoUpsell } from "../../component/dialog-go-upsell" @@ -472,6 +473,44 @@ export function Session() { } } + // kilocode_change start - message feedback + function submitFeedback(rating: "up" | "down", dialog: DialogContext) { + if (!Telemetry.isEnabled()) { + toast.show({ message: "Feedback disabled: telemetry is off", variant: "info" }) + dialog.clear() + return + } + const revertID = session()?.revert?.messageID + const lastAssistant = messages().findLast( + (msg): msg is AssistantMessage => msg.role === "assistant" && (!revertID || msg.id < revertID), + ) + if (!lastAssistant) { + toast.show({ message: "No assistant messages found", variant: "error" }) + dialog.clear() + return + } + const providerID = lastAssistant.providerID + const payload: Telemetry.FeedbackProperties = { + providerID, + modelID: lastAssistant.modelID, + rating, + } + const variant = (lastAssistant as AssistantMessage & { variant?: string }).variant + if (variant) payload.variant = variant + if (providerID.startsWith("kilo")) { + payload.sessionID = lastAssistant.sessionID + payload.messageID = lastAssistant.id + payload.parentMessageID = lastAssistant.parentID + } + Telemetry.trackFeedback(payload) + toast.show({ + message: rating === "up" ? "Thanks for the feedback!" : "Thanks — we'll use this to improve.", + variant: "success", + }) + dialog.clear() + } + // kilocode_change end + const command = useCommandDialog() command.register(() => [ { @@ -947,6 +986,22 @@ export function Session() { dialog.clear() }, }, + // kilocode_change start - message feedback + { + title: "Rate last assistant message helpful", + value: "messages.feedback.up", + keybind: "messages_feedback_up", + category: "Session", + onSelect: (dialog) => submitFeedback("up", dialog), + }, + { + title: "Rate last assistant message not helpful", + value: "messages.feedback.down", + keybind: "messages_feedback_down", + category: "Session", + onSelect: (dialog) => submitFeedback("down", dialog), + }, + // kilocode_change end { title: "Copy session transcript", value: "session.copy", diff --git a/packages/opencode/src/config/keybinds.ts b/packages/opencode/src/config/keybinds.ts index 214168858de..01860c12d3d 100644 --- a/packages/opencode/src/config/keybinds.ts +++ b/packages/opencode/src/config/keybinds.ts @@ -51,6 +51,10 @@ const KeybindsSchema = Schema.Struct({ messages_copy: keybind("y", "Copy message"), messages_undo: keybind("u", "Undo message"), messages_redo: keybind("r", "Redo message"), + // kilocode_change start - message feedback + messages_feedback_up: keybind("+", "Rate last assistant message helpful"), + messages_feedback_down: keybind("-", "Rate last assistant message not helpful"), + // kilocode_change end messages_toggle_conceal: keybind("h", "Toggle code block concealment in messages"), tool_details: keybind("none", "Toggle tool details visibility"), model_list: keybind("m", "List available models"), diff --git a/packages/opencode/test/kilocode/telemetry/feedback.test.ts b/packages/opencode/test/kilocode/telemetry/feedback.test.ts new file mode 100644 index 00000000000..548fd43848a --- /dev/null +++ b/packages/opencode/test/kilocode/telemetry/feedback.test.ts @@ -0,0 +1,9 @@ +// kilocode_change - new file +import { describe, expect, test } from "bun:test" +import { TelemetryEvent } from "@kilocode/kilo-telemetry" + +describe("TelemetryEvent.FEEDBACK_SUBMITTED", () => { + test("enum value is human-readable title case", () => { + expect(String(TelemetryEvent.FEEDBACK_SUBMITTED)).toBe("Feedback Submitted") + }) +}) diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index f7db04e664f..f5af5239ad2 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -111,6 +111,12 @@ const icons = { models: ``, discard: ``, // kilocode_change "arrow-undo-down": ``, + // kilocode_change start + "thumbs-up": ``, + "thumbs-down": ``, + "thumbs-up-filled": ``, + "thumbs-down-filled": ``, + // kilocode_change end } export interface IconProps extends ComponentProps<"svg"> { diff --git a/packages/ui/src/i18n/ar.ts b/packages/ui/src/i18n/ar.ts index f31f108cb13..cc31149a9e4 100644 --- a/packages/ui/src/i18n/ar.ts +++ b/packages/ui/src/i18n/ar.ts @@ -140,6 +140,11 @@ export const dict = { "ui.message.revertMessage": "إعادة التعيين إلى هذه النقطة", "ui.message.copyResponse": "نسخ الرد", "ui.message.copied": "تم النسخ!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "كان هذا مفيدًا", + "ui.message.feedback.notHelpful": "لم يكن هذا مفيدًا", + "ui.message.feedback.clearRating": "مسح التقييم", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "تمت المقاطعة", "ui.message.queued": "في الانتظار", diff --git a/packages/ui/src/i18n/br.ts b/packages/ui/src/i18n/br.ts index 6d4a826bcde..01063f8dd68 100644 --- a/packages/ui/src/i18n/br.ts +++ b/packages/ui/src/i18n/br.ts @@ -140,6 +140,11 @@ export const dict = { "ui.message.revertMessage": "Redefinir para este ponto", "ui.message.copyResponse": "Copiar resposta", "ui.message.copied": "Copiado!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Isso foi útil", + "ui.message.feedback.notHelpful": "Isso não foi útil", + "ui.message.feedback.clearRating": "Limpar avaliação", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "Interrompido", "ui.message.queued": "Na fila", diff --git a/packages/ui/src/i18n/bs.ts b/packages/ui/src/i18n/bs.ts index 62970c7279f..f991b8e57f1 100644 --- a/packages/ui/src/i18n/bs.ts +++ b/packages/ui/src/i18n/bs.ts @@ -144,6 +144,11 @@ export const dict = { "ui.message.revertMessage": "Resetuj na ovu tačku", "ui.message.copyResponse": "Kopiraj odgovor", "ui.message.copied": "Kopirano!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Ovo je bilo korisno", + "ui.message.feedback.notHelpful": "Ovo nije bilo korisno", + "ui.message.feedback.clearRating": "Obriši ocjenu", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "Prekinuto", "ui.message.queued": "U redu", diff --git a/packages/ui/src/i18n/da.ts b/packages/ui/src/i18n/da.ts index 20303221038..ae8c039256e 100644 --- a/packages/ui/src/i18n/da.ts +++ b/packages/ui/src/i18n/da.ts @@ -139,6 +139,11 @@ export const dict = { "ui.message.revertMessage": "Nulstil til dette punkt", "ui.message.copyResponse": "Kopier svar", "ui.message.copied": "Kopieret!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Dette var nyttigt", + "ui.message.feedback.notHelpful": "Dette var ikke nyttigt", + "ui.message.feedback.clearRating": "Ryd bedømmelse", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "Afbrudt", "ui.message.queued": "I kø", diff --git a/packages/ui/src/i18n/de.ts b/packages/ui/src/i18n/de.ts index bc0358ea056..7b31ca6371c 100644 --- a/packages/ui/src/i18n/de.ts +++ b/packages/ui/src/i18n/de.ts @@ -145,6 +145,11 @@ export const dict = { "ui.message.revertMessage": "Auf diesen Punkt zurücksetzen", "ui.message.copyResponse": "Antwort kopieren", "ui.message.copied": "Kopiert!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Das war hilfreich", + "ui.message.feedback.notHelpful": "Das war nicht hilfreich", + "ui.message.feedback.clearRating": "Bewertung löschen", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "Unterbrochen", "ui.message.queued": "In Warteschlange", diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 4ed9e9c01b6..87104057beb 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -152,6 +152,11 @@ export const dict: Record = { "ui.message.revertMessage": "Revert to here", "ui.message.copyResponse": "Copy response", "ui.message.copied": "Copied", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "This was helpful", + "ui.message.feedback.notHelpful": "This wasn't helpful", + "ui.message.feedback.clearRating": "Clear rating", + // kilocode_change end "ui.message.duration.seconds": "{{count}}s", "ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s", "ui.message.interrupted": "Interrupted", diff --git a/packages/ui/src/i18n/es.ts b/packages/ui/src/i18n/es.ts index 90899358219..3393999f819 100644 --- a/packages/ui/src/i18n/es.ts +++ b/packages/ui/src/i18n/es.ts @@ -140,6 +140,11 @@ export const dict = { "ui.message.revertMessage": "Restablecer a este punto", "ui.message.copyResponse": "Copiar respuesta", "ui.message.copied": "¡Copiado!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Esto fue útil", + "ui.message.feedback.notHelpful": "Esto no fue útil", + "ui.message.feedback.clearRating": "Borrar valoración", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "Interrumpido", "ui.message.queued": "En cola", diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts index 35f6702c5d4..94c43211c1a 100644 --- a/packages/ui/src/i18n/fr.ts +++ b/packages/ui/src/i18n/fr.ts @@ -140,6 +140,11 @@ export const dict = { "ui.message.revertMessage": "Réinitialiser à ce point", "ui.message.copyResponse": "Copier la réponse", "ui.message.copied": "Copié !", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "C'était utile", + "ui.message.feedback.notHelpful": "Ce n'était pas utile", + "ui.message.feedback.clearRating": "Effacer la notation", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "Interrompu", "ui.message.queued": "En file", diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts index 2daf8cf2443..8e52e788ff4 100644 --- a/packages/ui/src/i18n/ja.ts +++ b/packages/ui/src/i18n/ja.ts @@ -139,6 +139,11 @@ export const dict = { "ui.message.revertMessage": "この時点までリセット", "ui.message.copyResponse": "応答をコピー", "ui.message.copied": "コピーしました!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "役に立ちました", + "ui.message.feedback.notHelpful": "役に立ちませんでした", + "ui.message.feedback.clearRating": "評価をクリア", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "中断", "ui.message.queued": "待機中", diff --git a/packages/ui/src/i18n/ko.ts b/packages/ui/src/i18n/ko.ts index ee6e1f83096..ebe8991b06d 100644 --- a/packages/ui/src/i18n/ko.ts +++ b/packages/ui/src/i18n/ko.ts @@ -140,6 +140,11 @@ export const dict = { "ui.message.revertMessage": "이 시점으로 초기화", "ui.message.copyResponse": "응답 복사", "ui.message.copied": "복사됨!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "도움이 됐어요", + "ui.message.feedback.notHelpful": "도움이 안 됐어요", + "ui.message.feedback.clearRating": "평가 지우기", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "중단됨", "ui.message.queued": "대기 중", diff --git a/packages/ui/src/i18n/nl.ts b/packages/ui/src/i18n/nl.ts index c3cab8540d2..02712e4bce7 100644 --- a/packages/ui/src/i18n/nl.ts +++ b/packages/ui/src/i18n/nl.ts @@ -151,6 +151,11 @@ export const dict: Record = { "ui.message.copyMessage": "Bericht kopiëren", "ui.message.copyResponse": "Antwoord kopiëren", "ui.message.copied": "Gekopieerd", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Dit was nuttig", + "ui.message.feedback.notHelpful": "Dit was niet nuttig", + "ui.message.feedback.clearRating": "Beoordeling wissen", + // kilocode_change end "ui.message.forkMessage": "Fork to new session", "ui.message.revertMessage": "Hiernaar terugzetten", "ui.message.revert": "Hiernaar terugdraaien", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index 377fa7a95a8..855aba356ae 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -143,6 +143,11 @@ export const dict: Record = { "ui.message.revertMessage": "Tilbakestill til dette punktet", "ui.message.copyResponse": "Kopier svar", "ui.message.copied": "Kopiert!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Dette var nyttig", + "ui.message.feedback.notHelpful": "Dette var ikke nyttig", + "ui.message.feedback.clearRating": "Fjern vurdering", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "Avbrutt", "ui.message.queued": "I kø", diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts index 05a1a2e1662..67ff2e02314 100644 --- a/packages/ui/src/i18n/pl.ts +++ b/packages/ui/src/i18n/pl.ts @@ -139,6 +139,11 @@ export const dict = { "ui.message.revertMessage": "Zresetuj do tego punktu", "ui.message.copyResponse": "Kopiuj odpowiedź", "ui.message.copied": "Skopiowano!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "To było pomocne", + "ui.message.feedback.notHelpful": "To nie było pomocne", + "ui.message.feedback.clearRating": "Wyczyść ocenę", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "Przerwano", "ui.message.queued": "W kolejce", diff --git a/packages/ui/src/i18n/ru.ts b/packages/ui/src/i18n/ru.ts index 067cbd0e67b..75941554e69 100644 --- a/packages/ui/src/i18n/ru.ts +++ b/packages/ui/src/i18n/ru.ts @@ -139,6 +139,11 @@ export const dict = { "ui.message.revertMessage": "Сбросить до этого момента", "ui.message.copyResponse": "Копировать ответ", "ui.message.copied": "Скопировано!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Это было полезно", + "ui.message.feedback.notHelpful": "Это было бесполезно", + "ui.message.feedback.clearRating": "Очистить оценку", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "Прервано", "ui.message.queued": "В очереди", diff --git a/packages/ui/src/i18n/th.ts b/packages/ui/src/i18n/th.ts index 92299360185..6e3395b5f9e 100644 --- a/packages/ui/src/i18n/th.ts +++ b/packages/ui/src/i18n/th.ts @@ -141,6 +141,11 @@ export const dict = { "ui.message.revertMessage": "รีเซ็ตไปยังจุดนี้", "ui.message.copyResponse": "คัดลอกคำตอบ", "ui.message.copied": "คัดลอกแล้ว!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "สิ่งนี้มีประโยชน์", + "ui.message.feedback.notHelpful": "สิ่งนี้ไม่มีประโยชน์", + "ui.message.feedback.clearRating": "ล้างการให้คะแนน", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "ถูกขัดจังหวะ", "ui.message.queued": "อยู่ในคิว", diff --git a/packages/ui/src/i18n/tr.ts b/packages/ui/src/i18n/tr.ts index f7b55f6ad8e..3537faf07d8 100644 --- a/packages/ui/src/i18n/tr.ts +++ b/packages/ui/src/i18n/tr.ts @@ -147,6 +147,11 @@ export const dict = { "ui.message.revert": "Revert to here", "ui.message.copyResponse": "Yanıtı kopyala", "ui.message.copied": "Kopyalandı", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Bu yardımcı oldu", + "ui.message.feedback.notHelpful": "Bu yardımcı olmadı", + "ui.message.feedback.clearRating": "Değerlendirmeyi temizle", + // kilocode_change end "ui.message.interrupted": "Kesildi", "ui.message.queued": "Sırada", "ui.message.attachment.alt": "ek", diff --git a/packages/ui/src/i18n/uk.ts b/packages/ui/src/i18n/uk.ts index 3ebf9ce3396..86e6949612c 100644 --- a/packages/ui/src/i18n/uk.ts +++ b/packages/ui/src/i18n/uk.ts @@ -156,6 +156,11 @@ export const dict = { "ui.message.copyMessage": "Копіювати повідомлення", "ui.message.copyResponse": "Копіювати відповідь", "ui.message.copied": "Скопійовано", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "Це було корисно", + "ui.message.feedback.notHelpful": "Це не було корисно", + "ui.message.feedback.clearRating": "Очистити оцінку", + // kilocode_change end "ui.message.forkMessage": "Fork to new session", "ui.message.revertMessage": "Повернутися сюди", "ui.message.revert": "Повернутися до цього місця", diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 48423ab232c..14803bdeeff 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -144,6 +144,11 @@ export const dict = { "ui.message.revertMessage": "重置到此点", "ui.message.copyResponse": "复制回复", "ui.message.copied": "已复制!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "这有帮助", + "ui.message.feedback.notHelpful": "这没有帮助", + "ui.message.feedback.clearRating": "清除评分", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "已中断", "ui.message.queued": "排队中", diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts index 276db511540..4af7bda4bcb 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -144,6 +144,11 @@ export const dict = { "ui.message.revertMessage": "重設至此點", "ui.message.copyResponse": "複製回覆", "ui.message.copied": "已複製!", + // kilocode_change start - message feedback + "ui.message.feedback.helpful": "這有幫助", + "ui.message.feedback.notHelpful": "這沒有幫助", + "ui.message.feedback.clearRating": "清除評分", + // kilocode_change end "ui.message.revert": "Revert to here", "ui.message.interrupted": "已中斷", "ui.message.queued": "排隊中", From 56a7548115d7bf3fbf6dfde607902d731590a3f9 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 4 May 2026 14:06:42 -0400 Subject: [PATCH 2/4] fix: use = for thumbs-up keybind and wrap story providers - Replace + with = in messages_feedback_up: the TUI parser normalizes to 'leader+' and splits on '+', producing an empty key name for '+' so the binding never fires. The = key sits on the same physical key as + on most layouts, pairs visually with -, and parses to a real key name. - Wrap StoryProviders with FeedbackProvider so stories that render VscodeSessionTurn (e.g. Diff Summary Collapsed) don't throw 'useFeedback must be used within a FeedbackProvider' under Storybook visual regression. - Sync changeset and docs page to mention the new keybind. --- .changeset/message-feedback.md | 2 +- .../architecture/per-message-feedback.md | 2 +- .../webview-ui/src/stories/StoryProviders.tsx | 83 ++++++++++--------- packages/opencode/src/config/keybinds.ts | 2 +- 4 files changed, 46 insertions(+), 43 deletions(-) diff --git a/.changeset/message-feedback.md b/.changeset/message-feedback.md index 98d5c5c2215..080c200bd34 100644 --- a/.changeset/message-feedback.md +++ b/.changeset/message-feedback.md @@ -3,4 +3,4 @@ "@kilocode/cli": minor --- -Rate assistant responses with thumbs up/down. Click the thumbs buttons next to the copy button on any assistant message, or press `+` / `-` in the terminal UI. Only shown when telemetry is enabled; feedback is sent to Kilo to help improve model and prompt quality. +Rate assistant responses with thumbs up/down. Click the thumbs buttons next to the copy button on any assistant message, or press `=` / `-` in the terminal UI. Only shown when telemetry is enabled; feedback is sent to Kilo to help improve model and prompt quality. diff --git a/packages/kilo-docs/pages/contributing/architecture/per-message-feedback.md b/packages/kilo-docs/pages/contributing/architecture/per-message-feedback.md index b638dba13d2..834906dc84e 100644 --- a/packages/kilo-docs/pages/contributing/architecture/per-message-feedback.md +++ b/packages/kilo-docs/pages/contributing/architecture/per-message-feedback.md @@ -24,7 +24,7 @@ Add a thumbs-up / thumbs-down widget next to the existing copy button on every a | Surface | Approach | |---|---| | VS Code extension | Thumbs buttons inline next to the copy button | -| TUI | Keybinds (`+` / `-`) on the last assistant message | +| TUI | Keybinds (`=` / `-`) on the last assistant message | ### Telemetry Payload diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index ea309a6a669..ee4fbe235bd 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -13,6 +13,7 @@ import { createSignal, createMemo, type ParentComponent } from "solid-js" import { VSCodeProvider } from "../context/vscode" import { ServerProvider } from "../context/server" +import { FeedbackProvider } from "../context/feedback" import { ProviderContext } from "../context/provider" import { flattenModels, findModel as _findModel } from "../context/provider-utils" import { ConfigProvider, ConfigContext } from "../context/config" @@ -317,46 +318,48 @@ export const StoryProviders: ParentComponent = (props) => { return ( - - - - - "" as any, - t, - }} - > - "en", t }}> - - - - - - - - - {props.noPadding ? ( - props.children - ) : ( -
{props.children}
- )} -
-
-
-
-
-
-
-
-
-
-
-
-
-
+ + + + + + "" as any, + t, + }} + > + "en", t }}> + + + + + + + + + {props.noPadding ? ( + props.children + ) : ( +
{props.children}
+ )} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
) diff --git a/packages/opencode/src/config/keybinds.ts b/packages/opencode/src/config/keybinds.ts index 01860c12d3d..34cc8d27d81 100644 --- a/packages/opencode/src/config/keybinds.ts +++ b/packages/opencode/src/config/keybinds.ts @@ -52,7 +52,7 @@ const KeybindsSchema = Schema.Struct({ messages_undo: keybind("u", "Undo message"), messages_redo: keybind("r", "Redo message"), // kilocode_change start - message feedback - messages_feedback_up: keybind("+", "Rate last assistant message helpful"), + messages_feedback_up: keybind("=", "Rate last assistant message helpful"), messages_feedback_down: keybind("-", "Rate last assistant message not helpful"), // kilocode_change end messages_toggle_conceal: keybind("h", "Toggle code block concealment in messages"), From d388b62fefca624a4bba3e30e00571cd4b842c97 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 4 May 2026 23:30:25 -0400 Subject: [PATCH 3/4] fix(ui): use heroicons for thumbs up/down with hover-fill preview - Replace the fractional hand-drawn thumbs paths with Heroicons 20 outline (default / rated=false) and solid (filled / rated=true). - Add a CSS rule under assistant-copy-wrapper so hovering the outline button fills it with currentColor, giving a preview of the rated state before the user commits. --- packages/kilo-ui/src/components/message-part.css | 6 ++++++ packages/ui/src/components/icon.tsx | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index bac3a669ed0..942e77c45d9 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -31,6 +31,12 @@ width: 20px; height: 20px; } + + /* Thumbs up/down: fill the outline on hover to preview the rated state. */ + [data-component="icon-button"][data-icon="thumbs-up"]:hover [data-slot="icon-svg"] path, + [data-component="icon-button"][data-icon="thumbs-down"]:hover [data-slot="icon-svg"] path { + fill: currentColor; + } } } diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index f5af5239ad2..ebc54bb16e3 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -112,10 +112,10 @@ const icons = { discard: ``, // kilocode_change "arrow-undo-down": ``, // kilocode_change start - "thumbs-up": ``, - "thumbs-down": ``, - "thumbs-up-filled": ``, - "thumbs-down-filled": ``, + "thumbs-up": ``, + "thumbs-down": ``, + "thumbs-up-filled": ``, + "thumbs-down-filled": ``, // kilocode_change end } From f09f0bb98ecf635bf286daee8649318d6171c374 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Tue, 5 May 2026 01:26:39 -0400 Subject: [PATCH 4/4] fix(vscode): propagate telemetry opt-in/out to open webviews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VS Code exposes `env.onDidChangeTelemetryEnabled` for runtime changes to the telemetry consent setting. The extension was only reading `env.isTelemetryEnabled` once during `syncWebviewState`, so toggling `telemetry.telemetryLevel` while a Kilo webview was open left the feedback UI stuck on its previous visibility state until reload. Add `watchTelemetryState()` next to `pushTelemetryState()` and wire the disposable in `setupWebviewMessageHandler` alongside the existing `watchAutocompleteConfig`. Uses the same registration/dispose pattern as the autocomplete config watcher. Also bump the ESLint `max-lines` cap on AgentManagerApp.tsx from 3200 to 3210 to accommodate the FeedbackProvider wrapper that sits inside the provider chain (past precedent: 3100 → 3200 for terminal tabs). Closes #9872. --- packages/kilo-vscode/eslint.config.mjs | 5 ++++- packages/kilo-vscode/src/KiloProvider.ts | 6 +++++- .../kilo-vscode/src/services/telemetry/index.ts | 2 +- .../src/services/telemetry/webview-state.ts | 14 +++++++++++++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/kilo-vscode/eslint.config.mjs b/packages/kilo-vscode/eslint.config.mjs index b11a766659b..89e04572492 100644 --- a/packages/kilo-vscode/eslint.config.mjs +++ b/packages/kilo-vscode/eslint.config.mjs @@ -49,7 +49,10 @@ export default [ // (canvases must never leave the paint tree — see render.tsx), and // render-call wiring that must live at the top of // `AgentManagerContent` alongside the existing selection/session state. - rules: { complexity: ["error", 74], "max-lines": ["error", 3200] }, + // Raised from 3200 → 3210 for the per-message feedback `FeedbackProvider` + // wiring, which sits inside the provider chain and cannot be extracted + // without adding an intermediate wrapper component. + rules: { complexity: ["error", 74], "max-lines": ["error", 3210] }, }, { files: ["src/agent-manager/AgentManagerProvider.ts"], diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index ec471b915a9..45c21865ed2 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -16,7 +16,7 @@ import type { EditorContext, IndexingStatus } from "./services/cli-backend/types import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController" import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete" import { buildWebviewHtml } from "./utils" -import { TelemetryProxy, type TelemetryPropertiesProvider, pushTelemetryState } from "./services/telemetry" +import { TelemetryProxy, type TelemetryPropertiesProvider, pushTelemetryState, watchTelemetryState } from "./services/telemetry" // prettier-ignore import { sessionToWebview, indexProvidersById, @@ -211,6 +211,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private initConnectionPromise: Promise | null = null private webviewMessageDisposable: vscode.Disposable | null = null private autocompleteConfigDisposable: vscode.Disposable | null = null + private telemetryStateDisposable: vscode.Disposable | null = null private viewStateDisposable: vscode.Disposable | null = null private visibilityDisposable: vscode.Disposable | null = null private autoApproveBridge: ReturnType | null = null @@ -574,6 +575,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.webviewMessageDisposable?.dispose() this.autocompleteConfigDisposable?.dispose() this.autocompleteConfigDisposable = watchAutocompleteConfig((msg) => this.postMessage(msg)) + this.telemetryStateDisposable?.dispose() + this.telemetryStateDisposable = watchTelemetryState((msg) => this.postMessage(msg)) this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => { const intercepted = await interceptMessage(message, { workspaceDir: (sid) => this.getWorkspaceDirectory(sid ?? this.currentSession?.id), @@ -3398,6 +3401,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.visibilityDisposable?.dispose() this.webviewMessageDisposable?.dispose() this.autocompleteConfigDisposable?.dispose() + this.telemetryStateDisposable?.dispose() this.autoApproveBridge?.dispose() this.streams.dispose() this.isWebviewReady = false diff --git a/packages/kilo-vscode/src/services/telemetry/index.ts b/packages/kilo-vscode/src/services/telemetry/index.ts index 856b1afb975..d6ca56e8387 100644 --- a/packages/kilo-vscode/src/services/telemetry/index.ts +++ b/packages/kilo-vscode/src/services/telemetry/index.ts @@ -1,3 +1,3 @@ export { TelemetryEventName, type TelemetryPropertiesProvider } from "./types" export { TelemetryProxy } from "./telemetry-proxy" -export { pushTelemetryState } from "./webview-state" +export { pushTelemetryState, watchTelemetryState } from "./webview-state" diff --git a/packages/kilo-vscode/src/services/telemetry/webview-state.ts b/packages/kilo-vscode/src/services/telemetry/webview-state.ts index fd1c028ba2b..fdd31318b1c 100644 --- a/packages/kilo-vscode/src/services/telemetry/webview-state.ts +++ b/packages/kilo-vscode/src/services/telemetry/webview-state.ts @@ -1,9 +1,21 @@ import * as vscode from "vscode" +type Post = (msg: { type: "telemetryState"; enabled: boolean }) => void + /** * Push the current VS Code telemetry-enabled flag to a webview. Called on * webview ready / re-sync so the webview can gate feedback UI on the flag. */ -export function pushTelemetryState(post: (msg: { type: "telemetryState"; enabled: boolean }) => void): void { +export function pushTelemetryState(post: Post): void { post({ type: "telemetryState", enabled: vscode.env.isTelemetryEnabled }) } + +/** + * Re-push telemetry state whenever the user toggles the VS Code telemetry + * setting while a webview is open, so feedback UI shows/hides in real time. + */ +export function watchTelemetryState(post: Post): vscode.Disposable { + return vscode.env.onDidChangeTelemetryEnabled((enabled) => { + post({ type: "telemetryState", enabled }) + }) +}