Local interface — design¶
Design document for Annona's local UI. Status: specification. The current
UI (ui/, React 19 + Tauri, ~1,200 lines across five views) is a working
first pass; this describes what it should become and why, so the next change to
it is deliberate rather than incremental.
Reading of the brief
The direction given was "a cross between Hermes and Obsidian, local". Taken as: Hermes for the interaction model — keyboard-first, command-palette- driven, dense, fast, no chrome that does not earn its pixels — and Obsidian for the substance — a vault of plain markdown you own, panes, links, local search, no server required. If the intent was different, the principles below are the part to argue with; the layout follows from them.
What this interface is for¶
Not "a chat window with a file tree". Annona is a perimeter, and the interface has one job no other agent UI has:
Make it visible what the agent did, what it was allowed to do, and what left the machine.
Every design decision below follows from that. A UI for a sovereignty product that hides its egress is a UI that contradicts its product.
Principles¶
- The vault is the truth, the UI is a view. Everything the interface shows exists as a file on disk. Close the app, open the folder in any editor, lose nothing. This is Obsidian's contract and it is the reason to copy it.
- Keyboard-first, mouse-optional. A command palette (
⌘K) reaches every action. Anything reachable only by clicking is a bug for the users this product targets — people who live in a terminal and a text editor. - Local by default, visibly. Remote state is never the quiet default. The interface shows which backend answered and whether it was local, on every run.
- Show the work, not a spinner. An agentic run is a sequence of tool calls with arguments and results. That sequence is the interesting content; a progress bar throws it away.
- Denials are content. A refused tool call is not an error to hide behind a toast. It is the product working, and it belongs in the transcript with the same weight as a successful call.
- No telemetry, and it must be checkable. No analytics, no phone-home, no remote fonts or CDN assets. A reviewer should be able to confirm that from the network tab in thirty seconds.
Layout¶
┌──────────────────────────────────────────────────────────────────────────┐
│ ⌘K run a task… ● local · echo ⌥⇧P │
├───────────────┬──────────────────────────────────┬───────────────────────┤
│ │ │ │
│ VAULT │ RUN │ INSPECTOR │
│ │ │ │
│ ▸ pratiche │ ▸ summarise the Q1 report │ tool document_reader│
│ ▸ notes │ │ args path=/…/q1.txt │
│ ▸ runs │ ① explorer map /documents │ ─────────────────────│
│ │ → 2 dirs, 3 files │ result │
│ ── search ── │ │ Fatturato 412.000 │
│ q1 │ Margine 31,4 % │
│ │ ② document_reader q1_report │ ─────────────────────│
│ 3 results │ → 412.000 EUR, 31,4 % │ class C1 │
│ │ │ stayed local yes │
│ │ ⊘ filesystem ~/.ssh/id_rsa │ duration 41 ms │
│ │ denied — outside allowed │ │
│ │ │ │
│ │ ▸ Q1 2026: 142 pratiche… │ │
│ │ │ │
├───────────────┴──────────────────────────────────┴───────────────────────┤
│ ~/akaion-brain · 128 notes · 0 pending nothing left this machine│
└──────────────────────────────────────────────────────────────────────────┘
Three panes, because there are exactly three things: what you have (vault), what happened (run), why (inspector). A fourth pane would be a fourth concept, and there is not one.
The status bar is the product¶
The bottom-right slot is not decoration. It is a live, permanent statement of egress:
| State | Shown |
|---|---|
| Detached, no remote configured | nothing left this machine |
| Attached, nothing sent this run | nothing left this machine · cloud available |
| Attached, transcript sent | ⚠ 3 turns sent to akaion · 14 KB |
| Phase 3 | verified · 0 canaries · trace 8f3a… |
Today the middle rows are the honest ones. When Phase 1 lands, the same slot gains the class ceiling and the gate's decisions with no redesign, because the slot was designed for the eventual truth rather than the current one.
The command palette¶
⌘K is the primary surface. It runs tasks, opens notes, and changes perimeter
settings — the same verbs the CLI exposes, so muscle memory transfers in both
directions.
⌘K ▸ read the invoices in ~/Studio and list overdue ones
─────────────────────────────────────────────────────
run task in ~/Studio ⏎
new note "invoices" ⌘N
search vault "invoices" ⌘⇧F
─────────────────────────────────────────────────────
backend echo · local ⌥B
policy 3 grants, shell off ⌥P
Free text runs a task; everything else is a command. Typing is never ambiguous between the two, because commands are matched only after a prefix, never heuristically — a UI that guesses whether you meant to run something is a UI that occasionally runs something you did not mean.
The run view¶
A run is a document, not a chat log. It renders the AgentResult the loop already
returns: an ordered list of ToolInvocation plus the final answer.
Each row shows tool, arguments, and a one-line result preview. Selecting a row
fills the inspector. Denied calls use their own marker (⊘) and are never
collapsed by default.
The run is written to the vault as runs/YYYY-MM-DD-hhmm-<slug>.md — the same
capture the daemon already performs via runner.brain.capture. Reading a past run
is therefore reading a markdown file, with no separate history store to keep in
sync.
What the UI needs from the runner¶
The local API (127.0.0.1:7070) covers the vault and sync well. Three gaps stand
between it and this design:
| Need | Today | Required |
|---|---|---|
| Run a task from the UI | not exposed | POST /api/agent/run |
| Watch a run unfold | not exposed | GET /api/agent/run/{id}/events (SSE) |
| Show the perimeter state | not exposed | GET /api/perimeter — backend, is_local, policy summary |
Streaming matters more than it looks: principle 4 says show the work, and work shown after it finishes is a log, not a view. The loop already produces its sequence incrementally; the API needs to emit it.
GET /api/perimeter is the one to design carefully, because it is the endpoint
the status bar reads and therefore the one a user trusts. It should report what
the process is configured to do, derived from Capabilities.is_local and the
policy — never a hardcoded string.
Technology¶
Keep React 19 + Vite + Tauri. It works, it ships signed desktop bundles on three platforms, and the release pipeline exists. Nothing about this design needs a rewrite.
Constraints that follow from the principles:
- No remote assets. Fonts, icons and styles ship in the bundle. A UI that fetches from a CDN has already broken the promise on its own status bar.
- No component library with a network story. Local, unstyled primitives, own the CSS.
- The web build and the Tauri build are the same build.
http://127.0.0.1:7070in a browser is a supported way to use this, not a debug mode.
Phasing¶
| Phase | Work |
|---|---|
| A | Command palette; run view rendering AgentResult; POST /api/agent/run. Makes the UI able to do the thing the product is for. |
| B | SSE streaming; the inspector; runs written to the vault. |
| C | Status bar wired to GET /api/perimeter; denials as first-class rows. Lands with the Phase 1 perimeter. |
| D | Trace viewer and akaion-verify output in the UI. Lands with Phase 3. |
Phase A is the only one that blocks anything: until the UI can start a run, the desktop app is a note editor next to an agent it cannot reach.
Deliberately excluded¶
- A graph view. Obsidian's is beautiful and mostly ornamental. If it earns its place later it will be because someone needed it, not because we copied it.
- Plugins.
PluginsViewexists as a stub. A plugin API on a product whose claim is a verifiable perimeter is a decision to make on purpose, with a capability model, not by leaving a panel in place. - A chat transcript UI. Runs are documents. Making them look like a chat invites people to treat an agent with filesystem and shell access like a chatbot, which is exactly the mental model this product exists to correct.