From ecb001ec7a2e573d8dedf5064e860e5a3e7788fd Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Tue, 2 Jun 2026 14:49:49 +0900 Subject: feat(todo): port opencode's declarative whole-list todo tool Replace the imperative id-based CRUD todo tool (add/update/list/get/remove) with opencode's declarative whole-list design: a single `todos` param that replaces the entire list each call. No model-visible ids, no delta reasoning, no "task not found" spirals. - core: TaskItem { id, content, status }; statuses pending|in_progress| completed|cancelled. TaskList.setTasks/getTasks/onChange. New rich TODO_DESCRIPTION adapted from opencode's todowrite.txt. - api: TASK_MANAGEMENT_GUIDANCE system-prompt section (from anthropic.txt); updated TOOL_DESCRIPTIONS.todo. Reload fix: TabStatusSnapshot now carries per-tab tasks so getAllStatuses rehydrates the panel on reconnect. - frontend: mirror types; hydrate tasks from snapshot in both restore paths; upgrade sidebar Tasks panel to render content + all four statuses + progress. - tests: new core task-list.test.ts (15); updated api TaskList mocks + getAllStatuses task-snapshot coverage. bun run check clean; 569 tests pass; all packages typecheck. --- .../src/lib/components/TaskListPanel.svelte | 68 ++++++++++++---------- packages/frontend/src/lib/tabs.svelte.ts | 10 +++- packages/frontend/src/lib/types.ts | 8 ++- 3 files changed, 51 insertions(+), 35 deletions(-) (limited to 'packages/frontend/src') diff --git a/packages/frontend/src/lib/components/TaskListPanel.svelte b/packages/frontend/src/lib/components/TaskListPanel.svelte index 17ade55..1f84bb8 100644 --- a/packages/frontend/src/lib/components/TaskListPanel.svelte +++ b/packages/frontend/src/lib/components/TaskListPanel.svelte @@ -1,34 +1,55 @@
@@ -36,13 +57,11 @@ function isIndeterminate(status: TaskItem["status"]): boolean {

No tasks yet.

{:else}

- {doneCount}/{tasks.length} done{#if inProgressCount > 0}, {inProgressCount} in progress{/if} + {completedCount}/{activeTotal} completed{#if inProgressCount > 0}, {inProgressCount} in progress{/if}{#if cancelledCount > 0}, {cancelledCount} cancelled{/if}

diff --git a/packages/frontend/src/lib/tabs.svelte.ts b/packages/frontend/src/lib/tabs.svelte.ts index ec718bd..875287b 100644 --- a/packages/frontend/src/lib/tabs.svelte.ts +++ b/packages/frontend/src/lib/tabs.svelte.ts @@ -844,7 +844,9 @@ export function createTabStore() { modelId: row.modelId ?? null, reasoningEffort: DEFAULT_REASONING_EFFORT, currentAssistantId, - tasks: [], + // Rehydrate the todo list from the backend snapshot so a reload + // doesn't blank the Tasks panel mid-task. + tasks: snap?.tasks ?? [], injectedSkills: [], parentTabId: row.parentTabId ?? null, persistent: true, @@ -974,6 +976,10 @@ export function createTabStore() { updateTab(t.id, { agentStatus: backendStatus }); } + // Rehydrate the todo list from the snapshot (backend truth) + // so a reconnect/reload doesn't blank the Tasks panel. + updateTab(t.id, { tasks: snap?.tasks ?? [] }); + if (backendStatus === "running") { // Seed the in-flight assistant message from the snapshot. // This handles the "browser just reopened mid-stream" @@ -1940,7 +1946,7 @@ export function createTabStore() { `Persistent: ${tab.persistent}`, `Working directory: ${tab.workingDirectory ?? "default"}`, `Reasoning effort: ${tab.reasoningEffort}`, - `Pending tasks: ${tab.tasks.length}`, + `Todos: ${tab.tasks.length} (${tab.tasks.filter((t) => t.status === "completed").length} completed)`, "", ]; const TOOL_RESULT_MAX = 300; diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts index 173f68c..384028c 100644 --- a/packages/frontend/src/lib/types.ts +++ b/packages/frontend/src/lib/types.ts @@ -131,6 +131,8 @@ export interface TabStatusSnapshot { currentAssistantId?: string; /** turn_id of the in-flight turn; present iff status === "running". */ currentTurnId?: string; + /** The tab's todo list, for rehydrating the Tasks panel on reload. */ + tasks?: TaskItem[]; } export type AgentEvent = @@ -221,10 +223,10 @@ export type AgentEvent = | { type: "message-cancelled"; tabId: string; messageId: string }; export interface TaskItem { + /** Stable positional id for Svelte keying only; never shown to the model. */ id: string; - title: string; - description: string; - status: "pending" | "in_progress" | "done"; + content: string; + status: "pending" | "in_progress" | "completed" | "cancelled"; } export interface PermissionPrompt { -- cgit v1.2.3