From ecb001ec7a2e573d8dedf5064e860e5a3e7788fd Mon Sep 17 00:00:00 2001
From: Adam Malczewski <github@tradam.dev>
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 ++++++++++++----------
 1 file changed, 38 insertions(+), 30 deletions(-)

(limited to 'packages/frontend/src/lib/components/TaskListPanel.svelte')

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 @@
 <script lang="ts">
-interface TaskItem {
-	id: string;
-	title: string;
-	description: string;
-	status: "pending" | "in_progress" | "done";
-}
+import type { TaskItem } from "../types.js";
 
 const { tasks }: { tasks: TaskItem[] } = $props();
 
-const doneCount = $derived(tasks.filter((t) => t.status === "done").length);
+type Status = TaskItem["status"];
+
+const completedCount = $derived(tasks.filter((t) => t.status === "completed").length);
 const inProgressCount = $derived(tasks.filter((t) => t.status === "in_progress").length);
+const cancelledCount = $derived(tasks.filter((t) => t.status === "cancelled").length);
+// "Active" total excludes cancelled items, so progress reads as work that still counts.
+const activeTotal = $derived(tasks.length - cancelledCount);
 
-function checkboxClass(status: TaskItem["status"]): string {
+function checkboxClass(status: Status): string {
 	switch (status) {
 		case "pending":
 			return "checkbox checkbox-sm rounded-sm checkbox-secondary";
 		case "in_progress":
 			return "checkbox checkbox-sm rounded-sm checkbox-info";
-		case "done":
+		case "completed":
 			return "checkbox checkbox-sm rounded-sm checkbox-success";
+		case "cancelled":
+			return "checkbox checkbox-sm rounded-sm checkbox-neutral";
 	}
 }
 
-function isChecked(status: TaskItem["status"]): boolean {
-	return status === "done";
+function isChecked(status: Status): boolean {
+	return status === "completed";
 }
 
-function isIndeterminate(status: TaskItem["status"]): boolean {
+function isIndeterminate(status: Status): boolean {
 	return status === "in_progress";
 }
+
+function rowClass(status: Status): string {
+	if (status === "completed") return "opacity-60";
+	if (status === "cancelled") return "opacity-40";
+	return "";
+}
+
+function textClass(status: Status): string {
+	switch (status) {
+		case "completed":
+			return "line-through text-base-content/50";
+		case "cancelled":
+			return "line-through text-base-content/40";
+		case "in_progress":
+			return "font-semibold";
+		default:
+			return "";
+	}
+}
 </script>
 
 <div class="flex flex-col gap-2">
@@ -36,13 +57,11 @@ function isIndeterminate(status: TaskItem["status"]): boolean {
 		<p class="text-xs text-base-content/50">No tasks yet.</p>
 	{:else}
 		<p class="text-xs text-base-content/60">
-			{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}
 		</p>
 		<ul class="flex flex-col gap-0.5">
 			{#each tasks as task (task.id)}
-				<li
-					class="flex items-start gap-2 rounded p-1.5 transition-colors {task.status === 'done' ? 'opacity-60' : ''}"
-				>
+				<li class="flex items-start gap-2 rounded p-1.5 transition-colors {rowClass(task.status)}">
 					<input
 						type="checkbox"
 						class={checkboxClass(task.status)}
@@ -51,20 +70,9 @@ function isIndeterminate(status: TaskItem["status"]): boolean {
 						disabled
 						tabindex="-1"
 					/>
-					<div class="flex flex-col gap-0.5 min-w-0">
-						<span
-							class="text-xs leading-tight {task.status === 'done'
-								? 'line-through text-base-content/50'
-								: task.status === 'in_progress'
-									? 'font-semibold'
-									: ''}"
-						>
-							{task.title}
-						</span>
-						{#if task.description}
-							<p class="text-xs text-base-content/50 line-clamp-2">{task.description}</p>
-						{/if}
-					</div>
+					<span class="text-xs leading-tight min-w-0 {textClass(task.status)}">
+						{task.content}
+					</span>
 				</li>
 			{/each}
 		</ul>
-- 
cgit v1.2.3

