fix(revert): clarify workspace restoration (#12302)

* fix(revert): clarify workspace restoration

* fix(revert): integrate current session schemas

* chore: update kilo-vscode visual regression baselines

---------

Co-authored-by: kilo-maintainer[bot] <kilo-maintainer[bot]@users.noreply.github.com>
This commit is contained in:
Marius
2026-07-20 12:15:08 +02:00
committed by GitHub
co-authored by kilo-maintainer[bot]
parent d237196249
commit ad66c908a4
37 changed files with 469 additions and 4 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Clarify when reverting a conversation does not restore workspace changes and link disabled snapshots to the Checkpoints setting.
+1
View File
@@ -555,6 +555,7 @@ const SessionRevert = Schema.Struct({
partID: optionalOmitUndefined(PartID),
snapshot: optionalOmitUndefined(Schema.String),
diff: optionalOmitUndefined(Schema.String),
workspace: optionalOmitUndefined(Schema.Literals(["restored", "snapshots-disabled", "unavailable"])), // kilocode_change
})
const SessionModel = Schema.Struct({
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42c43e9ba12bc73da98c915dafca274beee1d0c893c168d8af4e3e3f41edd367
size 630896
oid sha256:3b1eb6cf15d5c2dc66c769a91fcf09bcf25bc2e8d17696ad0e9ab4181ed3038c
size 658647
@@ -194,7 +194,7 @@ export async function runWithMessageConfirmation<T>(
}
}
export function sessionToWebview(session: Session) {
export function sessionToWebview(session: Pick<Session, "id" | "parentID" | "title" | "time" | "summary" | "revert">) {
return {
id: session.id,
parentID: session.parentID ?? null,
@@ -165,6 +165,14 @@ describe("sessionToWebview", () => {
expect(result.revert).toBeNull()
expect(result.summary).toBeNull()
})
it("preserves the workspace restoration outcome from a revert response", () => {
const session = {
...makeSession(),
revert: { messageID: "msg-1", workspace: "snapshots-disabled" as const },
}
expect(sessionToWebview(session).revert).toMatchObject({ workspace: "snapshots-disabled" })
})
})
describe("indexProvidersById", () => {
@@ -5,9 +5,15 @@ import path from "node:path"
const ROOT = path.resolve(import.meta.dir, "../..")
const TURN_FILE = path.join(ROOT, "webview-ui/src/components/chat/VscodeSessionTurn.tsx")
const PROVIDER_FILE = path.join(ROOT, "src/KiloProvider.ts")
const BANNER_FILE = path.join(ROOT, "webview-ui/src/components/chat/RevertBanner.tsx")
const SESSION_FILE = path.join(ROOT, "webview-ui/src/types/messages/sessions.ts")
const SDK_FILE = path.join(ROOT, "../sdk/js/src/v2/gen/types.gen.ts")
const src = fs.readFileSync(TURN_FILE, "utf-8")
const provider = fs.readFileSync(PROVIDER_FILE, "utf-8")
const banner = fs.readFileSync(BANNER_FILE, "utf-8")
const session = fs.readFileSync(SESSION_FILE, "utf-8")
const sdk = fs.readFileSync(SDK_FILE, "utf-8")
function method(name: string, next: string) {
const start = provider.indexOf(` private async ${name}`)
@@ -49,3 +55,26 @@ describe("revert session synchronization", () => {
expect(provider).toMatch(/this\.setCurrentSession\(event\.properties\.info\)/)
})
})
describe("revert workspace restoration status", () => {
it("renders explicit conversation-only outcomes", () => {
expect(session).toContain('workspace?: "restored" | "snapshots-disabled" | "unavailable"')
expect(sdk).toMatch(
/export type Session = \{[\s\S]*?workspace\?: "restored" \| "snapshots-disabled" \| "unavailable"/,
)
expect(sdk).toMatch(
/export type KilocodeSessionImportSessionData = \{[\s\S]*?workspace\?: "restored" \| "snapshots-disabled" \| "unavailable"/,
)
expect(banner).toContain('"revert.banner.workspace.snapshotsDisabled"')
expect(banner).toContain('"revert.banner.workspace.unavailable"')
})
it("opens the checkpoints settings tab when snapshots are disabled", () => {
expect(banner).toContain('{ type: "openSettingsPanel", tab: "checkpoints" }')
expect(banner).toContain('"revert.banner.workspace.enableSnapshots"')
})
it("uses a legacy notice for reverts without an explicit outcome", () => {
expect(banner).toContain('"revert.banner.workspace.legacy"')
})
})
@@ -10,14 +10,17 @@ 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"
import { useVSCode } from "../../context/vscode"
export const RevertBanner: Component = () => {
const session = useSession()
const language = useLanguage()
const vscode = useVSCode()
const info = () => session.revert()
const count = () => session.revertedCount()
const diffs = () => session.summary()?.diffs
const workspace = () => info()?.workspace
const users = () => session.userMessages()
@@ -67,6 +70,31 @@ export const RevertBanner: Component = () => {
</For>
</div>
</Show>
<Show when={workspace() && workspace() !== "restored"}>
<div class="revert-banner-notice" role="status">
<span>
{language.t(
workspace() === "snapshots-disabled"
? "revert.banner.workspace.snapshotsDisabled"
: "revert.banner.workspace.unavailable",
)}
</span>
<Show when={workspace() === "snapshots-disabled"}>
<Button
variant="ghost"
size="small"
onClick={() => vscode.postMessage({ type: "openSettingsPanel", tab: "checkpoints" })}
>
{language.t("revert.banner.workspace.enableSnapshots")}
</Button>
</Show>
</div>
</Show>
<Show when={!workspace()}>
<div class="revert-banner-notice" role="status">
<span>{language.t("revert.banner.workspace.legacy")}</span>
</div>
</Show>
<div class="revert-banner-hint">{language.t("revert.banner.hint")}</div>
</div>
</Show>
+5
View File
@@ -136,6 +136,11 @@ export const dict = {
"revert.banner.redo": "إعادة",
"revert.banner.redo.all": "إعادة الكل",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled": "تم التراجع عن المحادثة. لم تُستعَد تغييرات الملفات لأن اللقطات معطّلة.",
"revert.banner.workspace.unavailable":
"تم التراجع عن المحادثة. لم تكن هناك نقطة تحقق للملفات، لذا لم تُستعَد تغييرات مساحة العمل.",
"revert.banner.workspace.legacy": "تم التراجع عن المحادثة. حالة استعادة مساحة العمل غير متاحة لهذا التراجع السابق.",
"revert.banner.workspace.enableSnapshots": "تفعيل اللقطات",
"revert.disabled.agentBusy": "انتظر انتهاء الوكيل",
"command.session.compact": "ضغط الجلسة",
"command.session.compact.description": "تلخيص الجلسة لتقليل حجم السياق",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "Refazer",
"revert.banner.redo.all": "Refazer Tudo",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Conversa revertida. As alterações nos arquivos não foram restauradas porque os snapshots estão desativados.",
"revert.banner.workspace.unavailable":
"Conversa revertida. Nenhum ponto de restauração dos arquivos estava disponível, portanto as alterações no espaço de trabalho não foram restauradas.",
"revert.banner.workspace.legacy":
"Conversa revertida. O status da restauração do espaço de trabalho não está disponível para esta reversão anterior.",
"revert.banner.workspace.enableSnapshots": "Ativar snapshots",
"revert.disabled.agentBusy": "Aguarde o agente terminar",
"command.session.compact": "Compactar sessão",
"command.session.compact.description": "Resumir a sessão para reduzir o tamanho do contexto",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "Ponovi",
"revert.banner.redo.all": "Ponovi Sve",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Razgovor je vraćen. Promjene datoteka nisu vraćene jer su snimci stanja onemogućeni.",
"revert.banner.workspace.unavailable":
"Razgovor je vraćen. Nije bila dostupna kontrolna tačka datoteka, pa promjene radnog prostora nisu vraćene.",
"revert.banner.workspace.legacy":
"Razgovor je vraćen. Status vraćanja radnog prostora nije dostupan za ovo ranije vraćanje.",
"revert.banner.workspace.enableSnapshots": "Omogući snimke stanja",
"revert.disabled.agentBusy": "Sačekajte da agent završi",
"command.session.compact": "Sažmi sesiju",
"command.session.compact.description": "Sažmi sesiju kako bi se smanjio kontekst",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "Gentag",
"revert.banner.redo.all": "Gentag alt",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Samtalen blev gendannet. Filændringer blev ikke gendannet, fordi snapshots er deaktiveret.",
"revert.banner.workspace.unavailable":
"Samtalen blev gendannet. Der var intet filkontrolpunkt tilgængeligt, så ændringer i arbejdsområdet blev ikke gendannet.",
"revert.banner.workspace.legacy":
"Samtalen blev gendannet. Status for gendannelse af arbejdsområdet er ikke tilgængelig for denne tidligere gendannelse.",
"revert.banner.workspace.enableSnapshots": "Aktivér snapshots",
"revert.disabled.agentBusy": "Vent på at agenten er færdig",
"command.session.compact": "Komprimér session",
"command.session.compact.description": "Opsummer sessionen for at reducere kontekststørrelsen",
@@ -143,6 +143,13 @@ export const dict = {
"revert.banner.redo": "Wiederholen",
"revert.banner.redo.all": "Alle wiederholen",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Unterhaltung zurückgesetzt. Dateiänderungen wurden nicht wiederhergestellt, weil Snapshots deaktiviert sind.",
"revert.banner.workspace.unavailable":
"Unterhaltung zurückgesetzt. Es war kein Dateiwiederherstellungspunkt verfügbar, daher wurden Änderungen im Arbeitsbereich nicht wiederhergestellt.",
"revert.banner.workspace.legacy":
"Unterhaltung zurückgesetzt. Der Status der Arbeitsbereichswiederherstellung ist für dieses frühere Zurücksetzen nicht verfügbar.",
"revert.banner.workspace.enableSnapshots": "Snapshots aktivieren",
"revert.disabled.agentBusy": "Warten bis der Agent fertig ist",
"command.session.compact": "Sitzung komprimieren",
"command.session.compact.description": "Sitzung zusammenfassen, um die Kontextgröße zu reduzieren",
@@ -136,6 +136,13 @@ export const dict = {
"revert.banner.redo": "Redo",
"revert.banner.redo.all": "Redo All",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Conversation reverted. File changes were not restored because snapshots are disabled.",
"revert.banner.workspace.unavailable":
"Conversation reverted. No file checkpoint was available, so workspace changes were not restored.",
"revert.banner.workspace.legacy":
"Conversation reverted. Workspace restoration status is unavailable for this earlier revert.",
"revert.banner.workspace.enableSnapshots": "Enable snapshots",
"revert.disabled.agentBusy": "Wait for agent to finish",
"command.session.compact": "Compact session",
"command.session.compact.description": "Summarize the session to reduce context size",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "Rehacer",
"revert.banner.redo.all": "Rehacer todo",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Conversación revertida. Los cambios en los archivos no se restauraron porque las instantáneas están desactivadas.",
"revert.banner.workspace.unavailable":
"Conversación revertida. No había ningún punto de control de archivos disponible, por lo que no se restauraron los cambios del espacio de trabajo.",
"revert.banner.workspace.legacy":
"Conversación revertida. El estado de restauración del espacio de trabajo no está disponible para esta reversión anterior.",
"revert.banner.workspace.enableSnapshots": "Activar instantáneas",
"revert.disabled.agentBusy": "Espera a que el agente termine",
"command.session.compact": "Compactar sesión",
"command.session.compact.description": "Resumir la sesión para reducir el tamaño del contexto",
+7
View File
@@ -138,6 +138,13 @@ export const dict = {
"revert.banner.redo": "Rétablir",
"revert.banner.redo.all": "Tout rétablir",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Conversation annulée. Les modifications de fichiers n’ont pas été restaurées, car les instantanés sont désactivés.",
"revert.banner.workspace.unavailable":
"Conversation annulée. Aucun point de contrôle de fichiers n’était disponible, les modifications de l’espace de travail n’ont donc pas été restaurées.",
"revert.banner.workspace.legacy":
"Conversation annulée. L’état de restauration de l’espace de travail n’est pas disponible pour cette annulation antérieure.",
"revert.banner.workspace.enableSnapshots": "Activer les instantanés",
"revert.disabled.agentBusy": "Attendre la fin de l'agent",
"command.session.compact": "Compacter la session",
"command.session.compact.description": "Résumer la session pour réduire la taille du contexte",
+7
View File
@@ -131,6 +131,13 @@ export const dict = {
"revert.banner.redo": "Ripeti",
"revert.banner.redo.all": "Ripeti tutto",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Conversazione ripristinata. Le modifiche ai file non sono state ripristinate perché gli snapshot sono disabilitati.",
"revert.banner.workspace.unavailable":
"Conversazione ripristinata. Non era disponibile alcun checkpoint dei file, quindi le modifiche all'area di lavoro non sono state ripristinate.",
"revert.banner.workspace.legacy":
"Conversazione ripristinata. Lo stato del ripristino dell'area di lavoro non è disponibile per questo ripristino precedente.",
"revert.banner.workspace.enableSnapshots": "Abilita snapshot",
"revert.disabled.agentBusy": "Attendi che l'agente finisca",
"command.session.compact": "Compatta sessione",
"command.session.compact.description": "Riassumi la sessione per ridurre la dimensione del contesto",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "やり直し",
"revert.banner.redo.all": "すべてやり直し",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"会話を元に戻しました。スナップショットが無効になっているため、ファイルの変更は復元されませんでした。",
"revert.banner.workspace.unavailable":
"会話を元に戻しました。利用可能なファイルのチェックポイントがなかったため、ワークスペースの変更は復元されませんでした。",
"revert.banner.workspace.legacy":
"会話を元に戻しました。この以前の復元では、ワークスペースの復元状態を利用できません。",
"revert.banner.workspace.enableSnapshots": "スナップショットを有効にする",
"revert.disabled.agentBusy": "エージェントの完了を待ってください",
"command.session.compact": "セッションを圧縮",
"command.session.compact.description": "セッションを要約してコンテキストサイズを削減",
+7
View File
@@ -141,6 +141,13 @@ export const dict = {
"revert.banner.redo": "다시 실행",
"revert.banner.redo.all": "모두 다시 실행",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"대화를 되돌렸습니다. 스냅샷이 비활성화되어 있어 파일 변경 사항은 복원되지 않았습니다.",
"revert.banner.workspace.unavailable":
"대화를 되돌렸습니다. 사용 가능한 파일 체크포인트가 없어 작업 공간 변경 사항은 복원되지 않았습니다.",
"revert.banner.workspace.legacy":
"대화가 되돌려졌습니다. 이 이전 되돌리기에서는 작업 영역 복원 상태를 확인할 수 없습니다.",
"revert.banner.workspace.enableSnapshots": "스냅샷 활성화",
"revert.disabled.agentBusy": "에이전트가 완료될 때까지 기다리세요",
"command.session.compact": "세션 압축",
"command.session.compact.description": "컨텍스트 크기를 줄이기 위해 세션 요약",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "Opnieuw uitvoeren",
"revert.banner.redo.all": "Alles opnieuw uitvoeren",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Conversatie teruggedraaid. Bestandswijzigingen zijn niet hersteld omdat snapshots zijn uitgeschakeld.",
"revert.banner.workspace.unavailable":
"Conversatie teruggedraaid. Er was geen bestandscheckpoint beschikbaar, dus wijzigingen in de werkruimte zijn niet hersteld.",
"revert.banner.workspace.legacy":
"Gesprek teruggedraaid. De herstelstatus van de werkruimte is niet beschikbaar voor deze eerdere terugdraaiing.",
"revert.banner.workspace.enableSnapshots": "Snapshots inschakelen",
"revert.disabled.agentBusy": "Wacht tot de agent klaar is",
"command.session.compact": "Sessie comprimeren",
"command.session.compact.description": "De sessie samenvatten om de contextgrootte te verkleinen",
+7
View File
@@ -140,6 +140,13 @@ export const dict = {
"revert.banner.redo": "Gjenta",
"revert.banner.redo.all": "Gjenta alt",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Samtalen ble tilbakestilt. Filendringer ble ikke gjenopprettet fordi øyeblikksbilder er deaktivert.",
"revert.banner.workspace.unavailable":
"Samtalen ble tilbakestilt. Ingen filkontrollpunkt var tilgjengelig, så endringer i arbeidsområdet ble ikke gjenopprettet.",
"revert.banner.workspace.legacy":
"Samtalen er tilbakeført. Statusen for gjenoppretting av arbeidsområdet er ikke tilgjengelig for denne tidligere tilbakeføringen.",
"revert.banner.workspace.enableSnapshots": "Aktiver øyeblikksbilder",
"revert.disabled.agentBusy": "Vent til agenten er ferdig",
"command.session.compact": "Komprimer sesjon",
"command.session.compact.description": "Oppsummer sesjonen for å redusere kontekststørrelsen",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "Ponów",
"revert.banner.redo.all": "Ponów wszystko",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Rozmowa została cofnięta. Zmiany w plikach nie zostały przywrócone, ponieważ migawki są wyłączone.",
"revert.banner.workspace.unavailable":
"Rozmowa została cofnięta. Żaden punkt kontrolny plików nie był dostępny, więc zmiany w obszarze roboczym nie zostały przywrócone.",
"revert.banner.workspace.legacy":
"Konwersacja została cofnięta. Stan przywracania obszaru roboczego nie jest dostępny dla tego wcześniejszego cofnięcia.",
"revert.banner.workspace.enableSnapshots": "Włącz migawki",
"revert.disabled.agentBusy": "Poczekaj aż agent zakończy",
"command.session.compact": "Kompaktuj sesję",
"command.session.compact.description": "Podsumuj sesję, aby zmniejszyć rozmiar kontekstu",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "Повторить",
"revert.banner.redo.all": "Повторить всё",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Разговор был отменён. Изменения файлов не были восстановлены, так как снимки отключены.",
"revert.banner.workspace.unavailable":
"Разговор был отменён. Контрольная точка файлов была недоступна, поэтому изменения в рабочей области не были восстановлены.",
"revert.banner.workspace.legacy":
"Диалог отменён. Статус восстановления рабочей области недоступен для этого более раннего отката.",
"revert.banner.workspace.enableSnapshots": "Включить снимки",
"revert.disabled.agentBusy": "Дождитесь завершения агента",
"command.session.compact": "Сжать сессию",
"command.session.compact.description": "Сократить сессию для уменьшения размера контекста",
+7
View File
@@ -136,6 +136,13 @@ export const dict = {
"revert.banner.redo": "ทำซ้ำ",
"revert.banner.redo.all": "ทำซ้ำทั้งหมด",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"ย้อนกลับการสนทนาแล้ว ไม่ได้กู้คืนการเปลี่ยนแปลงไฟล์เนื่องจากปิดใช้งานสแนปชอตอยู่",
"revert.banner.workspace.unavailable":
"ย้อนกลับการสนทนาแล้ว ไม่มีจุดตรวจสอบไฟล์ จึงไม่ได้กู้คืนการเปลี่ยนแปลงในพื้นที่ทำงาน",
"revert.banner.workspace.legacy":
"ย้อนกลับการสนทนาแล้ว ไม่ทราบสถานะการกู้คืนพื้นที่ทำงานสำหรับการย้อนกลับก่อนหน้านี้",
"revert.banner.workspace.enableSnapshots": "เปิดใช้งานสแนปชอต",
"revert.disabled.agentBusy": "รอให้เอเจนต์ทำงานเสร็จ",
"command.session.compact": "บีบอัดเซสชัน",
"command.session.compact.description": "สรุปเซสชันเพื่อลดขนาดบริบท",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "Yinele",
"revert.banner.redo.all": "Tümünü Yinele",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Konuşma geri alındı. Anlık görüntüler devre dışı olduğu için dosya değişiklikleri geri yüklenmedi.",
"revert.banner.workspace.unavailable":
"Konuşma geri alındı. Dosya kontrol noktası olmadığından çalışma alanı değişiklikleri geri yüklenmedi.",
"revert.banner.workspace.legacy":
"Konuşma geri alındı. Bu önceki geri alma için çalışma alanı geri yükleme durumu kullanılamıyor.",
"revert.banner.workspace.enableSnapshots": "Anlık Görüntüleri Etkinleştir",
"revert.disabled.agentBusy": "Ajanın bitmesini bekleyin",
"command.session.compact": "Oturumu sıkıştır",
"command.session.compact.description": "Bağlam boyutunu azaltmak için oturumu özetle",
+7
View File
@@ -137,6 +137,13 @@ export const dict = {
"revert.banner.redo": "Повторити",
"revert.banner.redo.all": "Повторити все",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled":
"Розмову скасовано. Зміни файлів не відновлено, оскільки знімки вимкнено.",
"revert.banner.workspace.unavailable":
"Розмову скасовано. Контрольна точка файлів була недоступна, тому зміни в робочому просторі не відновлено.",
"revert.banner.workspace.legacy":
"Розмову скасовано. Стан відновлення робочого простору для цього попереднього скасування недоступний.",
"revert.banner.workspace.enableSnapshots": "Увімкнути знімки",
"revert.disabled.agentBusy": "Зачекайте завершення агента",
"command.session.compact": "Стиснути сесію",
"command.session.compact.description": "Підсумувати сесію для зменшення розміру контексту",
+4
View File
@@ -138,6 +138,10 @@ export const dict = {
"revert.banner.redo": "重做",
"revert.banner.redo.all": "全部重做",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled": "会话已还原。由于快照已禁用,文件更改未恢复。",
"revert.banner.workspace.unavailable": "会话已还原。没有可用的文件检查点,因此未恢复工作区更改。",
"revert.banner.workspace.legacy": "会话已还原。此次早期还原的工作区恢复状态不可用。",
"revert.banner.workspace.enableSnapshots": "启用快照",
"revert.disabled.agentBusy": "等待智能体完成",
"command.session.compact": "精简会话",
"command.session.compact.description": "总结会话以减少上下文大小",
+4
View File
@@ -138,6 +138,10 @@ export const dict = {
"revert.banner.redo": "重做",
"revert.banner.redo.all": "全部重做",
"revert.banner.hint": "You can redo these changes until you send a new message",
"revert.banner.workspace.snapshotsDisabled": "工作階段已還原。由於快照已停用,未還原檔案變更。",
"revert.banner.workspace.unavailable": "工作階段已還原。沒有可用的檔案檢查點,因此未還原工作區變更。",
"revert.banner.workspace.legacy": "工作階段已還原。此次先前還原的工作區復原狀態無法取得。",
"revert.banner.workspace.enableSnapshots": "啟用快照",
"revert.disabled.agentBusy": "等待 Agent 完成",
"command.session.compact": "精簡工作階段",
"command.session.compact.description": "總結工作階段以減少上下文大小",
@@ -1082,6 +1082,11 @@ export const SearchPreviews: Story = {
summary: () => ({ diffs: [{ file: "src/app.ts", additions: 4, deletions: 1 }] }),
userMessages: () => [{ id: "tool-call-lab-user-message" }, { id: "tool-call-lab-user-message-next" }],
}
const conversationOnly = {
...reverted,
revert: () => ({ messageID: "tool-call-lab-user-message", workspace: "snapshots-disabled" as const }),
summary: () => undefined,
}
return (
<StoryProviders
@@ -1167,6 +1172,12 @@ export const SearchPreviews: Story = {
<RevertBanner />
</SessionContext.Provider>
</div>
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Conversation-only revert</span>
<SessionContext.Provider value={conversationOnly as any}>
<RevertBanner />
</SessionContext.Provider>
</div>
<div class="tool-call-lab-example">
<span class="tool-call-lab-example-label">Token limit outcome</span>
<SessionContext.Provider value={limit as any}>
@@ -130,6 +130,19 @@
text-align: left;
}
.revert-banner-notice {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 8px;
padding: 6px 8px;
border-left: 3px solid var(--vscode-editorWarning-foreground, #cca700);
background: var(--vscode-editorWarning-background, rgba(204, 167, 0, 0.1));
color: var(--text-base, var(--vscode-foreground));
font-weight: 500;
}
.revert-banner-hint {
margin-top: 6px;
color: var(--text-muted, var(--vscode-descriptionForeground));
@@ -50,6 +50,7 @@ export interface SessionInfo {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
} | null
summary?: {
additions: number
@@ -59,6 +59,7 @@ const SessionSchema = Schema.Struct({
partID: Schema.optional(Schema.String),
snapshot: Schema.optional(Schema.String),
diff: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.Literals(["restored", "snapshots-disabled", "unavailable"])),
}),
),
permission: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+15 -1
View File
@@ -2,6 +2,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer, Context, Schema } from "effect"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Config } from "@/config/config" // kilocode_change
import { Snapshot } from "../snapshot"
import { Storage } from "@/storage/storage"
import { Session } from "./session"
@@ -34,6 +35,7 @@ export const layer = Layer.effect(
const events = yield* EventV2Bridge.Service
const summary = yield* SessionSummary.Service
const state = yield* SessionRunState.Service
const config = yield* Config.Service // kilocode_change
const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) {
yield* state.assertNotBusy(input.sessionID)
@@ -67,12 +69,22 @@ export const layer = Layer.effect(
if (!rev) return session
// kilocode_change start
// A fresh snapshot only preserves the state needed for redo. File restoration
// is possible only when the historical turn retained checkpoint data.
const range = all.filter((msg) => msg.info.id >= rev.messageID)
const checkpoint = patches.length > 0
rev.workspace = checkpoint
? "restored"
: (yield* config.get()).snapshot === false
? "snapshots-disabled"
: "unavailable"
// kilocode_change end
rev.snapshot = session.revert?.snapshot ?? (yield* snap.track())
if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot)
// kilocode_change start - compute diffs BEFORE reverting files so the diff
// reflects changes being undone (files on disk still have AI modifications)
const range = all.filter((msg) => msg.info.id >= rev.messageID)
const diffs = yield* summary.computeDiff({ messages: range })
// kilocode_change end
@@ -165,6 +177,7 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Storage.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(SessionSummary.defaultLayer),
Layer.provide(Config.defaultLayer), // kilocode_change
),
)
@@ -175,6 +188,7 @@ export const node = LayerNode.make(layer, [
EventV2Bridge.node,
SessionSummary.node,
SessionRunState.node,
Config.node, // kilocode_change
])
export * as SessionRevert from "./revert"
+1
View File
@@ -204,6 +204,7 @@ const Revert = Schema.Struct({
partID: optionalOmitUndefined(PartID),
snapshot: optionalOmitUndefined(Schema.String),
diff: optionalOmitUndefined(Schema.String),
workspace: optionalOmitUndefined(Schema.Literals(["restored", "snapshots-disabled", "unavailable"])), // kilocode_change
})
const Model = Schema.Struct({
@@ -1,5 +1,7 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import fs from "node:fs/promises"
import path from "node:path"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -84,3 +86,160 @@ describe("partial assistant revert", () => {
),
)
})
describe("workspace revert status", () => {
it.live(
"reports disabled snapshots when conversation-only revert leaves files unchanged",
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const revert = yield* SessionRevert.Service
const session = yield* sessions.create({})
const file = path.join(dir, "created.txt")
const providerID = ProviderV2.ID.make("test")
yield* Effect.promise(() => fs.writeFile(file, "created"))
const user = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "user",
agent: "default",
model: { providerID, modelID: ModelV2.ID.make("test") },
time: { created: Date.now() },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.id,
sessionID: session.id,
type: "text",
text: "create a file",
})
const result = yield* revert.revert({ sessionID: session.id, messageID: user.id })
expect(result.revert?.workspace).toBe("snapshots-disabled")
expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe("created")
}),
{ git: true, config: { snapshot: false } },
),
)
it.live(
"reports unavailable when historical turns have no file checkpoint",
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const revert = yield* SessionRevert.Service
const session = yield* sessions.create({})
const file = path.join(dir, "created.txt")
const providerID = ProviderV2.ID.make("test")
yield* Effect.promise(() => fs.writeFile(file, "created"))
const user = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "user",
agent: "default",
model: { providerID, modelID: ModelV2.ID.make("test") },
time: { created: Date.now() },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.id,
sessionID: session.id,
type: "text",
text: "create a file",
})
const result = yield* revert.revert({ sessionID: session.id, messageID: user.id })
expect(result.revert?.workspace).toBe("unavailable")
expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe("created")
}),
{ git: true },
),
)
it.live(
"reports restored when historical patches restore a file",
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const sessions = yield* Session.Service
const revert = yield* SessionRevert.Service
const snapshot = yield* Snapshot.Service
const session = yield* sessions.create({})
const file = path.join(dir, "tracked.txt")
const providerID = ProviderV2.ID.make("test")
yield* Effect.promise(() => fs.writeFile(file, "before"))
const user = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "user",
agent: "default",
model: { providerID, modelID: ModelV2.ID.make("test") },
time: { created: Date.now() },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: user.id,
sessionID: session.id,
type: "text",
text: "change a file",
})
const assistant = yield* sessions.updateMessage({
id: MessageID.ascending(),
sessionID: session.id,
role: "assistant",
parentID: user.id,
mode: "default",
agent: "default",
path: { cwd: dir, root: dir },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
modelID: ModelV2.ID.make("test"),
providerID,
time: { created: Date.now() },
finish: "end_turn",
})
const before = yield* snapshot.track()
if (!before) throw new Error("expected snapshot")
yield* Effect.promise(() => fs.writeFile(file, "after"))
const after = yield* snapshot.track()
if (!after) throw new Error("expected snapshot")
const patch = yield* snapshot.patch(before)
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: assistant.id,
sessionID: session.id,
type: "step-start",
snapshot: before,
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: assistant.id,
sessionID: session.id,
type: "step-finish",
reason: "stop",
snapshot: after,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: assistant.id,
sessionID: session.id,
type: "patch",
hash: patch.hash,
files: patch.files,
})
const result = yield* revert.revert({ sessionID: session.id, messageID: user.id })
expect(result.revert?.workspace).toBe("restored")
expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe("before")
}),
{ git: true },
),
)
})
+1
View File
@@ -7686,6 +7686,7 @@ export class SessionImport extends HeyApiClient {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
permission?: {
[key: string]: unknown
+12
View File
@@ -429,6 +429,7 @@ export type Session = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -1983,6 +1984,7 @@ export type GlobalSession = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
project: ProjectSummary | null
worktreeName?: string
@@ -2365,6 +2367,7 @@ export type Session1 = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -2418,6 +2421,7 @@ export type Session2 = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -2478,6 +2482,7 @@ export type Session3 = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -2531,6 +2536,7 @@ export type Session4 = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -2584,6 +2590,7 @@ export type Session5 = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -2637,6 +2644,7 @@ export type Session6 = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -2690,6 +2698,7 @@ export type Session7 = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -2797,6 +2806,7 @@ export type Session8 = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -2850,6 +2860,7 @@ export type Session9 = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
}
@@ -13383,6 +13394,7 @@ export type KilocodeSessionImportSessionData = {
partID?: string
snapshot?: string
diff?: string
workspace?: "restored" | "snapshots-disabled" | "unavailable"
}
permission?: {
[key: string]: unknown
+48
View File
@@ -16895,6 +16895,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -24796,6 +24800,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -29284,6 +29292,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -30455,6 +30467,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -30624,6 +30640,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -30812,6 +30832,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -30981,6 +31005,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -31150,6 +31178,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -31319,6 +31351,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -31488,6 +31524,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -31819,6 +31859,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],
@@ -31988,6 +32032,10 @@
},
"diff": {
"type": "string"
},
"workspace": {
"type": "string",
"enum": ["restored", "snapshots-disabled", "unavailable"]
}
},
"required": ["messageID"],