From e45cab2a2d9d7bf5e48ace7111fd84b1b9bf2df3 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Thu, 11 Jun 2026 16:06:48 +0900 Subject: feat(cache-warming,surfaces,metrics,markdown): conversation-scoped surfaces, cache warming + retention, markdown Consumes the backend cache-warming + cache-rate handoffs end-to-end and adds supporting infra: - protocol/transport: conversation-scoped surfaces (conversationId on subscribe/invoke/surface + staleness routing); store auto-subscribes the catalog with the focused conversation and re-scopes on switch. - surface-host: generic Number field renderer + custom rendererId dispatch (graceful skip on unknown). - cache-warming feature: enabled toggle, min+sec interval, AUTHORITATIVE countdown from the surface's cache-warming-timer nextWarmAt, manual Warm now (POST /chat/warm), lastWarmAt-keyed history, cache-retention stat, expectedCacheRate headline. - metrics: cross-turn expected-cache (retention) derivation + bubble badge; cache-rate fix needs no code change (inputTokens now total). - markdown feature: marked + marked-highlight + highlight.js + dompurify, rendered in ChatView. - fixes (gemini review): {#key activeConversationId} remount of CacheWarmingView to stop history/feedback leaking across tabs; guard NaN interval inputs from committing 0. - docs/contracts: regenerated transport/ui-contract mirrors; backend-handoff updated (CR-3 resolved). Verified: svelte-check 0 errors, biome clean, 494 tests pass, vite build OK. --- .dispatch/ui-contract.reference.md | 54 +++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) (limited to '.dispatch/ui-contract.reference.md') diff --git a/.dispatch/ui-contract.reference.md b/.dispatch/ui-contract.reference.md index 3962fc1..00d354f 100644 --- a/.dispatch/ui-contract.reference.md +++ b/.dispatch/ui-contract.reference.md @@ -6,6 +6,13 @@ > file is for READING only. > > **Orchestrator:** this is a SNAPSHOT — regenerate it whenever `ui-contract` changes. +> +> **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`.) ```ts /** @@ -37,6 +44,7 @@ export type SurfaceField = | ProgressField | SelectorField | StatField + | NumberField | ButtonField | CustomField; @@ -71,6 +79,24 @@ export interface StatField { readonly value: string; } +/** + * 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 + * payload. Unlike `progress`/`stat` (read-only), this field is interactive. + */ +export interface NumberField { + readonly kind: "number"; + readonly label: string; + readonly value: number; + readonly min?: number; + readonly max?: number; + readonly step?: number; + readonly unit?: string; + readonly action: ActionRef; +} + /** A labelled action trigger. */ export interface ButtonField { readonly kind: "button"; @@ -106,10 +132,15 @@ export interface SurfaceCatalogEntry { /** 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. */ +/** + * 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. + */ export interface SurfaceUpdate { readonly surfaceId: string; readonly spec: SurfaceSpec; + readonly conversationId?: string; } // ── Surface WebSocket protocol (slice 1: surfaces only) ────────────────────── @@ -117,22 +148,34 @@ export interface SurfaceUpdate { /** 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. + */ export interface SubscribeMessage { readonly type: "subscribe"; readonly surfaceId: string; + readonly conversationId?: string; } +/** Stop receiving updates for a surface (and the same `conversationId`, if scoped). */ export interface UnsubscribeMessage { readonly type: "unsubscribe"; readonly surfaceId: string; + readonly conversationId?: string; } -/** Invoke a field's action; `payload` is the new value (e.g. a toggle's boolean). */ +/** + * Invoke a field's action; `payload` is the new value (e.g. a toggle's boolean, a + * `number` field's new number). For a conversation-scoped surface, include the + * `conversationId` the action targets. + */ export interface InvokeMessage { readonly type: "invoke"; readonly surfaceId: string; readonly actionId: string; readonly payload?: unknown; + readonly conversationId?: string; } /** A server → client message on the surface channel. */ @@ -148,10 +191,15 @@ export interface CatalogMessage { readonly catalog: SurfaceCatalog; } -/** The full current spec for a surface the client just subscribed to. */ +/** + * The full current spec for a surface the client just subscribed to. + * `conversationId` echoes the subscribe's conversation for a conversation-scoped + * surface (so the client routes it), and is absent for a global surface. + */ export interface SurfaceMessage { readonly type: "surface"; readonly spec: SurfaceSpec; + readonly conversationId?: string; } /** A live update for a subscribed surface. */ -- cgit v1.2.3