skip to content
$worker

ade

(alias: console)v1.9.31

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

iiiverified
1,075 installs0 in 7d0 today
install
$iii trigger compose::add worker=ade@1.9.31
binarylicense: Apache-2.0adeadminconsoledashboarduiweb
  • macOS: arm64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64

exact versions are immutable; binary and bundle artifacts are digest-pinned.

skill doc

skills/injectable-ui.md

Injectable console UI

A worker can ship pages, function and trigger-activity renderers, forms, and stylesheets into every console tab at runtime—no rebuild, no iframe, and hot reload. Treat database, console functions/triggers, iii-directory, and state as proven patterns, not visual templates: reuse their visual grammar and interaction mechanics while choosing the information architecture that best fits the worker.

How it works

A worker registers console:script and console:style triggers whose config.path identifies an asset and whose function_id serves {content} for {path}. The console hashes and serves those bytes, then pushes changes to open tabs. Tabs import() scripts and call their default setup(host); styles load as scoped assets. Re-registering a path hot-reloads it. Registration is deployment; disconnect is teardown.

Add the internal dependencies

This repository versions both sides of the contract together. Do not try to install them from a public registry:

  1. Add /ui to the root pnpm-workspace.yaml.

  2. Add the compile-time UI surface to /ui/package.json:

    {
      "name": "@iii-workers/mywork-ui",
      "private": true,
      "version": "0.0.0",
      "type": "module",
      "scripts": { "build": "tsc --noEmit && node build.mjs", "watch": "node build.mjs --watch" },
      "dependencies": { "@iii-dev/console-ui": "workspace:*" },
      "devDependencies": { "@types/react": "^19.2.14", "esbuild": "^0.25.0", "typescript": "^5.9.2" }
    }
  3. For a Rust worker, link the worker-side registration helper in /Cargo.toml:

    iii-console-ui = { path = "../crates/console-ui" }

@iii-dev/console-ui is types-only at build time; the console serves its runtime implementation from the active SPA. iii-console-ui registers the content function, asset triggers, and development watcher. Node workers have no worker-side helper and implement the wire contract directly.

Project layout

mywork/
  build.rs       # ensure dist assets exist before include_str!
  ui/
    page.tsx      # the script asset — default-exports setup(host)
    styles.css    # the style asset — every rule scoped
    build.mjs     # esbuild, five external specifiers
    package.json  # workspace dependency on @iii-dev/console-ui
    tsconfig.json
    src/           # page, renderer, config form, hooks, widgets
  src/
    ui.rs          # embed and register dist/page.js + dist/styles.css

Start from state/ui/tsconfig.json, state/build.rs, and state/src/ui.rs for the mechanical files; rename worker/asset paths and keep their tests. For UI structure, consult the references below before writing code.

Authoring workflow

  1. Read packages/console-ui/index.d.ts; never guess a component or prop.
  2. Select only the needed slots, then model the primary object, navigation, actions, async states, and state that must survive navigation or reload.
  3. Design wide and narrow flows deliberately; do not squeeze desktop UI.
  4. Build with shared primitives and minimal scoped CSS, then type-check, register, inspect the manifest, and exercise the real console.

Living references

Need Read Reuse
Public API packages/console-ui/index.d.ts Exact exports and props
Shared page chrome ade/web/src/components/ui/PageChrome.tsx PageShell, PageHeader, surface roles
Catalog/detail ade/ui/src/catalog/widgets.tsx, ade/ui/src/catalog/FunctionsPage.tsx, ade/ui/src/catalog/TriggersPage.tsx, ade/ui/styles.css Grouped rows, persistent hero, identity masthead, facts, tabs, contextual rail
Data workbench database/ui/src/page/index.tsx, database/ui/src/page/TableDataPanel.tsx, database/ui/styles.css Mode bar, schema tree, toolbars, data grid, inspector, nested container responses
List/detail editor iii-directory/ui/page.tsx, iii-directory/ui/src/page/browser.tsx, iii-directory/ui/styles.css setup(host), container-width drill-in, dirty-draft guards, per-tab state
Multi-level browser state/ui/page.tsx, state/ui/src/page/browser.tsx, state/ui/styles.css One-pane-at-a-time narrow flow, live state updates, stale-request guards
Rust delivery state/src/ui.rs, state/build.rs Embedding, registration, asset tests, build freshness

Copy delivery plumbing when it matches. Do not copy a reference page's sidebar count, breakpoints, controls, or visual hierarchy without deriving them from the new worker's content.

Visual quality is part of correctness

Choose one dominant archetype before writing JSX. Mixing all four produces a generic dashboard with too many panels.

Archetype Use for Required shape
Console catalog Many searchable objects with rich detail Grouped list → persistent hero or breadcrumb + identity masthead + tabs; add a contextual rail only for genuinely related information
Database workbench Several tools operating on one selected resource Compact mode switcher, collapsible resource tree, one active work surface, local toolbar/status bar, optional inspector
Directory editor Searchable documents with drafts or preview List → document identity → edit/preview modes; keep draft state mounted and put save status beside the work
State explorer Deep but compact hierarchy Progressive columns on wide panes and one-level-at-a-time drill-in on narrow panes

Apply the shared visual grammar

  • Build hierarchy with surfaces, not boxes: sidebar, panel, raised toolbar, hover/selected wash. Reserve 1 px edges for structural or tabular separation; avoid borders, shadows, or a card around every section.
  • Elevation is four shared tokens, never a hand-rolled stack: --shadow-raised for a card sitting on a panel, --shadow-floating for menus, popovers, and sheets, --shadow-lift for an instrument surface that must read as lifted off the canvas with a crisp edge (the chat composer), and --shadow-keycap for a key cap (the lift turned upside down, so the key reads as set into the surface). Each is a complete, theme-aware box-shadow value — write box-shadow: var(--shadow-lift) and nothing else: no border, ring, or extra drop beside it, and never a literal shadow color.
  • Use a restrained scale: 4/6/8 px for internal gaps, 12/14/20/24 px for section spacing, and the system 6 px radius. Oversized padding makes these dense operator tools look like marketing pages.
  • Set document/hero titles around 17–18 px at weight 600; body copy around 12.5–13 px with 1.55–1.65 line height and a 60–72ch measure; metadata around 10–11.5 px. Author interface copy in natural sentence/title case; never use CSS lowercase or uppercase transforms on tabs, buttons, menus, or forms.
  • Use sans for all interface chrome, labels, actions, explanations, and prose. Reserve mono for machine-produced ids, paths, schemas, values, payloads, code, and tabular data. Never make a whole panel or its controls mono.
  • Repeat one restrained identity glyph in the list row, empty hero, and detail masthead, as console functions/triggers do. Use Lucide icons at the shared 16 px baseline; do not add application icon usages, component defaults, or root SVGs below 16 px, emoji, or a new icon dependency.
  • Make list rows full-width targets with one strong primary line and at most one or two quieter supporting lines. Indicate selection with a surface wash, stronger ink, and an optional 2 px neutral edge—never accent color alone.
  • Build a compact sidebar hierarchy (sessions and their sub-agents, folders, scopes) on the uiClasses.tree* recipe rather than a private row: 28 px rows in 13 px/500 sans, one 16 px glyph tinted through data-color with the shared glyph tones, 14 px of indent per level set as --iii-ui-tree-depth on the row, the disclosure caret right after the label, quiet metadata and a hover-revealed X (Lucide X, never a trash can) on the trailing edge. Set data-narrow on the tree when the pane is a phone-sized drill-in so rows and controls grow to touch size.
  • Keep page actions in PageHeader; put resource actions in the identity masthead and work actions in the nearest toolbar. Show one clear primary action at the point of work; move rare actions into a menu.
  • Use shared line Tabs for peer views of the same object. They have a bottom rule, neutral active underline, 600 weight, natural casing, and a semantic 16 px icon by default. SegmentedControl variant="tabs" uses the same line recipe; reserve variant="radio" and its surface track for persisted mutually exclusive choices. Do not fork private boxed tab CSS.
  • Use compact fact sheets or stat tiles only for useful comparisons. Prefer a quiet --color-surface group with label/value rows over a grid of large KPI cards.
  • Put loading, empty, error, and success states where content will appear so the page silhouette stays stable. Use Skeleton, EmptyState, and StatusPanel; never present raw error text as the main design.
  • For simple tables, compose the shared TableViewport/TableFrame/Table family. Use natural-case sans headers, horizontal row dividers, comfortable page density or compact chat density, and mono only for technical cells. Make only interactive rows hoverable. Long data grids may add sticky headers, aligned tabular numbers, selection, and an inspector or context rail.

Reject generic generated UI

Do not ship card soup, gradients, glows, ornamental shadows, giant centered headings, excessive badges, random accent colors, repeated descriptions, or an empty canvas with controls floating in corners. Do not give navigation, metadata, and the primary task equal visual weight. Compare the result beside the closest reference at the same width in both themes; its structure may differ, but density, typography, surface hierarchy, and control treatment must feel native to the same console.

1. The script asset (ui/page.tsx)

Ordinary React. Import from react and @iii-dev/console-ui — both resolve at runtime through the console's import map, so they must stay external in your build. Default-export a setup(host) function and make every registration through host (the loader attributes registrations to your script so it can dispose them on reload):

import {
  type Host,
  PageHeader,
  PageMain,
  type PageRenderProps,
  PageShell,
} from '@iii-dev/console-ui'

function MyworkPage({
  host,
  onRequestClose,
}: PageRenderProps & { host: Host }) {
  return (
    <PageShell className="mywork-ui-shell">
      <PageHeader
        icon={<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" aria-hidden><circle cx="8" cy="8" r="5" /></svg>}
        title="Mywork"
        description={host.path}
        onClose={onRequestClose}
      />
      <PageMain className="mywork-ui-main">
        {/* Compose the chosen archetype here. */}
      </PageMain>
    </PageShell>
  )
}

export default function setup(host: Host) {
  host.pages.register({
    id: 'mywork-manager',           // page URL: #/ext/mywork-manager
    title: 'Mywork',                // nav label
    configurationId: 'mywork',      // host adds the standard settings action
    render: (props) => <MyworkPage host={host} {...props} />,
  })

  // Register other slots only when their implementations exist.
  // host.functionTriggers.register(createMyTriggerRenderer(host))
  // host.triggerRenderers?.register(createMyTriggerActivityRenderer())
  // host.configForms.register('mywork', MyConfigForm)
  // host.providerConfigForms?.register('my-provider', MyProviderConfigForm)
}

This is a delivery skeleton, not a finished design. Compose one archetype in its body before evaluating the UI. Imports from the shared package add zero bundle bytes because they resolve to the running console's React tree.

The shared component library

The package exports page chrome; List/ListItem, Card, CollapsibleCard, CardHighlight, Panel, Chip, IconButton, and semantic Table parts; line Tabs and SegmentedControl; Selector and Select; buttons, inputs, dialogs, menus and tooltips; status/empty/loading components; Markdown and JSON renderers; the terminal atoms (AnsiText, TerminalStream, TerminalCommandLine); CodeEditor, FileDiff; and settings primitives (SettingsSection, SettingsList, SettingsRow, SettingsField, RawValueInput, SettingsDeck, Switch). It also exports the stable uiClasses recipes (list, navigation tree, card, panel, chip, table, tabs, field, settings and motion) and the canonical tokens inventory. Read packages/console-ui/index.d.ts for the authoritative names and props.

Use Selector for searchable single-choice input, including grouped or disabled options, async caller-owned filtering, loading/empty/error states, validation, and explicitly enabled free-form creation. Use Select for a small finite non-searchable list. Use the shared Tooltip parts, or IconButton for an icon-only action; do not implement independent hover timers, geometry, or portals. Keep a local selector only for a genuinely different interaction such as hierarchical drill-in, multi-select, or a persistent command palette, and document that exception.

Use TabsList variant="line"/TabsTrigger or SegmentedControl variant="tabs" for content navigation. Shared tabs add a semantic icon by default; pass an explicit icon only when the default does not express the view, or icon={false} only when there is a documented space constraint. Use IconButton for icon-only actions such as Refresh or Configure so the 16 px glyph retains an accessible name and tooltip.

Configuration forms: one shared grammar

Every host.configForms implementation uses host-owned primitives. Do not paint native inputs, selects, switches, buttons, or a private collection deck to resemble the Console.

  • Structure ordinary settings as SettingsSectionSettingsListSettingsField/SettingsRow.
  • Use SettingsField for editable values. Pass every prop supplied by its renderControl callback into Input, Select, Selector, Switch, or a domain wrapper. It generates the clickable label, description/error ARIA, data-field, and standard control width. Use controlSize="fit" with layout="inline" for intrinsic controls such as Switch.
  • Use SettingsRow for values/actions that are not a single labelled field.
  • Use RawValueInput for ${ENV} templates and unknown/future scalar values. It may suggest a typed literal, but conversion happens only after the user invokes onUseLiteral. A non-string opaque value still belongs in a SettingsField with an explicit conversion button so field errors remain associated with the control.
  • Use the shared Select for finite choices and Selector for searchable choices. Both support id, name, and data-field; a native