summaryrefslogtreecommitdiffhomepage
path: root/.dispatch/ui-contract.reference.md
diff options
context:
space:
mode:
Diffstat (limited to '.dispatch/ui-contract.reference.md')
-rw-r--r--.dispatch/ui-contract.reference.md87
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;