# ade

> Web console for iii — bundles the React UI and proxies the engine WebSocket on a single port.

| field | value |
|-------|-------|
| version | 1.9.66 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii trigger compose::add worker=ade@1.9.66
```

## configuration

```yaml
- data_dir: data/ade
  http_host: 0.0.0.0
  http_port: 3113
```

## dependencies

- `configuration` @ `latest`

## readme

<div align="center">

# ade

**A single binary. A chat. A trace explorer. The whole [iii](https://github.com/iii-hq) engine in your browser.**

<p>
  <a href="#install"><img alt="Install: iii trigger compose::add worker=ade" src="https://img.shields.io/badge/install-iii%20trigger%20compose%3A%3Aadd%20worker%3Dade-0a84ff?style=flat-square"></a>
  <a href="../LICENSE"><img alt="License: Apache 2.0" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square"></a>
  <a href="https://www.rust-lang.org"><img alt="Built with Rust" src="https://img.shields.io/badge/built%20with-rust-orange?style=flat-square&logo=rust&logoColor=white"></a>
  <a href="https://react.dev"><img alt="React 19" src="https://img.shields.io/badge/react-19-149eca?style=flat-square&logo=react&logoColor=white"></a>
  <a href="https://vitejs.dev"><img alt="Vite 8" src="https://img.shields.io/badge/vite-8-646cff?style=flat-square&logo=vite&logoColor=white"></a>
</p>

</div>

<p align="center">
  <a href="https://raw.githubusercontent.com/iii-hq/workers/main/ade/docs/images/console-traces.webp">
    <img src="https://raw.githubusercontent.com/iii-hq/workers/main/ade/docs/images/console-traces.webp" alt="console — chat and OpenTelemetry trace explorer in a single binary" width="100%" />
  </a>
</p>

## Notice: renamed from `console`

This worker used to be called `console`. Only its identity changed, so nothing that talks to it needs to be rewritten:

- worker name, binary, Cargo package and registry entry: `console` → `ade` (`iii trigger compose::add worker=ade`, `npx skills add iii-hq/workers --skill ade`)
- source directory: `console/` → `ade/`; dependents pin `ade` in their `iii.worker.yaml`
- unchanged on purpose: the `console::*` function ids (`console::status`, `console::ui-manifest`, `console::workspace::*`, …), the `iii::console::*` stream topics, the `console` configuration entry, and the `console:script` / `console:style` trigger types shared through `iii-console-ui` / `@iii-dev/console-ui`

## Install

```bash
iii trigger compose::add worker=ade
```

This resolves the worker, writes its declaration to `worker-compose.yaml`, and reconciles the Compose project.

## Quickstart

```bash
curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
iii project init iii-app && cd iii-app
iii compose --up                 # engine on ws://127.0.0.1:49134
```

```bash
# New terminal, same folder
iii trigger compose::add worker=ade # UI + /ws proxy on :3113
open http://127.0.0.1:3113
```

The 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`](https://github.com/iii-hq/workers/tree/main/harness) durable turn loop. Its manifest declares the whole stack as dependencies — [`session-manager`](https://github.com/iii-hq/workers/tree/main/session-manager) (the conversation store the sidebar, transcripts, and live token rendering are backed by), [`llm-router`](https://github.com/iii-hq/workers/tree/main/llm-router) (generation + the model catalog), [`context-manager`](https://github.com/iii-hq/workers/tree/main/context-manager) (the `/compact` summariser), [`approval-gate`](https://github.com/iii-hq/workers/tree/main/approval-gate) (human-in-the-loop approvals), and the provider workers — so a single command resolves and installs all of it:

```bash
iii trigger compose::add worker=harness
```

### Connect a model: the setup wizard

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. The first time a person loads the ADE on a machine it opens a setup wizard that does this for you — never in a browser under automation (an e2e run, an agent's browser session), which gets the page it asked for. Reopen it any time from the command palette: **Set up the harness**.

1. **Models** — `console::onboarding::scan` looks for the Claude Code and Codex CLIs and whether each is signed in (presence and paths only, never a credential), and the step recommends what it found: a signed-in Claude Code or Codex adds [`provider-claude-code`](https://github.com/iii-hq/workers/tree/main/provider-claude-code) / [`provider-openai-codex`](https://github.com/iii-hq/workers/tree/main/provider-openai-codex) and needs no key; a CLI installed but not signed in says what to do, then **Scan again**. The [`secrets`](https://github.com/iii-hq/workers/tree/main/secrets) worker comes with `llm-router` (a dependency, so the wizard never asks to add it), and `secrets::detect` looks for provider keys in your shell profile and the project's `.env`, shows them masked (`sk-ant…9f2c`) and recommends their providers. Each key is kept where you choose: **Encrypted** (the default; a found key is imported by the secrets worker itself, a pasted one is stored there) or **Environment variable** (the variable already in `.env` is shared as it is, or a pasted key is written to `.env`). Either way only a reference, `secret://ANTHROPIC_API_KEY` or `env://ANTHROPIC_API_KEY`, is written to the `llm-router` configuration, so no key lands in `./config`. The same choice is in each provider's key field in the model picker and in **Settings → Workers → llm-router**. Every other provider worker in the registry is listed too.
2. **Browser** (shown only when the [`browser`](https://github.com/iii-hq/workers/tree/main/browser) worker is installed and `browser::chromium::status` finds no Chromium) — one click on **Download Chromium** runs `browser::chromium::install` (about 200 MB, once per machine) and follows its `browser::chromium-install-progress` events; **I'd rather install it myself** shows the command for the OS and **Check again**. The command palette's **Install Chromium for the browser worker** and an **Install Chromium** action on a failed `browser::*` call (`chromium_missing`) open the wizard on this step.
3. **Judge** (optional) — adds [`judge`](https://github.com/iii-hq/workers/tree/main/judge) with Jev ([`judge-typesafe`](https://github.com/iii-hq/workers/tree/main/judge-typesafe), its `TYPESAFE_API_KEY` stored encrypted by `secrets`) or a local judge, and explains where the harness uses it.
4. **Ready** — what is connected, every worker setup added, and the project's example prompts. **Finish** closes the wizard. The prompts come from `onboarding.yaml` in the project folder (`console::onboarding::prompts`, read on each call; a missing or invalid file shows none): each one has a `title`, an optional `description`, the `agent` profile it runs with, the `prompt`, and `models`, a priority list of `{ provider, model, effort? }`. Clicking one opens a new chat with the prompt in the message box (not sent), its agent profile selected, and the first listed model the router serves here, at that effort; with none available the chat keeps its usual model.

   `onboarding.yaml` may also list registry kits under `kits:` (`<handle>/<name>`, optionally `@<version>`; the harness template lists `iii/deepwork`). The wizard installs them in the background as soon as it opens, one at a time, with `directory::download-kit {kit, apply: true}`; the install keeps going if setup is skipped or closed, and runs once per page load. A kit already installed is `up_to_date`. Ready shows each kit: installing, installed, **needs a review** (its plan was not applied automatically, for example because a profile with the same id exists; **Review** opens Directory → Kits on it) or failed (**Try again**). A project whose iii-directory has no kit support shows nothing.

Nothing is added without a click, and every worker a step adds is named before it runs and logged as it is added: iii is composable, and each worker adds behavior to the project (`worker-compose.yaml`). The wizard keeps everything else in plain words (no function ids, configuration entries, key references or paths); the actions behind each step are `compose::add`, `secrets::import` / `secrets::set` and a configuration write. Finishing or skipping is remembered per machine in `<data_dir>/onboarding.json` (`console::onboarding::get` / `::set`).

The wizard opens by itself only where it can help: never once a model is connected (the router already serves one from a running provider worker), never when the router cannot answer, and never where it is turned off. A deployed ADE starts with an empty data directory, which reads as a first run, so turn it off there: `onboarding.auto_open: false` in the ADE configuration (**Settings → Workers → ADE → Setup**), or `III_CONSOLE_ONBOARDING_AUTO_OPEN=false` in the worker's environment for one environment whatever the configuration says. `console::onboarding::get` reports the result as `auto_open`. The command palette opens the wizard either way.

After setup, every place that takes a provider key uses the same field: **Configure** in the model picker (applied at once, then checked — a key the provider refuses lists no models and is flagged as such), **Settings → Workers → llm-router**, and **Settings → Workers → judge-typesafe** (both apply on Save). It shows the reference and the masked key with **Replace** and **Remove**, offers a key found on the machine, and moves a plain-text or `${VAR}` value into the secrets worker. Where the consumer resolves `env://` references (the `environment` prop; `llm-router` does, `judge-typesafe` does not yet), it also offers keeping the key as an environment variable in the project's `.env`. Injected worker forms get it as `host.components.SecretKeyField`. A configured provider that lists no models stays in the picker, so its key can be fixed from there.

Keys can come from the environment two ways. An `env://ANTHROPIC_API_KEY` reference is read by the secrets worker from the project's env file each time it is resolved, so an edit applies without a restart; the file is `.env` unless the secrets worker's `env_file` names another (one per environment or namespace, e.g. `.env.staging`), and the key field and the wizard name the configured file. Without any reference, `llm-router` falls back to a provider's credential env var (e.g. `ANTHROPIC_API_KEY`), read in the router's own process — in the harness template that means the project's `.env` plus `iii trigger compose::restart worker=llm-router`. See [`llm-router`](https://github.com/iii-hq/workers/tree/main/llm-router#configuration) for the credential model.

Pick a model in the composer and send — the turn streams back through the harness loop.

<details>
<summary><strong>Programmatic check from the SDK</strong></summary>

```rust
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.

</details>

## Why `console`

- **One port, one binary.** The React UI is baked into the executable with [`rust-embed`](https://docs.rs/rust-embed), and the engine WebSocket is reverse-proxied at `/ws` on the same origin. No CORS preflight, no side-car static server, no `dist/` directory to deploy. See [`src/assets.rs`](src/assets.rs) and [`src/proxy.rs`](src/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. See [`web/src/lib/iii-client.ts`](web/src/lib/iii-client.ts).
- **Built for production.** SIGINT *and* SIGTERM graceful shutdown (so `docker stop` and `kubectl delete` actually drain), URL credentials redacted from logs, immutable cache headers on content-hashed assets, and a fully self-contained binary with no runtime filesystem deps. See [`src/main.rs`](src/main.rs).

## Features

### Chat

A purpose-built agentic chat UI on top of [Lexical](https://lexical.dev). Lives in [`web/src/components/chat/`](web/src/components/chat).

- **Two modes** — `ask` and `agent` toggle right in the composer
- **Live model picker** — provider-grouped from `router::models::list`; static fallback (OpenAI, Anthropic, Google) when the catalog is unreachable
- **`@`-mentions** — one paged menu over every function registered against the engine _and_ the files under the session's working directory (the shell worker's `.gitignore`-aware quick-open search); a file pill carries an optional line window (`#file(src/a.ts:12-40)`), opens the shell explorer on those lines when clicked, and is what the shell's "Reference in chat" selection action inserts. A whole-file or folder pill is a reference — the token reaches the model as-is and the agent reads the file or lists the folder on demand; only a line window is read at send time and attached inline as an `<attached-file …>` block
- **`/compact` slash command** — summarises conversation history via the `context-manager` worker's `context::compact`, then persists a `compaction` custom session entry; the durable transcript is untouched — the marker renders from that entry and the summary anchors future turns (and the next `/compact`, which updates it in place instead of re-summarising from scratch). Text after the command — `/compact keep the schema decisions, drop the CSS detour` — is one-shot guidance for the summariser on what to keep, drop, or emphasise
- **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](https://github.com/FormidableLabs/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 `/compact` nudge
- **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`

### Import local conversations

Use **Import conversations** in the chat sidebar to discover, preview, and select histories on the machine running ADE. Codex uses `CODEX_HOME` (default `~/.codex`); Claude Code uses `CLAUDE_CONFIG_DIR` (default `~/.claude`). A container needs read access to these directories. The browser's local filesystem is not scanned.

- Every import creates a new, independent, editable ADE session. Importing the same source again creates another copy. There is no subsequent synchronization with the original.
- Copies include user and assistant text, the commands the agent ran with their recorded output, original message timestamps, and available model metadata. Reasoning, attachments, and subagent histories are omitted. Source files are never changed.
- Commands become the console's own function rows: Codex `CommandExecution`, `FileChange`, `McpToolCall`, and `WebSearch` items, and Claude Code `tool_use`/`tool_result` blocks, are stored as `function_call` blocks with matching `function_result` entries. They keep the source's tool names (`exec`, `apply_patch`, `Bash`, `Read`, …) and never re-run. Planning comes across too: Codex `update_plan` calls become function rows with their recorded output, a Codex plan-mode proposal (its latest revision) becomes an assistant message, and Claude Code's `TodoWrite` and `ExitPlanMode` calls are carried like any other tool. Results the source filed on a sibling branch of a parallel batch are placed right after the call they answer.
- Choose an ADE model and working directory to continue through the normal composer. The original project path is provenance only and grants no filesystem access. Rename, compact, and delete work normally.
- Native readers support Codex 0.154.0 completed-item events and Claude Code's main conversation branch (checked against Claude Agent SDK 0.3.173). Older Codex histories without completed-item events and rewound Codex histories are unsupported; discovery reports skipped histories.
- Discovery is paginated and reads files incrementally. Preview shows the last 50 messages; import copies the full history.

Deploy the matching ADE and Harness changes so a session with imported history and no prior turn requires its initial ADE model and working directory. Sessions that explicitly carry `read_only: true` remain protected.

### Mentions

Workers define what a chat can mention (see
[`crates/mention-contract`](../crates/mention-contract/README.md)). In the
composer, `@` lists the providers beside functions and files, and from two
characters on searches every provider, one group each; Tab (or Enter) on a
provider scopes the menu to `@<provider>:` and its own search. A picked item
is inserted as `@<provider>(id="…")` and shows as a pill with its icon and
name, in the composer and in sent messages; hovering a pill shows the item's
preview card — the generic one, or the worker's own
(`host.mentions.registerRenderer`) — and clicking it opens the item. What
the agent was told about the mentions shows as a quiet "Context for the
agent" row in the turn. The console itself provides `@trace`:
`console::mentions::trace::search` / `console::mentions::trace::get` wrap
`engine::traces::list`, and a trace pill opens the traces screen on that
trace.

### Traces

Full-fledged OpenTelemetry explorer over `engine::traces::*` and `engine::logs::list`. Lives in [`web/src/pages/TracesV2/`](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 updates** — the engine's coalesced `trace` trigger (trace ids only) drives a re-read of the touched traces; one seed read, then notify-then-query, no polling and no stream worker

### Worktrees

The human window into the [`worktree`](https://github.com/iii-hq/workers/tree/main/worktree) worker: parallel agent checkouts, ownership, and land outcomes. Lives in [`web/src/pages/Worktrees/`](web/src/pages/Worktrees) and [`web/src/components/chat/`](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 from `worktree::list { include_status: true }`, refreshed live off all six `worktree::*` 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 `+n` indicators, 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-blocked` events 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`](https://github.com/iii-hq/workers/tree/main/memory) worker: named banks of always-injected markdown rules and auto-extracted memories. Lives in [`web/src/pages/Memory/`](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 `learned` rule 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::preview` composes 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.

- `coder::search` (shell worker, path-only, fuzzy, `respect_gitignore`, no dot-entries) — the files half of the `@` menu, one call per settled keystroke → [`web/src/lib/file-search.ts`](web/src/lib/file-search.ts)
- `engine::functions::list` — TTL-cached function list (`VITE_FUNCTIONS_LIST_CACHE_MS`, default 10s) → [`web/src/lib/functions-catalog.ts`](web/src/lib/functions-catalog.ts)
- `router::models::list` — provider-grouped model catalog, refreshed live off the `router::models::changed` trigger type → [`web/src/lib/models-catalog.ts`](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 health probe for `iii worker info` smoke tests, plus the workspace functions that let an agent show the human a screen next to the conversation:

| Function | Input | Output |
|---|---|---|
| `console::status` | `{}` | `{ url, http_port, engine_url, version }` |
| `console::workspace::list` | `{}` | `{ tabs: [{ id, name?, columns, screens, sizes, active }], active_tab_id }` |
| `console::workspace::open` | `{ screen, session_id?, relative_to?, direction?, sizes?, activate? }` | `{ tab_id, column, placement, screens, sizes, activated }` |
| `console::workspace::close` | `{ screen, session_id? }` | `{ tab_ids }` |
| `console::workspace::get` (internal) | `{}` | `{ value, path }` — the raw layout document and the file it lives in |
| `console::workspace::set` (internal) | `{ value, expected_revision? }` | `{ ok }` — replaces the document wholesale (the SPA's read-modify-write path). Every write stamps the document's `revision`; with `expected_revision` older than the stored one, nothing is written and the call fails `WORKSPACE_CONFLICT` |

A screen is `chat`, `chat:<session-id>`, `traces`, `workers`, or `ext:<page-id>` for a worker page (`ext:ide`, `ext:browser`, `ext:editor`, ...). Call `open` with `{ screen: "chat", session_id: "<id>" }` to open a panel pinned to one conversation; the structured input is persisted as `chat:<session-id>`, and opening that exact session again reuses its existing panel. The workspace layout is stored by the console worker in `<data_dir>/workspace.json` (`data_dir` is a `console` configuration setting, default `data/ade`) and every write rings the `console::workspace::changed` trigger type (empty config, empty event: re-read with `list`), which each browser pointed at this engine binds to pick the change up at once (every binding is also rung once as it registers, which catches a browser up after its first read and after each reconnect; a tab polls only until that first ring arrives, and re-reads when it regains focus); it is ephemeral per-instance state and is deliberately kept out of the committed configuration YAML. `open` stays on the active tab when it already shows the screen, else switches to the tab that does, else places the screen beside an anchor column (adjacent empty column, any empty column, new column), else opens a fresh tab; `placement` reports which (`existing`, `empty_column`, `new_column`, `new_tab`). The anchor is `relative_to` — a screen in the same vocabulary, defaulting to `chat`, which matches whichever chat panel the active tab shows — and `direction` (`right`, the default, or `left`) picks the side of it. A named anchor (anything but `chat`) is looked for in every tab, the active one first, because the active pointer is whichever browser clicked last; an anchor mounted nowhere puts the column at the end of the active tab. `activate` (default `true`) stamps a function activation every time, pointer moved or not, so every open console switches to the tab holding the screen. `sizes` sets the tab's column widths — one positive number per column of the tab AFTER the call, normalized by their sum — in the same write that places the screen, so no other writer can land between placement and sizing; `list` reports the current widths to compute them from, and a list that does not match the column count fails with `WORKSPACE_INVALID_SIZES` rather than being ignored. It never replaces a mounted screen. `close` detaches the screen everywhere it is mounted and is idempotent; pass the same `screen: "chat"` and `session_id` to close one pinned conversation panel. Unknown screens fail with `WORKSPACE_INVALID_SCREEN`; an unreadable or unwritable `data_dir` with `WORKSPACE_UNAVAILABLE`.

Defined in [`src/functions/status.rs`](src/functions/status.rs) and [`src/functions/workspace.rs`](src/functions/workspace.rs); the file store is [`src/workspace_store.rs`](src/workspace_store.rs).

### Workers screen and the compose project

The `workers` screen lists every worker the compose daemon runs, grouped into what needs attention, registry packages and local-path workers, plus the workers connected to the engine outside compose. A container opens on its followed log (`compose::logs`), its Source (pin another registry version through `compose::add`, which restarts only that container, or point a local worker at another checkout), its Settings (run script, `start_after`, environment, `config_override`) and, while connected, the functions it registered. The project view shows the start order, which packages have newer releases, the compose file and the daemon; **Add worker** declares one from the registry or a local directory. Lifecycle is the daemon's own `compose::*` functions; `add`, `update` and `remove` run as compose operations the page follows with `compose::operation`. A change restarts only the container it touches (a Settings edit to a registry package restarts it explicitly, since compose only bounces a package whose version changed); because compose brings every declared container up after an add or a remove, the page names the stopped ones first.

The console adds what the daemon does not expose, read from the compose file and this host (the console runs beside the daemon):

| Function | Input | Output |
|---|---|---|
| `console::compose::project` | `{ file? }` | The compose file as declared: namespace, engine endpoint, timeouts, each container's source, version, `start_after`, environment keys and run script |
| `console::compose::versions` | `{ container? , name?, file? }` | `{ container, reference, declared, versions: [{ version, tags, created_at }] }` from the registry, newest first |
| `console::compose::search` | `{ query }` | `{ workers: [{ name, version, description, dependencies }] }`; engine workers are left out |
| `console::compose::inspect` | `{ path, run? }` | `{ path, exists, manifest, run_found, checkouts, workers }`: a directory's `iii.worker.yaml`, whether a relative run command is built, its git checkouts (branch, HEAD commit time, uncommitted changes), or the workers inside a folder |
| `console::compose::container` | `{ container, file? }` | One declaration; literal values whose names look like credentials are masked |
| `console::compose::edit` | `{ container, worker?, run?, start_after?, environment?: { set, unset }, config_override?, file? }` | The accepted `compose::add` operation. The change merges with the declared entry, so masked values keep their file values; a new path must keep the container's name |

`console::compose::changed` fires when the daemon writes `state.json` or the compose file changes (bind with an empty config; the event carries `kind`, `file`, `namespace`, `state_dir`, `path`, `captured_at`). Defined in [`src/compose/`](src/compose/).

## Architecture

```mermaid
flowchart LR
    Browser["Browser SPA<br/>(iii-browser-sdk)"] -->|"HTTP GET /"| Console
    Browser -->|"HTTP GET /runtime"| Console
    Browser -->|"WS /ws"| Console
    Console["console binary<br/>(axum + rust-embed)"] -->|"WebSocket"| Engine["iii engine<br/>:49134"]
    Console -. "registers console::status, console::workspace::*" .-> Engine
```

`console` is a thin HTTP server. It serves the embedded SPA bundle and
runtime connection settings, hosts injected worker UI assets, and transparently
proxies `/ws` to the iii engine. The browser only ever talks to one origin.

## Configuration

Console registers a `console` entry with the central `configuration` worker.
Its `http_port` value is authoritative after first registration: Console reads
it before binding, watches `configuration:updated`, and moves the listener live
when the port changes. The replacement port is bound and started before the old
listener is gracefully drained; a failed bind keeps the previous listener and
port active.

The local `config.yaml` is a first-registration seed and a fallback for direct
runs where the configuration worker is unavailable:

```yaml
http_host: 0.0.0.0    # HTTP bind address (default: all IPv4 interfaces)
http_port: 3113       # initial port seed for the UI + /ws (default: 3113)
injectable_ui: true   # kill switch for runtime-injected worker UI (default: true)
data_dir: data/ade    # ephemeral per-instance state: the workspace layout (default: data/ade)
```

| Key | Default | Description |
|---|---|---|
| `http_host` | `0.0.0.0` | HTTP bind address; set `127.0.0.1` for local access only. Applied at startup |
| `http_port` | `3113` | Initial TCP port seed for `/`, `/assets/*`, and `/ws`; the stored `console.http_port` wins thereafter |
| `injectable_ui` | `true` | When `false`, skips the `console:script` / `console:style` / `console:module` / `console:assets` trigger types, the `/ui` + `/vendor` routes, and the SPA loader (`console::ui-manifest` answers `disabled: true`) |
| `data_dir` | `data/ade` | Initial seed for the directory holding ephemeral per-instance state — the workspace tabs/panes layout (`workspace.json`). Relative paths resolve against `III_COMPOSE_DIR` (or the process directory outside Compose); absolute and `~/` paths keep their meaning. The stored `console.data_dir` wins thereafter |

The configuration entry also stores UI preferences and
`injectableUi.disabledWorkers`. Port, `data_dir` and per-worker UI changes apply
without a Console restart; the local `injectable_ui` kill switch remains
startup-only.

The workspace layout itself (tabs, panes, active tab) is **not** part of the
entry: it changes on every click, and the configuration YAML is meant to be
committed. It lives in `<data_dir>/workspace.json` (atomic writes; a missing
or unreadable file starts from the default chat + traces tab). Entries written
by older Console versions still carry a `workspace` section — on boot the
worker imports it into the file once (unless the file already exists) and
removes it from the entry.

### CLI flags

| Flag | Default | Description |
|---|---|---|
| `--config <path>` | `./config.yaml` | Path to the first-registration YAML seed/fallback |
| `--url <ws://…>` | `ws://127.0.0.1:49134` | iii engine WebSocket URL (`DEFAULT_ENGINE_URL` in [`src/config.rs`](src/config.rs)) |
| `--http-host <address>` | from seed | Overrides the YAML bind address; applied at startup |
| `--http-port <port>` | from seed | Overrides the YAML port seed; an existing configuration-worker value still wins |
| `--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 /runtime` | Runtime connection settings for the SPA, including the console worker namespace. `Cache-Control: no-store` |
| `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: "<hash>"` (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`](https://docs.rs/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, function-trigger
renderers, and layered trigger-activity renderers as plain React
components sharing the console's React instance
(spec: `iii/tech-specs/2026-07-17-injectable-ui`). The console owns four
injectable-UI 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:module` | workers | an ESM module served at `/ui/<path>` like a script (`.js`, same cap and per-worker toggle) but never imported at mount: a script loads it with `host.importModule(path)` (the ide's xterm terminal). Manifest and pushes carry it as kind `module` |
| `console:style` | workers | a CSS asset, applied as a `<link>` 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, opened
through `host.panels.open` or `console::workspace::open`; built-in screens open through `host.panels.openScreen` (below); `#/worker/<scope>/<id>`
renders one alone), `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), `host.triggerRenderers` (override
the compact timeline display, expanded details, source section, and raw-data
redaction for normalized registration/fired/retirement activities, with host
fallbacks for every slot), and
`host.configForms` (provide the deliberate form body for one configuration id
inside global Settings; dirty/save/reset and schema validation stay
host-owned), and `host.mentions` (draw the hover preview of the worker's own
`@<provider>(id=…)` chat mentions; the generic card covers a renderer that
is absent or throws). There is no generic schema-generated form fallback. 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="<worker>"`. `console::ui-manifest` (internal) lists the
loadable assets. The `state` worker's `ui/` directory is the broad delivery
reference; `cron/ui/` is the trigger-activity renderer reference.

## Tech stack

| Layer | Choice |
|---|---|
| Web server | [axum 0.7](https://github.com/tokio-rs/axum), [tokio](https://tokio.rs), [tokio-tungstenite](https://github.com/snapview/tokio-tungstenite) |
| Asset embedding | [`rust-embed` 8](https://docs.rs/rust-embed), [`mime_guess`](https://docs.rs/mime_guess) |
| Worker SDK | [`iii-sdk` 0.19.4](https://docs.rs/iii-sdk) |
| UI framework | [React 19](https://react.dev), [Vite 8](https://vitejs.dev), [TypeScript 6](https://www.typescriptlang.org) |
| Styling | [Tailwind CSS v4](https://tailwindcss.com), [Radix UI](https://www.radix-ui.com), [`class-variance-authority`](https://cva.style), [lucide-react](https://lucide.dev) |
| Editor | [Lexical 0.44](https://lexical.dev) |
| Data fetching | [TanStack Query 5](https://tanstack.com/query) |
| Trace graphs | [`@xyflow/react` 12](https://reactflow.dev) + [dagre](https://github.com/dagrejs/dagre), [TanStack Virtual](https://tanstack.com/virtual) |
| Markdown | [react-markdown](https://github.com/remarkjs/react-markdown) + [remark-gfm](https://github.com/remarkjs/remark-gfm), [prism-react-renderer](https://github.com/FormidableLabs/prism-react-renderer) |
| Browser SDK | [`iii-browser-sdk` 0.21.6](https://www.npmjs.com/package/iii-browser-sdk) |

<details>
<summary><strong>Build from source (contributors only)</strong></summary>

`cargo build` runs `pnpm install --frozen-lockfile && pnpm build` inside [`web/`](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:

```bash
cd web && pnpm install && pnpm build && cd ..
cargo build --release
```

Escape hatches (see [`build.rs`](build.rs)):

- `SKIP_WEB_BUILD=1` — skip the JS build step entirely; the existing `web/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:

```bash
cargo test                          # unit + manifest + e2e (e2e self-skips if `iii` isn't on PATH)
cd web && pnpm test                 # vitest
cd web && pnpm typecheck && pnpm lint
```

</details>

## License

Apache 2.0 — see [LICENSE](https://github.com/iii-hq/workers/blob/main/LICENSE).

## skills

This version ships 5 skill files for agents, also browsable on the worker page:

- [`SKILL.md`](https://workers.iii.dev/workers/ade?tab=skills&skill=SKILL.md&version=1.9.66)
- [`skills/design-console-ui.md`](https://workers.iii.dev/workers/ade?tab=skills&skill=skills%2Fdesign-console-ui.md&version=1.9.66)
- [`skills/design-system.md`](https://workers.iii.dev/workers/ade?tab=skills&skill=skills%2Fdesign-system.md&version=1.9.66)
- [`skills/injectable-ui.md`](https://workers.iii.dev/workers/ade?tab=skills&skill=skills%2Finjectable-ui.md&version=1.9.66)
- [`skills/mentions.md`](https://workers.iii.dev/workers/ade?tab=skills&skill=skills%2Fmentions.md&version=1.9.66)

### SKILL.md

# console

The console is a single Rust binary that serves the React SPA and proxies the
engine WebSocket on one port (`http_port`, 3113 by default; `console::status`
returns the actual `url`). It renders the
chat (on the `harness` turn loop), the OpenTelemetry trace explorer, the worker
catalog, and a tabbed workspace of floating panels the operator arranges. It is
also the delivery host for **injectable UI**: any worker registers a
`console:script` / `console:style` trigger whose `config.path` names an asset
and whose `function_id` serves the bytes, and every open tab imports the module,
calls its `setup(host)`, and hot-reloads it on re-registration. No console
rebuild, no iframe.

Reach for this worker in two situations. First, when a worker needs a face in
the console — a page, a function-call or trigger-activity renderer, a settings
form — build it against the three companion skills below rather than inventing
markup. Second, when an agent should change what the operator is looking at:
open a worker page beside the conversation, pin a chat, close a screen, or
propose that the session follow a project created elsewhere.

Prerequisites: the `configuration` worker (the console stores its own settings
and every worker's configuration entry there). Chat needs `harness` and its
stack; traces need the engine's OpenTelemetry export.

## When to Use

- A worker must ship a console page, renderer, or configuration form — read
  `ade/injectable-ui` (the delivery contract) first, then
  `ade/design-console-ui` (responsive UX and forms), with
  `ade/design-system` open for every visual rule and number.
- A worker's records should be mentionable in chat (`@calendar(id="…")`:
  composer autocomplete, pills with a hover preview, agents receiving the
  record resolved): read `ade/mentions` — two functions and a metadata
  descriptor, no console change.
- You changed a worker's UI and must prove it is loadable: read
  `console::ui-manifest` and require an empty `warnings` array.
- The user should watch something in the console: `console::workspace::open`
  with `ext:<page>`, `workers`, `traces`, or a pinned `chat`.
- You created or cloned a project in another directory and the user asked to
  continue there: `console::working-directory::propose` (never just to inspect
  a file).
- You need to know what the operator currently sees: `console::workspace::list`.

## Boundaries

- The console renders; it does not own domain data. Injected UI acts by
  calling its own worker's functions over `host.iii`, and configuration values
  live in the `configuration` worker.
- `console:assets` is the tab-side live-update subscription. Workers never
  register it; register `console:script` / `console:style` only.
- Injected styles are scoped under `[data-iii-ui="<worker>"]`; an unscoped
  rule restyles the whole console and is reported as a manifest warning.
- Register asset triggers through the SDK's Message path so they are
  garbage-collected on disconnect — not through the durable
  `engine::register_trigger`.
- `workspace::open` reuses a tab that already shows the screen; it never
  duplicates panels. Selection of the active tab is per browser tab and only
  follows a function-driven activation, which every activating `open` stamps.
  A page that asks for a panel of its own passes `relative_to` with its own
  screen id, so the panel lands beside the page, in whichever tab shows it,
  instead of beside the chat.
- An injected page placing a screen for the operator in front of it calls
  `host.panels.openScreen({ screen, relativeTo?, direction?, sizes? })`
  (built-in screens like `traces` included): local and optimistic, no bus
  round trip. Feature-detect it and fall back to `console::workspace::open`.
- Native console UI (`ade/web`) and worker UI change in separate pull
  requests; the shared component surface is `@iii-dev/console-ui`
  (`packages/console-ui`) and its `index.d.ts` is the only API contract.

## Functions

- `console::status` — runtime knobs: `url`, `http_port`, `engine_url`, `version`; use for liveness and readiness, and open ADE pages on this machine (browser sessions, curl) at `url` (`<url>/#/worker/<scope>/<page-id>`) instead of assuming port 3113. `url` is loopback when the console listens on every interface: to show the operator a page, use `console::workspace::open`, not a pasted link.
- `console::ui-manifest` — every injected asset currently loadable, with path, kind, content hash, and style-lint warnings; the authoritative check after registering UI.
- `console::workspace::list` — the operator's workspace: tabs, columns, screens, column widths, and the active tab.
- `console::workspace::open` — show a screen next to the conversation (`ext:<page>`, `workers`, `traces`, or a pinned `chat` by `session_id`). It lands right of the chat panel; `relative_to` names another mounted screen to sit beside and `direction` (`right`/`left`) picks the side. `sizes` sets the tab's column widths in the same write — read `workspace::list` first for the widths that are up.
- `console::workspace::close` — remove a screen wherever it is shown; idempotent.
- `console::working-directory::propose` — ask the operator to move the session (chat and paired shell) to a directory created or cloned elsewhere.
- `console::ui-content` — the console's own content function for its injected catalog pages; internal.
- `console::on-config-change`, `console::working-directory::inject-guidance`, `console::working-directory::stamp-session` — internal wiring; never call directly.
- `console::mentions::trace::search`, `console::mentions::trace::get` — the `@trace(id="<trace_id>")` chat mention provider over `engine::traces::list`; internal. Agents read a mentioned trace through `engine::traces::tree` (see `ade/mentions`).

## Reactive triggers

The console owns four trigger types. Two are the injectable-UI contract a
worker binds to ship UI; the third is tab-internal; the fourth reports layout
changes.

- `console:script` — an ESM JavaScript asset. `config: { path }` is the
  identity (`<worker>/page.js`); the trigger's `function_id` is the worker's
  content function returning `{ content, content_type? }` for `{ path }`.
- `console:style` — a CSS asset with the same contract and a `.css` path.
- `console:assets` — a tab's live-update subscription; the console registers
  it itself.
- `console::workspace::changed` — fires after every workspace layout write
  (`open`, `close`, a browser's own edit), and once when a binding registers.
  Empty config, empty event: a page that watches the layout binds it and
  re-reads `console::workspace::list`.

Bind the first two once per asset at worker startup, after the content
function is registered. Re-registering the same path with different bytes
hot-swaps the asset in every open tab; identical bytes are a no-op. Rust
workers use the `iii-console-ui` crate (`ConsoleUi::new("<worker>").script(..).style(..).register(&iii)`),
which also wires the `III_<WORKER>_UI_WATCH` development watcher; Node workers
register the content function and the two triggers directly.

## Skills shipped with this worker

- `ade/injectable-ui` — the delivery contract for worker UI: project
  layout, `setup(host)` and every slot, the wire contract, Rust and Node
  registration, the shared build driver and its lint, scoped CSS, the
  hooks/format/icon packages, hot reload, debugging, and the definition of done.
- `ade/design-console-ui` — responsive UX and forms: archetypes, pane-width
  (not viewport) behavior, phone drill-in and bottom sheets, state integrity,
  configuration and provider forms, and the validation matrix.
- `ade/design-system` — the iii Schematic design system: tokens, surface
  ramp, typography, radius, elevation, motion, the canonical components, and
  the one numbers table the other two skills link to.

The migration of the remaining worker UIs onto the shared package is tracked
in `docs/plans/2026-09-16-worker-ui-migration.md`.

## api reference

```json
{
  "functions": [
    {
      "description": "Read one container's declaration for editing: worker, version, start_after, env_file, environment (credential-like literals masked), run script, and config_override as YAML.",
      "metadata": {},
      "name": "console::compose::container",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "container": {
            "type": "string"
          },
          "file": {
            "description": "Compose file on the daemon host; defaults to the daemon project.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "container"
        ],
        "title": "ContainerInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "EnvVar": {
            "properties": {
              "key": {
                "type": "string"
              },
              "secret": {
                "type": "boolean"
              },
              "value": {
                "description": "`None` when the value is a secret or empty.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "key",
              "secret"
            ],
            "type": "object"
          }
        },
        "properties": {
          "config_override": {
            "description": "The override as YAML text, as the page edits it.",
            "type": [
              "string",
              "null"
            ]
          },
          "env_file": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "environment": {
            "items": {
              "$ref": "#/definitions/EnvVar"
            },
            "type": "array"
          },
          "name": {
            "type": "string"
          },
          "run": {
            "type": [
              "string",
              "null"
            ]
          },
          "start_after": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "version": {
            "type": [
              "string",
              "null"
            ]
          },
          "worker": {
            "type": "string"
          }
        },
        "required": [
          "env_file",
          "environment",
          "name",
          "start_after",
          "worker"
        ],
        "title": "ContainerEntry",
        "type": "object"
      }
    },
    {
      "description": "Change one container's worker path, run script, start_after, environment, or config_override. Merges with the declared entry (masked secrets keep their file values) and hands it to compose::add; returns the accepted compose operation.",
      "metadata": {},
      "name": "console::compose::edit",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "EnvPatch": {
            "properties": {
              "set": {
                "additionalProperties": {
                  "type": "string"
                },
                "default": {},
                "type": "object"
              },
              "unset": {
                "default": [],
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "type": "object"
          }
        },
        "properties": {
          "config_override": {
            "description": "YAML text; empty removes the override.",
            "type": [
              "string",
              "null"
            ]
          },
          "container": {
            "type": "string"
          },
          "environment": {
            "anyOf": [
              {
                "$ref": "#/definitions/EnvPatch"
              },
              {
                "type": "null"
              }
            ]
          },
          "file": {
            "description": "Compose file on the daemon host; defaults to the daemon project.",
            "type": [
              "string",
              "null"
            ]
          },
          "operation_id": {
            "description": "Caller-selected id for following the compose operation.",
            "type": [
              "string",
              "null"
            ]
          },
          "run": {
            "description": "New `scripts.run`; an empty string removes it.",
            "type": [
              "string",
              "null"
            ]
          },
          "start_after": {
            "items": {
              "type": "string"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "worker": {
            "description": "New worker source, e.g. `path:///home/me/other-checkout/web`. The container keeps its name, so the directory name must match it.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "container"
        ],
        "title": "EditInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "What `compose::add` answers once it accepts an edit.",
        "properties": {
          "operation_id": {
            "type": "string"
          },
          "requested": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "status": {
            "type": "string"
          }
        },
        "required": [
          "operation_id",
          "requested",
          "status"
        ],
        "title": "EditAccepted",
        "type": "object"
      }
    },
    {
      "description": "Inspect a directory on this host: its iii.worker.yaml, whether a relative run command is built, other git checkouts of the same worker, or the workers inside a plain folder.",
      "metadata": {},
      "name": "console::compose::inspect",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "path": {
            "description": "Absolute directory on the daemon host; `~/` expands to the home directory.",
            "type": "string"
          },
          "run": {
            "description": "The container's run command; a relative executable is checked for existence.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "path"
        ],
        "title": "InspectInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Checkout": {
            "properties": {
              "branch": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "committed_at": {
                "description": "Unix seconds of the checkout's HEAD commit.",
                "format": "int64",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "dirty": {
                "description": "Tracked files differ from HEAD.",
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "path": {
                "description": "This worker's directory inside the checkout.",
                "type": "string"
              }
            },
            "required": [
              "path"
            ],
            "type": "object"
          },
          "Manifest": {
            "properties": {
              "bin": {
                "description": "`bin`: the binary a Rust worker builds.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "dependencies": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "language": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "name": {
                "type": "string"
              },
              "path": {
                "type": "string"
              },
              "start": {
                "description": "`scripts.start`: how compose starts the worker when its entry has no `run`. Without either, the container cannot start.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "dependencies",
              "name",
              "path"
            ],
            "type": "object"
          }
        },
        "properties": {
          "checkouts": {
            "items": {
              "$ref": "#/definitions/Checkout"
            },
            "type": "array"
          },
          "exists": {
            "type": "boolean"
          },
          "manifest": {
            "anyOf": [
              {
                "$ref": "#/definitions/Manifest"
              },
              {
                "type": "null"
              }
            ]
          },
          "path": {
            "type": "string"
          },
          "run_found": {
            "description": "`Some(false)` when the run command is a relative executable missing here.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "workers": {
            "description": "Workers directly inside a directory that is not itself a worker.",
            "items": {
              "$ref": "#/definitions/Manifest"
            },
            "type": "array"
          }
        },
        "required": [
          "checkouts",
          "exists",
          "path",
          "workers"
        ],
        "title": "Inspection",
        "type": "object"
      }
    },
    {
      "description": "Read the compose file as declared: namespace, engine endpoint, timeouts, and each container's source, version, start_after, environment keys and run script.",
      "metadata": {},
      "name": "console::compose::project",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "file": {
            "description": "Compose file on the daemon host; defaults to the daemon project.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "ProjectInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ContainerSource": {
            "enum": [
              "path",
              "package",
              "unknown"
            ],
            "type": "string"
          },
          "DeclaredContainer": {
            "properties": {
              "environment": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "name": {
                "type": "string"
              },
              "ref": {
                "type": "string"
              },
              "run": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "source": {
                "$ref": "#/definitions/ContainerSource"
              },
              "start_after": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "version": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "environment",
              "name",
              "ref",
              "source",
              "start_after"
            ],
            "type": "object"
          }
        },
        "properties": {
          "containers": {
            "items": {
              "$ref": "#/definitions/DeclaredContainer"
            },
            "type": "array"
          },
          "engine_host": {
            "type": [
              "string",
              "null"
            ]
          },
          "engine_port": {
            "format": "uint16",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "engine_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "file": {
            "type": "string"
          },
          "namespace": {
            "type": [
              "string",
              "null"
            ]
          },
          "startup_timeout": {
            "type": [
              "string",
              "null"
            ]
          },
          "stop_timeout": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "containers",
          "file"
        ],
        "title": "ProjectDeclaration",
        "type": "object"
      }
    },
    {
      "description": "Search the worker registry by name for workers Compose can declare (engine workers are left out).",
      "metadata": {},
      "name": "console::compose::search",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "query": {
            "type": "string"
          }
        },
        "required": [
          "query"
        ],
        "title": "SearchInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "RegistryWorker": {
            "properties": {
              "dependencies": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "description": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            },
            "required": [
              "dependencies",
              "description",
              "name",
              "version"
            ],
            "type": "object"
          }
        },
        "properties": {
          "workers": {
            "items": {
              "$ref": "#/definitions/RegistryWorker"
            },
            "type": "array"
          }
        },
        "required": [
          "workers"
        ],
        "title": "SearchResult",
        "type": "object"
      }
    },
    {
      "description": "List registry versions, newest first, with their release channels: of a declared package container, or of a worker name on the default registry.",
      "metadata": {},
      "name": "console::compose::versions",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "container": {
            "description": "A declared package container.",
            "type": [
              "string",
              "null"
            ]
          },
          "file": {
            "description": "Compose file on the daemon host; defaults to the daemon project.",
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "description": "Or a worker name on the default registry, for one not declared yet.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "VersionsInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "PackageVersion": {
            "properties": {
              "created_at": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "tags": {
                "description": "Release channels pointing at this version, such as `latest` or `next`.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "version": {
                "type": "string"
              }
            },
            "required": [
              "tags",
              "version"
            ],
            "type": "object"
          }
        },
        "properties": {
          "container": {
            "type": "string"
          },
          "declared": {
            "description": "The selector the compose file declares: an exact version or a channel.",
            "type": [
              "string",
              "null"
            ]
          },
          "reference": {
            "type": "string"
          },
          "versions": {
            "description": "Newest first, as the registry lists them.",
            "items": {
              "$ref": "#/definitions/PackageVersion"
            },
            "type": "array"
          }
        },
        "required": [
          "container",
          "reference",
          "versions"
        ],
        "title": "VersionsResult",
        "type": "object"
      }
    },
    {
      "description": "Returns this worker instance's effective configuration entry ID, without its value.",
      "metadata": {
        "internal": true
      },
      "name": "console::configuration-id",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Empty user request. Ignore transport metadata such as `_caller_worker_id`, which the engine adds to routed calls; no payload field selects the entry.",
        "title": "ConfigurationIdentityRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Only the entry ID is exposed; configuration values and secrets stay in the service.",
        "properties": {
          "id": {
            "type": "string"
          }
        },
        "required": [
          "id"
        ],
        "title": "ConfigurationIdentityResponse",
        "type": "object"
      }
    },
    {
      "description": "Find selectable Codex or Claude Code histories on the ADE host.",
      "metadata": {
        "internal": true
      },
      "name": "console::conversations::discover",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Source": {
            "enum": [
              "codex",
              "claude-code"
            ],
            "type": "string"
          }
        },
        "properties": {
          "cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "query": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "$ref": "#/definitions/Source"
          }
        },
        "required": [
          "source"
        ],
        "title": "DiscoverRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ConversationSummary": {
            "properties": {
              "created_at": {
                "format": "int64",
                "type": "integer"
              },
              "cwd": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "id": {
                "type": "string"
              },
              "source": {
                "$ref": "#/definitions/Source"
              },
              "title": {
                "type": "string"
              },
              "updated_at": {
                "format": "int64",
                "type": "integer"
              }
            },
            "required": [
              "created_at",
              "id",
              "source",
              "title",
              "updated_at"
            ],
            "type": "object"
          },
          "Source": {
            "enum": [
              "codex",
              "claude-code"
            ],
            "type": "string"
          }
        },
        "properties": {
          "available": {
            "type": "boolean"
          },
          "conversations": {
            "items": {
              "$ref": "#/definitions/ConversationSummary"
            },
            "type": "array"
          },
          "directory": {
            "type": "string"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "$ref": "#/definitions/Source"
          },
          "warnings": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "available",
          "conversations",
          "directory",
          "source",
          "warnings"
        ],
        "title": "Catalog",
        "type": "object"
      }
    },
    {
      "description": "Create a new editable ADE conversation from a local history, commands included.",
      "metadata": {
        "internal": true
      },
      "name": "console::conversations::import",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Source": {
            "enum": [
              "codex",
              "claude-code"
            ],
            "type": "string"
          }
        },
        "properties": {
          "id": {
            "type": "string"
          },
          "source": {
            "$ref": "#/definitions/Source"
          }
        },
        "required": [
          "id",
          "source"
        ],
        "title": "Selection",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "imported_commands": {
            "description": "Tool calls that came across with their recorded results.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "imported_messages": {
            "description": "Every entry appended to the new session: turns, calls, and results.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "session_id": {
            "type": "string"
          },
          "total_messages": {
            "description": "User and assistant turns that carried text.",
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "imported_commands",
          "imported_messages",
          "session_id",
          "total_messages"
        ],
        "title": "ImportResult",
        "type": "object"
      }
    },
    {
      "description": "Preview a selected local conversation without importing or running it.",
      "metadata": {
        "internal": true
      },
      "name": "console::conversations::preview",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Source": {
            "enum": [
              "codex",
              "claude-code"
            ],
            "type": "string"
          }
        },
        "properties": {
          "id": {
            "type": "string"
          },
          "source": {
            "$ref": "#/definitions/Source"
          }
        },
        "required": [
          "id",
          "source"
        ],
        "title": "Selection",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ConversationSummary": {
            "properties": {
              "created_at": {
                "format": "int64",
                "type": "integer"
              },
              "cwd": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "id": {
                "type": "string"
              },
              "source": {
                "$ref": "#/definitions/Source"
              },
              "title": {
                "type": "string"
              },
              "updated_at": {
                "format": "int64",
                "type": "integer"
              }
            },
            "required": [
              "created_at",
              "id",
              "source",
              "title",
              "updated_at"
            ],
            "type": "object"
          },
          "ExternalCall": {
            "description": "A tool the source's agent called during a turn, as the source recorded it.",
            "properties": {
              "arguments": {
                "description": "The recorded arguments, verbatim."
              },
              "function_id": {
                "description": "The source's own tool name (`Bash`, `Read`, `exec`, `apply_patch`, `server::tool`).",
                "type": "string"
              },
              "id": {
                "description": "The source's own call identity; the matching result echoes it.",
                "type": "string"
              }
            },
            "required": [
              "arguments",
              "function_id",
              "id"
            ],
            "type": "object"
          },
          "ExternalMessage": {
            "properties": {
              "call_id": {
                "description": "On a `function_result`: the call it answers and the tool that ran.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "calls": {
                "description": "The calls an assistant turn made, in order; empty for every other role.",
                "items": {
                  "$ref": "#/definitions/ExternalCall"
                },
                "type": "array"
              },
              "function_id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "id": {
                "type": "string"
              },
              "is_error": {
                "description": "On a `function_result`: whether the source recorded the call as failed.",
                "type": "boolean"
              },
              "model": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "native_stop_reason": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "provider": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "role": {
                "description": "`user`, `assistant`, or `function_result`.",
                "type": "string"
              },
              "text": {
                "type": "string"
              },
              "timestamp": {
                "format": "int64",
                "type": "integer"
              }
            },
            "required": [
              "calls",
              "id",
              "is_error",
              "role",
              "text",
              "timestamp"
            ],
            "type": "object"
          },
          "Source": {
            "enum": [
              "codex",
              "claude-code"
            ],
            "type": "string"
          }
        },
        "properties": {
          "conversation": {
            "$ref": "#/definitions/ConversationSummary"
          },
          "messages": {
            "items": {
              "$ref": "#/definitions/ExternalMessage"
            },
            "type": "array"
          },
          "warnings": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "conversation",
          "messages",
          "warnings"
        ],
        "title": "Preview",
        "type": "object"
      }
    },
    {
      "description": "Resolve a @trace(id=…) chat mention to its pill, preview card and agent summary; null for an unknown trace. Declares the trace mention provider.",
      "metadata": {
        "internal": true,
        "mention": {
          "color": "teal",
          "description": "An engine trace, by trace id; its span tree reads through engine::traces::tree",
          "details": {
            "function_id": "engine::traces::tree",
            "id_field": "trace_id"
          },
          "icon": "trace",
          "label": "Traces",
          "name": "trace",
          "search": "console::mentions::trace::search",
          "v": 1
        },
        "trace_hidden": true
      },
      "name": "console::mentions::trace::get",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input of a provider's get function.",
        "properties": {
          "id": {
            "description": "The id from the token.",
            "type": "string"
          }
        },
        "required": [
          "id"
        ],
        "title": "MentionGetRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "anyOf": [
          {
            "$ref": "#/definitions/MentionView"
          },
          {
            "type": "null"
          }
        ],
        "definitions": {
          "MentionField": {
            "description": "One labelled value of a preview card.",
            "properties": {
              "label": {
                "type": "string"
              },
              "tone": {
                "description": "`neutral`, `info`, `success`, `warning` or `danger`.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "value": {
                "type": "string"
              }
            },
            "required": [
              "label",
              "value"
            ],
            "type": "object"
          },
          "MentionOpen": {
            "anyOf": [
              {
                "properties": {
                  "context": true,
                  "page": {
                    "description": "Console page id (as registered with `host.pages`).",
                    "type": "string"
                  }
                },
                "required": [
                  "page"
                ],
                "type": "object"
              },
              {
                "properties": {
                  "session": {
                    "description": "Session id to select.",
                    "type": "string"
                  }
                },
                "required": [
                  "session"
                ],
                "type": "object"
              },
              {
                "properties": {
                  "url": {
                    "type": "string"
                  }
                },
                "required": [
                  "url"
                ],
                "type": "object"
              }
            ],
            "description": "What clicking a mention opens: an injected console page, a chat session, or an http(s) URL."
          },
          "MentionView": {
            "description": "What a chat surface renders for one mention, and what an agent reads. A get function answers `null` for an id it does not know.",
            "properties": {
              "color": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "data": {
                "description": "The domain object, for a worker's own preview renderer."
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "fields": {
                "description": "Labelled values for the preview card (status, assignee, …).",
                "items": {
                  "$ref": "#/definitions/MentionField"
                },
                "type": "array"
              },
              "hint": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "icon": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "id": {
                "description": "The canonical id (may differ from the one asked for, e.g. a key resolved to a uuid).",
                "type": "string"
              },
              "label": {
                "type": "string"
              },
              "open": {
                "anyOf": [
                  {
                    "$ref": "#/definitions/MentionOpen"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "What clicking the mention opens."
              },
              "summary": {
                "description": "One line for an agent: what the item is and its state. Defaults to the label, hint, description and fields.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "updated_at": {
                "description": "RFC 3339 time of the item's last change.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "id",
              "label"
            ],
            "type": "object"
          }
        },
        "title": "Nullable_MentionView"
      }
    },
    {
      "description": "Search recent engine traces for the chat @trace mention menu, by span name or trace id; newest first.",
      "metadata": {
        "internal": true,
        "trace_hidden": true
      },
      "name": "console::mentions::trace::search",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "MentionSearchContext": {
            "description": "The chat a search was asked from.",
            "properties": {
              "session_id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "working_dir": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "type": "object"
          }
        },
        "description": "Input of a provider's search function.",
        "properties": {
          "context": {
            "anyOf": [
              {
                "$ref": "#/definitions/MentionSearchContext"
              },
              {
                "type": "null"
              }
            ],
            "description": "Where the search was asked from, for providers that rank by it."
          },
          "limit": {
            "description": "Maximum rows (default 8, at most 50).",
            "format": "uint",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "query": {
            "default": "",
            "description": "What the user typed after `@<name>:`. Empty asks for the most relevant recent items.",
            "type": "string"
          }
        },
        "title": "MentionSearchRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "MentionItem": {
            "description": "One search row.",
            "properties": {
              "color": {
                "description": "Overrides the provider's color for this row.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "description": {
                "description": "One line after the label (\"in progress · high\").",
                "type": [
                  "string",
                  "null"
                ]
              },
              "hint": {
                "description": "A short human handle shown before the label (\"KAN-12\").",
                "type": [
                  "string",
                  "null"
                ]
              },
              "icon": {
                "description": "Overrides the provider's icon for this row.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "id": {
                "description": "The id written into the token; stable for the item's lifetime.",
                "type": "string"
              },
              "label": {
                "description": "The item's name (\"Fix login redirect\").",
                "type": "string"
              }
            },
            "required": [
              "id",
              "label"
            ],
            "type": "object"
          }
        },
        "description": "Output of a provider's search function, best match first.",
        "properties": {
          "items": {
            "items": {
              "$ref": "#/definitions/MentionItem"
            },
            "type": "array"
          }
        },
        "required": [
          "items"
        ],
        "title": "MentionSearchResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: re-apply the ADE HTTP port, data directory and injectable-UI \\\n             toggles when its 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": "Read whether the ADE setup wizard was finished or dismissed on this machine, and whether it may open by itself here.",
      "metadata": {
        "internal": true
      },
      "name": "console::onboarding::get",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "GetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "OnboardingStatus": {
            "description": "The wizard's lifecycle on this machine.",
            "oneOf": [
              {
                "enum": [
                  "dismissed",
                  "completed"
                ],
                "type": "string"
              },
              {
                "description": "Never finished nor dismissed: the wizard opens on first load.",
                "enum": [
                  "new"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "auto_open": {
            "description": "Whether the wizard may open by itself on this ADE: `false` when the configuration's `onboarding.auto_open` or `III_CONSOLE_ONBOARDING_AUTO_OPEN` says so, or when the configuration could not be read. Opening it from the command palette is never affected.",
            "type": "boolean"
          },
          "completed_at": {
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/definitions/OnboardingStatus"
              }
            ],
            "default": "new"
          },
          "summary": {
            "description": "What the wizard set up, for the summary it shows when reopened."
          },
          "updated_at": {
            "default": 0,
            "description": "Milliseconds since the epoch of the last write; 0 when never written.",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "auto_open"
        ],
        "title": "GetOutput",
        "type": "object"
      }
    },
    {
      "description": "Read the project's onboarding.yaml for the ADE setup wizard: the example prompts its last step offers (each one's text, agent profile and model priority list) and the registry kits it installs in the background when it opens. Both lists are empty when the file is missing or invalid.",
      "metadata": {
        "internal": true
      },
      "name": "console::onboarding::prompts",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "PromptsInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ExamplePrompt": {
            "description": "An example prompt, as the wizard shows it.",
            "properties": {
              "agent": {
                "description": "Agent profile id the new chat selects.",
                "type": "string"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "models": {
                "description": "Model priority list: the first one this machine has wins.",
                "items": {
                  "$ref": "#/definitions/PromptModel"
                },
                "type": "array"
              },
              "prompt": {
                "description": "The text prefilled in the composer; the user sends it.",
                "type": "string"
              },
              "title": {
                "type": "string"
              }
            },
            "required": [
              "agent",
              "models",
              "prompt",
              "title"
            ],
            "type": "object"
          },
          "PromptModel": {
            "description": "One entry of a prompt's model priority list.",
            "properties": {
              "effort": {
                "description": "An ADE thinking level; absent keeps the chat's default.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "model": {
                "description": "The model id, bare or as the provider lists it (`claude-code/claude-sonnet-5-5`).",
                "type": "string"
              },
              "provider": {
                "description": "Router provider id (`claude-code`, `anthropic`).",
                "type": "string"
              }
            },
            "required": [
              "model",
              "provider"
            ],
            "type": "object"
          }
        },
        "properties": {
          "kits": {
            "description": "Registry kits the wizard installs in the background when it opens (`<handle>/<name>`, optionally `@<version>`), in file order.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "prompts": {
            "items": {
              "$ref": "#/definitions/ExamplePrompt"
            },
            "type": "array"
          }
        },
        "required": [
          "kits",
          "prompts"
        ],
        "title": "PromptsOutput",
        "type": "object"
      }
    },
    {
      "description": "Look for the Codex and Claude Code CLIs on the ADE host and whether each is signed in, for the setup wizard. Reports presence and paths only, never credentials.",
      "metadata": {
        "internal": true
      },
      "name": "console::onboarding::scan",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ScanInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ToolScan": {
            "properties": {
              "binary_path": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "credentials_path": {
                "description": "Where that sign-in lives, `~`-abbreviated (`~/.codex/auth.json`, `macOS Keychain`). Set whenever the file exists, signed in or not.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "id": {
                "description": "`claude-code` or `codex`.",
                "type": "string"
              },
              "installed": {
                "description": "The CLI was found on this host.",
                "type": "boolean"
              },
              "name": {
                "type": "string"
              },
              "provider_worker": {
                "description": "Registry worker that serves models from this sign-in.",
                "type": "string"
              },
              "sign_in_note": {
                "description": "Why a credentials file that exists does not count as signed in.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "signed_in": {
                "description": "A sign-in the matching provider worker can use exists on this host.",
                "type": "boolean"
              },
              "version": {
                "description": "First line of `<cli> --version`, when it answered in time.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "id",
              "installed",
              "name",
              "provider_worker",
              "signed_in"
            ],
            "type": "object"
          }
        },
        "properties": {
          "tools": {
            "items": {
              "$ref": "#/definitions/ToolScan"
            },
            "type": "array"
          }
        },
        "required": [
          "tools"
        ],
        "title": "ScanOutput",
        "type": "object"
      }
    },
    {
      "description": "Record that the ADE setup wizard was finished, dismissed, or reset.",
      "metadata": {
        "internal": true
      },
      "name": "console::onboarding::set",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "OnboardingStatus": {
            "description": "The wizard's lifecycle on this machine.",
            "oneOf": [
              {
                "enum": [
                  "dismissed",
                  "completed"
                ],
                "type": "string"
              },
              {
                "description": "Never finished nor dismissed: the wizard opens on first load.",
                "enum": [
                  "new"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "status": {
            "$ref": "#/definitions/OnboardingStatus"
          },
          "summary": {
            "default": null,
            "description": "Replaces the stored summary when present."
          }
        },
        "required": [
          "status"
        ],
        "title": "SetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "OnboardingStatus": {
            "description": "The wizard's lifecycle on this machine.",
            "oneOf": [
              {
                "enum": [
                  "dismissed",
                  "completed"
                ],
                "type": "string"
              },
              {
                "description": "Never finished nor dismissed: the wizard opens on first load.",
                "enum": [
                  "new"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "completed_at": {
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/definitions/OnboardingStatus"
              }
            ],
            "default": "new"
          },
          "summary": {
            "description": "What the wizard set up, for the summary it shows when reopened."
          },
          "updated_at": {
            "default": 0,
            "description": "Milliseconds since the epoch of the last write; 0 when never written.",
            "format": "int64",
            "type": "integer"
          }
        },
        "title": "OnboardingState",
        "type": "object"
      }
    },
    {
      "description": "Where this project's ADE listens: url (the base URL to open from this machine, for example http://127.0.0.1:3113; a worker page is <url>/#/worker/<scope>/<page-id>), http_port, engine_url, and version. Read-only; use it instead of assuming port 3113.",
      "metadata": {},
      "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"
          },
          "url": {
            "description": "Base URL to open the UI from this machine (e.g. `http://127.0.0.1:3113`), from the bound host and the live port.",
            "type": "string"
          },
          "version": {
            "description": "Worker version (matches `Cargo.toml`).",
            "type": "string"
          }
        },
        "required": [
          "engine_url",
          "http_port",
          "url",
          "version"
        ],
        "title": "StatusOutput",
        "type": "object"
      }
    },
    {
      "description": "Add an email address to the iii product-update list.",
      "metadata": {
        "internal": true
      },
      "name": "console::subscribe",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "email": {
            "type": "string"
          },
          "source": {
            "default": null,
            "description": "Where the address was captured, reported with it. Defaults to `console`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "email"
        ],
        "title": "SubscribeInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "subscribed": {
            "type": "boolean"
          }
        },
        "required": [
          "subscribed"
        ],
        "title": "SubscribeOutput",
        "type": "object"
      }
    },
    {
      "description": "Serve the console worker's injected console UI assets (content function for its console:script / console:style / console:module 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`/`module` in push payloads and the manifest.",
            "enum": [
              "script",
              "style",
              "module"
            ],
            "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"
      }
    },
    {
      "description": "Internal: append Console working-directory handoff guidance to Harness generations.",
      "metadata": {
        "internal": true
      },
      "name": "console::working-directory::inject-guidance",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "WorkingDirectoryGenerateContext": {
            "properties": {
              "system_prompt": {
                "default": "",
                "type": "string"
              },
              "tools": {
                "items": {
                  "$ref": "#/definitions/WorkingDirectoryTool"
                },
                "type": "array"
              }
            },
            "type": "object"
          },
          "WorkingDirectoryTool": {
            "properties": {
              "name": {
                "type": "string"
              }
            },
            "required": [
              "name"
            ],
            "type": "object"
          }
        },
        "properties": {
          "generate": {
            "$ref": "#/definitions/WorkingDirectoryGenerateContext"
          }
        },
        "title": "WorkingDirectoryGuidanceEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "WorkingDirectoryGuidanceMutations": {
            "properties": {
              "system_prompt": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "type": "object"
          }
        },
        "properties": {
          "mutations": {
            "$ref": "#/definitions/WorkingDirectoryGuidanceMutations"
          }
        },
        "required": [
          "mutations"
        ],
        "title": "WorkingDirectoryGuidanceResponse",
        "type": "object"
      }
    },
    {
      "description": "Propose switching the current session's working directory to a project created or cloned elsewhere; the console asks the user to confirm. Only when the user asked to continue there, never just to inspect a file.",
      "metadata": {},
      "name": "console::working-directory::propose",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "path": {
            "description": "Existing directory the user asked to continue working in.",
            "type": "string"
          },
          "reason": {
            "default": null,
            "description": "Short explanation shown with the confirmation.",
            "type": [
              "string",
              "null"
            ]
          },
          "session_id": {
            "default": null,
            "description": "Authoritative Harness context. In-turn calls are stamped before dispatch.",
            "type": [
              "string",
              "null"
            ]
          },
          "turn_id": {
            "default": null,
            "description": "Authoritative Harness turn. In-turn calls are stamped before dispatch.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "path"
        ],
        "title": "ProposeWorkingDirectoryRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "path": {
            "type": "string"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "requires_confirmation": {
            "type": "boolean"
          },
          "session_id": {
            "type": "string"
          }
        },
        "required": [
          "path",
          "requires_confirmation",
          "session_id"
        ],
        "title": "ProposeWorkingDirectoryResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: stamp the authoritative Harness session onto Console working-directory proposals.",
      "metadata": {
        "internal": true
      },
      "name": "console::working-directory::stamp-session",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "StampCall": {
            "properties": {
              "arguments": {
                "default": null
              }
            },
            "type": "object"
          }
        },
        "properties": {
          "call": {
            "anyOf": [
              {
                "$ref": "#/definitions/StampCall"
              },
              {
                "type": "null"
              }
            ]
          },
          "session_id": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "turn_id": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "StampSessionEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "anyOf": [
          {
            "$ref": "#/definitions/StampSessionResponse"
          },
          {
            "type": "null"
          }
        ],
        "definitions": {
          "HookMutations": {
            "properties": {
              "arguments": true
            },
            "required": [
              "arguments"
            ],
            "type": "object"
          },
          "StampSessionResponse": {
            "properties": {
              "decision": {
                "type": "string"
              },
              "mutations": {
                "$ref": "#/definitions/HookMutations"
              }
            },
            "required": [
              "decision",
              "mutations"
            ],
            "type": "object"
          }
        },
        "title": "Nullable_StampSessionResponse"
      }
    },
    {
      "description": "Remove a screen from the console workspace wherever it is shown. Pass `screen: chat` with `session_id` to close one pinned conversation panel. Idempotent: an unmounted screen returns an empty `tab_ids`.",
      "metadata": {},
      "name": "console::workspace::close",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "screen": {
            "type": "string"
          },
          "session_id": {
            "default": null,
            "description": "With `screen: \"chat\"`, close only this conversation session panel.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "screen"
        ],
        "title": "CloseInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "tab_ids": {
            "description": "Tabs the screen was detached from; empty when it was not mounted.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "tab_ids"
        ],
        "title": "CloseOutput",
        "type": "object"
      }
    },
    {
      "description": "Internal: read the raw console workspace layout document the SPA renders (`<data_dir>/workspace.json`). Agents use `console::workspace::list`.",
      "metadata": {
        "internal": true
      },
      "name": "console::workspace::get",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "GetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "path": {
            "description": "Where the document lives, for operators wondering what to delete.",
            "type": "string"
          },
          "value": {
            "description": "The stored layout document verbatim (`tabs`, `activeTabId`, `activatedAt`, `activatedBy`, …); `{}` when nothing was saved yet."
          }
        },
        "required": [
          "path",
          "value"
        ],
        "title": "GetOutput",
        "type": "object"
      }
    },
    {
      "description": "List the console workspace the human sees: every tab with its columns, screens and column widths, plus which tab is active. Screens are `chat`, `chat:<session-id>`, `traces`, `workers`, or `ext:<page>` for worker pages. Read this before passing `sizes` to `open`, so the widths you send are relative to the layout that is actually up.",
      "metadata": {},
      "name": "console::workspace::list",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ListInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "TabSummary": {
            "properties": {
              "active": {
                "type": "boolean"
              },
              "columns": {
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "id": {
                "type": "string"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "screens": {
                "description": "One entry per column; `null` is an empty column.",
                "items": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "type": "array"
              },
              "sizes": {
                "description": "One fraction per column, summing to 1. Equal widths when the stored layout has none, or has a list that does not match the column count — the same normalization the browser renders from.",
                "items": {
                  "format": "double",
                  "type": "number"
                },
                "type": "array"
              }
            },
            "required": [
              "active",
              "columns",
              "id",
              "screens",
              "sizes"
            ],
            "type": "object"
          }
        },
        "properties": {
          "active_tab_id": {
            "type": "string"
          },
          "tabs": {
            "items": {
              "$ref": "#/definitions/TabSummary"
            },
            "type": "array"
          }
        },
        "required": [
          "active_tab_id",
          "tabs"
        ],
        "title": "ListOutput",
        "type": "object"
      }
    },
    {
      "description": "Show a screen in the console workspace next to the conversation (reusing the tab that already shows it) and switch every open console to that tab. Screens: `ext:ide` (files), `ext:browser`, `ext:editor`, `workers`, or `{\"screen\":\"chat\",\"session_id\":\"<id>\"}` for a pinned chat. It lands right of the chat panel unless `relative_to` names another screen, which is found in whichever tab shows it; `direction` picks the side (`right` or `left`).",
      "metadata": {},
      "name": "console::workspace::open",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Direction": {
            "description": "Which side of the anchor column a new column lands on.",
            "enum": [
              "right",
              "left"
            ],
            "type": "string"
          },
          "PlacementRequest": {
            "description": "Where the caller wants the screen to land.",
            "oneOf": [
              {
                "description": "Reuse a tab already showing the screen, else place it beside `relative_to` (in the tab showing it when it is a named screen, the active tab otherwise), else open a fresh tab.",
                "enum": [
                  "auto"
                ],
                "type": "string"
              },
              {
                "description": "Always open a fresh tab, even when the screen is mounted elsewhere and even when the active tab has room.",
                "enum": [
                  "new-tab"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "activate": {
            "default": null,
            "description": "Make the tab holding the screen the active one in every connected browser (default true), even when it already was the active tab.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "direction": {
            "anyOf": [
              {
                "$ref": "#/definitions/Direction"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Which side of `relative_to` to land on: `right` (default) or `left`."
          },
          "placement": {
            "anyOf": [
              {
                "$ref": "#/definitions/PlacementRequest"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "`auto` (default) reuses a tab already showing the screen; `new-tab` always opens a fresh one. Pair `new-tab` with `activate: false` to put a screen aside without moving the operator off the tab they are on."
          },
          "relative_to": {
            "default": null,
            "description": "The screen the new column lands next to, in the same vocabulary as `screen`. Defaults to `chat`, which matches whichever chat panel the active tab shows. A named screen is looked for in every tab, the active one first, and the column lands in the tab showing it. A screen that is not mounted anywhere puts the column at the end of the active tab.",
            "type": [
              "string",
              "null"
            ]
          },
          "screen": {
            "description": "`chat`, `traces`, `workers`, or `ext:<page-id>` for a worker page.",
            "type": "string"
          },
          "session_id": {
            "default": null,
            "description": "With `screen: \"chat\"`, pin the panel to this conversation session.",
            "type": [
              "string",
              "null"
            ]
          },
          "sizes": {
            "default": null,
            "description": "Column widths for the tab AFTER this call — one positive number per column, normalized by their sum. Applied in the same write that places the screen, so there is no window for another writer in between. Omit to let the console share the widths out.",
            "items": {
              "format": "double",
              "type": "number"
            },
            "type": [
              "array",
              "null"
            ]
          }
        },
        "required": [
          "screen"
        ],
        "title": "OpenInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Placement": {
            "oneOf": [
              {
                "description": "The screen was already mounted; that tab was reused.",
                "enum": [
                  "existing"
                ],
                "type": "string"
              },
              {
                "description": "Placed into an empty column of the active tab.",
                "enum": [
                  "empty_column"
                ],
                "type": "string"
              },
              {
                "description": "A new column was added to the active tab.",
                "enum": [
                  "new_column"
                ],
                "type": "string"
              },
              {
                "description": "The active tab was full; a fresh tab was created.",
                "enum": [
                  "new_tab"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "activated": {
            "type": "boolean"
          },
          "column": {
            "format": "uint",
            "minimum": 0,
            "type": "integer"
          },
          "placement": {
            "$ref": "#/definitions/Placement"
          },
          "screens": {
            "items": {
              "type": [
                "string",
                "null"
              ]
            },
            "type": "array"
          },
          "sizes": {
            "description": "The tab's column widths as stored, whether they came from `sizes` or from the console sharing them out.",
            "items": {
              "format": "double",
              "type": "number"
            },
            "type": "array"
          },
          "tab_id": {
            "type": "string"
          }
        },
        "required": [
          "activated",
          "column",
          "placement",
          "screens",
          "sizes",
          "tab_id"
        ],
        "title": "OpenOutput",
        "type": "object"
      }
    },
    {
      "description": "Internal: replace the raw console workspace layout document wholesale (the SPA's read-modify-write path; `expected_revision` refuses a stale copy with WORKSPACE_CONFLICT). Agents use `console::workspace::open` and `close`.",
      "metadata": {
        "internal": true
      },
      "name": "console::workspace::set",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "expected_revision": {
            "default": null,
            "description": "The `revision` of the document `value` was computed from. When the stored layout has moved past it, nothing is written and the call fails `WORKSPACE_CONFLICT`: re-read and apply the change again. Omitted, the write is unconditional.",
            "format": "uint64",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "value": {
            "description": "The whole layout document; replaces what is stored."
          }
        },
        "required": [
          "value"
        ],
        "title": "SetInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "SetOutput",
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "Fires when the compose daemon writes state.json or the compose file changes. Bind with an empty config.",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "additionalProperties": false,
        "title": "ChangedTriggerSpec",
        "type": "object"
      },
      "metadata": {},
      "name": "console::compose::changed",
      "return_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ChangeKind": {
            "enum": [
              "state",
              "file"
            ],
            "type": "string"
          }
        },
        "properties": {
          "captured_at": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "file": {
            "type": "string"
          },
          "kind": {
            "$ref": "#/definitions/ChangeKind"
          },
          "namespace": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "state_dir": {
            "type": "string"
          }
        },
        "required": [
          "captured_at",
          "file",
          "kind",
          "namespace",
          "path",
          "state_dir"
        ],
        "title": "ChangedEvent",
        "type": "object"
      }
    },
    {
      "description": "Fires after every change to the console workspace layout: open, close, or a browser's own edit, and once when a binding registers. Bind with an empty config. The event is empty; re-read the layout with console::workspace::list.",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "additionalProperties": false,
        "title": "ChangedSpec",
        "type": "object"
      },
      "metadata": {},
      "name": "console::workspace::changed",
      "return_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "ChangedEvent",
        "type": "object"
      }
    },
    {
      "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 module the console serves but never imports at mount; pages load it on demand with host.importModule(path). Same contract as console:script with a .js path.",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "additionalProperties": false,
        "description": "Trigger config schema for every asset type (`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:module",
      "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 every asset type (`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 every asset type (`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": {}
    }
  ]
}
```
