console
v1.9.6Web console for iii — bundles the React UI and proxies the engine WebSocket on a single port.
- macOS: arm64 · x64
- Linux: arm64 · armv7 · x64
- Windows: arm64 · x64 · x86
exact versions are immutable; binary and bundle artifacts are digest-pinned.
full markdown
/workers/console.md?version=1.9.6. paste it into an llm prompt or pipe it through curl from a worker.install
configuration
- http_port: 3113dependencies
readme
Install
iii worker add consoleThis fetches the prebuilt binary, writes a console: block into ~/.iii/config.yaml, and the engine launches the worker the next time it boots.
Quickstart
curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
iii project init iii-app && cd iii-app
iii # engine on ws://127.0.0.1:49134# New terminal, same folder
iii worker add console # UI + /ws proxy on :3113
open http://127.0.0.1:3113The browser hits / for the SPA shell and upgrades /ws to the engine WebSocket — one origin, no CORS, no API base URL to configure.
Bring up the chat stack
Chat runs on the harness durable turn loop. Its manifest declares the whole stack as dependencies — session-manager (the conversation store the sidebar, transcripts, and live token rendering are backed by), llm-router (generation + the model catalog), context-manager (the /compact summariser), approval-gate (human-in-the-loop approvals), and the provider workers — so a single command resolves and installs all of it:
iii worker add harnessAdd a provider key
The provider workers install with the harness, but they need credentials before any model appears — until then the model picker reads no models and chat won't generate. Add a key either way:
- In the UI (recommended) — open the model picker and use configure anthropic / configure openai to paste a key. It's written to that provider's slice of the
llm-routerconfiguration entry, and the catalog populates within seconds. - From the environment —
llm-routerfalls back to a provider's credential env var (e.g.ANTHROPIC_API_KEY), read in the router's own process. Seellm-routerfor the credential model.
Pick a model in the composer and send — the turn streams back through the harness loop.
Programmatic check from the SDK
use iii_sdk::{register_worker, InitOptions, TriggerRequest};
use serde_json::json;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let iii = register_worker("ws://localhost:49134", InitOptions::default());
let result = iii.trigger(TriggerRequest {
function_id: "console::status".into(),
payload: json!({}),
action: None,
timeout_ms: Some(5_000),
}).await?;
println!("{result:#?}");
Ok(())
}Returns { http_port, engine_url, version } — useful for liveness and readiness probes.
Why console
- One port, one binary. The React UI is baked into the executable with
rust-embed, and the engine WebSocket is reverse-proxied at/wson the same origin. No CORS preflight, no side-car static server, nodist/directory to deploy. Seesrc/assets.rsandsrc/proxy.rs. - Live engine, live UI. Functions, models, traces, and chat sessions all stream over a single WebSocket via the iii browser SDK. Mention any registered function with
@, switch models on the fly, and watch the trace appear in the panel next to you. Seeweb/src/lib/iii-client.ts. - Built for production. SIGINT and SIGTERM graceful shutdown (so
docker stopandkubectl deleteactually drain), URL credentials redacted from logs, immutable cache headers on content-hashed assets, and a fully self-contained binary with no runtime filesystem deps. Seesrc/main.rs.
Features
Chat
A purpose-built agentic chat UI on top of Lexical. Lives in web/src/components/chat/.
- Two modes —
askandagenttoggle right in the composer - Live model picker — provider-grouped from
router::models::list; static fallback (OpenAI, Anthropic, Google) when the catalog is unreachable @-mentions — fuzzy-search every function registered against the engine/compactslash command — summarises conversation history via thecontext-managerworker'scontext::compact, then persists acompactioncustom session entry; the durable transcript is untouched — the marker renders from that entry and the summary anchors future turns- Attachments — multi-file picker with text/image previews
- Function calls — running / pending / error cards; consecutive calls collapse to the latest and expand as one tight stack, while rich code/screenshot displays, approvals, and live calls stay visible; intermediate agent prose summarizes the completed batch; pending approvals use approve/deny gating (
approval::resolve) - Streaming — abortable mid-flight; live thought stream removed from the DOM on completion
- Markdown — GFM, code blocks with prism-react-renderer, syntax-highlighted JSON inputs and outputs
- Conversation sidebar — create, inline rename, delete, auto-title from the first message
- Context-usage meter — token estimate with warn / danger thresholds and a
/compactnudge - Session ID — copyable, deep-links every conversation into the trace explorer via
iii.session.id - Persistence — conversations, active id, last model, sidebar state — all in
localStorage
Traces
Full-fledged OpenTelemetry explorer over engine::traces::* and engine::logs::list. Lives in web/src/pages/TracesV2/.
- Live timeline strip masthead — every span streamed in real time, with a shared funnel as the volume control
- Two detail visualizations — lane timeline (same visual grammar as the strip) and a waterfall tree virtualized for huge traces
- Rich filtering — status, time presets, min/max duration, arbitrary attribute key/value pairs, debounced free-text search, saved views
- Group by — server-side aggregation with lazy per-group member expansion
- Span detail tabs — info, attributes, events, errors, OTel logs, context (baggage), links
- Live streaming — spans append over iii streams (
iii:devtools:*) instead of polling; one seed read, then append
Worktrees
The human window into the worktree worker: parallel agent checkouts, ownership, and land outcomes. Lives in web/src/pages/Worktrees/ and web/src/components/chat/. The whole surface is presence-gated: it appears only while the worktree worker is connected to the engine (the nav entry and picker tab hide; a direct #/worktrees hit lands on an install notice).
- Graph page (
#/worktrees) — repo, worktree, and owning-session nodes fromworktree::list { include_status: true }, refreshed live off all sixworktree::*lifecycle trigger types (poll fallback while bindings are unavailable), with a per-worktree detail panel: branch, base, advisory dev port, clean / ahead / behind, diffstat, and the integrated marker for squash- or rebase-landed branches - Picker tab — the chat working-directory picker grows a worktrees tab next to directory browsing: picking a managed worktree validates the path and claims it for the conversation's session; the console-made claim auto-releases when the conversation points elsewhere (best-effort; the worker's prune sweep is the durable backstop). Worktrees with a land in progress are listed but not retargetable
- Working-dir badge — a conversation rooted in a managed worktree shows branch, short id, dirty
*/ ahead+nindicators, and a lifecycle dot instead of the plain path chip; the raw path stays reachable as the tooltip - Live land notices —
worktree::landed/worktree::land-blockedevents surface in the chat as notices (target branch and merged sha, or the block reason and conflicted files) and refresh the badge
Memory
The human window into the memory worker: named banks of always-injected markdown rules and auto-extracted memories. Lives in web/src/pages/Memory/. Presence-gated like Worktrees: the page appears only while the memory worker is connected (a direct #/memory hit lands on an install notice).
- Bank rail — every bank with live memory / pinned / rule counts and inline create
- Rules tab (first) — the bank's markdown rules as in-place editors (save appears only on edit; delete asks to confirm); the agent appends learned standing instructions to the auto-managed
learnedrule as you correct it in chat - Memories tab — server-paged newest-first list with pin / edit-in-place / tombstone delete, a show-history toggle, and search that runs
memory::recall(the ranked scorer, not a client filter) - Graph tab — entity hubs with memories as spokes: level-of-detail for large banks, draggable nodes, wheel zoom/pan, click a node for an inspect card
- Preview tab — the whole turn before it happens:
memory::previewcomposes the exact system-prompt rules section and the appended memories (ambient floor + budgets applied) for a hypothetical question, with clickable example questions from the bank's own content - Live — the page re-reads off
memory::item-changed/memory::bank-changed(poll fallback), so memories appear the moment they're learned - In chat — a bank picker in the composer (session metadata
memory_bank) and a memory chip on each assistant reply naming the bank, how many rules and memories fed the turn, and whether recall ran semantic; click it to expand the exact records
Live catalogs
The composer's @-mentions and the model picker pull from the engine in real time.
engine::functions::list— TTL-cached function list (VITE_FUNCTIONS_LIST_CACHE_MS, default 10s) →web/src/lib/functions-catalog.tsrouter::models::list— provider-grouped model catalog, refreshed live off therouter::models::changedtrigger type →web/src/lib/models-catalog.ts
Theming
Light and dark themes via data-theme + CSS custom properties. Persisted to localStorage with an inline init script in index.html to prevent flash-of-wrong-theme on first paint.
Worker SDK surface
console registers a single function against the engine for health probes and iii worker info smoke tests:
| Function | Input | Output |
|---|---|---|
console::status |
{} |
{ http_port, engine_url, version } |
Defined in src/functions/status.rs.
Architecture
flowchart LR
Browser["Browser SPA<br/>(iii-browser-sdk)"] -->|"HTTP GET /"| Console
Browser -->|"WS /ws"| Console
Console["console binary<br/>(axum + rust-embed)"] -->|"WebSocket"| Engine["iii engine<br/>:49134"]
Console -. "registers console::status" .-> Engineconsole is a thin HTTP server with exactly two jobs: serve the embedded SPA bundle (with appropriate cache headers) and transparently proxy /ws to the iii engine. The browser only ever talks to one origin.
Configuration
config.yaml
http_port: 3113 # port the UI + /ws are served on (default: 3113)
injectable_ui: true # kill switch for runtime-injected worker UI (default: true)| Key | Default | Description |
|---|---|---|
http_port |
3113 |
TCP port the worker binds for /, /assets/*, and /ws |
injectable_ui |
true |
When false, skips the console:script / console:style / console:assets trigger types, the /ui + /vendor routes, and the SPA loader (console::ui-manifest answers disabled: true) |
CLI flags
| Flag | Default | Description |
|---|---|---|
--config |
./config.yaml |
Path to the YAML config |
--url |
ws://127.0.0.1:49134 |
iii engine WebSocket URL (DEFAULT_ENGINE_URL in src/config.rs) |
--http-port |
from config | Overrides http_port from the config file |
--manifest |
— | Print the publish manifest as JSON and exit (used by the registry pipeline) |
Routes
| Path | Behavior |
|---|---|
GET / |
Embedded index.html (SPA shell, hash-routed client-side). Cache-Control: no-cache, must-revalidate |
GET /assets/* |
Embedded JS / CSS, content-hashed filenames. Cache-Control: public, max-age=31536000, immutable |
GET /ui |
Injected-asset manifest JSON (same shape as console::ui-manifest). no-cache |
GET /ui/* |
Current bytes for a registered injected UI asset. no-cache + ETag: " (304 on If-None-Match) |
GET /vendor/* |
Shared-dep ESM shims for injected scripts (react, @iii-dev/console-ui), generated at web build time. no-cache |
GET /ws (Upgrade) |
WebSocket upgrade; transparent proxy to engine_url (drops browser-originated registertriggertype frames; stamps metadata.internal on registerfunction) |
| anything else | 404 Not Found |
The SPA bundle is embedded into the binary at compile time via rust-embed — the released console has no separate dist/ directory, no side-car asset server, and no runtime filesystem dependency for the UI.
Injectable UI
Workers extend the console at runtime — whole pages and function-trigger
renderers as plain React components sharing the console's React instance
(spec: iii/tech-specs/2026-07-17-injectable-ui). The console owns three
trigger types:
| Type id | Registered by | Carries |
|---|---|---|
console:script |
workers | an ESM script asset; config.path (e.g. state/page.js) is its identity — re-registering a path overrides it (hot reload) |
console:style |
workers | a CSS asset, applied as a swap |
console:assets |
console tabs | the live-update subscription the console pushes sync/set/delete events to |
The trigger's function_id is the worker's content function
({path} → {content, content_type?}); the console fetches over the bus,
hashes, serves from /ui/*, and pushes invalidations so every open tab
disposes the old module and re-imports the new one. Injected scripts default-
export setup(host) and register through host.pages (whole pages at
#/ext/), host.functionTriggers (function-trigger message renderers —
injected renderers dispatch before the built-in families, so matching a
built-in id overrides it; metadata.display promotes the winning renderer's
rich result into the collapsed chat flow), and host.configForms (replace the workers-tab
form region for one configuration id; dirty/save/reset stay host-owned). A
configuration form can opt into { layout: 'full' } to receive the entire
available editor width and height; contained layout remains the default.
Renders are fenced by an ErrorBoundary and scoped under
data-iii-ui=". console::ui-manifest (internal) lists the
loadable assets. The state worker's ui/ directory is the working
reference implementation of all three slot kinds.
Tech stack
| Layer | Choice |
|---|---|
| Web server | axum 0.7, tokio, tokio-tungstenite |
| Asset embedding | rust-embed 8, mime_guess |
| Worker SDK | iii-sdk 0.19.4 |
| UI framework | React 19, Vite 8, TypeScript 6 |
| Styling | Tailwind CSS v4, Radix UI, class-variance-authority, lucide-react |
| Editor | Lexical 0.44 |
| Data fetching | TanStack Query 5 |
| Trace graphs | @xyflow/react 12 + dagre, TanStack Virtual |
| Markdown | react-markdown + remark-gfm, prism-react-renderer |
| Browser SDK | iii-browser-sdk 0.21.6 |
Build from source (contributors only)
cargo build runs pnpm install --frozen-lockfile && pnpm build inside web/ automatically when the web/dist/ bundle is missing or stale (Node + pnpm must be on PATH). To pre-build the bundle once and skip the embedded-asset rebuild loop:
cd web && pnpm install && pnpm build && cd ..
cargo build --releaseEscape hatches (see build.rs):
SKIP_WEB_BUILD=1— skip the JS build step entirely; the existingweb/dist/(if any) is embedded as-is. Useful in CI when the bundle was built in a previous stage.PNPM=/path/to/pnpm— override pnpm discovery.
Run the test suite:
cargo test # unit + manifest + e2e (e2e self-skips if `iii` isn't on PATH)
cd web && pnpm test # vitest
cd web && pnpm typecheck && pnpm lintLicense
Apache 2.0 — see LICENSE.
api reference (json)
{
"functions": [
{
"description": "Internal: re-apply the console's injectable-UI per-worker toggles when the console configuration entry changes.",
"metadata": {
"internal": true
},
"name": "console::on-config-change",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ConfigChangeRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ok": {
"type": "boolean"
}
},
"required": [
"ok"
],
"title": "ConfigChangeAck",
"type": "object"
}
},
{
"description": "Return the console worker's runtime knobs: http_port, engine_url, and version.",
"metadata": {
"internal": true
},
"name": "console::status",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StatusInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"engine_url": {
"description": "iii engine WebSocket URL the worker is proxying to.",
"type": "string"
},
"http_port": {
"description": "TCP port the worker is serving the UI and `/ws` on.",
"format": "uint16",
"minimum": 0,
"type": "integer"
},
"version": {
"description": "Worker version (matches `Cargo.toml`).",
"type": "string"
}
},
"required": [
"engine_url",
"http_port",
"version"
],
"title": "StatusOutput",
"type": "object"
}
},
{
"description": "Serve the console worker's injected console UI assets (content function for its console:script / console:style triggers).",
"metadata": {
"internal": true
},
"name": "console::ui-content",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Input of the content function: the console asks for one asset by path.",
"properties": {
"path": {
"description": "The asset path from the trigger config (e.g. `state/page.js`).",
"type": "string"
}
},
"required": [
"path"
],
"title": "UiContentInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Output of the content function.",
"properties": {
"content": {
"description": "The asset source, verbatim.",
"type": "string"
},
"content_type": {
"description": "MIME type the console should serve the asset with.",
"type": "string"
}
},
"required": [
"content",
"content_type"
],
"title": "UiContentResult",
"type": "object"
}
},
{
"description": "List the injected console UI assets currently loadable: path, kind (script/style), content hash, and style-lint warnings.",
"metadata": {
"internal": true
},
"name": "console::ui-manifest",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "UiManifestInput",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AssetKind": {
"description": "Short asset kind — trigger type ids are `console:*`, kinds stay `script`/`style` in push payloads and the manifest.",
"enum": [
"script",
"style"
],
"type": "string"
},
"ManifestAsset": {
"properties": {
"hash": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/AssetKind"
},
"path": {
"type": "string"
},
"warnings": {
"items": {
"type": "string"
},
"type": "array"
},
"worker": {
"description": "Best-effort registrant attribution. Not resolved in this build — always `null` (the `<worker>/` path prefix is the human-readable attribution).",
"type": [
"string",
"null"
]
}
},
"required": [
"hash",
"kind",
"path",
"warnings"
],
"type": "object"
},
"ManifestWorker": {
"description": "Per-worker summary in the manifest: every worker with at least one *registered* asset — including workers currently toggled off, whose assets are held but not served/announced.",
"properties": {
"assets": {
"description": "Number of registered assets (served only while enabled).",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"enabled": {
"description": "`false` while the worker is in the console configuration's `injectableUi.disabledWorkers` list.",
"type": "boolean"
},
"worker": {
"description": "Worker name — the first segment of its asset paths.",
"type": "string"
}
},
"required": [
"assets",
"enabled",
"worker"
],
"type": "object"
}
},
"properties": {
"assets": {
"description": "The loadable set: assets of per-worker-disabled workers are held but excluded here (and from tab syncs) until re-enabled.",
"items": {
"$ref": "#/definitions/ManifestAsset"
},
"type": "array"
},
"disabled": {
"description": "`true` when the `injectable_ui` kill switch is off — the trigger types are unregistered and `/ui` + `/vendor` are not served.",
"type": "boolean"
},
"workers": {
"description": "Per-worker summary — includes disabled workers (their toggle state lives in the console configuration's `injectableUi.disabledWorkers`).",
"items": {
"$ref": "#/definitions/ManifestWorker"
},
"type": "array"
}
},
"required": [
"assets",
"disabled",
"workers"
],
"title": "UiManifestOutput",
"type": "object"
}
}
],
"triggers": [
{
"description": "A console tab's live-update subscription. The trigger's function_id is the per-tab handler the console pushes sync/set/delete asset events to.",
"invocation_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"description": "`console:assets` subscriptions carry no config; every subscriber gets every event.",
"title": "SubscriptionTriggerConfig",
"type": "object"
},
"metadata": {},
"name": "console:assets",
"return_schema": {}
},
{
"description": "An ESM JavaScript asset injected into the console UI. `config.path` is the asset identity (re-registering a path overrides it); the trigger's function_id is the content function the console fetches source from.",
"invocation_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"description": "Trigger config schema for both asset types (`additionalProperties` enforced console-side; the engine treats trigger config schemas as advisory).",
"properties": {
"path": {
"description": "Asset identity. Convention: `<worker>/<name>.<ext>`. Re-registering a path overrides it.",
"type": "string"
}
},
"required": [
"path"
],
"title": "AssetTriggerConfig",
"type": "object"
},
"metadata": {},
"name": "console:script",
"return_schema": {}
},
{
"description": "A CSS asset injected into the console UI. Same contract as console:script with a .css path.",
"invocation_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"description": "Trigger config schema for both asset types (`additionalProperties` enforced console-side; the engine treats trigger config schemas as advisory).",
"properties": {
"path": {
"description": "Asset identity. Convention: `<worker>/<name>.<ext>`. Re-registering a path overrides it.",
"type": "string"
}
},
"required": [
"path"
],
"title": "AssetTriggerConfig",
"type": "object"
},
"metadata": {},
"name": "console:style",
"return_schema": {}
}
]
}