Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
abfbe7f
feat(checkpoints): per-write checkpoints, task-start baseline, and pe…
easonliang28 Aug 27, 2026
93a8329
feat(checkpoints): per-task change journal with torn-tail repair (B2,…
easonliang28 Aug 27, 2026
2500ab3
feat(checkpoints): per-step change cards and changeCardDetail setting…
easonliang28 Aug 27, 2026
d0c60bf
feat(checkpoints): per-file and per-step rollback service (B3c, #1375)
easonliang28 Aug 27, 2026
d64389c
fix(fws): validate checkpoint availability + contain symlinked restor…
easonliang28 Aug 28, 2026
630f273
fix(checkpoints): roll change-card files back to their pre-step state…
easonliang28 Aug 29, 2026
77c809c
ci: retry ubuntu unit test (flaky vitest-worker teardown race in Task…
easonliang28 Aug 29, 2026
39afe1b
fix(checkpoints): reject stale-card rollback and fail on unreadable c…
easonliang28 Aug 29, 2026
e0751cb
feat(webview): change cards UI and rollback buttons (B3b, #1375)
easonliang28 Aug 27, 2026
add3095
fix(fws): change-card step resolution, Tailwind v4 grow, sibling sett…
easonliang28 Aug 28, 2026
13d6364
feat(fws): add per-file open-in-editor control to change cards (B3b, …
easonliang28 Aug 28, 2026
cc31a93
fix(fws): use a native button for the compact-row open-file control (…
easonliang28 Aug 28, 2026
c82a372
fix(fws): type VSCodeCheckbox change events and the settings test dou…
easonliang28 Aug 28, 2026
7a1d4e6
feat(webview): add per-file restore-latest to change cards and correc…
easonliang28 Aug 29, 2026
a0693e9
fix(webview): correlate rollback failures, i18n no-task results, chan…
easonliang28 Aug 29, 2026
ea90ea8
fix(webview): cover the schema-invalid change-card path and correct i…
easonliang28 Aug 29, 2026
63dcd24
chore(ci): empty commit — re-trigger CI and the CodeRabbit current-he…
easonliang28 Aug 30, 2026
ab281f5
chore(ci): addendum 12 — empty commit to re-trigger the flaky ubuntu …
easonliang28 Aug 30, 2026
d45c28d
Merge remote-tracking branch 'upstream/main' into feat/change-cards-u…
Sep 28, 2026
19022c0
test(visual): regenerate electron chat-dark sidebar reference for #1412
Sep 29, 2026
354a8a3
Merge upstream/main (9c069e94ef) into feat/change-cards-ui-b3b
easonliang28 Oct 12, 2026
03d35ba
style(checkpoints): prettier-format files flagged by the new format:c…
easonliang28 Oct 12, 2026
ae70bed
i18n(tr): fix possessive link in changeCardDetail disabled-state sent…
easonliang28 Oct 12, 2026
d125039
fix(checkpoints): skip task-start baseline on history resume
easonliang28 Oct 12, 2026
f1cbb2d
fix(checkpoints): skip baseline on aborted task; share default constant
easonliang28 Oct 12, 2026
3816da4
fix(checkpoints): recheck cancellation after the baseline state lookup
easonliang28 Oct 12, 2026
2330d75
fix(checkpoints): validate containment via deepest existing ancestor
easonliang28 Oct 12, 2026
d2e148c
chore: retrigger CI for infra-flake windows job (robocopy 16)
easonliang28 Oct 12, 2026
38aef30
fix(checkpoints): reject dangling symlink ancestors in restoreFile
easonliang28 Oct 12, 2026
e4fe6ba
fix(checkpoints): restoreFile must name one regular file
easonliang28 Oct 12, 2026
6ee6d04
fix(checkpoints): surface unreadable checkpoint trees instead of dele…
easonliang28 Oct 12, 2026
c636ae7
chore: retrigger CI for flaky extension-host-visual pixel drift (font…
easonliang28 Oct 12, 2026
40bd53a
fix(checkpoints): restore exactly the requested path when names conta…
easonliang28 Oct 12, 2026
aa7f9e4
fix(tools): defer the queued-message handoff until the patch checkpoi…
easonliang28 Oct 13, 2026
93a9b65
fix(task): never dequeue queued messages for an aborted task
easonliang28 Oct 13, 2026
23928ae
chore: remove carried-over Stryker suppression directives from touche…
easonliang28 Oct 14, 2026
4b0a3f6
Merge remote-tracking branch 'upstream/main' into feat/change-cards-u…
easonLiangWorldedtech Oct 14, 2026
abb6a4a
fix: satisfy unused-variable lint after main refresh
easonliang28 Oct 14, 2026
fbf9229
Merge remote-tracking branch 'upstream/main' into feat/change-cards-u…
easonLiangWorldedtech Oct 15, 2026
862e02c
Merge upstream/main (tool-call state scoping) and adapt execute spec …
easonliang28 Oct 15, 2026
0cad6a4
style: apply Prettier formatting to callback fixture edits
easonliang28 Oct 15, 2026
8f37bd3
fix: skip change-card emission for no-op checkpoints
easonliang28 Oct 15, 2026
ade3987
fix: harden change-journal append and task-ID path containment
easonliang28 Oct 15, 2026
993e740
fix: propagate non-ENOENT tail-inspection errors and complete partial…
easonliang28 Oct 15, 2026
69b60e5
fix: make journal updates serialized and atomic per checkpoint
easonliang28 Oct 16, 2026
b7f52d9
fix(checkpoints): serialize full checkpoint transactions; reject inco…
easonliang28 Oct 16, 2026
db99ae9
fix(checkpoints): harden journal durability and defer success until p…
easonliang28 Oct 16, 2026
9f38daf
fix(webview): enforce workspace containment for change-card openFile
easonliang28 Oct 16, 2026
f876ba3
fix(checkpoints): require a stable journal location for write-metadat…
easonliang28 Oct 16, 2026
5b32095
fix(checkpoints): fail loudly on lost commits and keep card diffs out…
easonliang28 Oct 16, 2026
0558c2a
fix(checkpoints): reject unverified journal tails and double-encoded …
easonliang28 Oct 16, 2026
7c63102
fix(checkpoints): re-validate restore containment before mutation; fa…
easonliang28 Oct 17, 2026
69a9ecd
Merge remote-tracking branch 'refs/remotes/upstream-refresh/main' int…
easonliang28 Oct 17, 2026
1d719f0
chore: retrigger CI (e2e-mock flake window retest)
easonliang28 Oct 17, 2026
ce867dd
fix(checkpoints): user-disabled checkpoint saves resolve silently for…
easonliang28 Oct 17, 2026
dbaea36
fix(checkpoints): review follow-ups T1-T3 (journal tail, baseline gua…
easonliang28 Oct 17, 2026
18a81e3
fix(checkpoints): review follow-ups row 11 (excluded-path ordering, n…
easonliang28 Oct 17, 2026
787c986
fix(checkpoints): recover writes, open change-card paths as named, pi…
easonliang28 Oct 17, 2026
5e92385
Merge remote-tracking branch 'upstream/main' into feat/change-cards-u…
easonliang28 Oct 17, 2026
d8b7684
chore(checkpoints): satisfy the production knip gate for the split chain
easonliang28 Oct 17, 2026
e9f0cec
chore(ci): retrigger platform-unit-test (windows/ubuntu infra flakes)
easonliang28 Oct 17, 2026
f0818eb
fix(checkpoints): flush recorded writes when post-write bookkeeping t…
easonliang28 Oct 18, 2026
78cb053
fix(checkpoints): harden task-start baseline and service init per review
easonliang28 Oct 18, 2026
6669937
fix(tools): journal move source deletion and harden catch-path flush …
easonliang28 Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "./provider-settings.js"
import { telemetrySettingsSchema } from "./telemetry.js"
import { toolNamesSchema } from "./tool.js"
import { changeCardDetailSchema, type ChangeCardDetail } from "./message.js"
import { type Keys } from "./type-fu.js"
import { languagesSchema } from "./vscode.js"
import { providerIdentifiers } from "./provider-identifiers.js"
Expand Down Expand Up @@ -111,6 +112,21 @@ export const MAX_CHECKPOINT_TIMEOUT_SECONDS = 60
*/
export const DEFAULT_CHECKPOINT_TIMEOUT_SECONDS = 15

/**
* Whether per-write checkpoints and task-start baseline are enabled by default.
* Master switch for the B cluster of checkpoint features.
* @default true
*/
export const DEFAULT_PER_WRITE_CHECKPOINTS = true

/**
* Default detail level for per-step change cards (B3a).
* "summary" keeps cards compact (file list with +/− counts; the UI fetches
* diffs lazily); "full" carries the unified diff inline per file.
* @default "summary"
*/
export const DEFAULT_CHANGE_CARD_DETAIL: ChangeCardDetail = "summary"

/**
* GlobalSettings
*/
Expand Down Expand Up @@ -220,6 +236,19 @@ export const globalSettingsSchema = z.object({
.min(MIN_CHECKPOINT_TIMEOUT_SECONDS)
.max(MAX_CHECKPOINT_TIMEOUT_SECONDS)
.optional(),
/**
* Whether to record a shadow-git checkpoint after every successful write_to_file,
* edit_file, and apply_patch (per-write checkpoints), plus a task-start baseline.
* @default true
*/
perWriteCheckpoints: z.boolean().optional(),
/**
* Detail level for per-step change cards: "full" includes the unified diff
* inline for every changed file, "summary" carries only the file list with
* +/− counts (diffs are fetched lazily by the UI).
* @default "summary"
*/
changeCardDetail: changeCardDetailSchema.optional(),

ttsEnabled: z.boolean().optional(),
ttsSpeed: z.number().optional(),
Expand Down
45 changes: 45 additions & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
* - `mcp_server_response`: Response received from MCP server
* - `subtask_result`: Result of a completed subtask
* - `checkpoint_saved`: Indicates a checkpoint has been saved
* - `change_card`: Per-step change card summarizing the files a completed tool step wrote (B3a)
* - `rooignore_error`: Error related to .rooignore file processing
* - `diff_error`: Error occurred while applying a diff/patch
* - `condense_context`: Context condensation/summarization has started
Expand Down Expand Up @@ -163,6 +164,7 @@ export const clineSays = [
"mcp_server_response",
"subtask_result",
"checkpoint_saved",
"change_card",
"rooignore_error",
"diff_error",
"condense_context",
Expand Down Expand Up @@ -237,6 +239,49 @@ export const contextTruncationSchema = z.object({

export type ContextTruncation = z.infer<typeof contextTruncationSchema>

/**
* ChangeCard
*
* Payload of the per-step change card (B3a). The extension host emits one
* `say: "change_card"` message per completed tool write step, keyed by the
* shadow-git checkpoint the step produced. The JSON payload (see
* {@link ChangeCardData}) is carried in the message `text` field, the same
* way tool approval messages carry their serialized ClineSayTool.
*
* `detail: "full"` carries the unified diff inline for every file so the UI
* can render it directly; `detail: "summary"` carries only the file list with
* +/− counts and the UI fetches diffs lazily (B3b). Auto-approved steps are
* always emitted with `detail: "summary"` regardless of the user setting.
*/
export const changeCardDetailSchema = z.enum(["full", "summary"])

export type ChangeCardDetail = z.infer<typeof changeCardDetailSchema>

export const changeCardFileSchema = z.object({
path: z.string(),
additions: z.number(),
deletions: z.number(),
/**
* Unified diff for this file. Only present when the card was emitted with
* `detail: "full"`; summary cards leave it out to stay compact.
*/
diff: z.string().optional(),
})

export type ChangeCardFile = z.infer<typeof changeCardFileSchema>

export const changeCardSchema = z.object({
/** Opaque step identifier, reserved for future tool-step tracking. */
stepId: z.string().optional(),
/** Checkpoint commit SHAs produced by the step (one per per-write checkpoint). */
checkpointIds: z.array(z.string()),
files: z.array(changeCardFileSchema),
totalFiles: z.number(),
detail: changeCardDetailSchema,
})

export type ChangeCardData = z.infer<typeof changeCardSchema>

/**
* ClineMessage
*
Expand Down
79 changes: 78 additions & 1 deletion packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ProviderSettings, ProviderSettingsEntry } from "./provider-setting
import type { HistoryItem } from "./history.js"
import type { ModeConfig, PromptComponent } from "./mode.js"
import type { Experiments } from "./experiment.js"
import type { ClineMessage, QueuedMessage } from "./message.js"
import type { ChangeCardDetail, ClineMessage, QueuedMessage } from "./message.js"
import type { MarketplaceItem, MarketplaceInstalledMetadata, InstallMarketplaceItemOptions } from "./marketplace.js"
import type { TodoItem } from "./todo.js"
import type { CloudUserInfo, CloudOrganizationMembership, OrganizationAllowList, ShareVisibility } from "./cloud.js"
Expand Down Expand Up @@ -111,6 +111,7 @@ export interface ExtensionMessage {
| "originalContent"
| "rooHistoryImportProgress"
| "themeFixtureProbeRequest"
| "checkpointRollbackResult"
text?: string
/** For fileContent: { path, content, error? } */
fileContent?: { path: string; content: string | null; error?: string }
Expand Down Expand Up @@ -268,6 +269,34 @@ export interface ExtensionMessage {
copyProgressItemName?: string
// folderSelected
path?: string
/** For checkpointRollbackResult: outcome of a change-card rollback request (B3b). */
checkpointRollbackResult?: CheckpointRollbackResult
}

/**
* CheckpointRollbackResult
*
* Outcome of a change-card restore request (B3b), posted back to the webview
* that sent `checkpointRollbackFile` / `checkpointRollbackStep` /
* `checkpointRestoreLatestFile`. `cardTs` echoes the change-card message
* timestamp so the requesting card can correlate the result: per-file results
* carry `filePath`, per-step results carry the per-file outcomes in
* `files`, and `kind` tells a per-file result which control it belongs to
* (absent = rollback, so results posted before `kind` existed still route).
*/
export interface CheckpointRollbackResult {
/** The `ts` of the change_card message the result belongs to. */
cardTs: number
/** Per-file scope: the file that was restored. */
filePath?: string
success: boolean
error?: string
/** Per-step scope: the per-file outcomes. */
files?: { filePath: string; success: boolean; error?: string }[]
/** Per-file scope: which control the result belongs to. Absent = rollback. */
kind?: "rollback" | "restore-latest"
/** Per-file scope: true when a restore-latest found no recorded write and left the file as-is. */
noOp?: boolean
}

export interface OpenAiCodexRateLimitsMessage {
Expand Down Expand Up @@ -363,6 +392,8 @@ export type ExtensionState = Pick<

enableCheckpoints: boolean
checkpointTimeout: number // Timeout for checkpoint initialization in seconds (default: 15)
perWriteCheckpoints: boolean
changeCardDetail: ChangeCardDetail
maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)
maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500)
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
Expand Down Expand Up @@ -565,6 +596,9 @@ export interface WebviewMessage {
| "openCustomModesSettings"
| "checkpointDiff"
| "checkpointRestore"
| "checkpointRollbackFile"
| "checkpointRollbackStep"
| "checkpointRestoreLatestFile"
| "completionCheckpointDiff"
| "completionCheckpointRestore"
| "deleteMcpServer"
Expand Down Expand Up @@ -808,6 +842,46 @@ export const checkoutRestorePayloadSchema = z.object({

export type CheckpointRestorePayload = z.infer<typeof checkoutRestorePayloadSchema>

/**
* Payload of the `checkpointRollbackFile` webview message (B3b): restore one
* change-card file to the checkpoint commit the card was keyed by.
*/
export const checkpointRollbackFilePayloadSchema = z.object({
/** The `ts` of the change_card message the request comes from (echoed on the result). */
cardTs: z.number(),
checkpointId: z.string(),
filePath: z.string(),
})

export type CheckpointRollbackFilePayload = z.infer<typeof checkpointRollbackFilePayloadSchema>

/**
* Payload of the `checkpointRollbackStep` webview message (B3b): restore
* every file of a change-card step to the step's checkpoint.
*/
export const checkpointRollbackStepPayloadSchema = z.object({
cardTs: z.number(),
/** The step's checkpoint commit (the card's first checkpointId); optional. */
checkpointId: z.string().optional(),
filePaths: z.array(z.string()).min(1),
})

export type CheckpointRollbackStepPayload = z.infer<typeof checkpointRollbackStepPayloadSchema>

/**
* Payload of the `checkpointRestoreLatestFile` webview message (B3b): restore
* one change-card file to the latest recorded version of that file (the
* content of its most recent write checkpoint — the forward direction to a
* rollback).
*/
export const checkpointRestoreLatestFilePayloadSchema = z.object({
/** The `ts` of the change_card message the request comes from (echoed on the result). */
cardTs: z.number(),
filePath: z.string(),
})

export type CheckpointRestoreLatestFilePayload = z.infer<typeof checkpointRestoreLatestFilePayloadSchema>

export interface IndexingStatusPayload {
state: "Standby" | "Indexing" | "Indexed" | "Error" | "Stopping"
message: string
Expand All @@ -821,6 +895,9 @@ export interface IndexClearedPayload {
export type WebViewMessagePayload =
| CheckpointDiffPayload
| CheckpointRestorePayload
| CheckpointRollbackFilePayload
| CheckpointRollbackStepPayload
| CheckpointRestoreLatestFilePayload
| IndexingStatusPayload
| IndexClearedPayload
| UpdateTodoListPayload
Expand Down
108 changes: 108 additions & 0 deletions src/core/checkpoints/__tests__/changeCard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, expect, it } from "vitest"

import type { ChangeCardData } from "@roo-code/types"

import {
buildChangeCard,
buildChangeCardPayload,
isAutoApprovedStep,
resolveChangeCardDetail,
type ChangeCardWrite,
} from "../changeCard"

describe("changeCard (B3a)", () => {
function write(overrides: Partial<ChangeCardWrite> = {}): ChangeCardWrite {
return {
path: "src/a.ts",
diffStats: { additions: 2, deletions: 1 },
diff: "--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1,2 @@\n-old\n+new-1\n+new-2",
...overrides,
}
}

describe("isAutoApprovedStep", () => {
it("returns false for an empty step", () => {
expect(isAutoApprovedStep([])).toBe(false)
})

it("returns true only when every write was auto-approved", () => {
expect(isAutoApprovedStep([write({ autoApproved: true }), write({ autoApproved: true })])).toBe(true)
expect(isAutoApprovedStep([write({ autoApproved: true }), write()])).toBe(false)
expect(isAutoApprovedStep([write()])).toBe(false)
})
})

describe("resolveChangeCardDetail", () => {
it("forces summary for auto-approved steps even when the setting is full", () => {
const writes = [write({ autoApproved: true })]
expect(resolveChangeCardDetail(writes, "full")).toBe("summary")
expect(resolveChangeCardDetail(writes, undefined)).toBe("summary")
})

it("follows the setting for interactive steps, defaulting to summary when unset", () => {
const writes = [write()]
expect(resolveChangeCardDetail(writes, "full")).toBe("full")
expect(resolveChangeCardDetail(writes, "summary")).toBe("summary")
expect(resolveChangeCardDetail(writes, undefined)).toBe("summary")
})
})

describe("buildChangeCard", () => {
it("carries the inline diff per file for full detail on a multi-file step", () => {
const card = buildChangeCard(
"sha-1",
[write(), write({ path: "src/b.ts", diffStats: { additions: 1, deletions: 0 }, diff: "+b" })],
"full",
)

expect(card).toEqual({
checkpointIds: ["sha-1"],
files: [
{
path: "src/a.ts",
additions: 2,
deletions: 1,
diff: "--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1,2 @@\n-old\n+new-1\n+new-2",
},
{ path: "src/b.ts", additions: 1, deletions: 0, diff: "+b" },
],
totalFiles: 2,
detail: "full",
})
})

it("omits the diff per file for summary detail (lazy fetch is B3b)", () => {
const card = buildChangeCard("sha-1", [write()], "summary")

expect(card.files).toEqual([{ path: "src/a.ts", additions: 2, deletions: 1 }])
expect(card.files[0]).not.toHaveProperty("diff")
expect(card.detail).toBe("summary")
expect(card.totalFiles).toBe(1)
})

it("defaults missing diffStats to zero counts and keeps full detail without diff for a write without one", () => {
const card = buildChangeCard("sha-1", [write({ diffStats: undefined, diff: undefined })], "full")

expect(card.files[0]).toEqual({ path: "src/a.ts", additions: 0, deletions: 0 })
})
})

describe("buildChangeCardPayload", () => {
it("resolves the detail level and builds the payload in one call", () => {
// The expectations are typed against the shared ChangeCardData
// contract in @roo-code/types, so the builder's output is checked
// against the same single source of truth the webview consumes.
// Interactive step with the full setting: diff inline.
const full: ChangeCardData = buildChangeCardPayload("sha-1", [write()], "full")
expect(full.detail).toBe("full")
expect(full.files[0].diff).toBe("--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1,2 @@\n-old\n+new-1\n+new-2")

// Auto-approved step with the full setting: compact summary, no diff.
const compact: ChangeCardData = buildChangeCardPayload("sha-1", [write({ autoApproved: true })], "full")
expect(compact.detail).toBe("summary")
expect(compact.files[0]).not.toHaveProperty("diff")
expect(compact.checkpointIds).toEqual(["sha-1"])
expect(compact.totalFiles).toBe(1)
})
})
})
Loading
Loading