From dcaccf38658415819b72390255b9f6555e4795e5 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Fri, 1 May 2026 12:57:30 -0400 Subject: [PATCH 01/10] 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 2e037e3aa6b..16dbcc92201 100644 --- a/packages/kilo-telemetry/src/telemetry.ts +++ b/packages/kilo-telemetry/src/telemetry.ts @@ -244,6 +244,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 896e358356a..f2464cadf2b 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} /> ) @@ -1342,6 +1350,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 e226602ecd3..ce1beea0cdf 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, getWebviewFontSize } from "./utils" -import { TelemetryProxy, type TelemetryPropertiesProvider } from "./services/telemetry" +import { TelemetryProxy, type TelemetryPropertiesProvider, pushTelemetryState } from "./services/telemetry" import { sessionToWebview, indexProvidersById, @@ -357,10 +357,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 974ea6962e6..39edbb0ad0c 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" @@ -3169,13 +3170,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 67780750a04..998d610d579 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, @@ -63,6 +64,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 6aa389bbd84..cd1633ee78e 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[] = [] @@ -179,7 +181,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 6699ee5ca08..9641bef4ac3 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 @@ -768,6 +768,11 @@ export interface ExtensionDataReadyMessage { type: "extensionDataReady" } +export interface TelemetryStateMessage { + type: "telemetryState" + enabled: boolean +} + // ============================================ // Marketplace Messages // ============================================ @@ -984,4 +989,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 adfebc5d0b3..0093ac0ffe1 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 "@/cli/cmd/tui/plugin/runtime" import { DialogGoUpsell } from "../../component/dialog-go-upsell" @@ -474,6 +475,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(() => [ { @@ -949,6 +988,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 cc17a64165c..937239d5a61 100644 --- a/packages/ui/src/i18n/ar.ts +++ b/packages/ui/src/i18n/ar.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/br.ts b/packages/ui/src/i18n/br.ts index fcf3e32487b..1f9349adeac 100644 --- a/packages/ui/src/i18n/br.ts +++ b/packages/ui/src/i18n/br.ts @@ -139,6 +139,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 eddbbda3526..812bd7fb01c 100644 --- a/packages/ui/src/i18n/bs.ts +++ b/packages/ui/src/i18n/bs.ts @@ -143,6 +143,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 e110bbd4d44..75bacc372a7 100644 --- a/packages/ui/src/i18n/da.ts +++ b/packages/ui/src/i18n/da.ts @@ -138,6 +138,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 822dc077924..999ef060c6e 100644 --- a/packages/ui/src/i18n/de.ts +++ b/packages/ui/src/i18n/de.ts @@ -144,6 +144,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 6bab63ec07b..3412d770281 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -151,6 +151,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 da485a9b6a7..fdfdfecaa70 100644 --- a/packages/ui/src/i18n/es.ts +++ b/packages/ui/src/i18n/es.ts @@ -139,6 +139,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 ef860f2dbec..aeb58b035e9 100644 --- a/packages/ui/src/i18n/fr.ts +++ b/packages/ui/src/i18n/fr.ts @@ -139,6 +139,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 281d8d9bb6e..961c6b6de49 100644 --- a/packages/ui/src/i18n/ja.ts +++ b/packages/ui/src/i18n/ja.ts @@ -138,6 +138,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 d9b8e0fb968..6bd09f93d45 100644 --- a/packages/ui/src/i18n/ko.ts +++ b/packages/ui/src/i18n/ko.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/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 4c1ff647906..975fd891bff 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -142,6 +142,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 b262e9774de..0435c40a9b2 100644 --- a/packages/ui/src/i18n/pl.ts +++ b/packages/ui/src/i18n/pl.ts @@ -138,6 +138,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 9626c2f8043..dde141f71a4 100644 --- a/packages/ui/src/i18n/ru.ts +++ b/packages/ui/src/i18n/ru.ts @@ -138,6 +138,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 86d49b9fd6c..a1f679683fc 100644 --- a/packages/ui/src/i18n/th.ts +++ b/packages/ui/src/i18n/th.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/tr.ts b/packages/ui/src/i18n/tr.ts index b4a91ef84b9..997667435a0 100644 --- a/packages/ui/src/i18n/tr.ts +++ b/packages/ui/src/i18n/tr.ts @@ -146,6 +146,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 6630ae0a057..71e5cf7e375 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -143,6 +143,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 0ad253e2c2a..d578ac57767 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -143,6 +143,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 0eab152b25323eb532c446557d8c8d528037d383 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 4 May 2026 14:06:42 -0400 Subject: [PATCH 02/10] 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 19cbdf09610..e3b1fc5e136 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" @@ -318,46 +319,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 af88cbe203ad7af0447b1df561b07715f0bf9277 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Mon, 4 May 2026 23:30:25 -0400 Subject: [PATCH 03/10] 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 ac4a7ab620917802de12042e6488faef8c6ffaf2 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Tue, 5 May 2026 01:26:39 -0400 Subject: [PATCH 04/10] 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 7f8125e5dfe..de3e2b75e4d 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 ce1beea0cdf..b18192e38f4 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, getWebviewFontSize } from "./utils" -import { TelemetryProxy, type TelemetryPropertiesProvider, pushTelemetryState } from "./services/telemetry" +import { TelemetryProxy, type TelemetryPropertiesProvider, pushTelemetryState, watchTelemetryState } from "./services/telemetry" // prettier-ignore import { sessionToWebview, indexProvidersById, @@ -216,6 +216,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 @@ -579,6 +580,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), @@ -3402,6 +3405,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 }) + }) +} From 39923008576ed094f32542c8a5cde423bbe4e798 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Tue, 5 May 2026 08:57:12 -0400 Subject: [PATCH 05/10] fix(vscode): restrict feedback session IDs to the kilo provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `isKiloGateway` used `startsWith("kilo")` which matches fictional aliases like `kilo-dev`/`kilocloud`. The only real gateway provider ID in the codebase is plain `kilo` — every other usage of it checks `providerID === "kilo"`. Tighten to an exact match and drop the `isKiloGateway` tests (it is now a one-line equality check) along with the fabricated alias case in `buildFeedbackProperties`. --- .../tests/unit/feedback-payload.test.ts | 26 +------------------ .../src/context/feedback-payload.ts | 2 +- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/feedback-payload.test.ts b/packages/kilo-vscode/tests/unit/feedback-payload.test.ts index 309a066e816..67d18caadb5 100644 --- a/packages/kilo-vscode/tests/unit/feedback-payload.test.ts +++ b/packages/kilo-vscode/tests/unit/feedback-payload.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "bun:test" -import { buildFeedbackProperties, isKiloGateway } from "../../webview-ui/src/context/feedback-payload" +import { buildFeedbackProperties } from "../../webview-ui/src/context/feedback-payload" const baseInput = { messageID: "msg_abc", @@ -9,23 +9,6 @@ const baseInput = { 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" }) @@ -68,11 +51,4 @@ describe("buildFeedbackProperties — Kilo Gateway", () => { 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/src/context/feedback-payload.ts b/packages/kilo-vscode/webview-ui/src/context/feedback-payload.ts index 5cc5e7f6f60..c9bc7383050 100644 --- a/packages/kilo-vscode/webview-ui/src/context/feedback-payload.ts +++ b/packages/kilo-vscode/webview-ui/src/context/feedback-payload.ts @@ -21,7 +21,7 @@ export interface RateInput { } export function isKiloGateway(providerID: string): boolean { - return providerID.startsWith("kilo") + return providerID === "kilo" } export function buildFeedbackProperties(input: RateInput, previousRating?: Rating): Record { From 363e337ca8999ca74ac11d99cfedb339d1418f22 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 6 May 2026 10:20:56 -0400 Subject: [PATCH 06/10] refactor: move feedback additions out of upstream-shared files Addresses review feedback: Kilo-specific additions belong in Kilo-owned packages so the diff against upstream OpenCode stays minimal. - Icons: move thumbs-up/-down/-up-filled/-down-filled paths from packages/ui/src/components/icon.tsx into packages/kilo-ui's icon registry. Extend the kilo-ui Icon registry to carry per-icon viewBox (Heroicons thumbs are 20x20; existing Kilo icons are 16x16). Replace packages/kilo-ui/src/components/icon-button.tsx (previously a re-export of @opencode-ai/ui/icon-button) with a local implementation that uses the kilo-ui Icon so the new names resolve. - i18n: move the three feedback strings (helpful, notHelpful, clearRating) from each of 19 packages/ui/src/i18n/.ts files into the corresponding packages/kilo-i18n/src/.ts. The webview's language.tsx already merges kilo-i18n on top of upstream, so the runtime keys remain available. - TUI: extract submitFeedback from packages/opencode/src/cli/cmd/tui/routes/session/index.tsx into a new Kilo-owned helper at packages/opencode/src/kilocode/cli/cmd/tui/feedback.ts. The session route now imports and invokes it, passing { toast, session, messages }. Also tightens the Kilo Gateway gate from startsWith("kilo") to === "kilo" to match the equivalent fix in the webview. --- packages/kilo-i18n/src/ar.ts | 5 ++ packages/kilo-i18n/src/br.ts | 5 ++ packages/kilo-i18n/src/bs.ts | 5 ++ packages/kilo-i18n/src/da.ts | 5 ++ packages/kilo-i18n/src/de.ts | 5 ++ packages/kilo-i18n/src/en.ts | 5 ++ packages/kilo-i18n/src/es.ts | 5 ++ packages/kilo-i18n/src/fr.ts | 5 ++ packages/kilo-i18n/src/ja.ts | 5 ++ packages/kilo-i18n/src/ko.ts | 5 ++ packages/kilo-i18n/src/nl.ts | 5 ++ packages/kilo-i18n/src/no.ts | 5 ++ packages/kilo-i18n/src/pl.ts | 5 ++ packages/kilo-i18n/src/ru.ts | 5 ++ packages/kilo-i18n/src/th.ts | 5 ++ packages/kilo-i18n/src/tr.ts | 5 ++ packages/kilo-i18n/src/uk.ts | 5 ++ packages/kilo-i18n/src/zh.ts | 5 ++ packages/kilo-i18n/src/zht.ts | 5 ++ .../kilo-ui/src/components/icon-button.tsx | 30 +++++++++- packages/kilo-ui/src/components/icon.tsx | 33 +++++++++-- .../src/cli/cmd/tui/routes/session/index.tsx | 44 +------------- .../src/kilocode/cli/cmd/tui/feedback.ts | 58 +++++++++++++++++++ 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 -- 43 files changed, 213 insertions(+), 148 deletions(-) create mode 100644 packages/opencode/src/kilocode/cli/cmd/tui/feedback.ts diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 857501d6d69..7dd1f9304fa 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -92,4 +92,9 @@ export const dict = { "أوقف لقطات Kilo لهذا المشروع. ستفقد ميزة التراجع/الإعادة لتعديلات Kilo، لكن git سيستمر في تتبع كل شيء.", "ui.messagePart.openInDiffViewer": "فتح في عارض الفروقات", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "كان هذا مفيدًا", + "ui.message.feedback.notHelpful": "لم يكن هذا مفيدًا", + "ui.message.feedback.clearRating": "مسح التقييم", } diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index 8624128af45..e79ef39bf30 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -92,4 +92,9 @@ export const dict = { "Desligue os snapshots do Kilo para este projeto. Você perde desfazer/refazer das mudanças feitas pelo Kilo, mas o git continua rastreando tudo.", "ui.messagePart.openInDiffViewer": "Abrir no Visualizador de Diferenças", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Isso foi útil", + "ui.message.feedback.notHelpful": "Isso não foi útil", + "ui.message.feedback.clearRating": "Limpar avaliação", } diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index 8773799ea6d..e23e964bbd5 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -97,4 +97,9 @@ export const dict = { "Isključi Kilo snapshotove za ovaj projekat. Izgubićete poništi/vrati za izmjene koje napravi Kilo, ali git i dalje prati sve.", "ui.messagePart.openInDiffViewer": "Otvori u pregledniku razlika", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Ovo je bilo korisno", + "ui.message.feedback.notHelpful": "Ovo nije bilo korisno", + "ui.message.feedback.clearRating": "Obriši ocjenu", } diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index bbed59f1519..2a086970b40 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -92,4 +92,9 @@ export const dict = { "Slå Kilos snapshots fra for dette projekt. Du mister fortryd/gentag for Kilo-ændringer, men git sporer stadig alt.", "ui.messagePart.openInDiffViewer": "Åbn i diff-viser", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Dette var nyttigt", + "ui.message.feedback.notHelpful": "Dette var ikke nyttigt", + "ui.message.feedback.clearRating": "Ryd bedømmelse", } diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index 51f7adefc87..437f5fc649f 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -92,4 +92,9 @@ export const dict = { "Kilo-Snapshots für dieses Projekt ausschalten. Rückgängig/Wiederherstellen für Kilo-Änderungen ist nicht mehr möglich, aber git verfolgt weiterhin alles.", "ui.messagePart.openInDiffViewer": "Im Diff-Viewer öffnen", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Das war hilfreich", + "ui.message.feedback.notHelpful": "Das war nicht hilfreich", + "ui.message.feedback.clearRating": "Bewertung löschen", } diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index e9007fb2ca1..bd80d92857d 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -99,4 +99,9 @@ export const dict = { // Edit-tool header: hover-revealed action opening the diff in a full tab. "ui.messagePart.openInDiffViewer": "Open in Diff Viewer", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "This was helpful", + "ui.message.feedback.notHelpful": "This wasn't helpful", + "ui.message.feedback.clearRating": "Clear rating", } diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index 91d40b832a8..8b05522cb9f 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -92,4 +92,9 @@ export const dict = { "Apaga las instantáneas de Kilo para este proyecto. Perderás deshacer/rehacer de los cambios de Kilo, pero git seguirá rastreando todo.", "ui.messagePart.openInDiffViewer": "Abrir en el visor de diferencias", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Esto fue útil", + "ui.message.feedback.notHelpful": "Esto no fue útil", + "ui.message.feedback.clearRating": "Borrar valoración", } diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index f44f61b1420..b64ace8f1f9 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -92,4 +92,9 @@ export const dict = { "Désactivez les instantanés Kilo pour ce projet. Vous perdez l'annulation/restauration des modifications faites par Kilo, mais git continue de tout suivre.", "ui.messagePart.openInDiffViewer": "Ouvrir dans la visionneuse de différences", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "C'était utile", + "ui.message.feedback.notHelpful": "Ce n'était pas utile", + "ui.message.feedback.clearRating": "Effacer la notation", } diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index 604bbba38de..9d5214c7a48 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -90,4 +90,9 @@ export const dict = { "このプロジェクトでは Kilo のスナップショットを無効にします。Kilo による変更の取り消し/やり直しはできなくなりますが、git は引き続きすべてを追跡します。", "ui.messagePart.openInDiffViewer": "差分ビューアで開く", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "役に立ちました", + "ui.message.feedback.notHelpful": "役に立ちませんでした", + "ui.message.feedback.clearRating": "評価をクリア", } diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 678e80af0cb..6308cf9fc68 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -90,4 +90,9 @@ export const dict = { "이 프로젝트의 Kilo 스냅샷을 끕니다. Kilo 변경에 대한 실행 취소/다시 실행은 사용할 수 없지만 git은 여전히 모든 것을 추적합니다.", "ui.messagePart.openInDiffViewer": "Diff 뷰어에서 열기", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "도움이 됐어요", + "ui.message.feedback.notHelpful": "도움이 안 됐어요", + "ui.message.feedback.clearRating": "평가 지우기", } diff --git a/packages/kilo-i18n/src/nl.ts b/packages/kilo-i18n/src/nl.ts index 4529ff8eaaa..4fb55eddca5 100644 --- a/packages/kilo-i18n/src/nl.ts +++ b/packages/kilo-i18n/src/nl.ts @@ -94,4 +94,9 @@ export const dict = { "Zet Kilo-snapshots uit voor dit project. Je verliest ongedaan maken/opnieuw doen van Kilo-wijzigingen, maar git blijft alles volgen.", "ui.messagePart.openInDiffViewer": "Openen in diff-viewer", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Dit was nuttig", + "ui.message.feedback.notHelpful": "Dit was niet nuttig", + "ui.message.feedback.clearRating": "Beoordeling wissen", } diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index c49b1b48730..915801390af 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -92,4 +92,9 @@ export const dict = { "Slå av Kilos snapshots for dette prosjektet. Du mister angre/gjør om for Kilo-endringer, men git fortsetter å spore alt.", "ui.messagePart.openInDiffViewer": "Åpne i diff-viser", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Dette var nyttig", + "ui.message.feedback.notHelpful": "Dette var ikke nyttig", + "ui.message.feedback.clearRating": "Fjern vurdering", } diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index a42adca11b4..67b83e46c29 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -92,4 +92,9 @@ export const dict = { "Wyłącz snapshoty Kilo dla tego projektu. Stracisz cofanie/przywracanie zmian Kilo, ale git nadal śledzi wszystko.", "ui.messagePart.openInDiffViewer": "Otwórz w podglądzie różnic", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "To było pomocne", + "ui.message.feedback.notHelpful": "To nie było pomocne", + "ui.message.feedback.clearRating": "Wyczyść ocenę", } diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index 73e695f75b2..d9cff803db5 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -92,4 +92,9 @@ export const dict = { "Выключите снимки Kilo для этого проекта. Вы потеряете отмену/повтор изменений Kilo, но git по-прежнему отслеживает всё.", "ui.messagePart.openInDiffViewer": "Открыть в просмотрщике различий", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Это было полезно", + "ui.message.feedback.notHelpful": "Это было бесполезно", + "ui.message.feedback.clearRating": "Очистить оценку", } diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index 613b621ef0e..be90eddc10a 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -92,4 +92,9 @@ export const dict = { "ปิดสแน็ปช็อตของ Kilo สำหรับโปรเจกต์นี้ คุณจะสูญเสียการยกเลิก/ทำซ้ำสำหรับการเปลี่ยนแปลงของ Kilo แต่ git ยังคงติดตามทุกอย่าง", "ui.messagePart.openInDiffViewer": "เปิดในตัวดูความแตกต่าง", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "สิ่งนี้มีประโยชน์", + "ui.message.feedback.notHelpful": "สิ่งนี้ไม่มีประโยชน์", + "ui.message.feedback.clearRating": "ล้างการให้คะแนน", } diff --git a/packages/kilo-i18n/src/tr.ts b/packages/kilo-i18n/src/tr.ts index ef8ec7f0b08..0ebf2a73f24 100644 --- a/packages/kilo-i18n/src/tr.ts +++ b/packages/kilo-i18n/src/tr.ts @@ -92,4 +92,9 @@ export const dict = { "Bu proje için Kilo anlık görüntülerini kapat. Kilo değişiklikleri için geri alma/yeniden yapmayı kaybedersiniz, ancak git her şeyi izlemeye devam eder.", "ui.messagePart.openInDiffViewer": "Fark görüntüleyicide aç", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Bu yardımcı oldu", + "ui.message.feedback.notHelpful": "Bu yardımcı olmadı", + "ui.message.feedback.clearRating": "Değerlendirmeyi temizle", } diff --git a/packages/kilo-i18n/src/uk.ts b/packages/kilo-i18n/src/uk.ts index e12d8bb9c5c..3e325a33f7d 100644 --- a/packages/kilo-i18n/src/uk.ts +++ b/packages/kilo-i18n/src/uk.ts @@ -92,4 +92,9 @@ export const dict = { "Вимкніть знімки Kilo для цього проєкту. Ви втратите скасування/повторення для змін Kilo, але git продовжить відстежувати все.", "ui.messagePart.openInDiffViewer": "Відкрити у переглядачі відмінностей", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "Це було корисно", + "ui.message.feedback.notHelpful": "Це не було корисно", + "ui.message.feedback.clearRating": "Очистити оцінку", } diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index a891782e07e..29a57c5290d 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -87,4 +87,9 @@ export const dict = { "关闭本项目的 Kilo 快照。你将失去对 Kilo 更改的撤销/重做,但 git 仍会追踪所有内容。", "ui.messagePart.openInDiffViewer": "在差异查看器中打开", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "这有帮助", + "ui.message.feedback.notHelpful": "这没有帮助", + "ui.message.feedback.clearRating": "清除评分", } diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index 519d0be6ccb..08582812567 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -87,4 +87,9 @@ export const dict = { "關閉本專案的 Kilo 快照。你將失去對 Kilo 變更的撤銷/重做,但 git 仍會追蹤所有內容。", "ui.messagePart.openInDiffViewer": "在差異檢視器中開啟", + + // Message feedback (thumbs up/down per assistant response) + "ui.message.feedback.helpful": "這有幫助", + "ui.message.feedback.notHelpful": "這沒有幫助", + "ui.message.feedback.clearRating": "清除評分", } diff --git a/packages/kilo-ui/src/components/icon-button.tsx b/packages/kilo-ui/src/components/icon-button.tsx index 52c1b666f8d..457283aa039 100644 --- a/packages/kilo-ui/src/components/icon-button.tsx +++ b/packages/kilo-ui/src/components/icon-button.tsx @@ -1 +1,29 @@ -export * from "@opencode-ai/ui/icon-button" +import { Button as Kobalte } from "@kobalte/core/button" +import { type ComponentProps, splitProps } from "solid-js" +import { Icon, IconProps } from "./icon" + +export interface IconButtonProps extends ComponentProps { + icon: IconProps["name"] + size?: "small" | "normal" | "large" + iconSize?: IconProps["size"] + variant?: "primary" | "secondary" | "ghost" +} + +export function IconButton(props: ComponentProps<"button"> & IconButtonProps) { + const [split, rest] = splitProps(props, ["variant", "size", "iconSize", "class", "classList"]) + return ( + + + + ) +} diff --git a/packages/kilo-ui/src/components/icon.tsx b/packages/kilo-ui/src/components/icon.tsx index 130949d4cf3..e6b89f3e441 100644 --- a/packages/kilo-ui/src/components/icon.tsx +++ b/packages/kilo-ui/src/components/icon.tsx @@ -1,9 +1,31 @@ import { Icon as Upstream, type IconProps as Props } from "@opencode-ai/ui/icon" import { splitProps } from "solid-js" -const icons = { - "circuit-board": ``, - organization: ``, +const icons: Record = { + "circuit-board": { + viewBox: "0 0 16 16", + path: ``, + }, + organization: { + viewBox: "0 0 16 16", + path: ``, + }, + "thumbs-up": { + viewBox: "0 0 20 20", + path: ``, + }, + "thumbs-down": { + viewBox: "0 0 20 20", + path: ``, + }, + "thumbs-up-filled": { + viewBox: "0 0 20 20", + path: ``, + }, + "thumbs-down-filled": { + viewBox: "0 0 20 20", + path: ``, + }, } type Name = keyof typeof icons @@ -26,6 +48,7 @@ export function Icon(props: IconProps) { /> ) } + const def = icons[name] return (
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 0093ac0ffe1..51d28b27eb0 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -86,7 +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 { submitFeedback } from "@/kilocode/cli/cmd/tui/feedback" // kilocode_change import { getScrollAcceleration } from "../../util/scroll" import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime" import { DialogGoUpsell } from "../../component/dialog-go-upsell" @@ -475,44 +475,6 @@ 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(() => [ { @@ -994,14 +956,14 @@ export function Session() { value: "messages.feedback.up", keybind: "messages_feedback_up", category: "Session", - onSelect: (dialog) => submitFeedback("up", dialog), + onSelect: (dialog) => submitFeedback("up", dialog, { toast, session, messages }), }, { title: "Rate last assistant message not helpful", value: "messages.feedback.down", keybind: "messages_feedback_down", category: "Session", - onSelect: (dialog) => submitFeedback("down", dialog), + onSelect: (dialog) => submitFeedback("down", dialog, { toast, session, messages }), }, // kilocode_change end { diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/feedback.ts b/packages/opencode/src/kilocode/cli/cmd/tui/feedback.ts new file mode 100644 index 00000000000..813bfdd1e61 --- /dev/null +++ b/packages/opencode/src/kilocode/cli/cmd/tui/feedback.ts @@ -0,0 +1,58 @@ +// kilocode_change - new file +/** + * Per-message thumbs up/down feedback for the TUI. + * + * Wired via the `messages_feedback_up` / `messages_feedback_down` keybinds + * in the Session route. Kept out of `routes/session/index.tsx` so the + * upstream-shared session route stays free of Kilo telemetry plumbing. + */ +import { Telemetry } from "@kilocode/kilo-telemetry" +import type { AssistantMessage, Message } from "@kilocode/sdk/v2" +import type { DialogContext } from "@tui/ui/dialog" +import type { ToastContext } from "@tui/ui/toast" + +interface SessionRevert { + revert?: { messageID: string } +} + +interface Context { + toast: ToastContext + session: () => SessionRevert | undefined + messages: () => Message[] +} + +export function submitFeedback(rating: "up" | "down", dialog: DialogContext, ctx: Context): void { + if (!Telemetry.isEnabled()) { + ctx.toast.show({ message: "Feedback disabled: telemetry is off", variant: "info" }) + dialog.clear() + return + } + const revertID = ctx.session()?.revert?.messageID + const lastAssistant = ctx.messages().findLast( + (msg): msg is AssistantMessage => msg.role === "assistant" && (!revertID || msg.id < revertID), + ) + if (!lastAssistant) { + ctx.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 === "kilo") { + payload.sessionID = lastAssistant.sessionID + payload.messageID = lastAssistant.id + payload.parentMessageID = lastAssistant.parentID + } + Telemetry.trackFeedback(payload) + ctx.toast.show({ + message: rating === "up" ? "Thanks for the feedback!" : "Thanks — we'll use this to improve.", + variant: "success", + }) + dialog.clear() +} diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index ebc54bb16e3..f7db04e664f 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -111,12 +111,6 @@ 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 937239d5a61..cc17a64165c 100644 --- a/packages/ui/src/i18n/ar.ts +++ b/packages/ui/src/i18n/ar.ts @@ -139,11 +139,6 @@ 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 1f9349adeac..fcf3e32487b 100644 --- a/packages/ui/src/i18n/br.ts +++ b/packages/ui/src/i18n/br.ts @@ -139,11 +139,6 @@ 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 812bd7fb01c..eddbbda3526 100644 --- a/packages/ui/src/i18n/bs.ts +++ b/packages/ui/src/i18n/bs.ts @@ -143,11 +143,6 @@ 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 75bacc372a7..e110bbd4d44 100644 --- a/packages/ui/src/i18n/da.ts +++ b/packages/ui/src/i18n/da.ts @@ -138,11 +138,6 @@ 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 999ef060c6e..822dc077924 100644 --- a/packages/ui/src/i18n/de.ts +++ b/packages/ui/src/i18n/de.ts @@ -144,11 +144,6 @@ 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 3412d770281..6bab63ec07b 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -151,11 +151,6 @@ 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 fdfdfecaa70..da485a9b6a7 100644 --- a/packages/ui/src/i18n/es.ts +++ b/packages/ui/src/i18n/es.ts @@ -139,11 +139,6 @@ 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 aeb58b035e9..ef860f2dbec 100644 --- a/packages/ui/src/i18n/fr.ts +++ b/packages/ui/src/i18n/fr.ts @@ -139,11 +139,6 @@ 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 961c6b6de49..281d8d9bb6e 100644 --- a/packages/ui/src/i18n/ja.ts +++ b/packages/ui/src/i18n/ja.ts @@ -138,11 +138,6 @@ 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 6bd09f93d45..d9b8e0fb968 100644 --- a/packages/ui/src/i18n/ko.ts +++ b/packages/ui/src/i18n/ko.ts @@ -139,11 +139,6 @@ 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 02712e4bce7..c3cab8540d2 100644 --- a/packages/ui/src/i18n/nl.ts +++ b/packages/ui/src/i18n/nl.ts @@ -151,11 +151,6 @@ 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 975fd891bff..4c1ff647906 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -142,11 +142,6 @@ 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 0435c40a9b2..b262e9774de 100644 --- a/packages/ui/src/i18n/pl.ts +++ b/packages/ui/src/i18n/pl.ts @@ -138,11 +138,6 @@ 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 dde141f71a4..9626c2f8043 100644 --- a/packages/ui/src/i18n/ru.ts +++ b/packages/ui/src/i18n/ru.ts @@ -138,11 +138,6 @@ 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 a1f679683fc..86d49b9fd6c 100644 --- a/packages/ui/src/i18n/th.ts +++ b/packages/ui/src/i18n/th.ts @@ -140,11 +140,6 @@ 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 997667435a0..b4a91ef84b9 100644 --- a/packages/ui/src/i18n/tr.ts +++ b/packages/ui/src/i18n/tr.ts @@ -146,11 +146,6 @@ 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 86e6949612c..3ebf9ce3396 100644 --- a/packages/ui/src/i18n/uk.ts +++ b/packages/ui/src/i18n/uk.ts @@ -156,11 +156,6 @@ 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 71e5cf7e375..6630ae0a057 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -143,11 +143,6 @@ 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 d578ac57767..0ad253e2c2a 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -143,11 +143,6 @@ 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 0d64636b897ea3a8b09b21e6e2b12f3662b384c4 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 6 May 2026 12:07:57 -0400 Subject: [PATCH 07/10] fix(kilo-ui): make Icon reactive to name changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kilo-ui Icon was capturing the registry lookup into a non-reactive const at component setup, so callers that flip the icon prop (e.g. toggling between thumbs-up and thumbs-up-filled when a feedback rating changes) saw the SVG content stay frozen on the original icon. Fix by reading local.name directly inside the JSX expressions so Solid re-evaluates viewBox and innerHTML when the prop updates. Matches the upstream Icon pattern. Also rename the local 'def' temp to 'icon' as part of the inline refactor — clearer alongside the 'icons' registry. --- packages/kilo-ui/src/components/icon.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/kilo-ui/src/components/icon.tsx b/packages/kilo-ui/src/components/icon.tsx index e6b89f3e441..3eed7092fdd 100644 --- a/packages/kilo-ui/src/components/icon.tsx +++ b/packages/kilo-ui/src/components/icon.tsx @@ -36,8 +36,7 @@ export interface IconProps extends Omit { export function Icon(props: IconProps) { const [local, others] = splitProps(props, ["name", "size", "class", "classList"]) - const name = local.name as Name - if (!(name in icons)) { + if (!((local.name as Name) in icons)) { return ( ) } - const def = icons[name] return (
From bef018373859d241ab8a1ed1983191345a21ffca Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 6 May 2026 12:42:27 -0400 Subject: [PATCH 08/10] fix(vscode): close runtime telemetry consent gap with the CLI The previous `watchTelemetryState` fix updated the webview UI in real time but left the CLI subprocess's PostHog client stuck on its spawn-time `KILO_TELEMETRY_LEVEL` value. A user who started VS Code with telemetry off and toggled it on at runtime saw the thumbs UI appear (good) but every webview event was silently dropped at the CLI's `Client.capture()` gate (bad). Add a runtime sync channel: - New `POST /telemetry/setEnabled` Hono route on the CLI server that calls `Telemetry.setEnabled(enabled)` to flip the `posthog-node` client's opt state. - New `TelemetryProxy.setEnabled(enabled)` method that POSTs to it, using the same fire-and-forget pattern as `capture`. - Extension calls `telemetry.setEnabled(vscode.env.isTelemetryEnabled)` immediately after `telemetry.configure(...)` on every `connected` state change, so a freshly-spawned CLI gets corrected even when its spawn-time env var is stale. - Extension subscribes to `vscode.env.onDidChangeTelemetryEnabled` to forward runtime consent changes to the CLI as they happen. All three changes live in Kilo-owned files (the route is already a `kilocode_change - new file`, and the extension is Kilo-only). Zero upstream OpenCode merge surface. Closes #9872 fully (the previous `d67c5e307c` covered only the webview UI). --- packages/kilo-vscode/src/extension.ts | 13 +++ .../src/services/telemetry/telemetry-proxy.ts | 20 +++++ .../src/server/routes/instance/telemetry.ts | 89 ++++++++++++------- packages/sdk/js/src/v2/gen/sdk.gen.ts | 41 +++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 30 +++++++ 5 files changed, 163 insertions(+), 30 deletions(-) diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index b00c64ecf91..2ba3759cb5d 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -57,6 +57,11 @@ export function activate(context: vscode.ExtensionContext) { const config = connectionService.getServerConfig() if (config) { telemetry.configure(config.baseUrl, config.password) + // Sync the CLI's PostHog client with the current consent state. The + // CLI reads KILO_TELEMETRY_LEVEL once at spawn, so without this call + // a fresh CLI started while VS Code telemetry was off would stay + // opted out for the rest of the session. + telemetry.setEnabled(vscode.env.isTelemetryEnabled) } try { remoteService.setClient(connectionService.getClient()) @@ -72,6 +77,14 @@ export function activate(context: vscode.ExtensionContext) { } }) + // Propagate runtime telemetry consent changes to the CLI subprocess so its + // PostHog client stays in sync with the user's VS Code telemetry setting. + context.subscriptions.push( + vscode.env.onDidChangeTelemetryEnabled((enabled) => { + telemetry.setEnabled(enabled) + }), + ) + // Prewarm the CLI backend early so autocomplete is ready before first editor use. ensureBackendForAutocomplete(connectionService) diff --git a/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts b/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts index 410568378c5..2c85e931f3c 100644 --- a/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts +++ b/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts @@ -61,6 +61,26 @@ export class TelemetryProxy { }).catch((err) => console.error("[Kilo New] Telemetry capture failed:", err)) } + /** + * Propagate runtime telemetry consent changes to the CLI. The CLI subprocess + * reads `KILO_TELEMETRY_LEVEL` once at spawn — without this call, toggling + * VS Code telemetry consent leaves the CLI's PostHog client stuck on its + * spawn-time state until the process restarts. + */ + setEnabled(enabled: boolean) { + if (!this.url || !this.password) return + + const auth = buildTelemetryAuthHeader(this.password) + fetch(`${this.url}/telemetry/setEnabled`, { + method: "POST", + headers: { + Authorization: auth, + "Content-Type": "application/json", + }, + body: JSON.stringify({ enabled }), + }).catch((err) => console.error("[Kilo New] Telemetry setEnabled failed:", err)) + } + /** * No-op — the CLI server handles PostHog shutdown. */ diff --git a/packages/opencode/src/server/routes/instance/telemetry.ts b/packages/opencode/src/server/routes/instance/telemetry.ts index d6fd1577e00..245a1c4124e 100644 --- a/packages/opencode/src/server/routes/instance/telemetry.ts +++ b/packages/opencode/src/server/routes/instance/telemetry.ts @@ -7,39 +7,68 @@ import { lazy } from "@/util/lazy" import { errors } from "../../error" export const TelemetryRoutes = lazy(() => - new Hono().post( - "/capture", - describeRoute({ - summary: "Capture telemetry event", - description: "Forward a telemetry event to PostHog via kilo-telemetry.", - operationId: "telemetry.capture", - responses: { - 200: { - description: "Event captured", - content: { - "application/json": { - schema: resolver(z.boolean()), + new Hono() + .post( + "/capture", + describeRoute({ + summary: "Capture telemetry event", + description: "Forward a telemetry event to PostHog via kilo-telemetry.", + operationId: "telemetry.capture", + responses: { + 200: { + description: "Event captured", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, }, }, + ...errors(400), }, - ...errors(400), - }, - }), - validator( - "json", - z.object({ - event: z.string().meta({ description: "Event name" }), - properties: z.record(z.string(), z.any()).optional().meta({ description: "Event properties" }), }), + validator( + "json", + z.object({ + event: z.string().meta({ description: "Event name" }), + properties: z.record(z.string(), z.any()).optional().meta({ description: "Event properties" }), + }), + ), + async (c) => { + const body = c.req.valid("json") + try { + Telemetry.track(body.event as any, body.properties) + } catch { + // fire-and-forget: swallow errors + } + return c.json(true) + }, + ) + .post( + "/setEnabled", + describeRoute({ + summary: "Set PostHog telemetry enabled state", + description: + "Update the PostHog client's opt-in/out state at runtime. " + + "The CLI reads KILO_TELEMETRY_LEVEL once at spawn — this route lets clients " + + "(e.g. the VS Code extension) propagate runtime telemetry consent changes.", + operationId: "telemetry.setEnabled", + responses: { + 200: { + description: "State updated", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", z.object({ enabled: z.boolean() })), + async (c) => { + const body = c.req.valid("json") + Telemetry.setEnabled(body.enabled) + return c.json(true) + }, ), - async (c) => { - const body = c.req.valid("json") - try { - Telemetry.track(body.event as any, body.properties) - } catch { - // fire-and-forget: swallow errors - } - return c.json(true) - }, - ), ) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 9f35908ad3c..e4473d9e320 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -221,6 +221,8 @@ import type { SyncStartResponses, TelemetryCaptureErrors, TelemetryCaptureResponses, + TelemetrySetEnabledErrors, + TelemetrySetEnabledResponses, TextPartInput, ToolIdsErrors, ToolIdsResponses, @@ -4936,6 +4938,45 @@ export class Telemetry extends HeyApiClient { }, }) } + + /** + * Set PostHog telemetry enabled state + * + * Update the PostHog client's opt-in/out state at runtime. The CLI reads KILO_TELEMETRY_LEVEL once at spawn — this route lets clients (e.g. the VS Code extension) propagate runtime telemetry consent changes. + */ + public setEnabled( + parameters?: { + directory?: string + workspace?: string + enabled?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "enabled" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/telemetry/setEnabled", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }, + ) + } } export class Remote extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 1d260546e2e..6e0ded613d7 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -6465,6 +6465,36 @@ export type TelemetryCaptureResponses = { export type TelemetryCaptureResponse = TelemetryCaptureResponses[keyof TelemetryCaptureResponses] +export type TelemetrySetEnabledData = { + body?: { + enabled: boolean + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/telemetry/setEnabled" +} + +export type TelemetrySetEnabledErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TelemetrySetEnabledError = TelemetrySetEnabledErrors[keyof TelemetrySetEnabledErrors] + +export type TelemetrySetEnabledResponses = { + /** + * State updated + */ + 200: boolean +} + +export type TelemetrySetEnabledResponse = TelemetrySetEnabledResponses[keyof TelemetrySetEnabledResponses] + export type RemoteEnableData = { body?: never path?: never From e0d6a940fcdc006f90934a9e7434eee293c16fb6 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Thu, 7 May 2026 00:46:07 -0400 Subject: [PATCH 09/10] docs(kilo-docs): add user-facing message feedback page Walks through what the thumbs up/down feature does in both the VS Code extension and the CLI/TUI, why ratings matter, exactly what gets sent when a user rates a response, and how to disable it via VS Code's telemetry setting or KILO_TELEMETRY_LEVEL. Privacy claims are scoped to the rating event itself; broader telemetry questions defer to PRIVACY.md. Linked from the Productivity Tools group in the Code With AI nav, between Voice Transcription and the Agent Behavior subsection. --- packages/kilo-docs/lib/nav/code-with-ai.ts | 1 + .../code-with-ai/features/message-feedback.md | 76 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 packages/kilo-docs/pages/code-with-ai/features/message-feedback.md diff --git a/packages/kilo-docs/lib/nav/code-with-ai.ts b/packages/kilo-docs/lib/nav/code-with-ai.ts index 3668c18ef72..7a0abc28dd5 100644 --- a/packages/kilo-docs/lib/nav/code-with-ai.ts +++ b/packages/kilo-docs/lib/nav/code-with-ai.ts @@ -80,6 +80,7 @@ export const CodeWithAiNav: NavSection[] = [ children: "Git Commit Generation", }, { href: "/code-with-ai/features/speech-to-text", children: "Voice Transcription" }, + { href: "/code-with-ai/features/message-feedback", children: "Message Feedback" }, { href: "/code-with-ai/features/browser-use", children: "Agent Behavior", diff --git a/packages/kilo-docs/pages/code-with-ai/features/message-feedback.md b/packages/kilo-docs/pages/code-with-ai/features/message-feedback.md new file mode 100644 index 00000000000..09094a505fe --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/features/message-feedback.md @@ -0,0 +1,76 @@ +--- +title: "Message Feedback" +description: "Rate assistant responses with thumbs up/down to help Kilo improve" +--- + +# Message Feedback + +You can give a thumbs up or thumbs down to any response Kilo gives you. It's a quick way to tell us when something worked well, or when it didn't. + +{% callout type="info" %} +Feedback is only available when telemetry is on. If you turn telemetry off, the buttons no longer display. See [Turning it off](#turning-it-off) below. +{% /callout %} + +## How to use it + +{% tabs %} +{% tab label="VSCode" %} + +When Kilo finishes a response, you'll see thumbs-up and thumbs-down buttons next to the **Copy response** button. Click one to send your rating. Click the same button again to take it back, or click the other one to change your mind. + +If you reload the window or switch to a different session, the buttons will look unrated again — but don't worry, your earlier ratings were already sent. + +{% /tab %} + +{% tab label="CLI" %} + +In the terminal, two keybinds rate the most recent assistant message: + +| Action | Keybind | +|---|---| +| Helpful | `=` | +| Not helpful | `-` | + +`` defaults to `Ctrl+X`, so you press `Ctrl+X` and then `=` for thumbs-up, or `Ctrl+X` and then `-` for thumbs-down. + +{% /tab %} +{% /tabs %} + +## Why it matters + +Your feedback is the most direct way to tell us what's working and what isn't. We use it to tune the prompts behind Kilo's behavior, pick better default models, and find and fix problems faster than we could on our own. + +## What we send + +When you rate a response, here's what gets sent: + +- That you rated a response thumbs up or thumbs down. +- The provider and model ID that produced the response (for example, `anthropic` and `claude-sonnet-4-5`). +- For responses that came through Kilo Gateway, the request ID so we can match the rating back to the session. + +No further information is included in the rating feedback. For more information on telemetry, see [PRIVACY.md](https://github.com/Kilo-Org/kilocode/blob/main/PRIVACY.md). + +## Turning it off + +Feedback is part of telemetry, so turning telemetry off will cause the feedback buttons to no longer appear. + +{% tabs %} +{% tab label="VSCode" %} + +Open Settings (`Cmd+,` on Mac, `Ctrl+,` on Windows/Linux), search for **`telemetry.telemetryLevel`**, and set it to **Off**. The feedback buttons will no longer appear. Set it back to **All** to bring them back. (For more on what this setting controls, see [VS Code's telemetry docs](https://code.visualstudio.com/docs/getstarted/telemetry).) + +{% /tab %} + +{% tab label="CLI" %} + +Set `KILO_TELEMETRY_LEVEL=off` in your environment before starting Kilo: + +```bash +KILO_TELEMETRY_LEVEL=off kilo +``` + +With telemetry off, pressing the feedback keybinds shows a message saying feedback is disabled, and nothing is sent. + +{% /tab %} +{% /tabs %} + From 4a6eb4397026183ca2ab03e61ad83efc4298c497 Mon Sep 17 00:00:00 2001 From: Joshua Lambert <25085430+lambertjosh@users.noreply.github.com> Date: Thu, 7 May 2026 01:37:36 -0400 Subject: [PATCH 10/10] Apply suggestion from @lambertjosh --- .../kilo-docs/pages/code-with-ai/features/message-feedback.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/code-with-ai/features/message-feedback.md b/packages/kilo-docs/pages/code-with-ai/features/message-feedback.md index 09094a505fe..d03c6d1faef 100644 --- a/packages/kilo-docs/pages/code-with-ai/features/message-feedback.md +++ b/packages/kilo-docs/pages/code-with-ai/features/message-feedback.md @@ -18,7 +18,7 @@ Feedback is only available when telemetry is on. If you turn telemetry off, the When Kilo finishes a response, you'll see thumbs-up and thumbs-down buttons next to the **Copy response** button. Click one to send your rating. Click the same button again to take it back, or click the other one to change your mind. -If you reload the window or switch to a different session, the buttons will look unrated again — but don't worry, your earlier ratings were already sent. +If you reload the window or switch to a different session, the feedback status will be reset. {% /tab %}