Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/core/checkpoints/__tests__/checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 25 additions & 10 deletions src/core/checkpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand All @@ -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) {
Expand All @@ -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 = {
Expand Down
11 changes: 9 additions & 2 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,13 @@ export class Task extends EventEmitter<TaskEvents> 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
Expand Down Expand Up @@ -2756,8 +2763,8 @@ export class Task extends EventEmitter<TaskEvents> 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) {
Expand Down
10 changes: 8 additions & 2 deletions src/services/checkpoints/ShadowCheckpointService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export abstract class ShadowCheckpointService extends EventEmitter {

public async saveCheckpoint(
message: string,
options?: { allowEmpty?: boolean },
options?: { allowEmpty?: boolean; suppressMessage?: boolean },
): Promise<CheckpointResult | undefined> {
try {
this.log(
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/services/checkpoints/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
17 changes: 13 additions & 4 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -859,10 +859,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
// Remove the 500-message limit to prevent array index shifting
// Virtuoso is designed to efficiently handle large lists through virtualization
const newVisibleMessages = modifiedMessages.filter((message) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filtering logic for 'checkpoint_saved' messages now checks for a 'suppressMessage' flag and excludes those messages. This is a good approach; consider extracting this complex condition into a small helper function to improve readability and maintainability.

// 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
}
}
Expand Down
Loading