Skip to content

Commit 346c658

Browse files
feat(checkpoints): land the unit on upstream/main 332d30f
Refresh (task-106): the unit tree carries this branch's content merged with upstream/main 332d30f at the two conflicting paths, resolved as a union that keeps each side's payload. Shaped as a one-parent commit so the mutation gate prices the unit delta against main rather than main's own payload (mutation-testing.yml lines 52-56).
1 parent 332d30f commit 346c658

51 files changed

Lines changed: 6366 additions & 76 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎knip.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"playwright-ct": false,
1717
"workspaces": {
1818
"src": {
19-
"entry": ["extension.ts!", "extension/api.ts!", "workers/countTokens.ts!"],
19+
"entry": ["extension.ts!", "extension/api.ts!", "workers/countTokens.ts!", "core/checkpoints/rollback.ts!"],
2020
"project": [
2121
"**/*.ts!",
2222
"!**/__tests__/**!",

‎packages/types/src/global-settings.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "./provider-settings.js"
1313
import { telemetrySettingsSchema } from "./telemetry.js"
1414
import { toolNamesSchema } from "./tool.js"
15+
import { changeCardDetailSchema, type ChangeCardDetail } from "./message.js"
1516
import { type Keys } from "./type-fu.js"
1617
import { languagesSchema } from "./vscode.js"
1718
import { providerIdentifiers } from "./provider-identifiers.js"
@@ -114,6 +115,21 @@ export const MAX_CHECKPOINT_TIMEOUT_SECONDS = 60
114115
*/
115116
export const DEFAULT_CHECKPOINT_TIMEOUT_SECONDS = 15
116117

118+
/**
119+
* Whether per-write checkpoints and task-start baseline are enabled by default.
120+
* Master switch for the B cluster of checkpoint features.
121+
* @default true
122+
*/
123+
export const DEFAULT_PER_WRITE_CHECKPOINTS = true
124+
125+
/**
126+
* Default detail level for per-step change cards (B3a).
127+
* "summary" keeps cards compact (file list with +/− counts; the UI fetches
128+
* diffs lazily); "full" carries the unified diff inline per file.
129+
* @default "summary"
130+
*/
131+
export const DEFAULT_CHANGE_CARD_DETAIL: ChangeCardDetail = "summary"
132+
117133
/**
118134
* GlobalSettings
119135
*/
@@ -223,6 +239,19 @@ export const globalSettingsSchema = z.object({
223239
.min(MIN_CHECKPOINT_TIMEOUT_SECONDS)
224240
.max(MAX_CHECKPOINT_TIMEOUT_SECONDS)
225241
.optional(),
242+
/**
243+
* Whether to record a shadow-git checkpoint after every successful write_to_file,
244+
* edit_file, and apply_patch (per-write checkpoints), plus a task-start baseline.
245+
* @default true
246+
*/
247+
perWriteCheckpoints: z.boolean().optional(),
248+
/**
249+
* Detail level for per-step change cards: "full" includes the unified diff
250+
* inline for every changed file, "summary" carries only the file list with
251+
* +/− counts (diffs are fetched lazily by the UI).
252+
* @default "summary"
253+
*/
254+
changeCardDetail: changeCardDetailSchema.optional(),
226255

227256
ttsEnabled: z.boolean().optional(),
228257
ttsSpeed: z.number().optional(),

‎packages/types/src/message.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
134134
* - `mcp_server_response`: Response received from MCP server
135135
* - `subtask_result`: Result of a completed subtask
136136
* - `checkpoint_saved`: Indicates a checkpoint has been saved
137+
* - `change_card`: Per-step change card summarizing the files a completed tool step wrote (B3a)
137138
* - `rooignore_error`: Error related to .rooignore file processing
138139
* - `diff_error`: Error occurred while applying a diff/patch
139140
* - `condense_context`: Context condensation/summarization has started
@@ -163,6 +164,7 @@ export const clineSays = [
163164
"mcp_server_response",
164165
"subtask_result",
165166
"checkpoint_saved",
167+
"change_card",
166168
"rooignore_error",
167169
"diff_error",
168170
"condense_context",
@@ -237,6 +239,49 @@ export const contextTruncationSchema = z.object({
237239

238240
export type ContextTruncation = z.infer<typeof contextTruncationSchema>
239241

242+
/**
243+
* ChangeCard
244+
*
245+
* Payload of the per-step change card (B3a). The extension host emits one
246+
* `say: "change_card"` message per completed tool write step, keyed by the
247+
* shadow-git checkpoint the step produced. The JSON payload (see
248+
* {@link ChangeCardData}) is carried in the message `text` field, the same
249+
* way tool approval messages carry their serialized ClineSayTool.
250+
*
251+
* `detail: "full"` carries the unified diff inline for every file so the UI
252+
* can render it directly; `detail: "summary"` carries only the file list with
253+
* +/− counts and the UI fetches diffs lazily (B3b). Auto-approved steps are
254+
* always emitted with `detail: "summary"` regardless of the user setting.
255+
*/
256+
export const changeCardDetailSchema = z.enum(["full", "summary"])
257+
258+
export type ChangeCardDetail = z.infer<typeof changeCardDetailSchema>
259+
260+
export const changeCardFileSchema = z.object({
261+
path: z.string(),
262+
additions: z.number(),
263+
deletions: z.number(),
264+
/**
265+
* Unified diff for this file. Only present when the card was emitted with
266+
* `detail: "full"`; summary cards leave it out to stay compact.
267+
*/
268+
diff: z.string().optional(),
269+
})
270+
271+
export type ChangeCardFile = z.infer<typeof changeCardFileSchema>
272+
273+
export const changeCardSchema = z.object({
274+
/** Opaque step identifier, reserved for future tool-step tracking. */
275+
stepId: z.string().optional(),
276+
/** Checkpoint commit SHAs produced by the step (one per per-write checkpoint). */
277+
checkpointIds: z.array(z.string()),
278+
files: z.array(changeCardFileSchema),
279+
totalFiles: z.number(),
280+
detail: changeCardDetailSchema,
281+
})
282+
283+
export type ChangeCardData = z.infer<typeof changeCardSchema>
284+
240285
/**
241286
* ClineMessage
242287
*

‎packages/types/src/vscode-extension-host.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { ProviderSettings, ProviderSettingsEntry } from "./provider-setting
55
import type { HistoryItem } from "./history.js"
66
import type { ModeConfig, PromptComponent } from "./mode.js"
77
import type { Experiments } from "./experiment.js"
8-
import type { ClineMessage, QueuedMessage } from "./message.js"
8+
import type { ChangeCardDetail, ClineMessage, QueuedMessage } from "./message.js"
99
import type { MarketplaceItem, MarketplaceInstalledMetadata, InstallMarketplaceItemOptions } from "./marketplace.js"
1010
import type { TodoItem } from "./todo.js"
1111
import type { CloudUserInfo, CloudOrganizationMembership, OrganizationAllowList, ShareVisibility } from "./cloud.js"
@@ -370,6 +370,8 @@ export type ExtensionState = Pick<
370370

371371
enableCheckpoints: boolean
372372
checkpointTimeout: number // Timeout for checkpoint initialization in seconds (default: 15)
373+
perWriteCheckpoints: boolean
374+
changeCardDetail: ChangeCardDetail
373375
maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)
374376
maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500)
375377
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, expect, it } from "vitest"
2+
3+
import type { ChangeCardData } from "@roo-code/types"
4+
5+
import {
6+
buildChangeCard,
7+
buildChangeCardPayload,
8+
isAutoApprovedStep,
9+
resolveChangeCardDetail,
10+
type ChangeCardWrite,
11+
} from "../changeCard"
12+
13+
describe("changeCard (B3a)", () => {
14+
function write(overrides: Partial<ChangeCardWrite> = {}): ChangeCardWrite {
15+
return {
16+
path: "src/a.ts",
17+
diffStats: { additions: 2, deletions: 1 },
18+
diff: "--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1,2 @@\n-old\n+new-1\n+new-2",
19+
...overrides,
20+
}
21+
}
22+
23+
describe("isAutoApprovedStep", () => {
24+
it("returns false for an empty step", () => {
25+
expect(isAutoApprovedStep([])).toBe(false)
26+
})
27+
28+
it("returns true only when every write was auto-approved", () => {
29+
expect(isAutoApprovedStep([write({ autoApproved: true }), write({ autoApproved: true })])).toBe(true)
30+
expect(isAutoApprovedStep([write({ autoApproved: true }), write()])).toBe(false)
31+
expect(isAutoApprovedStep([write()])).toBe(false)
32+
})
33+
})
34+
35+
describe("resolveChangeCardDetail", () => {
36+
it("forces summary for auto-approved steps even when the setting is full", () => {
37+
const writes = [write({ autoApproved: true })]
38+
expect(resolveChangeCardDetail(writes, "full")).toBe("summary")
39+
expect(resolveChangeCardDetail(writes, undefined)).toBe("summary")
40+
})
41+
42+
it("follows the setting for interactive steps, defaulting to summary when unset", () => {
43+
const writes = [write()]
44+
expect(resolveChangeCardDetail(writes, "full")).toBe("full")
45+
expect(resolveChangeCardDetail(writes, "summary")).toBe("summary")
46+
expect(resolveChangeCardDetail(writes, undefined)).toBe("summary")
47+
})
48+
})
49+
50+
describe("buildChangeCard", () => {
51+
it("carries the inline diff per file for full detail on a multi-file step", () => {
52+
const card = buildChangeCard(
53+
"sha-1",
54+
[write(), write({ path: "src/b.ts", diffStats: { additions: 1, deletions: 0 }, diff: "+b" })],
55+
"full",
56+
)
57+
58+
expect(card).toEqual({
59+
checkpointIds: ["sha-1"],
60+
files: [
61+
{
62+
path: "src/a.ts",
63+
additions: 2,
64+
deletions: 1,
65+
diff: "--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1,2 @@\n-old\n+new-1\n+new-2",
66+
},
67+
{ path: "src/b.ts", additions: 1, deletions: 0, diff: "+b" },
68+
],
69+
totalFiles: 2,
70+
detail: "full",
71+
})
72+
})
73+
74+
it("omits the diff per file for summary detail (lazy fetch is B3b)", () => {
75+
const card = buildChangeCard("sha-1", [write()], "summary")
76+
77+
expect(card.files).toEqual([{ path: "src/a.ts", additions: 2, deletions: 1 }])
78+
expect(card.files[0]).not.toHaveProperty("diff")
79+
expect(card.detail).toBe("summary")
80+
expect(card.totalFiles).toBe(1)
81+
})
82+
83+
it("defaults missing diffStats to zero counts and keeps full detail without diff for a write without one", () => {
84+
const card = buildChangeCard("sha-1", [write({ diffStats: undefined, diff: undefined })], "full")
85+
86+
expect(card.files[0]).toEqual({ path: "src/a.ts", additions: 0, deletions: 0 })
87+
})
88+
})
89+
90+
describe("buildChangeCardPayload", () => {
91+
it("resolves the detail level and builds the payload in one call", () => {
92+
// The expectations are typed against the shared ChangeCardData
93+
// contract in @roo-code/types, so the builder's output is checked
94+
// against the same single source of truth the webview consumes.
95+
// Interactive step with the full setting: diff inline.
96+
const full: ChangeCardData = buildChangeCardPayload("sha-1", [write()], "full")
97+
expect(full.detail).toBe("full")
98+
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")
99+
100+
// Auto-approved step with the full setting: compact summary, no diff.
101+
const compact: ChangeCardData = buildChangeCardPayload("sha-1", [write({ autoApproved: true })], "full")
102+
expect(compact.detail).toBe("summary")
103+
expect(compact.files[0]).not.toHaveProperty("diff")
104+
expect(compact.checkpointIds).toEqual(["sha-1"])
105+
expect(compact.totalFiles).toBe(1)
106+
})
107+
})
108+
})

0 commit comments

Comments
 (0)