Files
Marcos Paulo 12c82f8b56
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
docs: assistant panel spec from the shadcn AI Elements audit
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 18:59:48 -03:00

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), plan entries (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 text content_summary), PermissionRequest (tool_call + options with kind allow_once/allow_always/reject_once), PromptDone{stop_reason}. usage_update (used, size, cost) arrives as SessionUpdate::Other and is parsed in chat/usage.rs.
  • Not parsed, dropped as Other or 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}, terminal blocks, 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. PromptBlock has only Text and ResourceLink (no image block). No session/set_config_option / set_mode call exists in crates/acp/src/command.rs.
  • Writes by the agent go through the engine vault MCP; with assistant.review_writes they land in assistant_queue.rs as PendingChange{id, tool, path, kind Create/Modify/Delete/Rename/Opaque, old, new} and are shown in state/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, ScrollArea with stick_to_bottom plus 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 in mentions.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.rs still use const &str literals, against .claude/rules/ui-egui.md. Every task below moves the strings it touches into crates/ui-egui/src/text/assistant_panel.rs + [ui.assistant_panel] in crates/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 in chat/. 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::OpenDiff and crate::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 from crates/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} where before is the previous text (cap 512 KiB like MAX_PENDING_TEXT; over cap = entry marked non-restorable and the checkpoint says so), the old path for renames, and for deletes the trash id (so trash.restore is the undo). Stored with write_private under .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.list for 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 for source: "assistant"): assistant.checkpoint_list {id}, assistant.checkpoint_restore {id, turn}. Register via register_x in commands.rs::register_core, add ids to tests/core_commands.rs CORE_IDS.
  • UI: a separator marker with a Restore button after each turn that changed notes (not after read-only turns). Click opens crate::dialog listing 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

  1. 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.
  2. Plan updates never add a second Plan message in one turn; Reasoning shows a duration after finishing.
  3. A permission answer leaves a visible resolved state; no card is turned into an "expired" line while still unanswered-but-valid.
  4. Changed notes tree lists pending and applied changes of the turn; accept/reject per file works with the existing Actions.
  5. 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.
  6. cargo xtask ci green, cargo xtask i18n 0, cargo xtask tree green; no file over 400 lines in touched dirs (conversation.rs is split first).
  7. 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:

  1. Panel overview: empty state with Suggestions; mid-turn streaming; finished turn.
  2. Message: user bubble, agent reply with footer actions (idle, copied, retry), edit-in-composer state.
  3. Trace rows kit: running / done / error / pending / denied, collapsed and open (shared by 4-8).
  4. Per-turn work summary (Chain of Thought + Task): collapsed running, collapsed done "Searched 3 notes, edited 1", open.
  5. Reasoning: thinking (streaming, open), thought for 5 s (closed).
  6. Plan + Queue: in-thread plan "2 of 5 done"; sticky To do / Completed strip above composer.
  7. Confirmation: requested (read-only vault with grant, review-on, review-off), allowed once, always allowed, denied.
  8. Changed notes tree: pending, applied, mixed, conflict error row, single-file collapsed form; bulk Accept all.
  9. Checkpoint: marker with Restore, confirm dialog (file list, conflicts), result line.
  10. Citations: [n] chip, [[wikilink]] chip, hover card (resolved, unresolved), Sources line open.
  11. Context card: closed trigger at 30/75/92 percent, open card.
  12. Model Selector popup (connections grouped, model, effort) and Prompt Input (idle, typing, attachments, busy with stop).
  13. Bases result table inside a message (rows, truncated, open in Base).