diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 7f92f3647f..7dffb92f70 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2985,7 +2985,8 @@ export class Task extends EventEmitter implements TaskLike { let responseText: string | undefined let responseImages: string[] | undefined - if (response === "messageResponse") { + // The Continue button (`yesButtonClicked`) can also carry chat box input. + if (response === "messageResponse" || text || (images && images.length > 0)) { await this.say("user_feedback", text, images) responseText = text responseImages = images diff --git a/src/core/task/__tests__/Task.persistence.spec.ts b/src/core/task/__tests__/Task.persistence.spec.ts index 25e97f2d44..f20154beb8 100644 --- a/src/core/task/__tests__/Task.persistence.spec.ts +++ b/src/core/task/__tests__/Task.persistence.spec.ts @@ -2217,6 +2217,90 @@ describe("Task persistence", () => { }) describe("resumeTaskFromHistory", () => { + const userMessageBlock = { type: "text", text: "\nAlso fix the tests\n" } + const imageBlock = { type: "image", source: { type: "base64", media_type: "image/png", data: "AAA" } } + + it.each([ + { + name: "text and image via Continue", + response: "yesButtonClicked" as const, + text: "Also fix the tests", + images: ["data:image/png;base64,AAA"], + expected: [userMessageBlock, imageBlock], + }, + { + name: "text only via Continue", + response: "yesButtonClicked" as const, + text: "Also fix the tests", + images: [], + expected: [userMessageBlock], + }, + { + name: "image only via Continue", + response: "yesButtonClicked" as const, + text: "", + images: ["data:image/png;base64,AAA"], + expected: [imageBlock], + }, + { + name: "text and image via Send", + response: "messageResponse" as const, + text: "Also fix the tests", + images: ["data:image/png;base64,AAA"], + expected: [userMessageBlock, imageBlock], + }, + ])("forwards chat box input ($name) to the resumed task", async ({ response, text, images, expected }) => { + mockReadApiMessages.mockResolvedValue([{ role: "assistant", content: [{ type: "text", text: "Working" }] }]) + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + historyItem: { + id: "resume-with-input", + number: 1, + ts: Date.now(), + task: "Original task", + tokensIn: 10, + tokensOut: 5, + totalCost: 0.001, + }, + startTask: false, + }) + vi.spyOn(task, "ask").mockResolvedValue({ response, text, images }) + const say = vi.spyOn(task, "say").mockResolvedValue(undefined) + const initiate = vi.spyOn(getTaskPersistenceAccess(task), "initiateTaskLoop").mockResolvedValue(undefined) + + await getTaskPersistenceAccess(task).resumeTaskFromHistory() + + expect(say).toHaveBeenCalledWith("user_feedback", text, images) + expect(initiate).toHaveBeenCalledWith(expected) + }) + + it("resumes without user feedback when Continue is clicked with an empty chat box", async () => { + mockReadApiMessages.mockResolvedValue([{ role: "assistant", content: [{ type: "text", text: "Working" }] }]) + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + historyItem: { + id: "resume-without-input", + number: 1, + ts: Date.now(), + task: "Original task", + tokensIn: 10, + tokensOut: 5, + totalCost: 0.001, + }, + startTask: false, + }) + vi.spyOn(task, "ask").mockResolvedValue({ response: "yesButtonClicked" }) + const say = vi.spyOn(task, "say").mockResolvedValue(undefined) + const initiate = vi.spyOn(getTaskPersistenceAccess(task), "initiateTaskLoop").mockResolvedValue(undefined) + + await getTaskPersistenceAccess(task).resumeTaskFromHistory() + + expect(say.mock.calls.filter(([type]) => type === "user_feedback")).toEqual([]) + expect(initiate).toHaveBeenCalledWith([{ type: "text", text: "[TASK RESUMPTION] Resuming task..." }]) + }) + it.each(["not_found", "invalid", "io_error"] as const)( "does not persist when hydration fails with %s", async (kind) => {