Operator console
The operator console is protoAgent's UI: a React + Vite single-page app served at /app, and the same app wrapped as a Tauri desktop binary with a frozen Python sidecar. It's the default and only UI — Gradio was removed; / redirects to /app. This guide covers running it, its layout, and how its surfaces behave. For the HTTP it speaks to, see the Operator REST API; to run with no UI at all, see Run headless.
Run it
python -m server # console at http://localhost:7870/app ( / → /app )The server mounts the console when apps/web/dist/index.html exists; otherwise it boots API-only. The --ui tier (env PROTOAGENT_UI) selects it:
console(default) — the React console at/app+ the full API/A2A surface.none— API + A2A +/metricsonly (headless servers, fleet members).fullis a deprecated alias forconsole(the old Gradio tier; it logs a warning).
Build the console from the @protoagent/web workspace:
npm ci
npm run build --workspace @protoagent/web # tsc + vite build → apps/web/dist
npm run dev --workspace @protoagent/web # Vite dev server (proxies the API)The repo ships a prebuilt dist/; rebuild after changing console source or pulling frontend changes. For an isolated dev instance (separate port + data), use scripts/dev.sh (:7871).
Layout
The shell (DS AppShell) is a left rail of grouped surfaces, a right sidebar of the agent's live state, a utility bar, and an optional bottom panel. The core rail surfaces — Chat, Activity (thread + inbox), Knowledge (a searchable store), Studio (workflows), Agent, Plugins, Settings — each fan out to sub-views via an in-surface segmented control. Enabled plugins add their own views (ADR 0026), each declaring a placement: rail, right (right-sidebar panel), or bottom. Press ⌘K / Ctrl-K for the command palette to jump anywhere.
The Agent surface is the agent's own makeup, tabbed: Identity (edit its name + SOUL.md inline — saving merge-applies config + hot-reloads the graph) · Tools (live inventory by source) · MCP (servers) · Subagents (the delegate roster) · Skills (the skill index) · Middleware (per-turn graph middleware).
The right sidebar holds the agent's working state + triggers — Goals (standing conditions, set in chat with /goal) · Tasks (its task board) · Schedule (cron/one-off fires), plus Notes (its notebook, which ships as the notes plugin).
In a fleet, the console is slug-routed (/app/agent/<id>/) and the hub reverse-proxies each window to its agent — switch agents in place or open two at once.
Chat
Multi-session: sessions persist in localStorage, hidden ones stay mounted so background streams keep running, and each carries its own status + goal panel. The composer has slash-command autocomplete (from GET /api/chat/commands) and renders assistant markdown.
- Live tool-call cards — each tool the agent invokes streams in as a collapsible card (name, running→done/error, input/result), via the
tool-call-v1DataPart (see Extensions § tool-call-v1). - Skill loads — when the agent loads a skill's procedure on demand it appears as an ordinary
load_skilltool-call card (progressive disclosure, Skills). - Mid-turn steering — send a message while a turn runs and it folds in at the next model call (Mid-turn steering).
Streaming uses A2A SendStreamingMessage in the browser.
Desktop (WKWebView) exception. WKWebView won't deliver a
text/event-streambody throughfetch(), so the desktop app detects the shell (isDesktopWebview()) and routes the turn through the non-streamingPOST /api/chat— one request, full reply, rendered once (no live token/tool-card streaming in the desktop chat; browsers keep the streaming/a2apath).
Reactive surfaces (ADR 0003)
The console holds one EventSource open to GET /api/events for its lifetime (lib/events.ts), backed by an in-process EventBus. The topbar live dot reflects the connection; producers bus.publish(...) and every connected console receives it.
Playwright note: a long-lived SSE connection never lets
networkidlesettle — navigate e2e withwaitUntil: "load".
- Activity (
activity/ActivitySurface.tsx) — the durable Activity thread: agent-initiated turns (e.g. scheduled fires) land here; the operator can reply into thesystem:activitycontext. An unread badge counts events that arrive while you're elsewhere. - Inbox — the read/dismiss view of the authenticated
POST /api/inboxintake channel (webhooks, scripts, sister agents). Items have anow/next/laterpriority;nowfires an Activity turn immediately, the rest queue for the agent'scheck_inboxtool.
Agent, Settings & Telemetry
- Settings is schema-driven:
GET /api/settings/schemareturns fields grouped by section (type, value, default, description,restartflag); the surface renders inputs generically, so new config fields appear without a UI change. Saving POSTs only changed fields, writes the YAML (secrets split tosecrets.yaml), and hot-reloads the agent in-process — most changes apply without a restart (those that don't carry arestartbadge). Secrets are never echoed ((set)/unset). Registry:graph/settings_schema.py. - Telemetry (Settings ▸ Overview, ADR 0006) — the local per-turn cost/latency rollup (totals, by-model table, recent turns) from
GET /api/telemetry/{summary,recent}. - Skills (Agent ▸ Skills) — browses the skill index: each skill is pinned (a
SKILL.mdon disk) or learned (non-disk, curated), with confidence + last-used, a search filter, and delete. (Surfaced viaGET /api/playbooks.)
Working memory & the filesystem fence
The agent's stores are agent-global — one instance-scoped store each, shared by the agent's tools and the console (no per-project selector). Tasks lives at $BEADS_DB_PATH; notes ship as the notes plugin (/api/plugins/notes/note).
operator.allowed_dirs in langgraph-config.yaml is the filesystem security fence for the agent's file/shell tools (unrelated to notes/tasks): the repo root is always allowed; add other roots, or set the working dir in the setup wizard's Workspace step. Out-of-allowlist paths are rejected before any I/O (.. and symlinks resolved before the containment check).
Desktop app (Tauri)
apps/desktop/ wraps the console as a Tauri v2 binary. apps/desktop/sidecar/build_sidecar.py PyInstaller-freezes the headless server (binaries/protoagent-server-<triple>), and src-tauri/src/lib.rs spawns it via externalBin with --ui console on port 7870. The frozen build bundles the plugins/ tree and --collect-alls tools/websockets/mcp (plugins load by file path, which PyInstaller's scan misses; a runtime-installed comms plugin, ADR 0058, can only import what's bundled). Signed macOS DMG / Linux AppImage+deb / Windows NSIS artifacts + an in-app updater ship from the desktop-build CI, dispatched manually per release (gh workflow run desktop-build.yml -f tag=vX.Y.Z) — see docs/guides/releasing.md § Desktop.
On macOS, spawn_sidecar augments the sidecar's PATH with the user's login-shell PATH (via $SHELL -ilc, plus the Homebrew/local fallbacks) before spawning. A Finder/Dock launch otherwise inherits only launchd's minimal PATH, so npx/node/ACP coding-agent adapters would be invisible and a delegate_to ACP launch would fail with binary not on PATH (#1299).
Testing the console
A Playwright smoke suite (apps/web/e2e/) drives the built SPA against a deterministic mock backend (mock-server.mjs serves dist/ + the API/a2a subset from fixtures.mjs) — no Python, model, or network. Specs cover tool-call cards, slash autocomplete, and that every surface mounts.
npm run test:e2e --workspace @protoagent/web # builds, boots the mock, runs headlessCI runs it as the Web E2E smoke job. When you add a console feature, extend the mock fixtures + a spec rather than reaching for a live backend.