fix: handle pending ask operations during checkpoint restore

- Add handleWebviewAskResponse call before checkpoint restore to resolve any pending ask promises
- This prevents "Current ask promise was ignored" errors when restoring checkpoints
- Add comprehensive tests for pending ask scenarios

Fixes #8177
This commit is contained in:
Roo Code 2025-09-19 11:16:40 +00:00
parent 513fce3f59
commit 760ef23b4f
2 changed files with 92 additions and 11 deletions

View file

@ -30,6 +30,8 @@ describe("checkpointRestoreHandler", () => {
mockCline.abort = true
}),
checkpointRestore: vi.fn(),
handleWebviewAskResponse: vi.fn(),
lastMessageTs: undefined,
clineMessages: [
{ ts: 1, type: "user", say: "user", text: "First message" },
{ ts: 2, type: "assistant", say: "assistant", text: "Response" },
@ -238,5 +240,75 @@ describe("checkpointRestoreHandler", () => {
"Error during checkpoint restore: Checkpoint restore failed",
)
})
it("should handle pending ask operations before checkpoint restore", async () => {
// Simulate a pending ask operation
mockCline.lastMessageTs = Date.now()
await handleCheckpointRestoreOperation({
provider: mockProvider,
currentCline: mockCline,
messageTs: 3,
messageIndex: 2,
checkpoint: { hash: "abc123" },
operation: "delete",
})
// Verify handleWebviewAskResponse was called to resolve pending ask
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith("messageResponse", "", undefined)
// Verify checkpoint restore was still called
expect(mockCline.checkpointRestore).toHaveBeenCalled()
})
it("should handle pending ask operations for edit operations", async () => {
// Simulate a pending ask operation
mockCline.lastMessageTs = Date.now()
const editData = {
editedContent: "Edited content",
images: [],
apiConversationHistoryIndex: 2,
}
await handleCheckpointRestoreOperation({
provider: mockProvider,
currentCline: mockCline,
messageTs: 3,
messageIndex: 2,
checkpoint: { hash: "abc123" },
operation: "edit",
editData,
})
// Verify handleWebviewAskResponse was called to resolve pending ask
expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith("messageResponse", "", undefined)
// Verify checkpoint restore was still called
expect(mockCline.checkpointRestore).toHaveBeenCalled()
// Verify pending edit operation was set
expect(mockProvider.setPendingEditOperation).toHaveBeenCalled()
})
it("should not call handleWebviewAskResponse if no pending ask", async () => {
// No pending ask operation (lastMessageTs is undefined)
mockCline.lastMessageTs = undefined
await handleCheckpointRestoreOperation({
provider: mockProvider,
currentCline: mockCline,
messageTs: 3,
messageIndex: 2,
checkpoint: { hash: "abc123" },
operation: "delete",
})
// Verify handleWebviewAskResponse was NOT called
expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled()
// Verify checkpoint restore was still called
expect(mockCline.checkpointRestore).toHaveBeenCalled()
})
})
})

View file

@ -27,18 +27,27 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore
const { provider, currentCline, messageTs, checkpoint, operation, editData } = config
try {
// For delete operations, ensure the task is properly aborted to handle any pending ask operations
// For both delete and edit operations, we need to handle any pending ask operations
// This prevents "Current ask promise was ignored" errors
// For edit operations, we don't abort because the checkpoint restore will handle it
if (operation === "delete" && currentCline && !currentCline.abort) {
currentCline.abortTask()
// Wait a bit for the abort to complete
await pWaitFor(() => currentCline.abort === true, {
timeout: 1000,
interval: 50,
}).catch(() => {
// Continue even if timeout - the abort flag should be set
})
if (currentCline) {
// Handle any pending ask operations by providing a response
// This unblocks any waiting ask promises before the checkpoint restore
if (currentCline.lastMessageTs) {
// Use the public method to set a response for any pending ask
currentCline.handleWebviewAskResponse("messageResponse", "", undefined)
}
// For delete operations, abort the task
if (operation === "delete" && !currentCline.abort) {
currentCline.abortTask()
// Wait a bit for the abort to complete
await pWaitFor(() => currentCline.abort === true, {
timeout: 1000,
interval: 50,
}).catch(() => {
// Continue even if timeout - the abort flag should be set
})
}
}
// For edit operations, set up pending edit data before restoration