diff options
Diffstat (limited to '.dispatch/ui-contract.reference.md')
| -rw-r--r-- | .dispatch/ui-contract.reference.md | 87 |
1 files changed, 66 insertions, 21 deletions
diff --git a/.dispatch/ui-contract.reference.md b/.dispatch/ui-contract.reference.md index 00d354f..d751af8 100644 --- a/.dispatch/ui-contract.reference.md +++ b/.dispatch/ui-contract.reference.md @@ -5,29 +5,49 @@ > hangs on a permission prompt). Your CODE still imports `@dispatch/ui-contract` normally — this > file is for READING only. > -> **Orchestrator:** this is a SNAPSHOT — regenerate it whenever `ui-contract` changes. +> **Orchestrator:** this is a SNAPSHOT of `[email protected]` — regenerate it whenever +> `ui-contract` changes. +> +> **2026-06-12 delta (CR-2/CR-4 handoff — package bumped `0.1.0` → `0.2.0`):** adds the optional +> `scope?: "global" | "conversation"` to `SurfaceCatalogEntry` so a client can skip re-subscribing +> GLOBAL surfaces on a conversation switch. ABSENT means assume conversation-scoped (the +> conservative always-send-conversationId policy remains correct for both). Emitted today: +> `loaded-extensions` → `"global"`, `cache-warming` → `"conversation"`. Also (CR-4d, no shape +> change): the initial `surface` reply to a conversation-scoped subscribe ECHOES `conversationId` +> as documented (was already on backend HEAD; verify with a freshly-booted backend). > > **2026-06 delta (cache-warming handoff):** adds the `NumberField` variant (`kind:"number"`) to > the `SurfaceField` union, and an OPTIONAL `conversationId?` to `SubscribeMessage` / > `UnsubscribeMessage` / `InvokeMessage` / `SurfaceMessage` / `SurfaceUpdate` so a surface can be > CONVERSATION-SCOPED (state differs per conversation, e.g. `cache-warming`) vs GLOBAL (one state for > all, e.g. `loaded-extensions`). All additive / backward-compatible: a global surface omits -> `conversationId` and behaves exactly as before. (Backend left the package version at `0.1.0`.) +> `conversationId` and behaves exactly as before. ```ts /** * UI contract — the frontend-agnostic vocabulary for backend-declared "surfaces". * * A SURFACE is a "data transportation surface": a typed description of what data an - * extension exposes, its semantics, and the actions that can act on it — NOT UI. - * Any client renders a surface in its own idiom (web/Svelte, CLI, future TUI/mobile). - * Types-only, zero runtime, zero `@dispatch/*` deps. + * extension exposes, its semantics, and the actions that can act on it — NOT UI. It + * carries STRUCTURE + SEMANTICS + ACTIONS, never styling and never a rendering- + * framework token. Any client (web/Svelte, CLI, future TUI/mobile) renders a surface + * in its own idiom, so swapping or adding a client is a zero-backend-change event. + * + * This package is types-only (zero runtime) and has ZERO `@dispatch/*` dependencies, + * so a separate client repo can depend on JUST this contract. */ -/** Where a surface mounts — a coarse, semantic placement hint, NOT layout/CSS. Open string. */ +/** + * Where a surface mounts — a coarse, semantic placement hint, NOT a layout/CSS + * instruction. A client maps a region to its own idiom; an unknown region falls back + * to the client's default placement. Deliberately left open (a `string`). + */ export type Region = string; -/** A typed reference to a backend action a field can invoke (client posts payload back). */ +/** + * A typed reference to a backend action a field can invoke. The client posts it back + * (with a payload); the surface id comes from context. + */ export interface ActionRef { readonly actionId: string; } @@ -38,7 +58,10 @@ export interface SurfaceOption { readonly label: string; } -/** A field within a surface — a SEMANTIC value, not a widget. `kind` is the discriminant. */ +/** + * A field within a surface — a SEMANTIC value, not a widget. `kind` is the + * discriminant a client switches on to pick a renderer. + */ export type SurfaceField = | ToggleField | ProgressField @@ -56,7 +79,7 @@ export interface ToggleField { readonly action: ActionRef; } -/** A bounded ratio in [0, 1] with a label. Read-only. */ +/** A bounded ratio in [0, 1] with a label (e.g. a cache-hit rate). Read-only. */ export interface ProgressField { readonly kind: "progress"; readonly label: string; @@ -83,7 +106,7 @@ export interface StatField { * A settable numeric value plus the action that sets it — the free-value * counterpart to `selector` (which is a fixed enum). Optional `min`/`max`/`step` * are SEMANTIC bounds a client may use to validate/step input; `unit` is a - * display hint (e.g. "ms", "s"). The client posts the new number as the action + * display hint (e.g. "ms", "min"). The client posts the new number as the action * payload. Unlike `progress`/`stat` (read-only), this field is interactive. */ export interface NumberField { @@ -105,8 +128,10 @@ export interface ButtonField { } /** - * The escape hatch: data that fits no semantic field kind. Opaque `payload` + a - * `rendererId`; clients WITH a renderer for that id show it, others GRACEFULLY SKIP. + * The escape hatch: data that fits no semantic field kind. Carries an opaque + * `payload` + a `rendererId`; clients WITH a renderer for that id show it, others + * GRACEFULLY SKIP. Keep rare — and the owning extension should export a typed + * payload type so its bespoke renderer narrows `payload` via a typed symbol. */ export interface CustomField { readonly kind: "custom"; @@ -114,7 +139,10 @@ export interface CustomField { readonly payload: unknown; } -/** A surface: an ordered set of fields mounted in a region, with a title. */ +/** + * A surface: an ordered set of fields mounted in a region, with a title. The atomic + * unit a backend extension contributes and a client renders. + */ export interface SurfaceSpec { readonly id: string; readonly region: Region; @@ -122,20 +150,33 @@ export interface SurfaceSpec { readonly fields: readonly SurfaceField[]; } -/** A surface-catalog entry — discovery metadata only (no field data). */ +/** + * A surface-catalog entry — discovery metadata only (no field data). + */ export interface SurfaceCatalogEntry { readonly id: string; readonly region: Region; readonly title: string; + /** + * Whether the surface's spec/values differ per conversation ("conversation") + * or are app-wide ("global"). A client may skip re-subscribing GLOBAL surfaces + * on a conversation switch (they ignore `conversationId`). Optional + additive: + * when absent, a client should assume conversation-scoped (the conservative + * "always send the focused conversationId" policy still works for both). + */ + readonly scope?: "global" | "conversation"; } /** The surface catalog: the list of available surfaces a client can choose to show. */ export type SurfaceCatalog = readonly SurfaceCatalogEntry[]; /** - * A live update for a subscribed surface. v1 carries the full new spec. - * `conversationId` is present only for a CONVERSATION-SCOPED surface (tells the - * client which conversation this update is for); a global surface omits it. + * A live update for a subscribed surface (pushed over the WS channel). v1 carries + * the full new spec (the simplest "patch"). + * + * `conversationId` is present only for a CONVERSATION-SCOPED surface (one whose + * spec/values differ per conversation, e.g. cache-warming controls): it tells the + * client which conversation this update pertains to. A global surface omits it. */ export interface SurfaceUpdate { readonly surfaceId: string; @@ -143,14 +184,18 @@ export interface SurfaceUpdate { readonly conversationId?: string; } -// ── Surface WebSocket protocol (slice 1: surfaces only) ────────────────────── +// ── Surface WebSocket protocol ──────────────────────────────────────────────── /** A client → server message on the surface channel. */ export type SurfaceClientMessage = SubscribeMessage | UnsubscribeMessage | InvokeMessage; /** - * Begin receiving live updates for a surface. For a CONVERSATION-SCOPED surface, - * include the `conversationId` whose state you want; omit it for a global surface. + * Begin receiving live updates for a surface (server replies with `surface`, then `update`s). + * + * For a CONVERSATION-SCOPED surface, include the `conversationId` whose state you + * want — the server resolves the spec for that conversation and pushes its updates. + * Omit it for a global surface (or to view a conversation-scoped surface with no + * conversation in focus → the surface decides its default/empty state). */ export interface SubscribeMessage { readonly type: "subscribe"; @@ -208,7 +253,7 @@ export interface SurfaceUpdateMessage { readonly update: SurfaceUpdate; } -/** A surface-scoped error. */ +/** A surface-scoped error (e.g. unknown surface id, invoke failed). */ export interface SurfaceErrorMessage { readonly type: "error"; readonly surfaceId?: string; |
