22 KiB
Assistant panel: shadcn AI Elements as design reference
Status: draft for owner approval. Research and spec only; no source touched.
Reference: https://ui.shadcn.com/docs/ai-elements (index) and https://ui.shadcn.com/docs/ai-elements/base/<slug> per component (fetched 2026-10-21). Docs quoted below are short snippets only.
Code read: crates/ui-egui/src/panels/assistant_panel/**, crates/acp/src/{events,client/*}.rs, crates/engine/src/ai/*, views/settings_view/assistant_connections*, docs/CODE-MAP.md, AGENTS.md.
1. Context
shadcn AI Elements is one family built on a single primitive, Trace (a collapsible with TraceLine = icon + title + description + status running/error/done). Reasoning, Sources, Task, Plan, Queue, Tool, Terminal, Stack Trace, Test Results, Chain of Thought and Subagent are all "a Trace line that opens to rows". So the design rule for NotesCraft: one egui trace row widget (being built now by another agent for subagent, see section 5), reused by every element below. Do not build twelve widgets.
Facts about the NotesCraft side that bound the work:
- ACP data we parse (
crates/acp/src/events.rs):agent_message_chunk,agent_thought_chunk(text only),planentries (content, status pending/in_progress/completed),tool_call(id, title, kind read/edit/delete/move/search/execute/other, status, locations, raw_input, parent_id, tool_name),tool_call_update(status, title, flattened textcontent_summary),PermissionRequest(tool_call + options with kind allow_once/allow_always/reject_once),PromptDone{stop_reason}.usage_update(used,size,cost) arrives asSessionUpdate::Otherand is parsed inchat/usage.rs. - Not parsed, dropped as
Otheror bookkeeping (model.rs::is_bookkeeping):available_commands_update,current_mode_update,config_option_update,available_modes_update,session_info_update. Structured tool content (diff blocks{type:"diff", path, oldText, newText},terminalblocks, image blocks) is flattened to text; diffs are not kept. - Client capabilities sent at
initialize: no fs, no terminal (client/wire.rs, SPEC 10.3). So ACP gives no live command output, exit codes or test results.PromptBlockhas onlyTextandResourceLink(no image block). Nosession/set_config_option/set_modecall exists incrates/acp/src/command.rs. - Writes by the agent go through the engine vault MCP; with
assistant.review_writesthey land inassistant_queue.rsasPendingChange{id, tool, path, kind Create/Modify/Delete/Rename/Opaque, old, new}and are shown instate/review.rs(strip + cards, accept/reject/preview diff). Gate:ai/assistant_gate.rs. Transcripts:.notescraft/chat/<id>.jsonl(assistant_chat*.rs). Vault history:history.list/history.read(local snapshots),trash.list/trash.restore. - Panel layout today:
chat/view.rs(header, review strip,ScrollAreawithstick_to_bottomplus a "Jump to latest" button, composer footer),chat/conversation.rs(437 lines, over the 400 limit: everything is drawn here),chat/header.rs(connection menu = our model selector),input/composer.rs(chips, caret@mentions inmentions.rs, quick action buttons, send/stop),chat/usage.rs(28 pt context bar + hover text),chat/sources*.rs(vault passages with[n]markers). - Strings:
conversation.rs,view.rs,review.rsstill useconst &strliterals, against.claude/rules/ui-egui.md. Every task below moves the strings it touches intocrates/ui-egui/src/text/assistant_panel.rs+[ui.assistant_panel]incrates/i18n/locales/en.toml(append at the END of the table). - Directory budget (
cargo xtask tree):chat/already holds 12 loose.rs(full). New code goes in new sub-folders (chat/message/,chat/changes/,chat/turn/, ...), never as new loose files inchat/.input/has 7,state/5,backend/8.
2. Gap table
Value is for a notes app. Effort is egui work (S under 1 day, M 1-3 days, L more or with engine/protocol work).
| # | Component | What it is (docs) | NotesCraft today | ACP / data feed | Value | Effort | Decision |
|---|---|---|---|---|---|---|---|
| 1 | Message | One turn: user bubble (align="end", muted), agent reply with MessageFooter actions: copy (check icon 2 s), retry (regenerate), good/bad toggles (aria-pressed), edit returns text to the input. Markdown streamed, fenced code to Code Block |
conversation.rs::item User bubble, Agent markdown via note_view::show_note; reply_actions = Retry + Copy buttons only under the LAST reply; copy-code button per fence; no per-turn grouping, no edit |
agent_message_chunk, PromptDone |
High | M | BUILD. Turn model, footer on every finished reply, edit-and-resend, copy swaps to check. Skip good/bad (no backend to receive it) |
| 2 | Chat | Thread in a Message Scroller: autoScroll follows the stream while at the live edge; a new user message "settles near the top" (scrollAnchor); MessageScrollerButton; empty state "What should we build?" |
view.rs: stick_to_bottom, "Jump to latest" pill (text, 138 pt), manual away_from_bottom; empty state only for "no agent configured"; no top anchor |
none | High | S-M | BUILD. Anchor new user turn near top, icon-only jump button with unread dot while streaming, real empty state with suggestions (#17) |
| 3 | Reasoning | Thinking line streams: "Thinking" + spinner, then "Thought for 5s"; opens while streaming, closes on finish, user choice wins; several parts = several lines | Message::Thought -> CollapsingHeader("Thinking…") closed, no timing, no spinner, stays "Thinking…" forever |
agent_thought_chunk text only; duration measured client-side (first chunk to next non-thought event) |
Med | S | BUILD on the trace widget. Needs started/secs on Message::Thought |
| 4 | Chain of Thought | "A summary that expands to show the steps": "Researched in 3 steps", steps with running/error, search results as hostname badges | tool_group: "3 tool calls" fold; no thought/plan in the group; no outcome title |
derived from thought + tool calls of one turn | Med | M | BUILD as the per-turn work summary (merge with #12 Task): one line "Searched 3 notes, edited 1" while running "Searching notes…", rows inside. Depends on the trace task from section 5 |
| 5 | Tool | Tool call as a Trace line from input to output; states input-streaming/available (running), approval-requested, approval-responded, output-available "Ran ...", output-error "Failed to run ...", output-denied "Denied ..."; long output opens in a Sheet | conversation.rs::tool card: kind icon, title button toggles summary, RUNNING/DONE/FAILED text, queued pill; locations as buttons |
tool_call(_update): kind, status, title, locations, raw_input, content_summary |
High | (other agent) | Being restyled now as trace rows by the other agent. This spec only adds: past-tense titles per kind ("Read note", "Edited note"), denied state, "queued for review" description, and location buttons as note links. Pointer only |
| 6 | Confirmation | Item with Deny / Approve; states requested -> accepted/rejected shown inline; Approve streams output, Deny gives output-denied |
Message::Permission: frame with title + one full-width button per option; on answer the card stays unchanged until expire_permissions (becomes "expired" System line on end of turn); write_requested heuristic in view.rs for the grant button |
PermissionRequest options (allow_once, allow_always, reject_once) |
High | S-M | BUILD. Resolved states ("Allowed once", "Denied") kept in the message; allow_always visually distinct; for edit/delete/move kinds show the write-grant and review-queue consequence ("will be queued for your review") |
| 7 | Plan | Steps the agent intends to take up front, progress "2 of 5 done", each step done/running/pending, collapsible; stream replaces the same part in place | Message::Plan frame with icons; each plan update pushes a NEW message (apply_update), so the thread fills with copies |
plan entries (full list each time) |
High | S | BUILD. Replace-in-place per turn, title "2 of 5 done", collapse when all done |
| 8 | Queue | Todo list split "To do" / "Completed" sections, status icons (circle / dot / check), optional description folds | none (same data as Plan) | same plan entries |
Med | S | BUILD as ONE widget with Plan: sticky strip above the composer showing To do / Completed sections while a turn runs; the in-thread Plan stays a compact record. One data source, two placements |
| 9 | Task | Steps appear as the agent works ("Updating the page metadata" -> "Updated ..."), several tasks in one Trace, running one open | tool_group |
tool calls | Med | - | MERGED into #4 (same per-turn summary) |
| 10 | Context | Hover card: percent trigger button, used/max tokens, Progress bar, input/output/cached rows |
chat/usage.rs: 28x4 pt bar (warn 70%, danger 90%) + hover text with used/size/cost |
usage_update {used, size, cost}. No input/output/cache split from ACP |
Med | S | BUILD. Percent ring or "74%" button that opens a small card (used/max/cost, bar, "start a new chat" hint at 90%). Skip breakdown rows (no data) |
| 11 | Model Selector | Two menus: model (grouped by provider, radio items) and effort; value sent with each message | header.rs::connection_menu picks the CONNECTION (agent/endpoint); model fixed per OpenAI connection (Connection.model); ACP agents choose their own model; config_option_update / available_modes_update ignored |
ACP config_option_update and session/set_config_option exist in the protocol but are NOT parsed or sent by crates/acp |
Med | M (UI) + M (protocol) | BUILD in two steps: (a) move the connection switcher into the composer bar as a Model Selector popup (grouped by kind ACP / OpenAI, shows model for OpenAI); (b) protocol: parse config options, send set_config_option, show model and effort menus only when the agent offers them |
| 12 | Prompt Input | Input group: textarea, attachment chips with remove, attach button, model + effort menus, send button (arrow idle, spinner/stop when busy). Enter sends, Shift+Enter newline | input/composer.rs: chips (file, folder, selection), @ mentions, dnd, auto chip for current note, send/stop text button, Enter/Shift+Enter ok |
PromptBlock::{Text, ResourceLink} only |
High | S (restyle) / L (images) | BUILD restyle: chips as Attachment pills with kind icon, icon send/stop button with busy state, selector in the bar. Image attachments NEED protocol work (PromptBlock::Image, promptCapabilities.image check): defer, separate spec |
| 13 | Suggestion | Row of outline pill buttons above the input; click sends; horizontal scroll with edge fade | QUICK_ACTIONS 3 buttons (Template/Calendar/Diagram) that only prefill, shown only when plugin tools on |
available_commands_update (slash commands) if agent sends them; otherwise static |
Med | S | BUILD. Empty-state chips (vault-aware: "Summarize this note", "Find notes that link here", "Make today's daily note") that SEND; keep the 3 plugin actions as chips; horizontal scroller |
| 14 | Sources | Collapsed line "Used 3 sources" with globe, opens to link rows, dedup by url, hostname fallback | sources_view::card + sources.rs: passages the app gave the agent (Message::Sources), [n] markers, sheet |
app-side (knowledge crate), not ACP | High | S | RESTYLE onto trace row: "Used 3 notes", rows = note name + heading + token count, click opens note at range (open_range exists) |
| 15 | Inline Citation | Hover card on a chip in the sentence: title, link, carousel for several sources | [n] markers are links (MARKER_SCHEME), click opens sheet; agent-written [[wikilinks]] render through note_view and open the note on click |
markers: app-side; wikilinks: any agent text | High | M | BUILD. [n] chip and [[Note]] chip get a hover card (title, heading, first 3 lines of passage/note). Preview text fetched via engine note.read on a worker (copy share_dialog/mod.rs pattern), cached, bounded |
| 16 | File Tree | Files the agent changed as an expandable folder tree; file buttons call onSelect; folders collapsible. "For a single file ... use a Tool line" |
review::strip + flat change_card list inside a 200 pt scroll area |
PendingChange (path, kind, old/new) from engine, not ACP |
High | M | BUILD: "Changed notes" tree card (folders by vault path, kind badge Create/Modify/Delete/Rename, +/- counts), per-file Accept / Reject / Open diff, folder-level and "Accept all". Single change = plain row, no tree. Also lists applied changes of the turn (feeds Checkpoint) |
| 17 | Checkpoint | Separator marker after a turn with a Restore button; Restore "keeps the chosen turn and removes every later message" | none | not ACP | High | L | BUILD as vault checkpoint: restore the NOTES to the state before an agent turn (section 4.3). Engine journal + commands + UI marker |
| 18 | Terminal | Command + streaming output, exit code status, collapsible CodeBlock max-h-64 |
none; execute kind tool shows title + flat summary |
None: client declares terminal:false, no terminal/* methods, no exit code, no live output |
Low | L | DO NOT BUILD. execute calls render as ordinary Tool rows with monospace summary (other agent's task). Revisit only if we ever implement ACP terminal (security decision, SPEC 10.3) |
| 19 | Stack Trace | Failed tool as one line "Failed: Error: ..." opening to frames with file links | failed tool shows FAILED + 1-line summary | failed tool_call_update text; no frames |
Low | S | NO separate widget. Failed Tool row expands to the full error text in monospace (covered by Tool restyle) |
| 20 | Test Results | Suites/tests/failures summary from a test run | none | None (no terminal, no structured results) | Low | - | SKIP. Not a notes concern. The nearest NotesCraft analogue is a lint run (crates/lint): later, as Tool row description |
| 21 | Subagent | Run of a delegated subagent as one line, opens to its tool calls and result | ToolCallInfo.parent_id / tool_name already parsed |
_meta.claudeCode.parentToolUseId |
Med | - | Owned by the other Sonnet agent, in progress in crates/ui-egui/src/panels/assistant_panel (new trace widget, model extension, tool calls restyled as trace rows). Not specified here beyond: every task below starts by re-reading that agent's result |
Summary: 13 to build (Message, Chat, Reasoning, Chain of Thought + Task as one, Confirmation, Plan + Queue as one, Context, Model Selector, Prompt Input restyle, Suggestion, Sources, Inline Citation, File Tree, Checkpoint), 2 pointers (Tool, Subagent), 3 not built because ACP gives nothing (Terminal, Stack Trace as widget, Test Results).
3. Does ACP give Terminal / Stack Trace / Test Results?
No. crates/acp/src/client/wire.rs sends "terminal": false and answers terminal/* and fs/* with -32601. An agent that runs commands does it in its own process; we only see a tool_call with kind: "execute", a title (usually the command) and, at best, flattened text of the result in content_summary. No exit code, no stream, no test structure. Rendering: ordinary Tool row (monospace summary, copy button). Building Terminal/Test Results would require implementing client-side terminals (spawn, output limits, kill) which AGENTS.md "Never crash" and SPEC 10.3 currently exclude. Not recommended.
4. Decisions and NotesCraft twists
4.1 One widget family
All trace-like elements use the trace widget from the subagent task (same row height, indent, chevron, status icon, shimmer on running). Elements here only supply data and titles. If that widget lacks description, status = pending or keep_open, the first task below extends it (same files as the other agent: wait for it).
4.2 Notes-specific twists
- Wikilinks as citations. Agent text containing
[[Note]]or[[Note|alias]]renders as a citation chip (note icon, alias or title). Hover card shows title, path, first lines; click opens the note (Action::OpenNote). Unresolved target: muted chip, "note not found" in the card, no click. Same chip for[n]source markers, which also show heading and token count. - Write grant and review queue. Confirmation for kinds edit/delete/move states what will happen: with writes off "Needs write access" + a "Grant for this session" button (reuses
Action::GrantWrite); with review on "Will be queued for your review"; with review off "Will change the note now". The tool row for a queued write says "Waiting for your review" (exists) and links to its row in the Changed notes tree. - Changed notes tree is the single place to review. It merges pending (waiting) and applied (accepted or direct) changes of the current turn, so the same data feeds Checkpoint. Open diff reuses
Action::OpenDiffandcrate::diff_view. - Bases results as a table in a message. When a tool row is
base.query/ a Bases tool result (JSON rows), the row opens to a bounded table (max 50 rows x 8 columns, "Open in Base" link). Needs the exact result shape fromcrates/engine/src/bases/; separate task, after the core elements. - Checkpoint = vault restore, not chat truncation. The agent process keeps its own context; deleting our messages would lie about it. Restore changes files only and appends a System line "Restored 3 notes to before this turn".
4.3 Checkpoint design (needs owner approval before the engine task)
- Per turn the engine records a journal of every write applied while
source: "assistant"(accepted pending change or direct write when review is off):{turn_id, seq, path, kind, before}wherebeforeis the previous text (cap 512 KiB likeMAX_PENDING_TEXT; over cap = entry marked non-restorable and the checkpoint says so), the old path for renames, and for deletes the trash id (sotrash.restoreis the undo). Stored withwrite_privateunder.notescraft/chat/<id>.checkpoints.jsonl(never inside notes; goes through craft-atomic like the transcripts). - Restore = replay the journal of that turn AND all later turns in reverse, each step through the normal Session write path (atomic, indexed, and snapshotted by the existing versions/history, so the restore itself is undoable). Per step conflict check by hash against the text the journal says the note had after the write; a note edited since is NOT overwritten: it is listed as a conflict and skipped (never silent, never clamp).
- Also usable without the journal as a fallback display:
history.listfor the note shows the same snapshots, so the confirm dialog links "Open version history" for conflicts. - New commands (user-only, NOT added to the assistant's callable lists in
assistant_gate.rs; add a test that they are denied forsource: "assistant"):assistant.checkpoint_list {id},assistant.checkpoint_restore {id, turn}. Register viaregister_xincommands.rs::register_core, add ids totests/core_commands.rsCORE_IDS. - UI: a separator marker with a Restore button after each turn that changed notes (not after read-only turns). Click opens
crate::dialoglisting files and kinds; confirm runs the command off-thread (worker pattern), result shows restored and conflicts.
4.4 Out of scope
Good/bad feedback buttons, message branching, image attachments (protocol), Terminal, Stack Trace widget, Test Results, ACP terminal/fs capabilities.
5. The subagent element
Pointer only: it is being built now by another Sonnet agent in crates/ui-egui/src/panels/assistant_panel (trace widget, model extension, ordinary tool calls restyled as trace rows). Docs: https://ui.shadcn.com/docs/ai-elements/base/subagent ("A subagent run, with its tool calls and result"). Data already parsed: ToolCallInfo.parent_id, tool_name. Every task here assumes that work has landed; task T0 verifies it.
6. Acceptance
- Each built element in section 2 renders at a 320 pt panel width without horizontal overflow, in Mocha and Latte, with reduced-motion on and off.
- Plan updates never add a second Plan message in one turn; Reasoning shows a duration after finishing.
- A permission answer leaves a visible resolved state; no card is turned into an "expired" line while still unanswered-but-valid.
- Changed notes tree lists pending and applied changes of the turn; accept/reject per file works with the existing
Actions. - Checkpoint restore returns the vault to the pre-turn state for create/modify/delete/rename, reports conflicts, and is itself undoable through version history; assistant cannot call the checkpoint commands.
cargo xtask cigreen,cargo xtask i18n0,cargo xtask treegreen; no file over 400 lines in touched dirs (conversation.rsis split first).- No ACP capability change (still no fs/terminal).
7. PaperCraft note
Designs for the panel go in the PaperCraft project NotesCraft Plugins (file id 3accb94a799e7b22) later; not drawn now. Use skills papercraft, papercraft-design, papercraft-design-system (reuse tokens/components from the NotesCraft design system; Mocha and Latte modes). Artboards needed, each at 320 pt wide (plus 420 pt for the wide dock), both themes:
- Panel overview: empty state with Suggestions; mid-turn streaming; finished turn.
- Message: user bubble, agent reply with footer actions (idle, copied, retry), edit-in-composer state.
- Trace rows kit: running / done / error / pending / denied, collapsed and open (shared by 4-8).
- Per-turn work summary (Chain of Thought + Task): collapsed running, collapsed done "Searched 3 notes, edited 1", open.
- Reasoning: thinking (streaming, open), thought for 5 s (closed).
- Plan + Queue: in-thread plan "2 of 5 done"; sticky To do / Completed strip above composer.
- Confirmation: requested (read-only vault with grant, review-on, review-off), allowed once, always allowed, denied.
- Changed notes tree: pending, applied, mixed, conflict error row, single-file collapsed form; bulk Accept all.
- Checkpoint: marker with Restore, confirm dialog (file list, conflicts), result line.
- Citations:
[n]chip,[[wikilink]]chip, hover card (resolved, unresolved), Sources line open. - Context card: closed trigger at 30/75/92 percent, open card.
- Model Selector popup (connections grouped, model, effort) and Prompt Input (idle, typing, attachments, busy with stop).
- Bases result table inside a message (rows, truncated, open in Base).