diff options
| author | Adam Malczewski <[email protected]> | 2026-06-21 14:34:22 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-21 14:34:22 +0900 |
| commit | d56fe9cf64719bb330c17b2daee58c0bafa057c9 (patch) | |
| tree | b80a25aaee57f959454d468e03f100c38e224b82 /frontend-todo-handoff.md | |
| parent | 8a4a624d16422467a8e85434c674bb591877e8ea (diff) | |
| download | dispatch-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.md | 91 |
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. |
