# Tasks: assistant AI Elements Spec: `specs/assistant-ai-elements/spec.md`. One task per Sonnet agent. Paths are under `crates/ui-egui/src/panels/assistant_panel/` unless marked (`P/` = that prefix). Every task: read `AGENTS.md`, `docs/CODE-MAP.md`, skills `notescraft-agent-workflow`, `notescraft-rust-safety`, `notescraft-egui-screen`, `notescraft-verify-loop`. Strings go to `crates/ui-egui/src/text/assistant_panel.rs` + `crates/i18n/locales/en.toml` (append at the END of the table); `rustfmt --edition 2024 ` only (never `cargo fmt --all`); do not commit; do not touch files of other agents (check `git status`). Common TESTS recipe (egui_kittest, copy `crates/ui-egui/tests/assistant_flow.rs` and `P/tests.rs`): build an `AssistantPanel`, feed `Event`s through `apply_event`, draw at `Vec2::new(320.0, 700.0)` panel width, assert with queries on labels and with real clicks (`Action` pushed), assert no horizontal overflow (content rect right <= panel right). Common PNG render: add the scenario to `P/chat/demo.rs` and run `cargo run -p notescraft-ui-egui --example snapshot -- --vault --out ui.png --size 1440x900` plus a 320 wide variant (`--size 320x700`), `--theme catppuccin-latte` once, then READ the PNG and compare with the spec section. Common VERIFY: `cargo test -p notescraft-ui-egui assistant`, `cargo clippy -p notescraft-ui-egui --all-targets -- -D warnings`, `cargo xtask i18n`, `cargo xtask tree`, `cargo xtask layers`. > T0 note (aiA, 2026-10-21): trace widget landed, uncommitted. Module `P/chat/trace/{line,view}.rs`, state `P/state/trace.rs`. > `trace::TraceStatus {Running, Done, Failed}` (`from_acp`); NO `Pending` yet. `line::Header {salt, status, title, description: Option<&str>, badge: Option<&str>, nested}`, shown with `line::show(ui, tokens, &header, |ui| body)`. > Open state lives in egui memory under the line id; there is no `default_open` argument yet (T7/T4 add it when needed). > `view::tool(ui, tokens, &ToolLine, actions)` and `view::subagent(ui, tokens, panel, &Subagent, actions)`; `conversation::show_tool` feeds them. > New `Message` variant: `Subagent(Box)` (id, title, status, started, nested calls). Strings: `text/panels/assistant_trace.rs`, `[ui.assistant-trace]`. > T1-T3 status: see the checkboxes below. ## Order and parallelism ``` T0 (gate: subagent trace task landed) -> T1 split conversation.rs [chat/conversation.rs, new chat/message/] -> T2 Message turns + footer [chat/message/*, state/model.rs (turn index)] -> T3 Chat scroll + empty state [chat/view.rs, chat/scroll/*] -> model.rs chain, strictly in sequence: T4 Reasoning -> T5 Plan+Queue -> T6 Confirmation -> T7 Work summary parallel with the chain (disjoint files, after T1): T8 Context card [chat/usage.rs] T9 Changed notes tree [state/review.rs, new chat/changes/*, chat/view.rs strip call] T10 Citations + Sources [chat/sources*.rs, new chat/cite/*] T11 Prompt Input restyle -> T12 Suggestions -> T13 Model Selector UI [input/composer.rs chain, header.rs] T14 ACP config options (crates/acp, then wire into T13) T15 Engine checkpoint journal + commands [crates/engine only] -> T16 Checkpoint UI [needs T9, T2] last: T17 Bases table in message [needs T7], T18 docs/status ``` Same-file conflicts: `chat/view.rs` = T3, T9 (one line), T12, T16: run in that order. `state/model.rs` = T2, T4, T5, T6, T7, T16: sequential. `input/composer.rs` = T11, T12, T13: sequential. `chat/header.rs` = T13 only. Tasks with disjoint OWNS may run in parallel: {T8}, {T9}, {T10}, {T11->T12->T13}, {T14}, {T15}, and the model chain. ## [x] T0 Gate: confirm the subagent/trace work landed (no code) GOAL: read the other agent's result (`git log -5`, `git status`, grep for the trace widget under `P/chat/`). Record in a 10-line note at the top of this file: widget module path, its API (status enum, description, open state), `Message` variants it added. Every later task uses that widget; if it lacks `Pending` status, a description line or `default_open`, T1 adds them (report to owner first, it is the other agent's file). OWNS: this file only. DO NOT: edit source. ## [x] T1 Split `chat/conversation.rs` (437 lines) with no behaviour change OWNS: `P/chat/conversation.rs` (keep as thin dispatcher), new `P/chat/message/{mod,markdown,system,typing}.rs`; `P/chat/mod.rs` (one `mod message;`). FILES TO OPEN: `P/chat/conversation.rs`, `P/chat/view.rs` (callers), `P/mod.rs` (the flat `use chat::conversation` aliases; add `use chat::message;` only if needed). GOAL: move `markdown*`, `fenced_blocks` (with its test), `system`, `typing`, to `chat/message/`. `conversation.rs` < 400 lines. Move strings it moves into the text table. Public paths unchanged. TESTS: existing tests unchanged and green (`P/tests.rs`, `chat/view_tests.rs`, `tests/assistant_flow.rs`). VERIFY: common + before/after snapshot PNG identical. DO NOT: change layout or behaviour; add features; touch `model.rs`. ## [x] T2 Message: turns and reply footer Docs: https://ui.shadcn.com/docs/ai-elements/base/message OWNS: new `P/chat/message/{turn,footer}.rs`, `P/chat/conversation.rs` (call), `P/state/actions.rs` (Action::EditPrompt(index) if needed), `P/state/model.rs` (helper `turn_of(index)` only). FILES TO OPEN: `P/chat/conversation.rs::{item, reply_actions}`, `P/state/actions.rs`, `P/state/model.rs`, `P/input/composer.rs` (`prefill`). GOAL: footer (icon buttons, tooltips) under EVERY finished agent reply, not only the last: Copy (icon swaps to check for 2 s, `crate::motion`), Retry only on the last, Edit on user bubbles (puts text in the composer through `prefill`, focus composer). Reply separated from next user turn by consistent spacing. Hidden in history-viewing mode for Retry/Edit. TESTS: kittest 320 pt: two turns -> two footers; click Copy -> clipboard text equals reply and icon state changes; click Edit -> `panel.composer()` equals the prompt; while `connection.running` no footer on the streaming reply. DO NOT: add good/bad buttons; change message data layout beyond the helper. ## [x] T3 Chat: scroll anchoring and empty state Docs: https://ui.shadcn.com/docs/ai-elements/base/chat OWNS: `P/chat/view.rs`, new `P/chat/scroll/{mod,anchor,empty}.rs`. FILES TO OPEN: `P/chat/view.rs::show` (scroll area, jump pill), `P/chat/demo.rs`. GOAL: (1) a new user message settles near the top of the viewport and the reply streams beneath; while the user is at the live edge the view follows, if they scrolled up it does not (keep `stick_to_bottom` semantics, no jitter). (2) The "Jump to latest" text pill becomes an icon button (arrow-down) with a dot when new content arrived while away. (3) Empty state when a connection exists and there are no messages: heading, one line, slot for the suggestion row (T12 fills it). Move `FOOTER_HEIGHT`/jump consts and strings out of `view.rs`; keep file < 400. TESTS: kittest: push 40 messages, scroll up, stream a chunk -> offset unchanged and dot visible; click jump -> at bottom; new user message -> its top within the first quarter of the viewport. DO NOT: change the composer, header or review strip. ## [x] T4 Reasoning on the trace widget Docs: https://ui.shadcn.com/docs/ai-elements/base/reasoning OWNS: `P/state/model.rs` (`Message::Thought` gets `started: Instant`, `secs: Option`; `chunk()`; close on next non-thought event), `P/chat/transcript.rs` (`line_for` unchanged text), new `P/chat/message/reasoning.rs`, `P/chat/conversation.rs` (one arm). FILES TO OPEN: `P/state/model.rs::{Message, chunk, apply_update}`, trace widget (T0), `P/state/model_tests.rs`. GOAL: "Thinking" + spinner + shimmer while the last message is the Thought and the turn runs; open while streaming, auto-closes when it finishes, a manual toggle wins (store in egui memory by message index); finished = "Thought for 5s" (under 1 s: "Thought"). Empty text = plain line, not expandable. Respect `motion::enabled`. TESTS: unit: duration is set when an Agent chunk or ToolCall follows; kittest: label "Thinking" while running, "Thought for" after; user closed state survives a later chunk. DO NOT: change `crates/acp` or persisted format (transcript keeps role Thought). ## [x] T5 Plan + Queue (one widget, two placements) Docs: https://ui.shadcn.com/docs/ai-elements/base/plan , https://ui.shadcn.com/docs/ai-elements/base/queue OWNS: `P/state/model.rs` (`apply_update` Plan arm: replace the existing `Message::Plan` of the current turn in place instead of pushing), new `P/chat/message/plan.rs`, new `P/chat/message/queue.rs` (sticky strip), `P/chat/view.rs` (one call above composer; sequence after T3). FILES TO OPEN: `P/state/model.rs`, `P/chat/conversation.rs` Plan arm, `crates/acp/src/events.rs::PlanEntry` (statuses pending/in_progress/completed), `P/chat/transcript.rs::line_for` (Plan -> text line). GOAL: in-thread Plan: title "Plan · 2 of 5 done", rows via trace widget (done check, running dot + shimmer, pending circle), collapsed automatically when all done, one per turn. Queue strip above the composer while the turn runs and entries are not all complete: sections "To do" and "Completed" (collapsible, completed collapsed), hidden when no plan. Unknown status string = pending. TESTS: unit: two Plan events in one turn -> one `Message::Plan`; a new user turn starts a new one. kittest 320 pt: counts text, sections, strip hidden after `PromptDone`. Persisted-transcript view still shows the plan text. DO NOT: add a Queue data source; change ACP parsing. ## [x] T6 Confirmation (permission requests) Docs: https://ui.shadcn.com/docs/ai-elements/base/confirmation OWNS: `P/state/model.rs` (`Message::Permission` gets `answer: Option` option name; `expire_permissions` only expires UNANSWERED ones), `P/state/actions.rs` (AnswerPermission records the answer), new `P/chat/message/permission.rs`, `P/chat/conversation.rs` arm, `P/state/permission_tests.rs`. FILES TO OPEN: those, `P/chat/view.rs::write_requested`, `P/chat/header.rs` (grant), `crates/engine/src/ai/assistant_gate.rs` (read only). GOAL: card = title (tool title via `tool_title`), what it touches (locations as note links), consequence line per twist (writes off / review on / review off), buttons Deny / Allow once / Always allow ordered by option kind (allow primary, reject outline). After answer the card collapses to one line "Allowed once" / "Always allowed" / "Denied" in success/muted/danger tone and stays in the thread; expired unanswered becomes "No answer" (not the current generic System line). Disabled in history mode. TESTS: kittest 320 pt: three options -> three buttons, click maps to the right option id; after click, line text; permission for kind `edit` with `capabilities.read_only` shows the grant consequence; `PromptDone` before answer -> "No answer". DO NOT: change the ACP answer call or the gate. ## [x] T7 Per-turn work summary (Chain of Thought + Task) Docs: https://ui.shadcn.com/docs/ai-elements/base/chain-of-thought , https://ui.shadcn.com/docs/ai-elements/base/task OWNS: `P/chat/conversation.rs` (replace `tool_group` call), new `P/chat/turn/{mod,summary}.rs`. FILES TO OPEN: `P/chat/conversation.rs::{show, tool_group}`, trace widget, T4/T5/T6 results. GOAL: consecutive non-text items of a turn (Thought, Tool, Plan stays outside) fold under one trace line. Running: present tense of the latest tool by kind ("Searching notes", "Reading Daily/2026-10-21"), spinner. Done: "Searched 3 notes, edited 1" (count by `kind`, max 3 clauses then "and N more"), error tone if any failed ("1 failed"). Auto-open while a call awaits review or permission; user toggle wins. Inside: rows from the Tool restyle (do not redraw them). TESTS: unit: summary string for mixes of kinds, singular/plural, failure; kittest: running vs done title, auto-open on pending permission. DO NOT: restyle the tool rows; invent kinds beyond ACP's seven. ## [x] T8 Context card Docs: https://ui.shadcn.com/docs/ai-elements/base/context OWNS: `P/chat/usage.rs` (+ its tests), `P/input/composer.rs` one call site only if the signature changes (coordinate: sequence before T11). GOAL: bar becomes a percent button ("74%") with a ring or bar; click or hover opens a small card: used / max, bar with warn/danger tokens (existing 70/90 rules), cost when > 0, at >= 90% hint "Start a new chat" with a New chat action. Keep `Usage` parsing. No input/output rows (ACP has none). TESTS: unit: ratio/label unchanged; kittest 320 pt: trigger text at 30/75/92, card opens on click, `Action::NewChat` at 92. DO NOT: change `Usage::from_update`. ## T9 Changed notes tree (File Tree + review) Docs: https://ui.shadcn.com/docs/ai-elements/base/file-tree OWNS: `P/state/review.rs`, new `P/chat/changes/{mod,tree,row}.rs`, `P/chat/view.rs` (replace the strip+scroll block with one call; sequence after T3), `P/state/model.rs` NOT touched (read `panel.pending`). FILES TO OPEN: `P/state/review.rs`, `P/state/actions.rs` (Accept/Reject/OpenDiff/AcceptAll/RejectAll), `crates/engine/src/ai/assistant_queue.rs::PendingChange`, `P/chat/view.rs` review block. GOAL: card "Changed notes · 4 waiting" with folder tree built from `PendingChange.path` (folders collapsible, default open under 8 files), row = kind badge (Create/Modify/Delete/Rename + `+a −d` from `diff_view::counts`), click opens diff, hover shows Accept / Reject icon buttons; folder row has Accept folder / Reject folder; footer Accept all / Reject all. One change = plain row without tree. Opaque changes (no path) listed under "Other". Show `pending_errors` per row. Bounded: 200 max (engine cap), render with `show_rows`. TESTS: unit: tree builder (nesting, sorting, single file, empty path); kittest 320 pt: click Accept on a row -> `Action::Accept(id)`, folder accept -> all ids; error text shown. DO NOT: change the engine queue or accept semantics; applied-changes list is T16. ## T10 Citations and Sources Docs: https://ui.shadcn.com/docs/ai-elements/base/inline-citation , https://ui.shadcn.com/docs/ai-elements/base/sources OWNS: `P/chat/sources_view.rs`, `P/chat/sources.rs` (marker only), new `P/chat/cite/{mod,card,preview,wikilink}.rs`, `P/chat/message/markdown.rs` (hook for chips; sequence after T1). FILES TO OPEN: `P/chat/sources_view.rs`, `P/chat/sources.rs::MARKER_SCHEME`, `crate::note_view` link result (`clicked_link`), `crates/ui-egui/src/views/share_dialog/mod.rs` (worker pattern), engine `note.read`. GOAL: Sources line "Used 3 notes" on the trace widget; rows = note name, heading, tokens; click -> `OpenNote` with `open_range`. `[n]` and `[[wikilink]]` in agent text render as chips with a hover card (title, path, heading, first 3 lines). Preview text loaded off the UI thread via the worker pattern, cached per path with a size cap (e.g. 64 entries, 2 KiB each), never `unwrap`. Unresolved link = muted chip, card says not found, no navigation. TESTS: kittest 320 pt: agent text "see [[Meeting notes]] [1]" -> two chips, hover shows card from a fixture vault note, click -> `Action::OpenNote`; unresolved chip not clickable. DO NOT: change the knowledge crate or how passages are sent; do not read vault files in the paint path. ## [x] T11 Prompt Input restyle Docs: https://ui.shadcn.com/docs/ai-elements/base/prompt-input OWNS: `P/input/composer.rs`, new `P/input/bar.rs` (bottom bar split out), `P/input/composer_tests.rs`. FILES TO OPEN: `P/input/composer.rs`, `P/input/context.rs` (`Chip`), `P/input/composer_keyboard.rs`. GOAL: chips as pills (kind icon, truncated name, x), consistent height; bottom bar: attach (+), current-note toggle, usage trigger slot, spacer, circular icon send button (arrow) that becomes stop while running and a spinner state while `starting`. Keep Enter / Shift+Enter, mentions, drag-drop. `composer.rs` < 400 lines. TESTS: existing composer tests green; kittest: running -> stop button sends `Action::Cancel`; chip x removes. DO NOT: add image attachments (needs `PromptBlock::Image`; separate spec); touch the model selector (T13). ## [x] T12 Suggestions Docs: https://ui.shadcn.com/docs/ai-elements/base/suggestion OWNS: `P/input/composer.rs` (remove `QUICK_ACTIONS`; sequence after T11), new `P/input/suggestions.rs`, `P/chat/scroll/empty.rs` (slot from T3), `P/chat/view.rs` (sequence after T9). GOAL: horizontally scrolling row of outline pills; click SENDS (`Action::Send` with the text) instead of prefilling. Empty state set: "Summarize this note" (only with an active note), "Find notes that link here", "What did I write this week?"; plugin chips (Template, Calendar, Diagram) only when `settings.plugin_tools` enables them. After a finished turn, show agent `available_commands_update` names if T14 parsed them (optional, leave a clear TODO-free seam: a `Vec` argument). TESTS: kittest 320 pt: row scrolls, click sends exact text, hidden while running and in history mode. DO NOT: invent LLM-generated follow-ups. ## [x] T13 Model Selector UI Docs: https://ui.shadcn.com/docs/ai-elements/base/model-selector OWNS: `P/chat/header.rs` (connection button + menu leave), new `P/input/model_selector.rs`, `P/input/bar.rs` (slot), `views/settings_view/assistant_connections*` NOT touched. FILES TO OPEN: `P/chat/header.rs::{connection_button, connection_menu}`, `P/backend/prompt.rs::selected`, `crates/engine/src/ai/connections.rs`. GOAL: popup anchored to the composer bar: groups "Agents" (ACP) and "Endpoints" (OpenAI, model name shown), radio selection, "Manage connections" -> `Action::OpenSettings`. Header keeps only the status dot, new chat, history, overflow. Effort/model menus: render nothing until T14 provides options. TESTS: kittest 320 pt: popup lists all saved connections grouped, selecting emits the existing select action, popup stays inside the panel rect. DO NOT: change connection storage or settings UI. ## T14 ACP config options (protocol) OWNS: `crates/acp/src/events.rs` (new `SessionUpdate::ConfigOptions`, lenient parse), `crates/acp/src/command.rs` + `client/wire.rs` (`session/set_config_option`), `crates/acp/tests/`, then a small wiring in `P/state/model.rs` and `P/input/model_selector.rs` (sequence after T6/T7 and T13). GOAL: parse `config_option_update` (id, name, category model/thought_level, options, current value) and the options returned by `session/new`; send `session/set_config_option`; selector shows model and effort menus only when the agent lists them. Unknown shapes stay `Other`. Verify the exact field names against the ACP schema (Context7 / agentclientprotocol.com) before coding. TESTS: `crates/acp/tests/protocol_flow.rs` style with `examples/stub_agent.rs` extended; kittest for menus. DO NOT: enable `fs` or `terminal` capabilities. ## [x] T15 Engine checkpoint journal and commands OWNS: new `crates/engine/src/ai/checkpoint/{mod,journal,restore,commands}.rs` (<= 400 lines each), `crates/engine/src/ai/mod.rs`, `crates/engine/src/commands.rs` (ONE `register_*` call), `crates/engine/tests/core_commands.rs` (`CORE_IDS`), new `crates/engine/tests/assistant_checkpoint.rs`. FILES TO OPEN: `ai/assistant_queue.rs` (accept path), `ai/assistant_chat_io.rs` (`write_private` use), `ai/assistant_gate.rs` (READ/WRITE lists), `tests/common/mod.rs`, `history_*` and `trash_*` commands. GOAL: spec section 4.3. Journal appended in the accept path and direct-write path for `source: "assistant"`; commands `assistant.checkpoint_list`, `assistant.checkpoint_restore` (reverse replay through the normal write path, hash conflict check, report `restored` and `conflicts`, never overwrite a changed note). Not callable by the assistant. TESTS: engine integration: create/modify/delete/rename turn -> restore -> vault equals before; edited-since note reported as conflict and untouched; over-cap entry marked non-restorable; assistant source denied; restore creates version-history snapshots. DO NOT: store old text inside notes or outside `.notescraft/`; skip `craft-atomic`; add a dependency. ## T16 Checkpoint UI Docs: https://ui.shadcn.com/docs/ai-elements/base/checkpoint OWNS: new `P/chat/changes/checkpoint.rs`, `P/chat/conversation.rs` (marker call), `P/state/actions.rs` (Action::RestoreCheckpoint), `P/state/model.rs` (turn ids; sequential chain), `P/chat/view.rs` (sequence last), engine call via `engine_command`. GOAL: after each turn that changed notes: separator marker with the file count and a Restore button; click -> `crate::dialog` listing files by kind; confirm runs `assistant.checkpoint_restore` on a worker; result System line with restored and conflicts (conflict row opens version history for the note). Applied-changes list for the turn appears in the Changed notes tree (T9) as "Applied". Not shown for read-only turns or in history mode. TESTS: kittest with a fixture vault: turn with an accepted modify -> marker visible -> restore -> note text equals old; conflict case shows the error. DO NOT: truncate the conversation; run IO in the paint path. ## T17 Bases result table in a message OWNS: new `P/chat/message/base_table.rs`, `P/chat/message/mod.rs`, row hook in the trace tool row (one call). FILES TO OPEN: `crates/engine/src/bases/` (query result shape), `views/base_view/table/` (read-only reuse if cheap), tool title `notescraft_base_*`. GOAL: a completed Bases query tool row opens to a bounded table (50 rows x 8 columns, truncated cells, "N more" line) and an "Open in Base" link. Parse defensively; malformed = plain text summary. TESTS: unit parse of a fixture result; kittest 320 pt table scrolls horizontally inside its own area, page does not. DO NOT: run queries from the UI; add dependencies. ## T18 Docs and status OWNS: `ROADMAP.md`, `docs/ARCHITECTURE.md` (assistant panel section), `docs/CODE-MAP.md` (new folders `chat/message`, `chat/turn`, `chat/changes`, `chat/cite`, `chat/scroll`, `input/*`). Update only status, public API, numbers. ## PaperCraft (designs, later, not a coding task) Project 'NotesCraft Plugins', file id `3accb94a799e7b22`; artboard list in spec section 7. Designs should be done before T3-T13 start if the owner wants pixel parity; otherwise implement from the spec and reconcile after.