summaryrefslogtreecommitdiffhomepage
path: root/frontend-todo-handoff.md
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 14:34:22 +0900
committerAdam Malczewski <[email protected]>2026-06-21 14:34:22 +0900
commitd56fe9cf64719bb330c17b2daee58c0bafa057c9 (patch)
treeb80a25aaee57f959454d468e03f100c38e224b82 /frontend-todo-handoff.md
parent8a4a624d16422467a8e85434c674bb591877e8ea (diff)
downloaddispatch-d56fe9cf64719bb330c17b2daee58c0bafa057c9.tar.gz
dispatch-d56fe9cf64719bb330c17b2daee58c0bafa057c9.zip
feat(todo): per-conversation task list tool + surface
New standard tool extension with a single todo_write tool (opencode todowrite pattern: full-list replace, returns JSON, no business-rule enforcement — the description guides the model). Per-conversation in-memory state + per-conversation surface (rendererId: todo, scope: conversation) via subscriber-notify (message-queue pattern). Wave 0 (kernel contract): added conversationId?: string to ToolExecuteContext (additive, backward-compatible). Wired in dispatch.ts — the kernel already had it but wasn't passing it through to tools. Wave 1 (todo extension): pure core (validateTodos — shape only; getTodos/ setTodos/clearTodos; buildTodoSpec; formatTodoResult). Shell: createTodoWriteTool + surface provider. Tool description matches opencode's todowrite.txt depth (when-to-use, examples, task states). Priority field removed (bloats the tool with little value). 25 tests. Wave 2 (host-bin): registered todo in CORE_EXTENSIONS + dep + root tsconfig ref. Verified: tsc EXIT 0, 1123 vitest, biome clean (314 files). Boot smoke clean. FE handoff: frontend-todo-handoff.md.
Diffstat (limited to 'frontend-todo-handoff.md')
-rw-r--r--frontend-todo-handoff.md91
1 files changed, 91 insertions, 0 deletions
diff --git a/frontend-todo-handoff.md b/frontend-todo-handoff.md
new file mode 100644
index 0000000..0edfa60
--- /dev/null
+++ b/frontend-todo-handoff.md
@@ -0,0 +1,91 @@
+# FE handoff — todo task list surface
+
+Courier this to `../dispatch-web` (cross-repo contract change; `lsp references` does
+not span repos — ORCHESTRATOR §7). All changes are ADDITIVE — nothing existing breaks.
+
+## What shipped (backend)
+
+A per-conversation **task list** the AI model maintains via a `todo_write` tool. The
+list is exposed to the frontend as a per-conversation **surface** (read-only). The
+model creates/updates the list during a turn; the surface updates live so the FE can
+render the current state.
+
+- **`todo_write` tool** — the model passes the FULL list each call (replaces the
+ existing list). Returns the list as JSON. The tool description guides the model on
+ when to use it (3+ step tasks, planning, etc.).
+- **State** — in-memory, per-conversation. No persistence (the list lives for the
+ process lifetime of the conversation).
+- **No new wire types, no version bumps.** The todo surface uses the existing
+ `custom` surface field kind (`ui-contract` unchanged). The `TodoItem` type is
+ defined by the `todo` extension and carried in the surface payload — it is NOT
+ in `@dispatch/wire` or `@dispatch/transport-contract`.
+
+## The surface
+
+The `todo` extension contributes a per-conversation surface:
+
+- **Surface id:** `"todo"`
+- **Scope:** `"conversation"` (subscribe with the `conversationId`)
+- **Region:** `"side"`
+- **Title:** `"Tasks"`
+- **One `custom` field**, `rendererId: "todo"`, `payload: TodoPayload`
+
+```ts
+interface TodoPayload {
+ todos: readonly TodoItem[];
+}
+
+interface TodoItem {
+ content: string;
+ status: "pending" | "in_progress" | "completed" | "cancelled";
+}
+```
+
+- **Read-only** — no `invoke` actions. The model mutates the list via the
+ `todo_write` tool; the FE only renders.
+- **Updates** on every `todo_write` call (subscriber-notify → full new spec with the
+ updated `todos` array).
+- **Empty list** — an idle conversation (no todo list created yet, or the model
+ cleared it with an empty array) renders `todos: []`. Hide the panel when empty.
+
+## What the FE needs to do
+
+1. **Subscribe** to the `todo` surface per conversation (same pattern as
+ `message-queue` and `cache-warming` — `scope: "conversation"`, pass
+ `conversationId` on subscribe).
+
+2. **Custom renderer** for `rendererId: "todo"` — render the `payload.todos` array
+ as a task list. Suggested UI:
+ - Each item shows `content` with a status indicator:
+ - `pending` — empty circle / checkbox
+ - `in_progress` — spinner / filled circle (highlight)
+ - `completed` — checkmark (strikethrough or dim the content)
+ - `cancelled` — X / dash (dim/strikethrough)
+ - Order is significant — items are in the order the model provided them (array
+ index = identity).
+ - Only one item should be `in_progress` at a time (the tool description enforces
+ this via guidance, not validation — but the model should comply).
+
+3. **Live updates** — the surface pushes a new spec on every `todo_write` call. No
+ polling needed. Just re-render from the new `payload.todos`.
+
+4. **Empty state** — when `todos` is `[]`, hide the panel (the model hasn't created
+ a list yet, or cleared it).
+
+## No other integration points
+
+- No new WS ops (no `chat.queue` equivalent — the model is the only writer).
+- No new HTTP endpoints (the list is tool-driven, not API-driven).
+- No new `AgentEvent` types (the list is not on the chat stream).
+- No version bumps in `@dispatch/wire` or `@dispatch/transport-contract`.
+
+## Notes
+
+- **In-memory only** — the todo list does NOT persist across server restarts. If
+ the server restarts, the list is cleared. The model recreates it on the next
+ `todo_write` call. This mirrors the message-queue behavior.
+- **Per-conversation** — each conversation has its own list. Switching conversations
+ means subscribing to a different `conversationId` and rendering that conversation's
+ list.
+- **Model-driven** — the FE has no control over the list (read-only surface). The
+ model creates, updates, and clears items. The FE just displays the current state.