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 diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index e6bbc09eb5b9..bc842c9f1879 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -131,13 +131,26 @@ async function checkGitInstallation( task.checkpointServiceInitializing = false }) - service.on("checkpoint", ({ fromHash: from, toHash: to }) => { + service.on("checkpoint", ({ fromHash: from, toHash: to, suppressMessage }) => { try { - provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to }) + // Always update the current checkpoint hash in the webview, including the suppress flag + provider?.postMessageToWebview({ + type: "currentCheckpointUpdated", + text: to, + suppressMessage: !!suppressMessage, + }) - task.say("checkpoint_saved", to, undefined, undefined, { from, to }, undefined, { - isNonInteractive: true, - }).catch((err) => { + // 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) }) @@ -164,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) { @@ -174,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 98e235c062dd..af4a4194859c 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -890,6 +890,13 @@ 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. + // Suppress the checkpoint_saved chat row for this particular checkpoint to keep the timeline clean. + if (askResponse === "messageResponse") { + void this.checkpointSave(false, true) + } + // Mark the last follow-up question as answered if (askResponse === "messageResponse" || askResponse === "yesButtonClicked") { // Find the last unanswered follow-up message using findLastIndex @@ -2756,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 } }