#1. Problem
Three separate tools serve overlapping views of the same underlying data: session transcripts, engagement state, and agent lifecycle. The session browser (dead, port 18795) had the best session display — DAG trees with succession connectors. The engagement viewer (port 18820) had the best structural navigation — engagement-first, cross-agent round streams, context bars. The console (port 18792) had the control plane — chat, terminal bridge, agent lifecycle, dispatch. No single tool provides all three. Switching between them fragments attention and duplicates server infrastructure.
The unified browser merges all three into one process on one port.
#2. Identity
Name: HAAK Browser.
Port: 18820. The engagement viewer's port. It already serves the closest thing to the unified view. The console (18792) shuts down after merge. The session browser (18795) and old viewer (18791) are already dead.
Process: Single Python server. stdlib + websockets. No frameworks. Serves index.html over HTTP, handles WebSocket connections for terminal bridge, command channel, chat streaming, and live data push.
#3. Layout
Four zones, all visible simultaneously. No tabs, no mode switches.
#Header bar
HAAK title (left). Living agent context indicators (center) — one colored bar per living agent showing context % consumed, colored green/amber/red with pulse animation at 90%+. FTS5 search box (right) — queries over round content across all sessions. Connection status indicator (WebSocket state).
#Left panel (250-280px, resizable)
Primary section: engagements. Each engagement entry shows: name, round count badge, contributing agents as colored dots, date range. Sorted by most recent activity. Clicking an engagement loads its cross-agent round stream in the main area.
Secondary section: agent roster. Each agent shows status indicator (filled circle = living, hollow circle = frozen, x = dead), name, model, current context %. Clicking an agent shows their session DAG tree in the main area. Action buttons per agent: Chat (opens chat view), Terminal (opens terminal view), and lifecycle controls (resume, kill, bury) shown contextually.
#Main area (flex, takes remaining width)
Content determined by navigation state:
- Firehose (default/home): all recent rounds across all engagements, most recent first. Each round carries an engagement pill (colored badge linking to that engagement). Infinite scroll with virtual rendering.
- Engagement view: selected engagement's rounds from all contributing agents, chronological order. Agent badges on each round. Timeline bar at top shows temporal extent.
- Session view: single session's rounds with engagement pills. DAG tree visible in a collapsible sidebar showing the session's position in its parent-child succession chain. Full filter controls (Section 5).
- Chat view: streaming conversation with a selected agent. Message input at bottom with file upload (drag-drop for images, PDFs, text). Connects via WebSocket to the router room or direct API. Rendered inline in the main area, not a modal.
- Terminal view: xterm.js terminal connected to the agent's tmux pane via WebSocket PTY bridge. Full terminal emulation. Rendered inline in the main area.
#Timeline (bottom, ~100-120px, collapsible)
Canvas element showing engagement spans as colored horizontal bars, session blocks nested inside them, time axis below. Click any bar to navigate to that engagement or session in the main area. Toggle button for auto-follow: when enabled, timeline viewport tracks the currently selected engagement/session.
#4. Navigation model
Navigation is fluid — clicking elements changes the main area content without page transitions. Breadcrumbs always visible below the header:
Home > engagement-name > agent/session-id
Each segment is clickable. Home returns to firehose.
Navigation paths:
| Action | Result |
|---|---|
| Click engagement in left panel | Engagement view: cross-agent round stream |
| Click agent badge on a round | Session view: that agent's session containing this round |
| Click agent name in roster | Session DAG tree + most recent session's rounds |
| Chat button on agent | Chat view: streaming conversation |
| Terminal button on agent | Terminal view: xterm.js to tmux pane |
| Click engagement pill on a round | Engagement view for that engagement |
| Click session node in DAG tree | Session view for that session |
| Search submit | Search results view: matching rounds with context snippets |
Browser back/forward works. Each navigation state pushes to history via pushState.
#5. Session display
Carried forward from the session browser's design, which had the best implementation of these features.
#DAG tree
Sessions related by succession (parent spawned child) are shown as a tree with box-drawing connectors:
agent-name/a1b2c3d4 (12 rounds, 45% ctx)
|- agent-name/e5f6g7h8 (8 rounds, 82% ctx)
| |- agent-name/i9j0k1l2 (3 rounds, 15% ctx)
|- agent-name/m3n4o5p6 (22 rounds, DEAD)
Each node shows: session ID (truncated), round count, context % or terminal state. Click to load that session's rounds.
#Filter toggles
Row of toggle buttons above the round list:
| Toggle | Content type |
|---|---|
| user | User messages |
| assistant | Agent responses |
| thinking | Model thinking blocks |
| tool_use | Tool invocations |
| tool_result | Tool return values |
| subagent | Subagent spawns and returns |
| system | System/router messages |
Presets: "conversation" activates user + assistant only. "full" activates everything. Presets are buttons alongside the individual toggles.
#Block count
Status line: "N / M rounds" showing visible (post-filter) vs total. Updates live as toggles change.
#Session metadata
Shown in a compact header when viewing a session: name (if set via /rename), date range, model name, total round count, current context %, engagement affiliations as pills.
#6. Round display
Carried forward from the engagement viewer's design, which had the best implementation.
#Collapsed state (default)
One line per round. Fields left to right: round number, timestamp (relative, e.g. "2h ago"), agent badge (colored dot + name), user message preview (truncated, gray), arrow, agent response preview (truncated), tool count badge (if any tools were called).
#Expanded state (click to toggle)
Sections appear in order:
- User text — always visible, full content, no collapsing.
- Thinking — nested collapsible, gray background, monospace. Header: thinking icon + "thinking" + duration.
- Tool calls — each tool call is a separate nested collapsible. Header: wrench icon + tool name + label (if any). Expanded shows arguments and result. Status indicator for pending/running calls.
- Tool results — paired with their tool call, shown as a sub-section. Return icon + result preview.
- Assistant text — always visible, full content, markdown rendered.
#Agent color coding
Each agent gets a consistent color throughout the entire interface — badges, dots, timeline bars, DAG tree nodes, round borders. Colors assigned deterministically from the agent name (hash to hue). Suggested defaults for known agents: claude = blue, sill = purple, reed = green, veda = amber, rune = coral.
#7. Control features
These come from the console. They are not a separate mode — they are actions available on agents in the roster panel and in context menus.
#Chat
Streaming responses over WebSocket (/chat endpoint). The chat view in the main area shows the conversation history and a message input. File upload via drag-drop or button — files are base64-encoded and sent as part of the message payload. The server forwards to the router (if running) or directly to the Anthropic API via the agent runner.
#Agent lifecycle
Buttons on each agent in the roster:
| Button | Action | Visibility |
|---|---|---|
| Create | Spawn new agent session | Always (in roster header) |
| Resume | Resume frozen session | Frozen agents only |
| Kill | Send SIGTERM, mark dead | Living agents only |
| Bury | Archive dead session | Dead agents only |
All lifecycle commands go through the /cmd WebSocket channel to the server, which executes them via tmux commands on the agent's pane.
#Dispatch
Send button on each agent opens a compact form: subject + body. Submits to the agent's mailbox via agent_mailbox.py. Alternatively, typing /dispatch @agent-name message in the chat input sends a mailbox message.
#Terminal bridge
WebSocket connection (/ws/<pane>) to a tmux pane. The server uses a PTY to bridge between the WebSocket and the tmux session. xterm.js renders the terminal in the main area. Resize events propagated. Multiple terminal views can be open (one per agent), but only one visible at a time in the main area.
#8. Backend architecture
Single Python process. Three concerns: HTTP serving, WebSocket handling, data access.
#HTTP endpoints
| Endpoint | Method | Returns |
|---|---|---|
/ | GET | index.html (the entire frontend) |
/api/engagements | GET | List of active engagements with metadata |
/api/engagements/<id>/rounds | GET | Rounds for an engagement, cross-agent |
/api/sessions | GET | Session list with DAG relationships |
/api/sessions/<id>/rounds | GET | Rounds for a specific session |
/api/agents | GET | Agent roster (living, frozen, dead) |
/api/search?q=<query> | GET | FTS5 search over round content |
/api/timeline | GET | Engagement + session temporal data for timeline rendering |
#WebSocket channels
| Path | Protocol | Purpose |
|---|---|---|
/ws/live | JSON push | Live updates: new rounds, agent state changes, engagement transitions |
/ws/<pane> | Binary (PTY) | Terminal bridge to tmux pane |
/ws/cmd | JSON bidirectional | Agent lifecycle commands (create, kill, resume, bury) |
/ws/chat | JSON bidirectional | Streaming chat with agents (chunk/done protocol from Architecture 36) |
#Data sources
All reads are local. No external API calls for display.
| Data | Source |
|---|---|
| Session transcripts | data/sessions/<agent_id>/<uuid>/ — SQLite per session |
| Agent roster | data/agent-roster.jsonl |
| Agent mailbox | data/agent-mailbox.jsonl |
| Engagement state | engagement.md files in project directories (YAML frontmatter) |
| Room logs | data/rooms/ |
| Session DAG | Derived from roster entries (parent_session field) |
#Live updates
The /ws/live channel pushes events when:
- A new round is written to any session database (filesystem watcher on
data/sessions/) - An agent's roster entry changes (state transition, context % update)
- An engagement state file is modified
The frontend maintains a WebSocket connection and updates the visible view in place — no polling, no refresh.
#9. What gets retired
| Port | Service | Fate |
|---|---|---|
| 18795 | Session browser | Already dead |
| 18791 | Viewer (data browser) | Already on ice |
| 18792 | Console | Shut down after unified browser is verified |
| 18820 | Engagement viewer | Becomes the unified browser |
The engagement viewer's server.py is the starting point. It already handles session/engagement data access. The migration adds WebSocket handlers for terminal bridge, command channel, and chat — code currently in the console's server.py.
#10. Migration path
Five steps, each independently testable.
- Build the unified frontend. Single
index.htmlcombining layout from all three tools. The engagement viewer's current frontend is the skeleton; session browser's DAG tree and filter toggles are added; console's chat panel and terminal bridge UI are added. Test: loads on 18820, shows engagements and sessions correctly.
- Add WebSocket handlers. Terminal bridge (
/ws/<pane>), command channel (/ws/cmd), and chat (/ws/chat) handlers added to the engagement viewer'sserver.py. Code ported from console's server. Test: can open terminal to a tmux pane, can send lifecycle commands, can chat with an agent.
- Add live push. Filesystem watcher on
data/sessions/anddata/agent-roster.jsonl. Changes pushed to/ws/livesubscribers. Test: new rounds appear in the UI without refresh.
- Verify all features. Checklist: engagement navigation, session DAG tree, round filtering, round expansion, agent roster with status, chat streaming, terminal bridge, agent lifecycle (create/resume/kill/bury), dispatch, search, timeline. Each feature tested against the original tool's behavior.
- Shut down 18792. Remove console's launchd plist. Update
infra/daemons/services.md. The console's code remains in the repo (archived) but the service stops running.
#11. Design constraints
Single HTML file. The frontend is one index.html with inline CSS and JS. No build step, no npm, no bundler. External dependency: xterm.js loaded from CDN for terminal emulation. Everything else is vanilla JS and CSS.
No framework. The server is Python stdlib (http.server, asyncio) plus the websockets library. No Flask, no FastAPI, no Django. The engagement viewer already works this way.
Responsive but desktop-first. The four-zone layout assumes a wide screen. On narrow screens, the left panel collapses to an icon bar and the timeline hides. Mobile is not a target.
URL-driven state. Every navigation state has a URL: / (firehose), /engagement/<id>, /session/<id>, /agent/<name>/chat, /agent/<name>/terminal. Browser back/forward and bookmarks work. The server returns the same index.html for all routes; the frontend reads the URL and renders accordingly.
architecture . 40 . unified browser . 2026-03-21 . zach + claude
Architecture 40 — Unified Browser — 2026 — Zachary F. Mainen / HAAK