mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat(checkpoints): create checkpoint when user sends a message (#7713)
* feat(checkpoints): create checkpoint on user message send * fix(checkpoints): suppress implicit user-message checkpoint row; keep current checkpoint updated without a chat row * 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) * 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 * 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 --------- Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Hannes Rudolph <hrudolph@gmail.com> Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
parent
ae01a90151
commit
ed765a3e7d
6 changed files with 57 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -2774,8 +2781,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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue