Skip to content
Z-M-Huang edited this page Apr 28, 2026 · 4 revisions

UI

UI extensions render events, drive interactions, and contribute renderer-local regions. A UI extension may be a subscriber (reads events, renders), an interactor (answers typed request/response from core), a region contributor (customizes a target UI's sub-region), or a combination of those roles.

contractVersion: 1.0.0


Intent

Core owns the event loop. UI extensions receive events and may optionally respond to Interaction Protocol requests. Some UI extensions contribute components to another UI's renderer-local region ABI. This split lets stud-cli run with multiple viewers (TUI in a terminal, web UI in a browser, headless logger) while Interaction Protocol authority stays on the typed request/response rail. Multiple interactors may be active concurrently; the first response wins.


Contract shape

classDiagram
    class UIContract {
        kind : "UI"
        contractVersion : SemVer
        requiredCoreVersion : SemVerRange
        configSchema : JSONSchema
        loadedCardinality : unlimited
        activeCardinality : unlimited
        stateSlot : optional
        discoveryRules : DiscoveryRules
        reloadBehavior : between-turns
        roles : ReadonlyArray~UIRole~
        onEvent : SubscriberHandler?
        onInteraction : InteractorHandler?
        regions : RegionContribution[]?
    }
Loading

roles is a non-empty array drawn from { 'subscriber', 'interactor', 'region' }. Multiple values may appear in the same roles array.

Role Reads events? Answers Interaction Protocol? Contributes renderer regions? Handler / declaration required
subscriber Yes No No onEvent
interactor No Yes No onInteraction
region No by default No by default Yes regions[]

An extension that wants only to answer prompts (e.g., a headless approval bridge) declares roles: ['interactor']. An extension that only renders its own surface (e.g., a dashboard) declares roles: ['subscriber']. A status-line customization for the bundled TUI declares roles: ['region'] and targets default-tui. The default TUI declares roles: ['subscriber', 'interactor'].

Load-time validation emits Validation/UIRoleHandlerMissing when a declared role has no matching handler or region declaration.


Configuration schema

Every UI's configSchema must accept:

Field Meaning
enabled Whether this UI participates in the session.
targetUI Required for region contributors; the UI id whose renderer-local ABI this extension targets.

Plus any UI-specific options (theme, pane layout, bindings). There is no priority field — every loaded UI whose roles array includes interactor is concurrently active and participates in race-to-answer fan-out (see Interaction Protocol § Multiple interactors).


Renderer-local extension points

A UI may expose its own renderer-local extension points for sub-regions such as a status line, transcript renderer, or input chrome. Region contributors are still kind: "UI" extensions, but the region ABI is UI-owned, not a new core authority surface.

Core only sees UI extensions through this contract:

  • Event subscription still flows through onEvent.
  • Authoritative answers still flow through onInteraction.
  • Region contribution still flows through roles: ['region'], targetUI, and the target UI's region declaration.
  • Commands still dispatch through the command bus.
  • Region code does not receive direct tool-call authority, raw env access, or another extension's state slot.

The bundled Default TUI exposes Ink / React region plugins for startup, transcript, composer, statusLine, and dialogs. Another full UI may expose a different region ABI or no region ABI at all. Region plugins that target a specific UI must declare that target in their own manifest/config and degrade cleanly when the target UI is not loaded.

Renderer-local regions are allowed because they customize projection and input chrome; they must not become a hidden control plane. A region plugin that needs to perform a session action must call a command or answer an Interaction Protocol request through the owning UI's controlled callback.


Lifecycle

Phase UI responsibilities
init Validate config; allocate the terminal/window/transport.
activate Subscribe to the event bus; if interactor, announce availability; if region contributor, register with the target UI's region registry.
deactivate Drain pending interaction requests (error them if not answerable); unregister region contributions; unsubscribe.
dispose Release the terminal/window/transport. Idempotent.

A UI that cannot complete init fails validation with a clear diagnostic. Headless sessions may run without any UI — see Headless and Interactor.


Cardinality

Loaded: unlimited.

Active: unlimited for every UI role (Q-9 resolution).

Role Active cardinality Notes
subscriber unlimited Every loaded subscriber receives every event.
interactor unlimited Core fans out to all active interactors; first-to-respond wins.
region unlimited Target UIs compose region contributions according to their own renderer-local ABI.

Multiple interactors may be simultaneously active. When core issues an Interaction Protocol request:

  1. Core fans out the request to every active interactor concurrently.
  2. The first accepted or rejected response wins.
  3. Core immediately emits InteractionAnswered on the event bus, carrying the correlationId and the winning response.
  4. Interactors that have not yet responded should subscribe to InteractionAnswered and dismiss their own in-flight dialogs.
  5. A response that arrives after the winner has already been recorded rejects with Session/InteractionAlreadyAnswered.

See Cardinality and Activation and Interaction Protocol.


State slot

Optional. Typical uses:

  • Saved pane sizes, selected theme, scroll positions.
  • Last-seen event watermark so a reattach doesn't re-render the full history.
  • Region-plugin preferences, such as selected status-line widgets or collapsed transcript sections.

Some of this may also live in config. Config for defaults; state slot for runtime user choices that persist across resumes. See Extension State.


Discovery path

UIs live in a ui/ folder under each configuration scope layer:

Layer Location
Bundled Ships with the package.
Global ~/.stud/ui/…
Project <cwd>/.stud/ui/…

Ordering

UIs are an unordered category. Event delivery to subscribers is deterministic but the order across subscribers is not a concern of the contract — each subscriber sees the same event stream.


Reload behavior

reloadBehavior: between-turns. Reloading a UI mid-turn would drop in-flight rendering and confuse the user. Reloading the interactor while an Interaction Protocol request is in flight is forbidden — core waits for the request to complete, then applies the reload.


Interaction with core

A UI reads and writes only through the Host API:

  • host.events — subscriber role consumes the event stream.
  • Interaction Protocol — interactor role handles typed requests (Ask, Approve, Select, Auth.DeviceCode, Auth.Password, Confirm).
  • host.session.stateSlot(extId) — own slot for user preferences.

A UI never:

  • Calls a tool directly.
  • Writes to another extension's state slot.
  • Reads secret values (the interactor is handed the question, not the env slot content).

Event projection, not authority

Events are projection only. A UI that uses event timing to drive authoritative decisions (cancel a tool call based on an event arrival) is non-conformant. Authority flows through the Interaction Protocol, state machines, and commands. See Event Bus.


Security notes

  • The interactor never receives raw secrets. An Auth.Password request describes the purpose; the answer is captured and handed to the requesting extension without the UI logging it.
  • UI code runs in-process in v1 (see Extension Isolation). A third-party UI has the same access surface as any other extension and is gated by the same Project Trust prompt.
  • Autocompletion in the input field is a disclosure channel. A UI that autocompletes from the env provider's known keys can accidentally leak names to screenshots or paste buffers. UIs should require an explicit action to disclose env values into the input. See LLM Context Isolation.
  • Interactor departure matters. A UI that disappears mid-session (crash, disconnection) must release any in-flight interaction state cleanly. Other active interactors continue to receive fan-out; if none remain, headless behavior applies.

Related pages


Changelog

1.0.0 — initial

  • UI contract as documented above. roles: ReadonlyArray<UIRole> declares which of subscriber / interactor / region the extension performs.
  • Both cardinality axes are unlimited — multiple subscribers, interactors, and region contributors may be active concurrently.
  • First-interactor-wins fan-out for Interaction Protocol requests; InteractionAnswered broadcast; late responses reject with Session/InteractionAlreadyAnswered.
  • Load-time Validation/UIRoleHandlerMissing when a declared role has no matching handler or region declaration.
  • No validationSeverity field — failed UIs are disabled. The "interactor required" check is enforced by the Headless / Interactor decision logic.

Introduction

Reading

Core runtime

Contracts

Category contracts

Context

Security

Runtime behavior

Operations

Providers (bundled)

Integrations

Reference extensions

Tools

UI

Session Stores

Loggers

Providers

Hooks

Context Providers

Commands

Case studies

Flows

Maintainers

Clone this wiki locally