Skip to content

Commit 317e775

Browse files
committed
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)
1 parent 3fd4fa2 commit 317e775

File tree

5 files changed

+51
-30
lines changed

5 files changed

+51
-30
lines changed

src/core/checkpoints/index.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -131,21 +131,26 @@ async function checkGitInstallation(
131131
task.checkpointServiceInitializing = false
132132
})
133133

134-
service.on("checkpoint", ({ fromHash: from, toHash: to }) => {
134+
service.on("checkpoint", ({ fromHash: from, toHash: to, suppressMessage }) => {
135135
try {
136-
// Always update the current checkpoint hash in the webview
137-
provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to })
138-
139-
// Optionally suppress the chat row for the next checkpoint (used for the
140-
// implicit checkpoint created on user message submission).
141-
if (task.suppressNextCheckpointMessage) {
142-
task.suppressNextCheckpointMessage = false
143-
return
144-
}
145-
146-
task.say("checkpoint_saved", to, undefined, undefined, { from, to }, undefined, {
147-
isNonInteractive: true,
148-
}).catch((err) => {
136+
// Always update the current checkpoint hash in the webview, including the suppress flag
137+
provider?.postMessageToWebview({
138+
type: "currentCheckpointUpdated",
139+
text: to,
140+
suppressMessage: !!suppressMessage,
141+
})
142+
143+
// Always create the chat message but include the suppress flag in the payload
144+
// so the chatview can choose not to render it while keeping it in history.
145+
task.say(
146+
"checkpoint_saved",
147+
to,
148+
undefined,
149+
undefined,
150+
{ from, to, suppressMessage: !!suppressMessage },
151+
undefined,
152+
{ isNonInteractive: true },
153+
).catch((err) => {
149154
log("[Task#getCheckpointService] caught unexpected error in say('checkpoint_saved')")
150155
console.error(err)
151156
})
@@ -172,7 +177,7 @@ async function checkGitInstallation(
172177
}
173178
}
174179

175-
export async function checkpointSave(task: Task, force = false) {
180+
export async function checkpointSave(task: Task, force = false, suppressMessage = false) {
176181
const service = await getCheckpointService(task)
177182

178183
if (!service) {
@@ -182,10 +187,12 @@ export async function checkpointSave(task: Task, force = false) {
182187
TelemetryService.instance.captureCheckpointCreated(task.taskId)
183188

184189
// Start the checkpoint process in the background.
185-
return service.saveCheckpoint(`Task: ${task.taskId}, Time: ${Date.now()}`, { allowEmpty: force }).catch((err) => {
186-
console.error("[Task#checkpointSave] caught unexpected error, disabling checkpoints", err)
187-
task.enableCheckpoints = false
188-
})
190+
return service
191+
.saveCheckpoint(`Task: ${task.taskId}, Time: ${Date.now()}`, { allowEmpty: force, suppressMessage })
192+
.catch((err) => {
193+
console.error("[Task#checkpointSave] caught unexpected error, disabling checkpoints", err)
194+
task.enableCheckpoints = false
195+
})
189196
}
190197

191198
export type CheckpointRestoreOptions = {

src/core/task/Task.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
281281
isStreaming = false
282282
currentStreamingContentIndex = 0
283283
currentStreamingDidCheckpoint = false
284-
suppressNextCheckpointMessage = false
285284
assistantMessageContent: AssistantMessageContent[] = []
286285
presentAssistantMessageLocked = false
287286
presentAssistantMessageHasPendingUpdates = false
@@ -895,8 +894,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
895894
// Use allowEmpty=true to ensure a checkpoint is recorded even if there are no file changes.
896895
// Suppress the checkpoint_saved chat row for this particular checkpoint to keep the timeline clean.
897896
if (askResponse === "messageResponse") {
898-
this.suppressNextCheckpointMessage = true
899-
void this.checkpointSave(true)
897+
void this.checkpointSave(true, true)
900898
}
901899

902900
// Mark the last follow-up question as answered
@@ -2765,8 +2763,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
27652763

27662764
// Checkpoints
27672765

2768-
public async checkpointSave(force: boolean = false) {
2769-
return checkpointSave(this, force)
2766+
public async checkpointSave(force: boolean = false, suppressMessage: boolean = false) {
2767+
return checkpointSave(this, force, suppressMessage)
27702768
}
27712769

27722770
public async checkpointRestore(options: CheckpointRestoreOptions) {

src/services/checkpoints/ShadowCheckpointService.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export abstract class ShadowCheckpointService extends EventEmitter {
200200

201201
public async saveCheckpoint(
202202
message: string,
203-
options?: { allowEmpty?: boolean },
203+
options?: { allowEmpty?: boolean; suppressMessage?: boolean },
204204
): Promise<CheckpointResult | undefined> {
205205
try {
206206
this.log(
@@ -221,7 +221,13 @@ export abstract class ShadowCheckpointService extends EventEmitter {
221221
const duration = Date.now() - startTime
222222

223223
if (result.commit) {
224-
this.emit("checkpoint", { type: "checkpoint", fromHash, toHash, duration })
224+
this.emit("checkpoint", {
225+
type: "checkpoint",
226+
fromHash,
227+
toHash,
228+
duration,
229+
suppressMessage: options?.suppressMessage ?? false,
230+
})
225231
}
226232

227233
if (result.commit) {

src/services/checkpoints/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface CheckpointEventMap {
2828
fromHash: string
2929
toHash: string
3030
duration: number
31+
suppressMessage?: boolean
3132
}
3233
restore: { type: "restore"; commitHash: string; duration: number }
3334
error: { type: "error"; error: Error }

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -859,10 +859,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
859859
// Remove the 500-message limit to prevent array index shifting
860860
// Virtuoso is designed to efficiently handle large lists through virtualization
861861
const newVisibleMessages = modifiedMessages.filter((message) => {
862-
// Filter out checkpoint_saved messages that are associated with user messages
863-
if (message.say === "checkpoint_saved" && message.text) {
864-
// Use O(1) Set lookup instead of O(n) array search
865-
if (userMessageCheckpointHashes.has(message.text)) {
862+
// Filter out checkpoint_saved messages that should be suppressed
863+
if (message.say === "checkpoint_saved") {
864+
// Check if this checkpoint has the suppressMessage flag set
865+
if (
866+
message.checkpoint &&
867+
typeof message.checkpoint === "object" &&
868+
"suppressMessage" in message.checkpoint &&
869+
message.checkpoint.suppressMessage
870+
) {
871+
return false
872+
}
873+
// Also filter out checkpoint messages associated with user messages (legacy behavior)
874+
if (message.text && userMessageCheckpointHashes.has(message.text)) {
866875
return false
867876
}
868877
}

0 commit comments

Comments
 (0)