feat(vscode): expose undo/redo in the UI with per-message revert button (#7186)

This commit is contained in:
Marius
2026-03-17 15:42:21 +00:00
committed by GitHub
parent f3472af532
commit d01a51dc7c
44 changed files with 493 additions and 8 deletions
+34
View File
@@ -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<void> {
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<void> {
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.
*/
@@ -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<typeof sessionToWebview> }
| { type: "sessionUpdated"; session: ReturnType<typeof sessionToWebview> }
| { 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 {
@@ -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<MessageListProps> = (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<MessageListProps> = (props) => {
)
}}
</For>
<Show when={boundary()}>
<RevertBanner />
</Show>
<WorkingIndicator />
</Show>
</div>
@@ -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 (
<Show when={info()}>
<div class="revert-banner">
<div class="revert-banner-header">
<div class="revert-banner-info">
<Icon name="arrow-left" size="small" />
<span class="revert-banner-count">
{count() === 1
? language.t("revert.banner.count_one", { count: count() })
: language.t("revert.banner.count_other", { count: count() })}
</span>
</div>
<div class="revert-banner-actions">
<Button variant="ghost" size="small" onClick={handleRedo}>
{language.t("revert.banner.redo")}
</Button>
<Show when={count() > 1}>
<Button variant="ghost" size="small" onClick={() => session.unrevertSession()}>
{language.t("revert.banner.redo.all")}
</Button>
</Show>
</div>
</div>
<Show when={diffs()?.length}>
<div class="revert-banner-files">
<For each={diffs()!}>
{(file) => (
<div class="revert-banner-file">
<span class="revert-banner-filename">{file.file}</span>
<DiffChanges changes={file} />
</div>
)}
</For>
</div>
</Show>
<div class="revert-banner-hint">{language.t("revert.banner.hint")}</div>
</div>
</Show>
)
}
@@ -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<VscodeSessionTurnProps> = (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<VscodeSessionTurnProps> = (props) => {
parts={parts() as unknown as Parameters<typeof UserMessageDisplay>[0]["parts"]}
interrupted={interrupted()}
queued={props.queued}
onRevert={
assistantMessages().length > 0 && session.status() === "idle" && !session.revert()
? () => session.revertSession(props.messageID)
: undefined
}
/>
</div>
@@ -135,7 +135,14 @@ interface SessionContextValue {
currentVariant: () => string | undefined
selectVariant: (value: string) => void
// Revert/undo state for the current session
revert: Accessor<SessionInfo["revert"]>
revertedCount: Accessor<number>
summary: Accessor<SessionInfo["summary"]>
// 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,
@@ -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": "تشعب من الرسالة",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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": "メッセージからフォーク",
@@ -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": "메시지에서 분기",
@@ -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",
@@ -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",
@@ -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": "Создать ответвление",
@@ -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": "แตกแขนงจากข้อความ",
@@ -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": "从消息分叉",
@@ -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": "從訊息分支",
@@ -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,
@@ -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
============================================ */
@@ -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
+6 -6
View File
@@ -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"
@@ -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: {
</Show>
</span>
</Show>
<Show when={props.onRevert}>
<Tooltip value={i18n.t("ui.message.revert")} placement="right" gutter={4}>
<IconButton
icon="arrow-left"
size="normal"
variant="ghost"
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
props.onRevert?.()
}}
aria-label={i18n.t("ui.message.revert")}
/>
</Tooltip>
</Show>
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
placement="right" // kilocode_change: avoid overlapping message text
+1
View File
@@ -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": "مرفق",
+1
View File
@@ -127,6 +127,7 @@ export const dict = {
"ui.message.copyMessage": "Copiar mensagem",
"ui.message.copyResponse": "Copiar resposta",
"ui.message.copied": "Copiado!",
"ui.message.revert": "Revert to here",
"ui.message.interrupted": "Interrompido",
"ui.message.queued": "Na fila",
"ui.message.attachment.alt": "anexo",
+1
View File
@@ -131,6 +131,7 @@ export const dict = {
"ui.message.copyMessage": "Kopiraj poruku",
"ui.message.copyResponse": "Kopiraj odgovor",
"ui.message.copied": "Kopirano!",
"ui.message.revert": "Revert to here",
"ui.message.interrupted": "Prekinuto",
"ui.message.queued": "U redu",
"ui.message.attachment.alt": "prilog",
+1
View File
@@ -126,6 +126,7 @@ export const dict = {
"ui.message.copyMessage": "Kopier besked",
"ui.message.copyResponse": "Kopier svar",
"ui.message.copied": "Kopieret!",
"ui.message.revert": "Revert to here",
"ui.message.interrupted": "Afbrudt",
"ui.message.queued": "I kø",
"ui.message.attachment.alt": "vedhæftning",
+1
View File
@@ -132,6 +132,7 @@ export const dict = {
"ui.message.copyMessage": "Nachricht kopieren",
"ui.message.copyResponse": "Antwort kopieren",
"ui.message.copied": "Kopiert!",
"ui.message.revert": "Revert to here",
"ui.message.interrupted": "Unterbrochen",
"ui.message.queued": "In Warteschlange",
"ui.message.attachment.alt": "Anhang",
+1
View File
@@ -128,6 +128,7 @@ export const dict: Record<string, string> = {
"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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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": "添付ファイル",
+1
View File
@@ -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": "첨부 파일",
+1
View File
@@ -130,6 +130,7 @@ export const dict: Record<Keys, string> = {
"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",
+1
View File
@@ -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",
+1
View File
@@ -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": "вложение",
+1
View File
@@ -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": "ไฟล์แนบ",
+1
View File
@@ -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",
+1
View File
@@ -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": "附件",
+1
View File
@@ -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": "附件",