mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #9718 from Kilo-Org/feature/message-feedback
feat: per-message thumbs up/down feedback
This commit is contained in:
@@ -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 `<leader>=` / `<leader>-` in the terminal UI. Only shown when telemetry is enabled; feedback is sent to Kilo to help improve model and prompt quality.
|
||||
@@ -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",
|
||||
|
||||
@@ -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 feedback status will be reset.
|
||||
|
||||
{% /tab %}
|
||||
|
||||
{% tab label="CLI" %}
|
||||
|
||||
In the terminal, two keybinds rate the most recent assistant message:
|
||||
|
||||
| Action | Keybind |
|
||||
|---|---|
|
||||
| Helpful | `<leader>=` |
|
||||
| Not helpful | `<leader>-` |
|
||||
|
||||
`<leader>` 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 %}
|
||||
|
||||
@@ -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 (`<leader>+` / `<leader>-`) on the last assistant message |
|
||||
| TUI | Keybinds (`<leader>=` / `<leader>-`) on the last assistant message |
|
||||
|
||||
### Telemetry Payload
|
||||
|
||||
|
||||
Generated
+5
@@ -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": "مسح التقييم",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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": "評価をクリア",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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": "평가 지우기",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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ę",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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": "Очистить оценку",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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": "ล้างการให้คะแนน",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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": "Очистити оцінку",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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": "清除评分",
|
||||
}
|
||||
|
||||
Generated
+5
@@ -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": "清除評分",
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ export enum TelemetryEvent {
|
||||
// Config Events
|
||||
TELEMETRY_DISABLED = "Telemetry Disabled",
|
||||
|
||||
// Feedback
|
||||
FEEDBACK_SUBMITTED = "Feedback Submitted",
|
||||
|
||||
// Errors
|
||||
ERROR = "Error",
|
||||
}
|
||||
|
||||
@@ -244,6 +244,22 @@ export namespace Telemetry {
|
||||
track(TelemetryEvent.ERROR, { error, context })
|
||||
}
|
||||
|
||||
// Feedback
|
||||
export interface FeedbackProperties extends Record<string, unknown> {
|
||||
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<void> {
|
||||
await Client.shutdown()
|
||||
}
|
||||
|
||||
@@ -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<typeof Kobalte> {
|
||||
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 (
|
||||
<Kobalte
|
||||
{...rest}
|
||||
data-component="icon-button"
|
||||
data-icon={props.icon}
|
||||
data-size={split.size || "normal"}
|
||||
data-variant={split.variant || "secondary"}
|
||||
classList={{
|
||||
...split.classList,
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
>
|
||||
<Icon name={props.icon} size={split.iconSize ?? (split.size === "large" ? "normal" : "small")} />
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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": `<path d="M12.5 1H3.5C2.121 1 1 2.121 1 3.5V12.5C1 13.879 2.121 15 3.5 15H12.5C13.879 15 15 13.879 15 12.5V3.5C15 2.121 13.879 1 12.5 1ZM6 6.5C6 6.775 5.775 7 5.5 7C5.225 7 5 6.775 5 6.5C5 6.225 5.225 6 5.5 6C5.775 6 6 6.225 6 6.5ZM12.5 14H6V11.5C6 11.225 6.225 11 6.5 11H9.092C9.299 11.581 9.849 12 10.5 12C11.327 12 12 11.327 12 10.5C12 9.673 11.327 9 10.5 9C9.849 9 9.299 9.419 9.092 10H6.5C5.673 10 5 10.673 5 11.5V14H3.5C2.673 14 2 13.327 2 12.5V3.5C2 2.673 2.673 2 3.5 2H5V5.092C4.419 5.299 4 5.849 4 6.5C4 7.327 4.673 8 5.5 8C6.327 8 7 7.327 7 6.5C7 5.849 6.581 5.299 6 5.092V2H12.5C13.327 2 14 2.673 14 3.5V6H10.908C10.701 5.419 10.151 5 9.5 5C8.673 5 8 5.673 8 6.5C8 7.327 8.673 8 9.5 8C10.151 8 10.701 7.581 10.908 7H14V12.5C14 13.327 13.327 14 12.5 14ZM10 10.5C10 10.225 10.225 10 10.5 10C10.775 10 11 10.225 11 10.5C11 10.775 10.775 11 10.5 11C10.225 11 10 10.775 10 10.5ZM10 6.5C10 6.775 9.775 7 9.5 7C9.225 7 9 6.775 9 6.5C9 6.225 9.225 6 9.5 6C9.775 6 10 6.225 10 6.5Z" fill="currentColor"/>`,
|
||||
organization: `<path d="M6.00195 4.00002C6.00195 2.89655 6.89649 2.00201 7.99995 2.00201C9.10342 2.00201 9.99796 2.89655 9.99796 4.00002C9.99796 5.10348 9.10342 5.99802 7.99995 5.99802C6.89649 5.99802 6.00195 5.10348 6.00195 4.00002ZM7.99995 3.00201C7.44877 3.00201 7.00195 3.44883 7.00195 4.00002C7.00195 4.5512 7.44877 4.99802 7.99995 4.99802C8.55114 4.99802 8.99796 4.5512 8.99796 4.00002C8.99796 3.44883 8.55114 3.00201 7.99995 3.00201ZM11 4.5C11 3.67157 11.6716 3 12.5 3C13.3284 3 14 3.67157 14 4.5C14 5.32843 13.3284 6 12.5 6C11.6716 6 11 5.32843 11 4.5ZM12.5 4C12.2239 4 12 4.22386 12 4.5C12 4.77614 12.2239 5 12.5 5C12.7761 5 13 4.77614 13 4.5C13 4.22386 12.7761 4 12.5 4ZM3.5 3C2.67157 3 2 3.67157 2 4.5C2 5.32843 2.67157 6 3.5 6C4.32843 6 5 5.32843 5 4.5C5 3.67157 4.32843 3 3.5 3ZM3 4.5C3 4.22386 3.22386 4 3.5 4C3.77614 4 4 4.22386 4 4.5C4 4.77614 3.77614 5 3.5 5C3.22386 5 3 4.77614 3 4.5ZM4.26756 6.99969C4.09739 7.29387 4 7.63541 4 7.99969L2 7.99969V10.5C2 11.3285 2.67157 12 3.5 12C3.71194 12 3.91361 11.9561 4.09639 11.8768C4.1705 12.2082 4.28572 12.524 4.43643 12.8187C4.14721 12.9356 3.83112 13 3.5 13C2.11929 13 1 11.8807 1 10.5V7.99969C1 7.44741 1.44772 6.99969 2 6.99969H4.26756ZM11.5636 12.8187C11.8528 12.9356 12.1689 13 12.5 13C13.8807 13 15 11.8807 15 10.5V7.99969C15 7.44741 14.5523 6.99969 14 6.99969H11.7324C11.9026 7.29387 12 7.63541 12 7.9997L14 7.99969V10.5C14 11.3285 13.3284 12 12.5 12C12.2881 12 12.0864 11.9561 11.9036 11.8768C11.8295 12.2082 11.7143 12.524 11.5636 12.8187ZM6 6.99969C5.44772 6.99969 5 7.44741 5 7.99969V11C5 12.6569 6.34315 14 8 14C9.65685 14 11 12.6569 11 11V7.99969C11 7.44741 10.5523 6.99969 10 6.99969H6ZM6 7.99969L10 7.99969V11C10 12.1046 9.10457 13 8 13C6.89543 13 6 12.1046 6 11V7.99969Z" fill="currentColor"/>`,
|
||||
const icons: Record<string, { path: string; viewBox: string }> = {
|
||||
"circuit-board": {
|
||||
viewBox: "0 0 16 16",
|
||||
path: `<path d="M12.5 1H3.5C2.121 1 1 2.121 1 3.5V12.5C1 13.879 2.121 15 3.5 15H12.5C13.879 15 15 13.879 15 12.5V3.5C15 2.121 13.879 1 12.5 1ZM6 6.5C6 6.775 5.775 7 5.5 7C5.225 7 5 6.775 5 6.5C5 6.225 5.225 6 5.5 6C5.775 6 6 6.225 6 6.5ZM12.5 14H6V11.5C6 11.225 6.225 11 6.5 11H9.092C9.299 11.581 9.849 12 10.5 12C11.327 12 12 11.327 12 10.5C12 9.673 11.327 9 10.5 9C9.849 9 9.299 9.419 9.092 10H6.5C5.673 10 5 10.673 5 11.5V14H3.5C2.673 14 2 13.327 2 12.5V3.5C2 2.673 2.673 2 3.5 2H5V5.092C4.419 5.299 4 5.849 4 6.5C4 7.327 4.673 8 5.5 8C6.327 8 7 7.327 7 6.5C7 5.849 6.581 5.299 6 5.092V2H12.5C13.327 2 14 2.673 14 3.5V6H10.908C10.701 5.419 10.151 5 9.5 5C8.673 5 8 5.673 8 6.5C8 7.327 8.673 8 9.5 8C10.151 8 10.701 7.581 10.908 7H14V12.5C14 13.327 13.327 14 12.5 14ZM10 10.5C10 10.225 10.225 10 10.5 10C10.775 10 11 10.225 11 10.5C11 10.775 10.775 11 10.5 11C10.225 11 10 10.775 10 10.5ZM10 6.5C10 6.775 9.775 7 9.5 7C9.225 7 9 6.775 9 6.5C9 6.225 9.225 6 9.5 6C9.775 6 10 6.225 10 6.5Z" fill="currentColor"/>`,
|
||||
},
|
||||
organization: {
|
||||
viewBox: "0 0 16 16",
|
||||
path: `<path d="M6.00195 4.00002C6.00195 2.89655 6.89649 2.00201 7.99995 2.00201C9.10342 2.00201 9.99796 2.89655 9.99796 4.00002C9.99796 5.10348 9.10342 5.99802 7.99995 5.99802C6.89649 5.99802 6.00195 5.10348 6.00195 4.00002ZM7.99995 3.00201C7.44877 3.00201 7.00195 3.44883 7.00195 4.00002C7.00195 4.5512 7.44877 4.99802 7.99995 4.99802C8.55114 4.99802 8.99796 4.5512 8.99796 4.00002C8.99796 3.44883 8.55114 3.00201 7.99995 3.00201ZM11 4.5C11 3.67157 11.6716 3 12.5 3C13.3284 3 14 3.67157 14 4.5C14 5.32843 13.3284 6 12.5 6C11.6716 6 11 5.32843 11 4.5ZM12.5 4C12.2239 4 12 4.22386 12 4.5C12 4.77614 12.2239 5 12.5 5C12.7761 5 13 4.77614 13 4.5C13 4.22386 12.7761 4 12.5 4ZM3.5 3C2.67157 3 2 3.67157 2 4.5C2 5.32843 2.67157 6 3.5 6C4.32843 6 5 5.32843 5 4.5C5 3.67157 4.32843 3 3.5 3ZM3 4.5C3 4.22386 3.22386 4 3.5 4C3.77614 4 4 4.22386 4 4.5C4 4.77614 3.77614 5 3.5 5C3.22386 5 3 4.77614 3 4.5ZM4.26756 6.99969C4.09739 7.29387 4 7.63541 4 7.99969L2 7.99969V10.5C2 11.3285 2.67157 12 3.5 12C3.71194 12 3.91361 11.9561 4.09639 11.8768C4.1705 12.2082 4.28572 12.524 4.43643 12.8187C4.14721 12.9356 3.83112 13 3.5 13C2.11929 13 1 11.8807 1 10.5V7.99969C1 7.44741 1.44772 6.99969 2 6.99969H4.26756ZM11.5636 12.8187C11.8528 12.9356 12.1689 13 12.5 13C13.8807 13 15 11.8807 15 10.5V7.99969C15 7.44741 14.5523 6.99969 14 6.99969H11.7324C11.9026 7.29387 12 7.63541 12 7.9997L14 7.99969V10.5C14 11.3285 13.3284 12 12.5 12C12.2881 12 12.0864 11.9561 11.9036 11.8768C11.8295 12.2082 11.7143 12.524 11.5636 12.8187ZM6 6.99969C5.44772 6.99969 5 7.44741 5 7.99969V11C5 12.6569 6.34315 14 8 14C9.65685 14 11 12.6569 11 11V7.99969C11 7.44741 10.5523 6.99969 10 6.99969H6ZM6 7.99969L10 7.99969V11C10 12.1046 9.10457 13 8 13C6.89543 13 6 12.1046 6 11V7.99969Z" fill="currentColor"/>`,
|
||||
},
|
||||
"thumbs-up": {
|
||||
viewBox: "0 0 20 20",
|
||||
path: `<path d="M1 8.25a1.25 1.25 0 1 1 2.5 0v7.5a1.25 1.25 0 1 1-2.5 0v-7.5ZM11 3V1.7c0-.268.14-.526.395-.607A2 2 0 0 1 14 3c0 .995-.182 1.948-.514 2.826-.204.54.166 1.174.744 1.174h2.52c1.243 0 2.261 1.01 2.146 2.247a23.864 23.864 0 0 1-1.341 5.974C17.153 16.323 16.072 17 14.9 17h-3.192a3 3 0 0 1-1.341-.317l-2.734-1.366A3 3 0 0 0 6.292 15H5V8h.963c.685 0 1.258-.483 1.612-1.068a4.011 4.011 0 0 1 2.166-1.73c.432-.143.853-.386 1.011-.814.16-.432.248-.9.248-1.388Z" stroke="currentColor" fill="none" stroke-linejoin="round"/>`,
|
||||
},
|
||||
"thumbs-down": {
|
||||
viewBox: "0 0 20 20",
|
||||
path: `<path d="M18.905 12.75a1.25 1.25 0 0 1-2.5 0v-7.5a1.25 1.25 0 0 1 2.5 0v7.5ZM8.905 17v1.3c0 .268-.14.526-.395.607A2 2 0 0 1 5.905 17c0-.995.182-1.948.514-2.826.204-.54-.166-1.174-.744-1.174h-2.52c-1.243 0-2.261-1.01-2.146-2.247a23.864 23.864 0 0 1 1.341-5.974C2.547 3.677 3.628 3 4.8 3h3.192a3 3 0 0 1 1.341.317l2.734 1.366A3 3 0 0 0 13.408 5H15v7h-.963c-.685 0-1.258.483-1.612 1.068a4.011 4.011 0 0 1-2.166 1.73c-.432.143-.853.386-1.011.814-.16.432-.248.9-.248 1.388Z" stroke="currentColor" fill="none" stroke-linejoin="round"/>`,
|
||||
},
|
||||
"thumbs-up-filled": {
|
||||
viewBox: "0 0 20 20",
|
||||
path: `<path d="M1 8.25a1.25 1.25 0 1 1 2.5 0v7.5a1.25 1.25 0 1 1-2.5 0v-7.5ZM11 3V1.7c0-.268.14-.526.395-.607A2 2 0 0 1 14 3c0 .995-.182 1.948-.514 2.826-.204.54.166 1.174.744 1.174h2.52c1.243 0 2.261 1.01 2.146 2.247a23.864 23.864 0 0 1-1.341 5.974C17.153 16.323 16.072 17 14.9 17h-3.192a3 3 0 0 1-1.341-.317l-2.734-1.366A3 3 0 0 0 6.292 15H5V8h.963c.685 0 1.258-.483 1.612-1.068a4.011 4.011 0 0 1 2.166-1.73c.432-.143.853-.386 1.011-.814.16-.432.248-.9.248-1.388Z" fill="currentColor"/>`,
|
||||
},
|
||||
"thumbs-down-filled": {
|
||||
viewBox: "0 0 20 20",
|
||||
path: `<path d="M18.905 12.75a1.25 1.25 0 0 1-2.5 0v-7.5a1.25 1.25 0 0 1 2.5 0v7.5ZM8.905 17v1.3c0 .268-.14.526-.395.607A2 2 0 0 1 5.905 17c0-.995.182-1.948.514-2.826.204-.54-.166-1.174-.744-1.174h-2.52c-1.243 0-2.261-1.01-2.146-2.247a23.864 23.864 0 0 1 1.341-5.974C2.547 3.677 3.628 3 4.8 3h3.192a3 3 0 0 1 1.341.317l2.734 1.366A3 3 0 0 0 13.408 5H15v7h-.963c-.685 0-1.258.483-1.612 1.068a4.011 4.011 0 0 1-2.166 1.73c-.432.143-.853.386-1.011.814-.16.432-.248.9-.248 1.388Z" fill="currentColor"/>`,
|
||||
},
|
||||
}
|
||||
|
||||
type Name = keyof typeof icons
|
||||
@@ -14,8 +36,7 @@ export interface IconProps extends Omit<Props, "name"> {
|
||||
|
||||
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 (
|
||||
<Upstream
|
||||
{...others}
|
||||
@@ -35,8 +56,8 @@ export function Icon(props: IconProps) {
|
||||
}}
|
||||
data-slot="icon-svg"
|
||||
fill="none"
|
||||
viewBox="0 0 16 16"
|
||||
innerHTML={icons[name]}
|
||||
viewBox={icons[local.name as Name].viewBox}
|
||||
innerHTML={icons[local.name as Name].path}
|
||||
aria-hidden="true"
|
||||
{...others}
|
||||
/>
|
||||
|
||||
@@ -24,12 +24,19 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 2px;
|
||||
margin-top: 2px;
|
||||
|
||||
[data-component="icon-button"] {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MessagePartProps>
|
||||
@@ -971,6 +978,7 @@ export function Part(props: MessagePartProps) {
|
||||
turnDiffSummary={props.turnDiffSummary}
|
||||
animate={props.animate}
|
||||
working={props.working}
|
||||
feedback={props.feedback}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
@@ -1342,6 +1350,52 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyResponse")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Show when={props.feedback?.enabled}>
|
||||
<Tooltip
|
||||
value={
|
||||
props.feedback?.rating === "up"
|
||||
? i18n.t("ui.message.feedback.clearRating")
|
||||
: i18n.t("ui.message.feedback.helpful")
|
||||
}
|
||||
placement="top"
|
||||
gutter={4}
|
||||
>
|
||||
<IconButton
|
||||
icon={props.feedback?.rating === "up" ? "thumbs-up-filled" : "thumbs-up"}
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
onMouseDown={(e) => 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")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
value={
|
||||
props.feedback?.rating === "down"
|
||||
? i18n.t("ui.message.feedback.clearRating")
|
||||
: i18n.t("ui.message.feedback.notHelpful")
|
||||
}
|
||||
placement="top"
|
||||
gutter={4}
|
||||
>
|
||||
<IconButton
|
||||
icon={props.feedback?.rating === "down" ? "thumbs-down-filled" : "thumbs-down"}
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
onMouseDown={(e) => 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")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={summary()}>
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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, watchTelemetryState } from "./services/telemetry" // prettier-ignore
|
||||
import {
|
||||
sessionToWebview,
|
||||
indexProvidersById,
|
||||
@@ -216,6 +216,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
private initConnectionPromise: Promise<void> | 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<typeof createAutoApproveBridge> | null = null
|
||||
@@ -357,10 +358,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) {
|
||||
@@ -581,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),
|
||||
@@ -3404,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { TelemetryEventName, type TelemetryPropertiesProvider } from "./types"
|
||||
export { TelemetryProxy } from "./telemetry-proxy"
|
||||
export { pushTelemetryState, watchTelemetryState } from "./webview-state"
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +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: 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 })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { buildFeedbackProperties } 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("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",
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 = () => {
|
||||
<DisplayProvider>
|
||||
<NotificationsProvider>
|
||||
<SessionProvider>
|
||||
<IndexingProvider>
|
||||
<WorktreeModeProvider>
|
||||
<DataBridge>
|
||||
<AgentManagerContent />
|
||||
</DataBridge>
|
||||
</WorktreeModeProvider>
|
||||
</IndexingProvider>
|
||||
<FeedbackProvider>
|
||||
<IndexingProvider>
|
||||
<WorktreeModeProvider>
|
||||
<DataBridge>
|
||||
<AgentManagerContent />
|
||||
</DataBridge>
|
||||
</WorktreeModeProvider>
|
||||
</IndexingProvider>
|
||||
</FeedbackProvider>
|
||||
</SessionProvider>
|
||||
</NotificationsProvider>
|
||||
</DisplayProvider>
|
||||
|
||||
@@ -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 = () => {
|
||||
<IndexingProvider>
|
||||
<NotificationsProvider>
|
||||
<SessionProvider>
|
||||
<DataBridge>
|
||||
<AppContent />
|
||||
</DataBridge>
|
||||
<FeedbackProvider>
|
||||
<DataBridge>
|
||||
<AppContent />
|
||||
</DataBridge>
|
||||
</FeedbackProvider>
|
||||
</SessionProvider>
|
||||
</NotificationsProvider>
|
||||
</IndexingProvider>
|
||||
|
||||
@@ -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<T extends { tool?: { callID: string; messageID: string
|
||||
interface AssistantMessageProps {
|
||||
message: SDKAssistantMessage
|
||||
showAssistantCopyPartID?: string | null
|
||||
feedback?: MessageFeedbackControls
|
||||
}
|
||||
|
||||
function TodoToolCard(props: { part: ToolPart }) {
|
||||
@@ -168,6 +170,7 @@ export const AssistantMessage: Component<AssistantMessageProps> = (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" ||
|
||||
|
||||
@@ -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<VscodeSessionTurnProps> = (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<VscodeSessionTurnProps> = (props) => {
|
||||
<Show when={assistantMessages().length > 0}>
|
||||
<div class="vscode-session-turn-assistant">
|
||||
<For each={assistantMessages()}>
|
||||
{(msg) => <AssistantMessage message={msg} showAssistantCopyPartID={showAssistantCopyPartID()} />}
|
||||
{(amsg) => (
|
||||
<AssistantMessage
|
||||
message={amsg}
|
||||
showAssistantCopyPartID={showAssistantCopyPartID()}
|
||||
feedback={{
|
||||
enabled: feedback.telemetryEnabled(),
|
||||
rating: feedback.getRating(amsg.id),
|
||||
onRate: (next) =>
|
||||
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,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -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 === "kilo"
|
||||
}
|
||||
|
||||
export function buildFeedbackProperties(input: RateInput, previousRating?: Rating): Record<string, unknown> {
|
||||
const properties: Record<string, unknown> = {
|
||||
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
|
||||
}
|
||||
@@ -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<boolean>
|
||||
getRating: (messageID: string) => Rating | undefined
|
||||
rate: (input: RateInput) => void
|
||||
}
|
||||
|
||||
const FeedbackContext = createContext<FeedbackContextValue>()
|
||||
|
||||
export const FeedbackProvider: ParentComponent = (props) => {
|
||||
const vscode = useVSCode()
|
||||
const [telemetryEnabled, setTelemetryEnabled] = createSignal(false)
|
||||
const [ratings, setRatings] = createSignal<Record<string, Rating>>({})
|
||||
|
||||
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 <FeedbackContext.Provider value={value}>{props.children}</FeedbackContext.Provider>
|
||||
}
|
||||
|
||||
export function useFeedback(): FeedbackContextValue {
|
||||
const context = useContext(FeedbackContext)
|
||||
if (!context) throw new Error("useFeedback must be used within a FeedbackProvider")
|
||||
return context
|
||||
}
|
||||
@@ -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<StoryProvidersProps> = (props) => {
|
||||
return (
|
||||
<VSCodeProvider>
|
||||
<ServerProvider>
|
||||
<ConfigWrapper config={props.config} onConfigChange={props.onConfigChange}>
|
||||
<DisplayProvider>
|
||||
<MockProviderProvider>
|
||||
<DialogProvider>
|
||||
<LanguageContext.Provider
|
||||
value={{
|
||||
locale,
|
||||
setLocale: noop,
|
||||
userOverride: () => "" as any,
|
||||
t,
|
||||
}}
|
||||
>
|
||||
<I18nProvider value={{ locale: () => "en", t }}>
|
||||
<NotificationsContext.Provider value={notifications}>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<IndexingProvider>
|
||||
<DataProvider data={data()} directory="/project/">
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>
|
||||
<FileComponentProvider component={File}>
|
||||
<MarkedProvider>
|
||||
{props.noPadding ? (
|
||||
props.children
|
||||
) : (
|
||||
<div style={{ padding: "12px" }}>{props.children}</div>
|
||||
)}
|
||||
</MarkedProvider>
|
||||
</FileComponentProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</DataProvider>
|
||||
</IndexingProvider>
|
||||
</SessionContext.Provider>
|
||||
</NotificationsContext.Provider>
|
||||
</I18nProvider>
|
||||
</LanguageContext.Provider>
|
||||
</DialogProvider>
|
||||
</MockProviderProvider>
|
||||
</DisplayProvider>
|
||||
</ConfigWrapper>
|
||||
<FeedbackProvider>
|
||||
<ConfigWrapper config={props.config} onConfigChange={props.onConfigChange}>
|
||||
<DisplayProvider>
|
||||
<MockProviderProvider>
|
||||
<DialogProvider>
|
||||
<LanguageContext.Provider
|
||||
value={{
|
||||
locale,
|
||||
setLocale: noop,
|
||||
userOverride: () => "" as any,
|
||||
t,
|
||||
}}
|
||||
>
|
||||
<I18nProvider value={{ locale: () => "en", t }}>
|
||||
<NotificationsContext.Provider value={notifications}>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<IndexingProvider>
|
||||
<DataProvider data={data()} directory="/project/">
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>
|
||||
<FileComponentProvider component={File}>
|
||||
<MarkedProvider>
|
||||
{props.noPadding ? (
|
||||
props.children
|
||||
) : (
|
||||
<div style={{ padding: "12px" }}>{props.children}</div>
|
||||
)}
|
||||
</MarkedProvider>
|
||||
</FileComponentProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</DataProvider>
|
||||
</IndexingProvider>
|
||||
</SessionContext.Provider>
|
||||
</NotificationsContext.Provider>
|
||||
</I18nProvider>
|
||||
</LanguageContext.Provider>
|
||||
</DialogProvider>
|
||||
</MockProviderProvider>
|
||||
</DisplayProvider>
|
||||
</ConfigWrapper>
|
||||
</FeedbackProvider>
|
||||
</ServerProvider>
|
||||
</VSCodeProvider>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 { 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"
|
||||
@@ -949,6 +950,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, { 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, { toast, session, messages }),
|
||||
},
|
||||
// kilocode_change end
|
||||
{
|
||||
title: "Copy session transcript",
|
||||
value: "session.copy",
|
||||
|
||||
@@ -51,6 +51,10 @@ const KeybindsSchema = Schema.Struct({
|
||||
messages_copy: keybind("<leader>y", "Copy message"),
|
||||
messages_undo: keybind("<leader>u", "Undo message"),
|
||||
messages_redo: keybind("<leader>r", "Redo message"),
|
||||
// kilocode_change start - message feedback
|
||||
messages_feedback_up: keybind("<leader>=", "Rate last assistant message helpful"),
|
||||
messages_feedback_down: keybind("<leader>-", "Rate last assistant message not helpful"),
|
||||
// kilocode_change end
|
||||
messages_toggle_conceal: keybind("<leader>h", "Toggle code block concealment in messages"),
|
||||
tool_details: keybind("none", "Toggle tool details visibility"),
|
||||
model_list: keybind("<leader>m", "List available models"),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
@@ -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<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
enabled?: boolean
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
{ in: "body", key: "enabled" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<TelemetrySetEnabledResponses, TelemetrySetEnabledErrors, ThrowOnError>(
|
||||
{
|
||||
url: "/telemetry/setEnabled",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class Remote extends HeyApiClient {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user