Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@
*/
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

Check warning on line 122 in packages/types/src/global-settings.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/global-settings.ts:122: Survived BooleanLiteral mutant (replacement: false). See the job summary for the complete list and resolution guidance.

Check warning on line 122 in packages/types/src/global-settings.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/global-settings.ts:122: Survived BooleanLiteral mutant (replacement: false). See the job summary for the complete list and resolution guidance.

/**
* GlobalSettings
*/
Expand Down Expand Up @@ -223,6 +230,12 @@
.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(),

ttsEnabled: z.boolean().optional(),
ttsSpeed: z.number().optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export type ExtensionState = Pick<

enableCheckpoints: boolean
checkpointTimeout: number // Timeout for checkpoint initialization in seconds (default: 15)
perWriteCheckpoints: boolean
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
466 changes: 466 additions & 0 deletions src/core/checkpoints/__tests__/changeJournal.spec.ts

Large diffs are not rendered by default.

112 changes: 111 additions & 1 deletion src/core/checkpoints/__tests__/checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe("Checkpoint functionality", () => {
// saveCheckpoint should have been called
expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledWith(
expect.stringContaining("Task: test-task-id"),
{ allowEmpty: true, suppressMessage: false },
{ allowEmpty: true, suppressMessage: false, suppressBroadcast: false },
)

// Result should contain the commit hash
Expand Down Expand Up @@ -473,6 +473,116 @@ describe("Checkpoint functionality", () => {
})
})

describe("checkpointSave - task-start baseline broadcast suppression", () => {
type CheckpointPayload = {
type: "checkpoint"
fromHash: string
toHash: string
duration: number
suppressMessage?: boolean
suppressBroadcast?: boolean
}
type PostedWebviewMessage = { type?: string; text?: string; suppressMessage?: boolean }

const broadcastPosts = (): PostedWebviewMessage[] =>
mockProvider.postMessageToWebview.mock.calls
.map(([message]: PostedWebviewMessage[]) => message)
.filter((message: PostedWebviewMessage) => message?.type === "currentCheckpointUpdated")

const registerCheckpointHandler = async () => {
// The handler is registered when the service is created, so drop the pre-made
// double and let getCheckpointService wire it up against the same double.
mockTask.checkpointService = undefined
mockTask.checkpointServiceInitializing = false

await getCheckpointService(mockTask)

const registration = mockCheckpointService.on.mock.calls.find(
([event]: [string]) => event === "checkpoint",
) as [string, (payload: CheckpointPayload) => void] | undefined
if (!registration) {
throw new Error("the checkpoint handler was never registered on the service")
}
return registration[1]
}

const emitOfLastSave = () => {
const call = mockCheckpointService.saveCheckpoint.mock.calls.at(-1)
if (!call) {
throw new Error("no checkpoint save reached the service")
}
return call[1] as { suppressMessage?: boolean; suppressBroadcast?: boolean }
}

it("threads the suppression flag to the service for the save that asks for it", async () => {
await checkpointSave(mockTask, true, true, undefined, true)

expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledWith(
expect.stringContaining("Task: test-task-id"),
{ allowEmpty: true, suppressMessage: true, suppressBroadcast: true },
)
})

it("keeps allowEmpty in force so a clean workspace still commits the baseline", async () => {
await expect(checkpointSave(mockTask, true, true, undefined, true)).resolves.toMatchObject({
commit: "test-commit-hash",
})
})

it("suppresses only the current-checkpoint broadcast for the baseline save", async () => {
const handler = await registerCheckpointHandler()
mockCheckpointService.saveCheckpoint.mockResolvedValue({ commit: HASH_2 })

await checkpointSave(mockTask, true, true, undefined, true)
const options = emitOfLastSave()
handler({
type: "checkpoint",
fromHash: HASH_1,
toHash: HASH_2,
duration: 1,
suppressMessage: options.suppressMessage,
suppressBroadcast: options.suppressBroadcast,
})

expect(broadcastPosts()).toHaveLength(0)
expect(mockTask.say).toHaveBeenCalledWith(
"checkpoint_saved",
HASH_2,
undefined,
undefined,
{ from: HASH_1, to: HASH_2, suppressMessage: true },
undefined,
{ isNonInteractive: true },
)
})

it("still broadcasts the current checkpoint after a real user checkpoint", async () => {
// The user-facing shape at Task.ts:2485 - a save whose chat row is suppressed
// but whose checkpoint the user can still roll back to and see changes for.
const handler = await registerCheckpointHandler()
mockCheckpointService.saveCheckpoint.mockResolvedValue({ commit: HASH_2 })

await checkpointSave(mockTask, false, true)
const options = emitOfLastSave()
handler({
type: "checkpoint",
fromHash: HASH_1,
toHash: HASH_2,
duration: 1,
suppressMessage: options.suppressMessage,
suppressBroadcast: options.suppressBroadcast,
})

const broadcasts = broadcastPosts()
expect(broadcasts).toHaveLength(1)
expect(broadcasts[0]).toEqual({
type: "currentCheckpointUpdated",
text: HASH_2,
suppressMessage: true,
})
})
})

describe("getCheckpointService", () => {
it("should return existing service if available", async () => {
const service = await getCheckpointService(mockTask)
Expand Down
Loading
Loading