Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/72-surfacing-pane-tool-primitives.md

13 KiB

TASK 72 — the surfacing pane: tool primitives, status, and an honest bottom bar

Status: interim bridge composed and QuickShell restarted, 2026-08-11. Patches 0002 and 0003 are applied on souveraine/primary; 0004 fixes their QML compatibility. The running instance logged Configuration Loaded. This proves composition and startup only: a live agent turn containing a tool call has not yet exercised the new cards.

2026-08-12 — steps 1 and 2 committed, source only. surfaces/quickshell/modules/souveraine/agent/ now exists and is owned: ToolVocabulary (all 19 registry tools — kind, icon, summary, transcribed from src/ui/chat/tool_renderers.rs rather than reinvented), ToolCard, ThinkingCard, AgentMessage. Commits 3def292 and 6d0f891.

The gap that motivated it, measured: the vendor ToolCallBlock names 8 tools; the registry holds 19. The 11 it cannot name are precisely the interiority verbs — outfit, nickname, subagent, atmosphere, reach, consult, itinerary, todo, schedule, halt, intrusive. Filesystem verbs rendered richly; every verb about being someone rendered as an anonymous grey block.

MessageTextBlock / MessageCodeBlock are deliberately still imported from the vendor snapshot. They are markdown renderers, not agent vocabulary; the ii-base rule forbids landing agent-surface features there, not importing a markdown engine we already have.

Evidence, labelled per this task's own instruction:

  • source committed — yes, pushed to gitea/primary.
  • composed — no. New files plus MANIFEST lines are inert until deploy.sh runs. Nothing imports the module.
  • candidate activated — no. The activating edit (delegate: AiMessageAgentMessage, plus one import) is queued as surfaces/quickshell/patches/0008, applies clean against HEAD, one line to revert.
  • real tool round rendered — no, and not claimed.

Gate: Qt6 qmllint (/usr/lib/qt6/bin/qmllint — the /usr/bin one is Qt5 and silent on every input), run against a staged copy of the composed tree so qs.* resolves. Zero syntax findings across all four files. Residual warnings are the widget-resolution class; the vendor AiMessage produces 38 of the same family against 46 here, which is order-parity for a file that draws two additional card types. The gate was proven able to fail on a deliberately broken input.

Still step 2b, not done: AgentPaneHost / AgentChat do not exist yet, so the sidebar still owns list lifecycle. The 0003 ii-base modifications are therefore not deleted — parity has not been proven by a load, and this task says not to call that complete from a clean startup alone.

Known parity gaps carried deliberately into the owned delegate: in-place message editing (Ctrl+S) is not reimplemented, and the attached-file indicator is not drawn — it belongs with the image-intake half, which has no picker yet either. Source: Casey, this morning. Adjacent: TASK-70 (the island — same renderer, different surface), TASK-65 (subconscious stall wedges the surface), TASK-59 (her face).

Why this is one lane and not seven papercuts

Every defect found in the 2026-08-10 night session had the same shape: the state arrives and nothing draws it. Tools and thoughts render with identical weight; atmosphere is live and undrawn; per-turn cost numbers serialise into the message and nothing shows them; todos and itinerary have no surface; subconscious progress has no indicator; used-context is absent while set-context is shown; image intake is missing at the UI end though the read sensor already lifts images fine.

The root of the rendering half is now known and patched (patch 0002): the server emits typed events — assistant / reasoning / tool call / tool return — and the shell flattens them into one markdown string, marking reasoning with literal <think> fences. The channel is structural on the wire and becomes punctuation at the surface. That flattening is what this task removes.

The ask (Casey's words, expanded)

  1. Tools as distinct UI primitives, not text. Separate visual identities for: sensors (read/glob/grep/list_dir), bash, file edit/write, memory, thinking, and outcome (success/failure).
  2. Collapse by default, expand on demand. A few kinds Casey wants visible inline; most should be a one-line snippet that opens.
  3. Status indication — running / done / failed, per call, visible without reading the payload.
  4. The bottom bar is currently wasted: it reads "souveraine souveraine" plus a subconscious icon. Those slots should carry live, useful state.
  5. The primary pane stays conversation. Everything else earns a tab.

Prior art we already own

The TUI has per-tool visual styles — bash and file edits look different — and this is the single largest transcription win available. Same project, same event stream, solved once already. Read the TUI's renderer before designing anything new.

Structural precondition

Stop flattening. The delegate should consume the typed event list, not a markdown blob. Once segments are first-class:

  • the think-fence collision cannot recur (it is a string-level bug),
  • per-tool delegates become a dispatch on segment kind,
  • collapse/expand is per-segment state rather than text surgery.

Patch 0002 is a correct stopgap on the wrong layer. Do not treat it as the fix; treat it as the thing that keeps the surface readable until the layer moves.

Current bridge — useful now, deliberately not the destination

Patch 0003 carries the wire's assistant/reasoning/tool-call/tool-return kinds in a per-message segment list. The sidebar dispatches those segments directly; tool returns bind to calls by wire id (with a fallback for old id-less frames), and the new cards give bash, file, sensor, and memory calls compact summaries, folded output, and explicit running/done/failed state. Patch 0004 removes object-spread expressions which the installed QML runtime rejected at load.

That bridge is live because Casey needed the panel usable before a larger rewrite. It is architectural debt, not permission to keep building there: 0003 modifies ii-base, while surfaces/quickshell/II-BASE.md and the shell decision say Souveraine work belongs in original modules, not in the ii vendor snapshot. No further agent-surface feature should land in ii-base.

Owned destination — a reloadable agent-surface package

Reasoned design; implement and prove it in this order. The chat surface becomes an original Souveraine module, for example surfaces/quickshell/modules/souveraine/agent/, with a small, legible split:

  • AgentPaneHost owns lifecycle and the stable pane boundary.
  • AgentChat and AgentMessage render typed segments only.
  • ThinkingCard, ToolCallCard, PasteIndicator, and InProgressIndicator are explicit primitives; tool cards own their collapsed state and status.
  • services/Ai.qml remains the data/substrate boundary. It preserves wire segment type and call identity, but it does not choose pixels. The old flat content projection exists only for compatibility consumers such as copy and speech; no renderer may re-parse it.

This is the path away from ii: first host the owned module inside the existing Souveraine compose, then replace the borrowed AiChat host, then remove this bridge and its ii-base changes. TASK-70 remains a separate surface which may reuse the renderer but does not own the sidebar's lifecycle.

Pane-local update and recovery

The desired operation is not "edit a live QML file and hope QuickShell notices." An installed candidate version loads beside the active pane. Only after its ready signal and basic data binding succeed does AgentPaneHost swap the visible loader; a failed candidate leaves the previous pane intact. Expose that as a narrow IPC action such as agentPane.reload, with status and the active version visible in the host. The first implementation must be proven in an isolated config before it is offered as a daily control. This is reasoned, not yet hardware-proven.

Package boundary

The Rust substrate package stays a substrate package. Add a surface package family which installs immutable, versioned QML/assets under /usr/share/souveraine/surfaces/, with a small common package and explicit desktop/phone profile packages as needed. A user-level launcher starts QuickShell by explicit qs --path, rather than treating a mutable ~/.config/quickshell compose as the product. Package upgrade stages a new candidate; it does not silently restart, swap, or claim the UI adopted it. Activation is explicit, reversible, and leaves the last working candidate available.

The existing deploy.sh composer is a development bridge: it copies ii-base to ~/.config/quickshell/ii, symlinks Souveraine-owned manifest files into ~/.config/quickshell/souveraine, and does not restart QuickShell. It is not a surface package manager.

Delivery sequence and proof

  1. Transcribe the TUI's tool semantics into the owned module, including the paste flag and honest in-progress state; exercise one real tool turn of each major kind.
  2. Replace the sidebar's borrowed chat host without changing the wire/data boundary; delete the 0003 ii-base modifications once parity is proven.
  3. Add the candidate pane loader and prove a bad candidate leaves the old pane drawn and usable.
  4. Package the owned assets, build them in Gitea Actions, and prove package install plus explicit activation on the target display.

Label the evidence separately at each step: source committed, package built, package installed, candidate activated, and real tool round rendered. Do not call the last item complete from a clean QML startup alone.

Candidate tabs (Casey)

  • Agent Health — context used vs set, subconscious alive/failed with the actual error, memory git state (dirty/ahead/behind), energy balance, todos, itinerary. Every item already exists as live state and is drawn nowhere. Note: used-context has no truthful source todayresolved 2026-08-12. 7530d6d made ContentBlock::countable_text() the exhaustive authority, f266136 named the wire fields (tokens_used / context_limit were being conflated — the pill was printing the limit as the usage), and r467 is installed and running. Patch 0007 landed the pill. The breakdown endpoint GET /v1/conversations/:id/tokens already returns per-role figures with zero consumers — that is the click-through. Group the breakdown by block kind, not role: measured on a real thread it is tool_result 43% / tool_use 31% / text 18% / reasoning 7%, and per-role filing puts 74% of that under "assistant", which reads as the agent being chatty and points at the wrong lever.
  • Conversations list / instantiation selector (Casey, 2026-08-12) — moves into the pane. Souveraine.newConversation() already exists and is what /new calls; GET /v1/conversations is the listing surface, sorted by updated_at. Note there is no conversation search anywhere in the codebase — 148 conversations for this agent alone — so a flat list will stop scaling early. QMD (tobi/qmd, AUR) indexes markdown, which would want a markdown projection with the JSONL staying authoritative.
  • Reflection / subconscious — her thread, her inbox, her failures.
  • Anime/persona — moves off the top level; it is a persona surface, not a system surface.
  • Subagents — deferred: the standing no-subagent rule makes this a window onto an empty room. Decide the rule before building the viewer.

Also wanted, same lane

  • Paste affordance. The TUI flagged pasted content and wrapped it for the agent. The GUI does not, which is why a handoff document addressed to another instance was read as a message to me on 2026-08-10.
  • Image intake at the UI end. The server side landed 2026-08-11 (souveraine cfad688): the message API now accepts "content" as an ordered array of text/image parts, gates on the model's vision capability at the door, and refuses engine-owned block kinds. Ai.qml::attachFile() still refuses attachments and its comment now cites a limitation that no longer exists. Owner of the inbound contract is docs/substrate/tasks/multimodal-and-input-metadata.md; the picker, the chip, the base64 read and the rendering are this task's.
  • Resume offer (patch 0001) has no continue affordance until this lane lands; default-fresh works, one-tap-continue does not exist.

Discipline (non-negotiable, learned the hard way)

Do not edit the composed config. Souveraine-owned manifest files there are symlinks into the repo, while ii-base is a copied vendor snapshot; both are wrong places to develop. Stage changes in a detached worktree and keep patch queue order explicit. The 2026-08-11 deployment was a Casey-authorised bridge deployment, not a new normal.

There is no laptop build claim here. Gitea Actions owns reproducible builds. For a deliberate compose/restart, inspect the exact QuickShell instance, run deploy.sh, restart only that instance, inspect its fresh log for ERROR, and require Configuration Loaded. Never use an empty search result as proof that the source is absent; resolve symlinks and use grep -R where the composed tree is involved.