Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 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
9b0787a
fix(fws): test the pre-hydration change-card detail default
easonliang28 Aug 28, 2026
1f36d80
feat(fws): record per-write checkpoint and change card for apply_diff…
easonliang28 Aug 28, 2026
94fea2f
test(fws): keep the apply_diff change-card spec green on the trial st…
easonliang28 Aug 28, 2026
341a9c2
test(fws): assert the perWriteCheckpoints initializer default alongsi…
easonliang28 Aug 28, 2026
67d8525
feat(fws): edit and search_replace writes now record the per-write ch…
easonliang28 Aug 29, 2026
c202745
fix(fws): type the settings Slider test double and correct the nl cha…
easonliang28 Aug 29, 2026
8c5c53b
test(fws): type the VSCodeCheckbox/VSCodeLink test doubles in the set…
easonliang28 Aug 29, 2026
0d31ea0
chore(ci): empty commit — re-trigger CI and the CodeRabbit current-he…
easonliang28 Aug 30, 2026
0c11570
fix(settings): address CodeRabbit review feedback (PR 1411)
easonliang28 Aug 30, 2026
fb9aa06
Merge remote-tracking branch 'upstream/main' into feat/change-cards-b3a
Sep 28, 2026
86b3c25
test(visual): regenerate electron chat-dark sidebar reference for #1411
Sep 29, 2026
39c1eff
Merge upstream/main (9c069e94ef) into feat/change-cards-b3a
easonliang28 Oct 12, 2026
c72617c
style(checkpoints): prettier-format files flagged by the new format:c…
easonliang28 Oct 12, 2026
9633e18
i18n(tr): fix possessive link in changeCardDetail disabled-state sent…
easonliang28 Oct 12, 2026
c3bef96
fix(checkpoints): skip task-start baseline on history resume
easonliang28 Oct 12, 2026
f73f38c
fix(checkpoints): skip baseline on aborted task; share default constant
easonliang28 Oct 12, 2026
525c023
fix(checkpoints): recheck cancellation after the baseline state lookup
easonliang28 Oct 12, 2026
578d8fb
fix(tools): defer the queued-message handoff until the patch checkpoi…
easonliang28 Oct 13, 2026
e0f15d2
fix(task): never dequeue queued messages for an aborted task
easonliang28 Oct 13, 2026
2ef4573
chore: remove carried-over Stryker suppression directives from touche…
easonliang28 Oct 14, 2026
f37b9bf
Merge remote-tracking branch 'upstream/main' into feat/change-cards-b3a
easonLiangWorldedtech Oct 14, 2026
1ee917c
fix: satisfy unused-variable lint after main refresh
easonliang28 Oct 14, 2026
d1998f9
test: de-indent the delete-patch fixture markers
easonliang28 Oct 15, 2026
7191d82
Merge remote-tracking branch 'upstream/main' into feat/change-cards-b3a
easonLiangWorldedtech Oct 15, 2026
e25d427
Merge upstream/main (tool-call state scoping) and adapt execute spec …
easonliang28 Oct 15, 2026
047595e
style: apply Prettier formatting to callback fixture edits
easonliang28 Oct 15, 2026
1790ba3
test: cover the change-card Zod contracts directly
easonliang28 Oct 15, 2026
501d206
fix: skip change-card emission for no-op checkpoints
easonliang28 Oct 15, 2026
eae754b
fix: harden change-journal append and task-ID path containment
easonliang28 Oct 15, 2026
b66edb1
fix: propagate non-ENOENT tail-inspection errors and complete partial…
easonliang28 Oct 15, 2026
edeb407
fix: validate journal entry shape on load and sync the new file's dir…
easonliang28 Oct 16, 2026
ce72f5c
fix: make journal updates serialized and atomic per checkpoint
easonliang28 Oct 16, 2026
8148928
fix(checkpoints): typed partial-persistence errors; explicit tool fai…
easonliang28 Oct 16, 2026
f7da3b1
fix(checkpoints): harden journal durability and defer success until p…
easonliang28 Oct 16, 2026
fca9a55
fix(checkpoints): require a stable journal location for write-metadat…
easonliang28 Oct 16, 2026
4a0799b
fix(checkpoints): fail loudly on lost commits and keep card diffs out…
easonliang28 Oct 16, 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions packages/types/src/__tests__/message.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// pnpm --filter @roo-code/types test src/__tests__/message.test.ts

import {
changeCardDetailSchema,
changeCardFileSchema,
changeCardSchema,
clineAsks,
clineMessageSchema,
getCompletionCheckpoint,
Expand Down Expand Up @@ -78,3 +81,52 @@ describe("getCompletionCheckpoint", () => {
expect(getCompletionCheckpoint(messages)).toBeUndefined()
})
})

describe("change-card schemas", () => {
it("accepts a valid full-detail change card", () => {
const card = {
stepId: "step-1",
checkpointIds: ["abc123"],
files: [{ path: "src/a.ts", additions: 3, deletions: 1, diff: "@@ -1 +1 @@" }],
totalFiles: 1,
detail: "full",
}
expect(changeCardSchema.parse(card)).toEqual(card)
})

it("accepts a summary card without stepId and without per-file diffs", () => {
const card = {
checkpointIds: [],
files: [{ path: "src/b.ts", additions: 0, deletions: 2 }],
totalFiles: 1,
detail: "summary",
}
expect(changeCardSchema.parse(card)).toEqual(card)
})

it("rejects invalid detail values", () => {
expect(() => changeCardDetailSchema.parse("detailed")).toThrow()
expect(() => changeCardDetailSchema.parse(undefined)).toThrow()
expect(() => changeCardSchema.parse({ checkpointIds: [], files: [], totalFiles: 0, detail: "diff" })).toThrow()
})

it("rejects incomplete file entries", () => {
// Missing additions/deletions.
expect(() => changeCardFileSchema.parse({ path: "src/a.ts" })).toThrow()
// Missing path.
expect(() => changeCardFileSchema.parse({ additions: 1, deletions: 0 })).toThrow()
// Wrong stat type.
expect(() => changeCardFileSchema.parse({ path: "src/a.ts", additions: "1", deletions: 0 })).toThrow()
})

it("rejects incomplete payloads", () => {
// Missing checkpointIds.
expect(() => changeCardSchema.parse({ files: [], totalFiles: 1, detail: "full" })).toThrow()
// Missing files.
expect(() => changeCardSchema.parse({ checkpointIds: [], totalFiles: 1, detail: "full" })).toThrow()
// Missing totalFiles.
expect(() => changeCardSchema.parse({ checkpointIds: [], files: [], detail: "full" })).toThrow()
// Missing detail.
expect(() => changeCardSchema.parse({ checkpointIds: [], files: [], totalFiles: 1 })).toThrow()
})
})
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"])
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
4 changes: 3 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 @@ -352,6 +352,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
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")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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