Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
e11ef14
fix(mcp): preserve concurrent MCP settings during initial creation (f…
easonliang28 Aug 27, 2026
4ab3bf4
perf(write-path): remove artificial write delays by default (part of …
easonliang28 Aug 27, 2026
352df16
fix(task): guard saveClineMessages against abandoned tasks (fixes #1021)
easonliang28 Aug 27, 2026
3f8db83
test(webview): cover writeDelayMs in getStateToPostToWebview
easonliang28 Aug 27, 2026
ce34d48
test(task): document double assertion in abandoned-guard test
easonliang28 Aug 27, 2026
332253a
test(mcp): cover getMcpSettingsFilePath fallback branches
easonliang28 Aug 27, 2026
44b6791
test(dv): assert default saveChanges delay via DEFAULT_WRITE_DELAY_MS
easonliang28 Aug 27, 2026
131d18d
feat(file-safety): add version token for the guarded-write path (A1, …
easonliang28 Aug 27, 2026
13188d2
docs(file-safety): correct ino precision bounds in version token (A1,…
easonliang28 Aug 27, 2026
2c1582b
fix(file-safety): derive the version token from exact BigInt stats (A…
easonliang28 Aug 27, 2026
a3c6f86
feat(experiments): make chat-diff the default approval path (L2, #1375)
easonliang28 Aug 27, 2026
165bff4
test(experiments): cover unset-experiment default routing in all writ…
easonliang28 Aug 27, 2026
c0ef476
test(experiments): add explicit enabled-state routing tests for chat-…
easonliang28 Aug 27, 2026
2965ad1
feat(task): per-task file observation registry (A2, #1375)
easonliang28 Aug 27, 2026
a37dd24
feat(file-safety): atomic text publish primitive + safeWriteJson refa…
easonliang28 Aug 27, 2026
991ab69
feat(editor): async post-save diagnostics on chat-diff save path (L1,…
easonliang28 Aug 27, 2026
3dd8700
feat(file-safety): add version token for the guarded-write path (A1, …
easonliang28 Aug 27, 2026
ba1332e
docs(file-safety): correct ino precision bounds in version token (A1,…
easonliang28 Aug 27, 2026
6813013
fix(file-safety): derive the version token from exact BigInt stats (A…
easonliang28 Aug 27, 2026
588b95f
feat(task): per-task file observation registry (A2, #1375)
easonliang28 Aug 27, 2026
7a25fc0
feat(tools): guarded write CAS core with per-path FIFO chain (S4a, #1…
easonliang28 Aug 27, 2026
68be264
feat(tools): wire guarded writes into the diff-view save paths (S4b, …
easonliang28 Aug 27, 2026
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
4c16c00
feat(webview): change cards UI and rollback buttons (B3b, #1375)
easonliang28 Aug 27, 2026
c3f4484
merge: file-write-safety trial — include PR #1380
easonliang28 Aug 28, 2026
490f27f
merge: file-write-safety trial — include PR #1381
easonliang28 Aug 28, 2026
95f3023
merge: file-write-safety trial — include PR #1382
easonliang28 Aug 28, 2026
3afa66a
merge: file-write-safety trial — include PR #1383
easonliang28 Aug 28, 2026
558afa7
merge: file-write-safety trial — include PR #1395
easonliang28 Aug 28, 2026
71d3ab5
merge: file-write-safety trial — include PR #1394
easonliang28 Aug 28, 2026
d786497
merge: file-write-safety trial — include PR #1405
easonliang28 Aug 28, 2026
d8cf26d
merge: file-write-safety trial — include PR #1403
easonliang28 Aug 28, 2026
8fcf0a9
merge: file-write-safety trial — include PR #1384
easonliang28 Aug 28, 2026
02cad7e
merge: file-write-safety trial — include PR #1408 (S4b guarded-write …
easonliang28 Aug 28, 2026
201174a
merge: file-write-safety trial — include PR #1412 (B stack: B1+B2+B3a…
easonliang28 Aug 28, 2026
6a0feb1
test(trial): reconcile write-tool specs with the composed L2 default …
easonliang28 Aug 28, 2026
376e013
test(trial): compose the L2 default with the edit/write specs and rec…
easonliang28 Aug 28, 2026
178e6f4
fix(fws): address CodeRabbit findings on the trial aggregate
easonliang28 Aug 28, 2026
d2239ce
fix(fws): contain symlinked restoreFile targets (CodeRabbit security …
easonliang28 Aug 28, 2026
4fc14c4
fix(fws): address CodeRabbit round-4 findings (unknown guard + pre-hy…
easonliang28 Aug 28, 2026
0e021ef
fix(fws): change-card step resolution, Tailwind v4 grow, sibling sett…
easonliang28 Aug 28, 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
7f6d136
feat(fws): add per-file open-in-editor control to change cards (B3b, …
easonliang28 Aug 28, 2026
94fea2f
test(fws): keep the apply_diff change-card spec green on the trial st…
easonliang28 Aug 28, 2026
aa1a214
merge: file-write-safety trial — re-include updated PR #1411 (apply_d…
easonliang28 Aug 28, 2026
3c5dfa3
merge: file-write-safety trial — re-include updated PR #1412 (change-…
easonliang28 Aug 28, 2026
c48522c
fix(fws): use a native button for the compact-row open-file control (…
easonliang28 Aug 28, 2026
83d83a3
merge: file-write-safety trial — re-include updated PR #1412 (native-…
easonliang28 Aug 28, 2026
341a9c2
test(fws): assert the perWriteCheckpoints initializer default alongsi…
easonliang28 Aug 28, 2026
502f8ca
fix(fws): type VSCodeCheckbox change events and the settings test dou…
easonliang28 Aug 28, 2026
2bd79a4
merge: file-write-safety trial — re-include updated PR #1411 (perWrit…
easonliang28 Aug 28, 2026
eaabaa0
merge: file-write-safety trial — re-include updated PR #1412 (typed c…
easonliang28 Aug 28, 2026
67d8525
feat(fws): edit and search_replace writes now record the per-write ch…
easonliang28 Aug 29, 2026
d6d08e8
merge: file-write-safety trial — re-include updated PR #1411 (edit + …
easonliang28 Aug 29, 2026
f0f3313
fix(checkpoints): roll change-card files back to their pre-step state…
easonliang28 Aug 29, 2026
7591bc8
feat(webview): add per-file restore-latest to change cards and correc…
easonliang28 Aug 29, 2026
779cb4b
ci: retry mocked E2E (flaky 30s timeout in subtask approvals test, no…
easonliang28 Aug 29, 2026
22a3f33
merge: file-write-safety trial — addendum 10 (CodeRabbit review fixes…
easonliang28 Aug 29, 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
37 changes: 34 additions & 3 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ 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"

/**
* Default delay in milliseconds after writes to allow diagnostics to detect potential problems.
* This delay is particularly important for Go and other languages where tools like goimports
* need time to automatically clean up unused imports.
* Defaults to 0: the write path adds no artificial pacing by default, and post-save
* diagnostics are reported after the (zero) delay. Users who rely on auto-formatters that
* settle asynchronously (e.g. goimports for Go) can raise this setting to give formatters
* time to settle before diagnostics are captured.
*/
export const DEFAULT_WRITE_DELAY_MS = 1000
export const DEFAULT_WRITE_DELAY_MS = 0
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Default values for the "auto-close files Zoo opened" settings.
Expand Down Expand Up @@ -99,6 +102,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 @@ -200,6 +218,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 @@ -162,6 +163,7 @@ export const clineSays = [
"mcp_server_response",
"subtask_result",
"checkpoint_saved",
"change_card",
"rooignore_error",
"diff_error",
"condense_context",
Expand Down Expand Up @@ -235,6 +237,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 @@ -108,6 +108,7 @@ export interface ExtensionMessage {
| "fileContent"
| "rooHistoryImportProgress"
| "themeFixtureProbeRequest"
| "checkpointRollbackResult"
text?: string
/** For fileContent: { path, content, error? } */
fileContent?: { path: string; content: string | null; error?: string }
Expand Down Expand Up @@ -254,6 +255,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 @@ -348,6 +377,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 @@ -544,6 +575,9 @@ export interface WebviewMessage {
| "openCustomModesSettings"
| "checkpointDiff"
| "checkpointRestore"
| "checkpointRollbackFile"
| "checkpointRollbackStep"
| "checkpointRestoreLatestFile"
| "completionCheckpointDiff"
| "completionCheckpointRestore"
| "deleteMcpServer"
Expand Down Expand Up @@ -786,6 +820,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 @@ -799,6 +873,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