From 1954d458dc855ef7d9bb98f93852ce6a157f6821 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 5 Sep 2025 19:10:53 +0000 Subject: [PATCH 1/5] feat(checkpoints): create checkpoint on user message send --- src/core/task/Task.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 98e235c062dd..2434d0e8df72 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -890,6 +890,12 @@ export class Task extends EventEmitter implements TaskLike { this.askResponseText = text this.askResponseImages = images + // Create a checkpoint whenever the user sends a message. + // Use allowEmpty=true to ensure a checkpoint is recorded even if there are no file changes. + if (askResponse === "messageResponse") { + void this.checkpointSave(true) + } + // Mark the last follow-up question as answered if (askResponse === "messageResponse" || askResponse === "yesButtonClicked") { // Find the last unanswered follow-up message using findLastIndex From 3fd4fa2df71670154b2fab28ccf527ed9adc244c Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Fri, 5 Sep 2025 14:54:36 -0600 Subject: [PATCH 2/5] fix(checkpoints): suppress implicit user-message checkpoint row; keep current checkpoint updated without a chat row --- src/core/checkpoints/index.ts | 8 ++++++++ src/core/task/Task.ts | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index e6bbc09eb5b9..a889bae06512 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -133,8 +133,16 @@ async function checkGitInstallation( service.on("checkpoint", ({ fromHash: from, toHash: to }) => { try { + // Always update the current checkpoint hash in the webview provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to }) + // Optionally suppress the chat row for the next checkpoint (used for the + // implicit checkpoint created on user message submission). + if (task.suppressNextCheckpointMessage) { + task.suppressNextCheckpointMessage = false + return + } + task.say("checkpoint_saved", to, undefined, undefined, { from, to }, undefined, { isNonInteractive: true, }).catch((err) => { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 2434d0e8df72..c3b10d3cbe89 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -281,6 +281,7 @@ export class Task extends EventEmitter implements TaskLike { isStreaming = false currentStreamingContentIndex = 0 currentStreamingDidCheckpoint = false + suppressNextCheckpointMessage = false assistantMessageContent: AssistantMessageContent[] = [] presentAssistantMessageLocked = false presentAssistantMessageHasPendingUpdates = false @@ -892,7 +893,9 @@ export class Task extends EventEmitter implements TaskLike { // Create a checkpoint whenever the user sends a message. // Use allowEmpty=true to ensure a checkpoint is recorded even if there are no file changes. + // Suppress the checkpoint_saved chat row for this particular checkpoint to keep the timeline clean. if (askResponse === "messageResponse") { + this.suppressNextCheckpointMessage = true void this.checkpointSave(true) } From 317e7754bf26e4756e998212035fda7f84d3b199 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 5 Sep 2025 16:29:00 -0500 Subject: [PATCH 3/5] Fix checkpoint suppression for user messages - Propagate suppressMessage flag through event chain properly - Update ChatView to check checkpoint metadata for suppressMessage flag - Ensure checkpoint messages are created but not rendered when suppressed - Fix bug where checkpointSave(false) should have been checkpointSave(true) --- src/core/checkpoints/index.ts | 45 +++++++++++-------- src/core/task/Task.ts | 8 ++-- .../checkpoints/ShadowCheckpointService.ts | 10 ++++- src/services/checkpoints/types.ts | 1 + webview-ui/src/components/chat/ChatView.tsx | 17 +++++-- 5 files changed, 51 insertions(+), 30 deletions(-) diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index a889bae06512..bc842c9f1879 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -131,21 +131,26 @@ async function checkGitInstallation( task.checkpointServiceInitializing = false }) - service.on("checkpoint", ({ fromHash: from, toHash: to }) => { + service.on("checkpoint", ({ fromHash: from, toHash: to, suppressMessage }) => { try { - // Always update the current checkpoint hash in the webview - provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to }) - - // Optionally suppress the chat row for the next checkpoint (used for the - // implicit checkpoint created on user message submission). - if (task.suppressNextCheckpointMessage) { - task.suppressNextCheckpointMessage = false - return - } - - task.say("checkpoint_saved", to, undefined, undefined, { from, to }, undefined, { - isNonInteractive: true, - }).catch((err) => { + // Always update the current checkpoint hash in the webview, including the suppress flag + provider?.postMessageToWebview({ + type: "currentCheckpointUpdated", + text: to, + suppressMessage: !!suppressMessage, + }) + + // Always create the chat message but include the suppress flag in the payload + // so the chatview can choose not to render it while keeping it in history. + task.say( + "checkpoint_saved", + to, + undefined, + undefined, + { from, to, suppressMessage: !!suppressMessage }, + undefined, + { isNonInteractive: true }, + ).catch((err) => { log("[Task#getCheckpointService] caught unexpected error in say('checkpoint_saved')") console.error(err) }) @@ -172,7 +177,7 @@ async function checkGitInstallation( } } -export async function checkpointSave(task: Task, force = false) { +export async function checkpointSave(task: Task, force = false, suppressMessage = false) { const service = await getCheckpointService(task) if (!service) { @@ -182,10 +187,12 @@ export async function checkpointSave(task: Task, force = false) { TelemetryService.instance.captureCheckpointCreated(task.taskId) // Start the checkpoint process in the background. - return service.saveCheckpoint(`Task: ${task.taskId}, Time: ${Date.now()}`, { allowEmpty: force }).catch((err) => { - console.error("[Task#checkpointSave] caught unexpected error, disabling checkpoints", err) - task.enableCheckpoints = false - }) + return service + .saveCheckpoint(`Task: ${task.taskId}, Time: ${Date.now()}`, { allowEmpty: force, suppressMessage }) + .catch((err) => { + console.error("[Task#checkpointSave] caught unexpected error, disabling checkpoints", err) + task.enableCheckpoints = false + }) } export type CheckpointRestoreOptions = { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index c3b10d3cbe89..ba17fadf3985 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -281,7 +281,6 @@ export class Task extends EventEmitter implements TaskLike { isStreaming = false currentStreamingContentIndex = 0 currentStreamingDidCheckpoint = false - suppressNextCheckpointMessage = false assistantMessageContent: AssistantMessageContent[] = [] presentAssistantMessageLocked = false presentAssistantMessageHasPendingUpdates = false @@ -895,8 +894,7 @@ export class Task extends EventEmitter implements TaskLike { // Use allowEmpty=true to ensure a checkpoint is recorded even if there are no file changes. // Suppress the checkpoint_saved chat row for this particular checkpoint to keep the timeline clean. if (askResponse === "messageResponse") { - this.suppressNextCheckpointMessage = true - void this.checkpointSave(true) + void this.checkpointSave(true, true) } // Mark the last follow-up question as answered @@ -2765,8 +2763,8 @@ export class Task extends EventEmitter implements TaskLike { // Checkpoints - public async checkpointSave(force: boolean = false) { - return checkpointSave(this, force) + public async checkpointSave(force: boolean = false, suppressMessage: boolean = false) { + return checkpointSave(this, force, suppressMessage) } public async checkpointRestore(options: CheckpointRestoreOptions) { diff --git a/src/services/checkpoints/ShadowCheckpointService.ts b/src/services/checkpoints/ShadowCheckpointService.ts index 03e019ed6013..ba56b8abc6ac 100644 --- a/src/services/checkpoints/ShadowCheckpointService.ts +++ b/src/services/checkpoints/ShadowCheckpointService.ts @@ -200,7 +200,7 @@ export abstract class ShadowCheckpointService extends EventEmitter { public async saveCheckpoint( message: string, - options?: { allowEmpty?: boolean }, + options?: { allowEmpty?: boolean; suppressMessage?: boolean }, ): Promise { try { this.log( @@ -221,7 +221,13 @@ export abstract class ShadowCheckpointService extends EventEmitter { const duration = Date.now() - startTime if (result.commit) { - this.emit("checkpoint", { type: "checkpoint", fromHash, toHash, duration }) + this.emit("checkpoint", { + type: "checkpoint", + fromHash, + toHash, + duration, + suppressMessage: options?.suppressMessage ?? false, + }) } if (result.commit) { diff --git a/src/services/checkpoints/types.ts b/src/services/checkpoints/types.ts index 7513dae87b31..45c9f352b8c7 100644 --- a/src/services/checkpoints/types.ts +++ b/src/services/checkpoints/types.ts @@ -28,6 +28,7 @@ export interface CheckpointEventMap { fromHash: string toHash: string duration: number + suppressMessage?: boolean } restore: { type: "restore"; commitHash: string; duration: number } error: { type: "error"; error: Error } diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index a517821af1ab..3e13905bc9fa 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -859,10 +859,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - // Filter out checkpoint_saved messages that are associated with user messages - if (message.say === "checkpoint_saved" && message.text) { - // Use O(1) Set lookup instead of O(n) array search - if (userMessageCheckpointHashes.has(message.text)) { + // Filter out checkpoint_saved messages that should be suppressed + if (message.say === "checkpoint_saved") { + // Check if this checkpoint has the suppressMessage flag set + if ( + message.checkpoint && + typeof message.checkpoint === "object" && + "suppressMessage" in message.checkpoint && + message.checkpoint.suppressMessage + ) { + return false + } + // Also filter out checkpoint messages associated with user messages (legacy behavior) + if (message.text && userMessageCheckpointHashes.has(message.text)) { return false } } From 7b5898029257a2538f741c5eb9f85781d45aa6de Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 5 Sep 2025 16:54:29 -0500 Subject: [PATCH 4/5] fix: only create checkpoint on user message when files have changed - Changed allowEmpty from true to false in checkpointSave call - Checkpoints will now only be created when there are actual file changes - This avoids creating empty commits in the shadow git repository --- src/core/task/Task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index ba17fadf3985..af4a4194859c 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -894,7 +894,7 @@ export class Task extends EventEmitter implements TaskLike { // Use allowEmpty=true to ensure a checkpoint is recorded even if there are no file changes. // Suppress the checkpoint_saved chat row for this particular checkpoint to keep the timeline clean. if (askResponse === "messageResponse") { - void this.checkpointSave(true, true) + void this.checkpointSave(false, true) } // Mark the last follow-up question as answered From cfaa4714b55a555f02fa7a7d34747a605a4c9d2e Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Fri, 5 Sep 2025 16:59:56 -0500 Subject: [PATCH 5/5] test: update checkpoint test to include suppressMessage parameter - Fixed test expectation to match the new function signature - saveCheckpoint now expects both allowEmpty and suppressMessage parameters --- src/core/checkpoints/__tests__/checkpoint.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/checkpoints/__tests__/checkpoint.test.ts b/src/core/checkpoints/__tests__/checkpoint.test.ts index 80b30756b96a..e073c0cb9223 100644 --- a/src/core/checkpoints/__tests__/checkpoint.test.ts +++ b/src/core/checkpoints/__tests__/checkpoint.test.ts @@ -111,7 +111,7 @@ describe("Checkpoint functionality", () => { // saveCheckpoint should have been called expect(mockCheckpointService.saveCheckpoint).toHaveBeenCalledWith( expect.stringContaining("Task: test-task-id"), - { allowEmpty: true }, + { allowEmpty: true, suppressMessage: false }, ) // Result should contain the commit hash