mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
feat(vscode): expose undo/redo in the UI with per-message revert button (#7186)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "مرفق",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "添付ファイル",
|
||||
|
||||
@@ -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": "첨부 파일",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "вложение",
|
||||
|
||||
@@ -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": "ไฟล์แนบ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "附件",
|
||||
|
||||
@@ -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": "附件",
|
||||
|
||||
Reference in New Issue
Block a user