# compose-ui

> Compose project supervision in the Console — a page over compose::* with live container state, lifecycle actions, worker packages, and per-container log tails.

| field | value |
|-------|-------|
| version | 0.1.9 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | aarch64-apple-darwin, 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=compose-ui@0.1.9
```

## dependencies

- `ade` @ `latest`

## readme

# compose-ui

The compose daemon in the Console. A **Compose** page opens on a topology map (engine, namespace group, containers laid out by `start_after`, ports and sources on the cards, upstream/downstream tracing on select) and lists every container the
daemon supervises with live state, PID, and last error; starts, stops, and
restarts containers or the whole project in dependency order; adds, updates,
and removes declared worker packages; and tails each container's log. Every
lifecycle action is the daemon's own `compose::*` function, so the page and
`iii trigger compose::…` are the same operation. The worker adds the two things
the daemon does not expose on the bus: a `compose-ui::changed` trigger type
that fires when the daemon writes its durable project state or the compose
file changes on disk, and `compose-ui::logs`, a read-only log tail.

## Install

```bash
iii compose add compose-ui
```

Compose starts it beside the Console in the project's namespace. The page
appears in the Console nav as **Compose** (page `compose`) and in the command
palette as "Open Compose"; the palette also searches containers by name.

### Companion workers

- `console` ≥ 1.9.14 hosts the injectable page.
- A running `iii compose` daemon answers `compose::*` in the same namespace;
  without one the page shows how to start it.

## Quickstart

Explain a failed container from an agent or a script:

```bash
iii trigger compose::status
iii trigger compose-ui::logs container=provider-openai lines=50
```

```json
{
  "container": "provider-openai",
  "path": "/home/me/.iii/compose/app/harness-3d528231/logs/provider-openai.log",
  "lines": ["… last 50 lines …"],
  "size": 17910,
  "truncated": true,
  "missing": false
}
```

See what the project declares and what each container listens on:

```bash
iii trigger compose-ui::project
```

```json
{
  "namespace": "my-project",
  "engine_url": "ws://127.0.0.1:49134",
  "containers": [
    { "name": "ade", "source": "path", "ref": "../ade", "pid": 27129, "ports": [{ "port": 3113, "address": "*" }] },
    { "name": "web", "source": "package", "ref": "api.workers.iii.dev/web", "version": "1.2.10", "ports": [] }
  ]
}
```

React to supervisor changes without polling — bind the trigger type with an
empty config and re-read `compose::status` on each event:

```ts
iii.registerTrigger({ type: 'compose-ui::changed', function_id: 'my-worker::on-compose-change', config: {} })
```

The payload is `{ kind: 'state' | 'file', file, namespace, state_dir, path, captured_at }`.

## Configuration

None. The worker locates the project through `compose::status` (compose file,
namespace, state directory) the first time a page or trigger binds, and again
if the watch is lost. Environment:

| Variable | Purpose |
|---|---|
| `III_URL` | Engine address; compose sets it. |
| `III_NAMESPACE` | Project namespace for a process started by hand; compose sets it. |
| `III_COMPOSE_UI_UI_WATCH` | `1` serves the page from `ui/dist` and hot-reloads it into open Console tabs while `pnpm --dir ui watch` runs. |

## Custom trigger types

| Type | Config | Fires |
|---|---|---|
| `compose-ui::changed` | `{}` | Once per coalesced burst (200 ms) of writes to the daemon's `state.json` (`kind: state`) or edits to the compose file (`kind: file`). |

## Run from source with compose

```yaml
containers:
  compose-ui:
    worker: path://../compose-ui
    scripts:
      run: cargo run --release
```

A worker started by hand needs `III_NAMESPACE=<compose namespace>` or the
Console never sees its page.

## skills

This version ships 1 skill file for agents, also browsable on the worker page:

- [`SKILL.md`](https://workers.iii.dev/workers/compose-ui?tab=skills&skill=SKILL.md&version=0.1.9)

### SKILL.md

# compose-ui

The compose-ui worker puts the compose daemon in the Console. Its page (Compose,
page `compose`) opens on a topology map of the engine, the namespace, and the
containers laid out by `start_after`, and shows every container the daemon supervises with its live
state, PID, and last error; starts, stops, and restarts containers or the whole
project in dependency order; adds, updates, and removes declared worker
packages; and tails each container's log. Every lifecycle action is the
daemon's own `compose::*` function, so what the page does and what
`iii trigger compose::…` does are the same operation.

The worker itself owns two things the daemon does not expose on the bus: a
`compose-ui::changed` trigger type that fires when the daemon writes its
durable project state or the compose file changes on disk, and
`compose-ui::logs`, a read-only tail of one container's log.

## When to Use

- A person asks why a container is `failed` or stuck in `starting`:
  `compose::status` gives the state and `last_error`; `compose-ui::logs` gives
  the last lines the process wrote before it died.
- A person wants to see the fleet, restart something, or add a worker without
  leaving the chat: open the Compose page with `compose` in the command palette
  or `host.panels.open({ pageId: 'compose', context: { container } })` from
  another worker's UI.
- A worker needs to react to supervisor changes (a crash cascade, a new
  container, a compose file edit) without polling: bind
  `compose-ui::changed` with an empty config and re-read `compose::status`.

## Boundaries

- Lifecycle belongs to the daemon. Call `compose::up`, `compose::down`,
  `compose::restart`, `compose::add`, `compose::remove`, and `compose::update`
  directly; this worker adds no proxies for them.
- `compose-ui::project` reports environment variable names only, never values.
- `compose-ui::logs` reads at most 500 lines and 256 KiB from the end of one
  file and never lists or reads anything else in the state directory.
- The worker must run in the compose project's namespace (compose supplies it;
  a standalone process needs `III_NAMESPACE`).

## Functions

| Function | Purpose |
|---|---|
| `compose-ui::logs` | `{ container, lines?, file? }` → last lines of `<state_dir>/logs/<container>.log` with `size`, `truncated`, `missing`. |
| `compose-ui::project` | `{ file? }` → the compose file as declared (namespace, engine endpoint, timeouts, each container's source, version, `start_after`, environment keys, run script) plus each running container's PID and listening TCP ports. |

## Trigger types

| Type | Config | Payload |
|---|---|---|
| `compose-ui::changed` | `{}` | `{ kind: 'state' \| 'file', file, namespace, state_dir, path, captured_at }` |

## api reference

```json
{
  "functions": [
    {
      "description": "Read the last lines of one compose container's log file (default 200, at most 500; a missing log answers with missing: true).",
      "metadata": {},
      "name": "compose-ui::logs",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "container": {
            "description": "Container name as declared in the compose file.",
            "type": "string"
          },
          "file": {
            "description": "Compose file on the daemon host; defaults to the daemon project.",
            "type": [
              "string",
              "null"
            ]
          },
          "lines": {
            "description": "Lines from the end; defaults to 200 and is capped at 500.",
            "format": "int64",
            "maximum": 500,
            "minimum": 1,
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "container"
        ],
        "title": "LogsInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "container": {
            "type": "string"
          },
          "lines": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "missing": {
            "type": "boolean"
          },
          "path": {
            "type": "string"
          },
          "size": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "truncated": {
            "type": "boolean"
          }
        },
        "required": [
          "container",
          "lines",
          "missing",
          "path",
          "size",
          "truncated"
        ],
        "title": "LogTail",
        "type": "object"
      }
    },
    {
      "description": "Read the declared compose file plus what each container listens on: namespace, engine endpoint, timeouts, worker source, dependencies, environment keys, run script, PID, listening TCP ports.",
      "metadata": {},
      "name": "compose-ui::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"
          },
          "ListeningPort": {
            "properties": {
              "address": {
                "type": "string"
              },
              "port": {
                "format": "uint16",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "address",
              "port"
            ],
            "type": "object"
          },
          "ProjectContainer": {
            "properties": {
              "environment": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "name": {
                "type": "string"
              },
              "pid": {
                "format": "uint32",
                "minimum": 0,
                "type": [
                  "integer",
                  "null"
                ]
              },
              "ports": {
                "items": {
                  "$ref": "#/definitions/ListeningPort"
                },
                "type": "array"
              },
              "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",
              "ports",
              "ref",
              "source",
              "start_after"
            ],
            "type": "object"
          }
        },
        "properties": {
          "containers": {
            "items": {
              "$ref": "#/definitions/ProjectContainer"
            },
            "type": "array"
          },
          "daemon_pid": {
            "format": "uint32",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "daemon_ports": {
            "items": {
              "$ref": "#/definitions/ListeningPort"
            },
            "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",
          "daemon_ports",
          "file"
        ],
        "title": "ProjectResult",
        "type": "object"
      }
    },
    {
      "description": "Serve the compose-ui worker's injected console UI assets (content function for its console:script / console:style triggers).",
      "metadata": {
        "internal": true
      },
      "name": "compose-ui::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"
      }
    }
  ],
  "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": "compose-ui::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"
      }
    }
  ]
}
```
