diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 09e98a3cda2..6393350eca2 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -365,6 +365,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "abort": await this.handleAbort(message.sessionID) break + case "revertSession": + this.handleRevertSession(message.sessionID, message.messageID).catch((e) => + console.error("[Kilo New] handleRevertSession failed:", e), + ) + break + case "unrevertSession": + this.handleUnrevertSession(message.sessionID).catch((e) => + console.error("[Kilo New] handleUnrevertSession failed:", e), + ) + break case "permissionResponse": await this.handlePermissionResponse( message.permissionId, @@ -1697,6 +1707,30 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } + private async handleRevertSession(sessionID: string, messageID: string): Promise { + if (!this.client) return + const dir = this.getWorkspaceDirectory(sessionID) + const { data, error } = await this.client.session.revert({ sessionID, messageID, directory: dir }) + if (error) { + console.error("[Kilo New] KiloProvider: Failed to revert session:", error) + this.postMessage({ type: "error", message: "Failed to revert session", sessionID }) + return + } + if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) }) + } + + private async handleUnrevertSession(sessionID: string): Promise { + if (!this.client) return + const dir = this.getWorkspaceDirectory(sessionID) + const { data, error } = await this.client.session.unrevert({ sessionID, directory: dir }) + if (error) { + console.error("[Kilo New] KiloProvider: Failed to unrevert session:", error) + this.postMessage({ type: "error", message: "Failed to redo session", sessionID }) + return + } + if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) }) + } + /** * Handle compact (context summarization) request from the webview. */ diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index c6caa869aa8..99d89a2ef20 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -44,6 +44,11 @@ export function sessionToWebview(session: Session) { title: session.title, createdAt: new Date(session.time.created).toISOString(), updatedAt: new Date(session.time.updated).toISOString(), + // Use null (not undefined) so the value survives postMessage JSON serialization. + // Without this, unrevert responses lose the revert key entirely and the + // SolidJS store merge never clears the existing revert state. + revert: session.revert ?? null, + summary: session.summary ?? null, } } @@ -174,6 +179,7 @@ export type WebviewMessage = | { type: "permissionError"; permissionID: string } | { type: "sessionCreated"; session: ReturnType } | { type: "sessionUpdated"; session: ReturnType } + | { type: "messageRemoved"; sessionID: string; messageID: string } | null export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | undefined): WebviewMessage { @@ -209,6 +215,14 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un }, } } + case "message.removed": { + const props = event.properties as { sessionID: string; messageID: string } + return { + type: "messageRemoved", + sessionID: props.sessionID, + messageID: props.messageID, + } + } case "session.status": { const info = event.properties.status return { diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx index df3d4c3d79d..944927b538c 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx @@ -20,6 +20,7 @@ import { formatRelativeDate } from "../../utils/date" import { CloudImportDialog } from "./CloudImportDialog" import { FeedbackDialog } from "./FeedbackDialog" import { VscodeSessionTurn } from "./VscodeSessionTurn" +import { RevertBanner } from "./RevertBanner" import { WorkingIndicator } from "../shared/WorkingIndicator" import { activeUserMessageID as getActiveUserMessageID } from "../../context/session-queue" @@ -64,8 +65,14 @@ export const MessageList: Component = (props) => { } }) - const userMessages = () => session.userMessages() - const isEmpty = () => userMessages().length === 0 && !session.loading() + const allUserMessages = () => session.userMessages() + const boundary = () => session.revert()?.messageID + const userMessages = createMemo(() => { + const b = boundary() + if (!b) return allUserMessages() + return allUserMessages().filter((m) => m.id < b) + }) + const isEmpty = () => userMessages().length === 0 && !session.loading() && !boundary() const recent = createMemo(() => [...session.sessions()] @@ -153,6 +160,9 @@ export const MessageList: Component = (props) => { ) }} + + + diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/RevertBanner.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/RevertBanner.tsx new file mode 100644 index 00000000000..fe23f966b0b --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/chat/RevertBanner.tsx @@ -0,0 +1,74 @@ +/** + * RevertBanner component + * Shows when the session is in a reverted state, displaying the number of + * reverted messages, per-file diff stats, and Redo / Redo All actions. + */ + +import { Component, For, Show } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" +import { Icon } from "@kilocode/kilo-ui/icon" +import { DiffChanges } from "@kilocode/kilo-ui/diff-changes" +import { useSession } from "../../context/session" +import { useLanguage } from "../../context/language" + +export const RevertBanner: Component = () => { + const session = useSession() + const language = useLanguage() + + const info = () => session.revert() + const count = () => session.revertedCount() + const diffs = () => session.summary()?.diffs + + const users = () => session.userMessages() + + const handleRedo = () => { + const boundary = info()?.messageID + if (!boundary) return + const next = users().find((m) => m.id > boundary) + if (!next) { + session.unrevertSession() + return + } + session.revertSession(next.id) + } + + return ( + +
+
+
+ + + {count() === 1 + ? language.t("revert.banner.count_one", { count: count() }) + : language.t("revert.banner.count_other", { count: count() })} + +
+
+ + 1}> + + +
+
+ +
+ + {(file) => ( +
+ {file.file} + +
+ )} +
+
+
+
{language.t("revert.banner.hint")}
+
+
+ ) +} 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 e109f4791d8..c1092f1eaf5 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx @@ -29,6 +29,7 @@ import type { } from "@kilocode/sdk/v2" import { ErrorDisplay } from "./ErrorDisplay" import { useServer } from "../../context/server" +import { useSession } from "../../context/session" function getDirectory(path: string): string { const sep = path.includes("/") ? "/" : "\\" @@ -53,6 +54,7 @@ export const VscodeSessionTurn: Component = (props) => { const i18n = useI18n() const diffComponent = useDiffComponent() const server = useServer() + const session = useSession() const emptyMessages: SDKMessage[] = [] const emptyParts: SDKPart[] = [] @@ -168,6 +170,11 @@ export const VscodeSessionTurn: Component = (props) => { parts={parts() as unknown as Parameters[0]["parts"]} interrupted={interrupted()} queued={props.queued} + onRevert={ + assistantMessages().length > 0 && session.status() === "idle" && !session.revert() + ? () => session.revertSession(props.messageID) + : undefined + } /> diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 091272f4b50..700d31cc41b 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -135,7 +135,14 @@ interface SessionContextValue { currentVariant: () => string | undefined selectVariant: (value: string) => void + // Revert/undo state for the current session + revert: Accessor + revertedCount: Accessor + summary: Accessor + // Actions + revertSession: (messageID: string) => void + unrevertSession: () => void sendMessage: (text: string, providerID?: string, modelID?: string, files?: FileAttachment[]) => void abort: () => void compact: () => void @@ -493,6 +500,10 @@ export const SessionProvider: ParentComponent = (props) => { handleSessionDeleted(message.sessionID) break + case "messageRemoved": + handleMessageRemoved(message.sessionID, message.messageID) + break + case "error": // Only clear loading if the error is for the current session // (or has no sessionID for backwards compatibility) @@ -903,6 +914,18 @@ export const SessionProvider: ParentComponent = (props) => { }) } + // Matches desktop app's event-reducer.ts: message.removed handler. + // Splices the message from the store and deletes its parts. + function handleMessageRemoved(sessionID: string, messageID: string) { + setStore("messages", sessionID, (msgs = []) => msgs.filter((m) => m.id !== messageID)) + setStore( + "parts", + produce((parts) => { + delete parts[messageID] + }), + ) + } + function handleCloudSessionDataLoaded(cloudSessionId: string, title: string, messages: Message[]) { if (cloudPreviewId() !== cloudSessionId) return const key = `cloud:${cloudSessionId}` @@ -1262,6 +1285,47 @@ export const SessionProvider: ParentComponent = (props) => { const userMessages = createMemo(() => messages().filter((m) => m.role === "user")) + const revert = createMemo(() => { + const id = currentSessionID() + // revert can be null (cleared by unrevert) or undefined (never set) — treat both as "no revert" + return id ? (store.sessions[id]?.revert ?? undefined) : undefined + }) + + const revertedCount = createMemo(() => { + const boundary = revert()?.messageID + if (!boundary) return 0 + return userMessages().filter((m) => m.id >= boundary).length + }) + + const summary = createMemo(() => { + const id = currentSessionID() + return id ? (store.sessions[id]?.summary ?? undefined) : undefined + }) + + function revertSession(messageID: string) { + const id = currentSessionID() + if (!id) return + // Restore the reverted user message's prompt text into the input. + // Dispatch as a window message so PromptInput picks it up via onMessage. + const parts = store.parts[messageID] + if (parts) { + const text = parts + .filter((p) => p.type === "text" && !(p as { synthetic?: boolean }).synthetic) + .map((p) => (p as { text: string }).text ?? "") + .join("") + if (text) window.postMessage({ type: "setChatBoxMessage", text }, "*") + } + vscode.postMessage({ type: "revertSession", sessionID: id, messageID }) + } + + function unrevertSession() { + const id = currentSessionID() + if (!id) return + // Clear the prompt input on full redo (matching TUI/desktop behavior) + window.postMessage({ type: "setChatBoxMessage", text: "" }, "*") + vscode.postMessage({ type: "unrevertSession", sessionID: id }) + } + function syncSession(sessionID: string) { vscode.postMessage({ type: "syncSession", sessionID }) } @@ -1360,6 +1424,11 @@ export const SessionProvider: ParentComponent = (props) => { variantList, currentVariant, selectVariant, + revert, + revertedCount, + summary, + revertSession, + unrevertSession, sendMessage, abort, compact, diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index b63df90802a..b064704bcac 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "تراجع عن الرسالة الأخيرة", "command.session.redo": "إعادة", "command.session.redo.description": "إعادة الرسالة التي تم التراجع عنها", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "ضغط الجلسة", "command.session.compact.description": "تلخيص الجلسة لتقليل حجم السياق", "command.session.fork": "تشعب من الرسالة", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 5a1681d09d8..fcb2bd1b2d1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Desfazer a última mensagem", "command.session.redo": "Refazer", "command.session.redo.description": "Refazer a última mensagem desfeita", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Compactar sessão", "command.session.compact.description": "Resumir a sessão para reduzir o tamanho do contexto", "command.session.fork": "Bifurcar da mensagem", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 1ca43e55e50..b2ff6c5f3b8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Poništi posljednju poruku", "command.session.redo": "Vrati", "command.session.redo.description": "Vrati posljednju poništenu poruku", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Sažmi sesiju", "command.session.compact.description": "Sažmi sesiju kako bi se smanjio kontekst", "command.session.fork": "Fork iz poruke", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 4fe53e1b592..7ebdbb2fac0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Fortryd den sidste besked", "command.session.redo": "Omgør", "command.session.redo.description": "Omgør den sidste fortrudte besked", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Komprimér session", "command.session.compact.description": "Opsummer sessionen for at reducere kontekststørrelsen", "command.session.fork": "Forgren fra besked", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 3eb052d0a77..29469221cd8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -82,6 +82,14 @@ export const dict = { "command.session.undo.description": "Letzte Nachricht rückgängig machen", "command.session.redo": "Wiederherstellen", "command.session.redo.description": "Letzte rückgängig gemachte Nachricht wiederherstellen", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Sitzung komprimieren", "command.session.compact.description": "Sitzung zusammenfassen, um die Kontextgröße zu reduzieren", "command.session.fork": "Von Nachricht abzweigen", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index eb40f32b141..ae2b5a4340f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Undo the last message", "command.session.redo": "Redo", "command.session.redo.description": "Redo the last undone message", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Compact session", "command.session.compact.description": "Summarize the session to reduce context size", "command.session.fork": "Fork from message", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 9f34b9094ea..4a67c75c9d6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Deshacer el último mensaje", "command.session.redo": "Rehacer", "command.session.redo.description": "Rehacer el último mensaje deshecho", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Compactar sesión", "command.session.compact.description": "Resumir la sesión para reducir el tamaño del contexto", "command.session.fork": "Bifurcar desde mensaje", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index dabceedd183..b343cb3876a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -79,6 +79,14 @@ export const dict = { "command.session.undo.description": "Annuler le dernier message", "command.session.redo": "Rétablir", "command.session.redo.description": "Rétablir le dernier message annulé", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Compacter la session", "command.session.compact.description": "Résumer la session pour réduire la taille du contexte", "command.session.fork": "Bifurquer à partir du message", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 2dea7cfde2f..7fdb68e8de9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "最後のメッセージを元に戻す", "command.session.redo": "やり直す", "command.session.redo.description": "元に戻したメッセージをやり直す", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "セッションを圧縮", "command.session.compact.description": "セッションを要約してコンテキストサイズを削減", "command.session.fork": "メッセージからフォーク", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 8c3b8ce5db2..1389d5121ae 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -82,6 +82,14 @@ export const dict = { "command.session.undo.description": "마지막 메시지 실행 취소", "command.session.redo": "다시 실행", "command.session.redo.description": "마지막 실행 취소된 메시지 다시 실행", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "세션 압축", "command.session.compact.description": "컨텍스트 크기를 줄이기 위해 세션 요약", "command.session.fork": "메시지에서 분기", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index bd1981ca990..073781cdac7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -81,6 +81,14 @@ export const dict = { "command.session.undo.description": "Angre siste melding", "command.session.redo": "Gjør om", "command.session.redo.description": "Gjør om siste angrede melding", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Komprimer sesjon", "command.session.compact.description": "Oppsummer sesjonen for å redusere kontekststørrelsen", "command.session.fork": "Forgren fra melding", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index be0542a5e45..5af63b6d036 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Cofnij ostatnią wiadomość", "command.session.redo": "Ponów", "command.session.redo.description": "Ponów ostatnią cofniętą wiadomość", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Kompaktuj sesję", "command.session.compact.description": "Podsumuj sesję, aby zmniejszyć rozmiar kontekstu", "command.session.fork": "Rozwidlij od wiadomości", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index e68ca6e9f6a..4f112c73096 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Отменить последнее сообщение", "command.session.redo": "Повторить", "command.session.redo.description": "Повторить отменённое сообщение", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Сжать сессию", "command.session.compact.description": "Сократить сессию для уменьшения размера контекста", "command.session.fork": "Создать ответвление", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 8112e4e2be6..7e974c6bc3c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "ยกเลิกข้อความล่าสุด", "command.session.redo": "ทำซ้ำ", "command.session.redo.description": "ทำซ้ำข้อความที่ถูกยกเลิกล่าสุด", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "บีบอัดเซสชัน", "command.session.compact.description": "สรุปเซสชันเพื่อลดขนาดบริบท", "command.session.fork": "แตกแขนงจากข้อความ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 2d24ab47542..d7c856ae4ee 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -82,6 +82,14 @@ export const dict = { "command.session.undo.description": "撤销上一条消息", "command.session.redo": "重做", "command.session.redo.description": "重做上一条撤销的消息", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "精简会话", "command.session.compact.description": "总结会话以减少上下文大小", "command.session.fork": "从消息分叉", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index eababfc302b..6746fe6c6cf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -82,6 +82,14 @@ export const dict = { "command.session.undo.description": "復原上一則訊息", "command.session.redo": "重做", "command.session.redo.description": "重做上一則復原的訊息", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "精簡工作階段", "command.session.compact.description": "總結工作階段以減少上下文大小", "command.session.fork": "從訊息分支", diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index cdfc06c47ce..3cdf84c201a 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -152,6 +152,11 @@ export function mockSessionValue(overrides?: { getSessionModel: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }), setSessionModel: noop, setSessionAgent: noop, + revert: () => undefined, + revertedCount: () => 0, + summary: () => undefined, + revertSession: noop, + unrevertSession: noop, variantList: () => [], currentVariant: () => undefined, selectVariant: noop, diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index ccf6db1df4c..604d54edc2e 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -446,6 +446,75 @@ background: var(--vscode-button-hoverBackground); } +/* ============================================ + Revert Banner + ============================================ */ + +.revert-banner { + margin: 8px 12px; + padding: 8px 12px; + border-radius: 4px; + background: var(--surface-secondary, var(--vscode-editorWidget-background, #252526)); + border: 1px solid var(--border-base, var(--vscode-editorWidget-border, #454545)); + font-size: 12px; +} + +.revert-banner-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.revert-banner-info { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-muted, var(--vscode-descriptionForeground)); +} + +.revert-banner-count { + font-weight: 500; +} + +.revert-banner-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.revert-banner-files { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.revert-banner-file { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 1px 0; + color: var(--text-base, var(--vscode-foreground)); +} + +.revert-banner-filename { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + direction: rtl; + text-align: left; +} + +.revert-banner-hint { + margin-top: 6px; + color: var(--text-muted, var(--vscode-descriptionForeground)); + font-style: italic; + font-size: 11px; +} + /* ============================================ Prompt Input ============================================ */ diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 61ffec879d7..27600116722 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -113,12 +113,34 @@ export interface Message { tokens?: TokenUsage } +// File diff info (matches Snapshot.FileDiff from CLI backend) +export interface SessionFileDiff { + file: string + before: string + after: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + // Session info (simplified for webview) export interface SessionInfo { id: string title?: string createdAt: string updatedAt: string + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } | null + summary?: { + additions: number + deletions: number + files: number + diffs?: SessionFileDiff[] + } | null } // Cloud session info (from Kilo cloud API) @@ -448,6 +470,12 @@ export interface SessionDeletedMessage { sessionID: string } +export interface MessageRemovedMessage { + type: "messageRemoved" + sessionID: string + messageID: string +} + export interface MessagesLoadedMessage { type: "messagesLoaded" sessionID: string @@ -1102,6 +1130,7 @@ export type ExtensionMessage = | SessionCreatedMessage | SessionUpdatedMessage | SessionDeletedMessage + | MessageRemovedMessage | MessagesLoadedMessage | MessageCreatedMessage | SessionsLoadedMessage @@ -1198,6 +1227,17 @@ export interface AbortRequest { sessionID: string } +export interface RevertSessionRequest { + type: "revertSession" + sessionID: string + messageID: string +} + +export interface UnrevertSessionRequest { + type: "unrevertSession" + sessionID: string +} + export interface PermissionResponseRequest { type: "permissionResponse" permissionId: string @@ -1679,6 +1719,8 @@ export interface SetDefaultBaseBranchRequest { export type WebviewMessage = | SendMessageRequest | AbortRequest + | RevertSessionRequest + | UnrevertSessionRequest | PermissionResponseRequest | CreateSessionRequest | ClearSessionRequest diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 961b27df1df..7b4b30d6dbf 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -81,14 +81,16 @@ "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", + "@kilocode/kilo-gateway": "workspace:*", + "@kilocode/kilo-telemetry": "workspace:*", + "@kilocode/plugin": "workspace:*", + "@kilocode/sdk": "workspace:*", "@modelcontextprotocol/sdk": "1.25.2", "@morphllm/morphsdk": "0.2.148", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", - "@kilocode/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", - "@kilocode/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", "@opentui/core": "0.1.86", @@ -122,6 +124,7 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", + "simple-git": "3.31.1", "solid-js": "catalog:", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", @@ -133,10 +136,7 @@ "xdg-basedir": "5.1.0", "yargs": "18.0.0", "zod": "catalog:", - "zod-to-json-schema": "3.24.5", - "@kilocode/kilo-gateway": "workspace:*", - "@kilocode/kilo-telemetry": "workspace:*", - "simple-git": "3.31.1" + "zod-to-json-schema": "3.24.5" }, "overrides": { "drizzle-orm": "1.0.0-beta.12-a5629fb" diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 1a384237554..aead588d809 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -880,6 +880,7 @@ export function UserMessageDisplay(props: { parts: PartType[] interrupted?: boolean queued?: boolean + onRevert?: () => void }) { const data = useData() const dialog = useDialog() @@ -1018,6 +1019,21 @@ export function UserMessageDisplay(props: { + + + e.preventDefault()} + onClick={(event) => { + event.stopPropagation() + props.onRevert?.() + }} + aria-label={i18n.t("ui.message.revert")} + /> + + = { "ui.message.copyMessage": "Copy message", "ui.message.copyResponse": "Copy response", "ui.message.copied": "Copied", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Interrupted", "ui.message.queued": "Queued", "ui.message.attachment.alt": "attachment", diff --git a/packages/ui/src/i18n/es.ts b/packages/ui/src/i18n/es.ts index 9ee95d82459..5df36d5d92b 100644 --- a/packages/ui/src/i18n/es.ts +++ b/packages/ui/src/i18n/es.ts @@ -127,6 +127,7 @@ export const dict = { "ui.message.copyMessage": "Copiar mensaje", "ui.message.copyResponse": "Copiar respuesta", "ui.message.copied": "¡Copiado!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Interrumpido", "ui.message.queued": "En cola", "ui.message.attachment.alt": "adjunto", diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts index 431abe56830..df53d9c5584 100644 --- a/packages/ui/src/i18n/fr.ts +++ b/packages/ui/src/i18n/fr.ts @@ -127,6 +127,7 @@ export const dict = { "ui.message.copyMessage": "Copier le message", "ui.message.copyResponse": "Copier la réponse", "ui.message.copied": "Copié !", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Interrompu", "ui.message.queued": "En file", "ui.message.attachment.alt": "pièce jointe", diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts index c6cb2ac401e..90448a8562b 100644 --- a/packages/ui/src/i18n/ja.ts +++ b/packages/ui/src/i18n/ja.ts @@ -126,6 +126,7 @@ export const dict = { "ui.message.copyMessage": "メッセージをコピー", "ui.message.copyResponse": "応答をコピー", "ui.message.copied": "コピーしました!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "中断", "ui.message.queued": "待機中", "ui.message.attachment.alt": "添付ファイル", diff --git a/packages/ui/src/i18n/ko.ts b/packages/ui/src/i18n/ko.ts index cd306e879e5..2eb297f860e 100644 --- a/packages/ui/src/i18n/ko.ts +++ b/packages/ui/src/i18n/ko.ts @@ -127,6 +127,7 @@ export const dict = { "ui.message.copyMessage": "메시지 복사", "ui.message.copyResponse": "응답 복사", "ui.message.copied": "복사됨!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "중단됨", "ui.message.queued": "대기 중", "ui.message.attachment.alt": "첨부 파일", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index ddfe0946188..dc56d918cc9 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -130,6 +130,7 @@ export const dict: Record = { "ui.message.copyMessage": "Kopier melding", "ui.message.copyResponse": "Kopier svar", "ui.message.copied": "Kopiert!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Avbrutt", "ui.message.queued": "I kø", "ui.message.attachment.alt": "vedlegg", diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts index 73fa96afae3..9e34769bfa7 100644 --- a/packages/ui/src/i18n/pl.ts +++ b/packages/ui/src/i18n/pl.ts @@ -126,6 +126,7 @@ export const dict = { "ui.message.copyMessage": "Kopiuj wiadomość", "ui.message.copyResponse": "Kopiuj odpowiedź", "ui.message.copied": "Skopiowano!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Przerwano", "ui.message.queued": "W kolejce", "ui.message.attachment.alt": "załącznik", diff --git a/packages/ui/src/i18n/ru.ts b/packages/ui/src/i18n/ru.ts index 085be28436f..0a747a24319 100644 --- a/packages/ui/src/i18n/ru.ts +++ b/packages/ui/src/i18n/ru.ts @@ -126,6 +126,7 @@ export const dict = { "ui.message.copyMessage": "Копировать сообщение", "ui.message.copyResponse": "Копировать ответ", "ui.message.copied": "Скопировано!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Прервано", "ui.message.queued": "В очереди", "ui.message.attachment.alt": "вложение", diff --git a/packages/ui/src/i18n/th.ts b/packages/ui/src/i18n/th.ts index 705f68d1b4f..5d480880648 100644 --- a/packages/ui/src/i18n/th.ts +++ b/packages/ui/src/i18n/th.ts @@ -128,6 +128,7 @@ export const dict = { "ui.message.copyMessage": "คัดลอกข้อความ", "ui.message.copyResponse": "คัดลอกคำตอบ", "ui.message.copied": "คัดลอกแล้ว!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "ถูกขัดจังหวะ", "ui.message.queued": "อยู่ในคิว", "ui.message.attachment.alt": "ไฟล์แนบ", diff --git a/packages/ui/src/i18n/tr.ts b/packages/ui/src/i18n/tr.ts index 8e536c51768..8db027784ad 100644 --- a/packages/ui/src/i18n/tr.ts +++ b/packages/ui/src/i18n/tr.ts @@ -133,6 +133,7 @@ export const dict = { "ui.message.copyMessage": "Mesajı kopyala", "ui.message.copyResponse": "Yanıtı kopyala", "ui.message.copied": "Kopyalandı", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Kesildi", "ui.message.queued": "Sırada", "ui.message.attachment.alt": "ek", diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 571574d92e6..7c3b1b1e38b 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -131,6 +131,7 @@ export const dict = { "ui.message.copyMessage": "复制消息", "ui.message.copyResponse": "复制回复", "ui.message.copied": "已复制!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "已中断", "ui.message.queued": "排队中", "ui.message.attachment.alt": "附件", diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts index edbc96b12f4..6a5637d8a30 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -131,6 +131,7 @@ export const dict = { "ui.message.copyMessage": "複製訊息", "ui.message.copyResponse": "複製回覆", "ui.message.copied": "已複製!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "已中斷", "ui.message.queued": "排隊中", "ui.message.attachment.alt": "附件",