The trigger was a navigation audit. Two separate nav bars, one for BBBrain's dashboard and one for the Archon SPA, were each doing roughly half a job. Clicking between them caused a page navigation, and the Archon panels (Conversations, Runs, Workflows, Settings) lived at their own routes besides. The chat surface was a third thing entirely. Three UIs, none of which knew the other two existed.
The fix took two days. By the end, index.html had a single flat nav bar reading DASHBOARD | CHAT | DOCS | CONVERSATIONS | RUNS | WORKFLOWS | SETTINGS. Archon panels swapped in inline rather than navigating away. The chat input had grown a command palette, session persistence, arrow-key history, and a live bash execution pipe. Three surfaces collapsed into one.
This is the account of how that happened and why the trade-offs hold up.
The nav collapse first
The unified nav went in on 2026-05-04. The change is in index.html: two nav bars replaced with one, CONVERSATIONS | RUNS | WORKFLOWS | SETTINGS wired to swap the corresponding Archon panel inline. No page navigation on any of those clicks.
The trade-off is coupling. BBBrain's dashboard and the Archon SPA now share a single nav structure, which means a change to one implies awareness of the other. That's the correct trade-off here. The whole point is that they're one product surface, not two tools that happen to coexist on the same host. Keeping them separate was the mistake the two-nav architecture had been encoding.
Out of scope: the internals of the Archon panels themselves. They swap in unchanged. The unification is at the nav and routing layer only.
Session persistence: why localStorage and not a server round-trip
Same day, same commit: the chat session now persists via localStorage. Messages survive a reload. Whatever you were mid-typing is there when you come back.
The alternative was persisting to the Brain server and rehydrating on load. That's the right call for multi-device or multi-user scenarios. This is neither. The chat surface is a single-operator local tool. localStorage is synchronous, requires no round-trip, and survives a browser restart without any server state to manage. The trade-off is that clearing browser storage wipes the session. That's acceptable; the Brain's own conversation log is the canonical record, not the chat UI's localStorage.
The same commit added the ACTIONS button to the chat input row. It opens a 16-command palette: four Archon workflow shortcuts plus twelve Brain slash commands. Selecting an item inserts its text into the textarea. No magic execution. Insert, then send. That deliberate two-step matters: it keeps the command palette from being a hidden execution surface. You see what you're about to send.
Terminal feel: what it is and what it's for
The next day's work is three things bundled into one commit: arrow-key command history, inline slash-command autocomplete, and the !bash client.
History is sessionStorage, 100-entry cap, arrow keys navigate it. The cap is an explicit call: unbounded history in sessionStorage isn't a performance problem, it's a UX one. A hundred entries covers every realistic working session; beyond that you're looking at a log, not a history.
Autocomplete surfaces on / keypress and filters the slash-command list inline. It inserts on Tab or click. It disappears on Escape. The implementation is in chat.html and deliberately doesn't replicate the ACTIONS palette. The palette is for browsing and discovery; the autocomplete is for recall when you already know the command. Two different jobs.
The !bash prefix is the consequential one.
!bash and the SSE pipe
!cmd in the input triggers a short-circuit. Instead of routing to the Brain's LLM endpoint, the client sends a POST /bash to brain_server.py and opens an SSE stream on the response. The server runs the command in a subprocess, streams stdout, stderr, and an exit frame back as SSE events, and enforces a 30-second hard kill. Output renders inline in the chat, styled to read like terminal output.
The server side has a blocklist gate. Commands on the blocklist are rejected before the subprocess spawns. The PR review that followed the initial implementation added a regression test suite: 13 cases covering the blocklist. That suite exists because blocklist enforcement is exactly the kind of thing that silently regresses when someone edits the list for an unrelated reason.
One review finding worth calling out: the original client implementation had comments that inaccurately described server-side enforcement. The comments implied the client was doing enforcement it wasn't. That's the kind of comment that causes someone to skip a server-side check later because they believe the client already handled it. The fix was to correct the comments, not add client-side enforcement. Enforcement belongs on the server.
The same review added a behavioural detail: on !bash short-circuit, the attachment queue is cleared. If you've queued files for context and then invoke !bash, those attachments don't silently travel along. The bash invocation is clean. That's the correct behaviour; a bash command shouldn't carry conversational context it can't use.
What the surface looks like now
One localStorage-persisted input. Arrow keys navigate the last hundred commands. / opens autocomplete against the Brain's slash-command list. ACTIONS opens the 16-item palette. !cmd prefix streams bash output inline via SSE. The nav above it surfaces the full BBBrain + Archon panel set in one bar.
The feature set that used to require navigating between three UIs now routes through one textarea: issuing Brain commands, triggering Archon workflows, running ad-hoc bash. That's not consolidation for its own sake. It's the recognition that operator context doesn't reset between those tasks, so the interface shouldn't pretend it does.
What the 30-second kill actually protects
The hard kill on /bash subprocess execution deserves a note because it looks like a safety feature and it's partly that but mostly something else.
A hung subprocess in an SSE endpoint blocks the response stream. The client is waiting for the exit frame. Without the kill, a long-running or stalled command holds that connection open indefinitely. On a local single-operator tool that's recoverable. Close the tab, restart the server. But it's friction you don't want at 11pm when you're debugging a deploy. Thirty seconds is long enough for every legitimate one-liner this surface is designed to run. If a task needs longer, it doesn't belong in the chat surface; it belongs in a proper job runner.
Out of scope: auth on the /bash endpoint. This is a localhost tool. If that changes, the blocklist-plus-kill model is not sufficient and the endpoint needs revisiting.
The coherence argument
Interfaces that split operator attention across surfaces have a cost that compounds. Each context switch isn't just the time to navigate; it's the reset of whatever partial state you were holding. Two nav bars encoding two products meant two mental models maintained in parallel.
The single-surface model has its own cost. The chat input now has three modes (conversational, slash-command, bash), differentiated by prefix convention (/ vs !). That's learnable but it's not zero overhead. The bet is that one surface with a shallow convention is cheaper than two surfaces with independent mental models. Over a working day, it holds up.
The localStorage persistence is what makes it hold up in practice. A persistent session means the surface accumulates context. You can scroll back, see what you ran, see what it returned. The Archon panels inline mean you can switch to Runs, check a workflow result, and switch back without losing that context. That coherence is the actual product.
Where the log lives
The full implementation record is in docs/devlog.md in the BBBrain repository, under entries dated 2026-05-04 and 2026-05-05. The regression test suite for the bash blocklist is the thing worth reading if you're adapting this pattern. The 13 cases are a reasonable starting inventory for what a blocklist needs to explicitly cover.