From 5efddc6e9dbdf9af6e6c74132b3b0b0302cabf0f Mon Sep 17 00:00:00 2001 From: Dhruv Arora <35871415+dhruv1220@users.noreply.github.com> Date: Sat, 14 Nov 2026 03:06:47 -0800 Subject: [PATCH] Tasks list: offer Reopen for a done task whose question still waits (#103) --- .../app/src/components/shell/tasks-active.tsx | 7 ++- openbot/app/tests/tasks-screen.test.tsx | 45 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/openbot/app/src/components/shell/tasks-active.tsx b/openbot/app/src/components/shell/tasks-active.tsx index 3d4264cde..e0200de43 100644 --- a/openbot/app/src/components/shell/tasks-active.tsx +++ b/openbot/app/src/components/shell/tasks-active.tsx @@ -388,12 +388,17 @@ function TaskLine({ onCheck: (done: boolean, failed: () => void) => void; }) { const { task, status } = item; + // The check follows the resolution timestamp, like the task page does — not the + // grouped status, which stays "needs_input" while a question waits even after + // the task was marked done. Otherwise the row offers "Mark done" again and + // resolves a second time instead of reopening. + const done = task.resolvedAt !== null; return ( <>
diff --git a/openbot/app/tests/tasks-screen.test.tsx b/openbot/app/tests/tasks-screen.test.tsx index 5ac72a8b7..13ea4c20b 100644 --- a/openbot/app/tests/tasks-screen.test.tsx +++ b/openbot/app/tests/tasks-screen.test.tsx @@ -766,6 +766,51 @@ test("a done task's circle is filled, and pressing it reopens the task", async ( ); }); +test("a done task still offers Reopen under Needs input while its question waits", async () => { + quiet(); + inbox = { + requests: [], + questions: [ + { + id: "q-1", + botId: "openduet", + threadId: "question-task-thread", + question: "Which deck should I review?", + createdAt: ago(10), + }, + ], + }; + tasks = [ + task({ + id: "question-task", + title: "Choose a deck", + status: "needs_input", + resolvedAt: ago(3), + }), + ]; + serving(); + const view = draw(); + const needs = await view.findByRole("region", { name: "Needs input" }); + expect(within(needs).getByText("Choose a deck")).toBeTruthy(); + const circle = await view.findByRole("button", { + name: "Reopen Choose a deck", + }); + expect(circle.getAttribute("aria-pressed")).toBe("true"); + fireEvent.click(circle); + await waitFor(() => + expect(sent).toContainEqual({ + method: "POST", + path: "/api/openduet/tasks/question-task/reopen", + body: undefined, + }), + ); + expect(sent).not.toContainEqual({ + method: "POST", + path: "/api/openduet/tasks/question-task/resolve", + body: undefined, + }); +}); + test("Resolved keeps a week of done tasks, newest first, folded until its heading is pressed", async () => { quiet(); tasks = [