import { AssistantToolStep } from '../ui/AssistantToolActivity'; import { AssistantPreviewEvent, AssistantPreviewRun } from './types'; /** * The state a product's preview surface shows, and the rules for changing it. * * ★★★ WHY THIS IS HERE AND NOT IN EACH APP. It was written twice — once in * HealthyBowl, once in VibeControls — and over five review rounds the SAME nine * defects were found and fixed in both copies, four of them cross-account leaks. * Three were introduced by the fix for the previous one. The surface itself is * rightly per-product (its copy, its testids, its markdown); the state machine * underneath is not, and every day it stayed duplicated it cost two fixes and * risked them diverging. * * What a product still owns: what the reading view LOOKS like. What it no longer * owns: deciding which events may write to it. */ /** * ★ `ended` is the run finishing in a way THIS version does not recognise. * fe-libs owns `outcome`, so a future one can reach an app that has not been * rebuilt. Without a named phase for it the phase became whatever string * arrived, and a product's status lookup returned `undefined`. */ export type PreviewPhase = "idle" | "generating" | "complete" | "error" | "abandoned" | "ended"; /** * Who an answer belongs to. * * ★★ A STRUCT, NOT A `workspaceId|actorId` STRING. Both apps carried the string * form and had to split it to compare halves, which is how one of them ended up * validating only the workspace and letting a second account in the same * workspace read the first one's answer. */ export interface PreviewOwner { readonly workspaceId: string | null; readonly actorId: string | null; } export interface PreviewState { readonly phase: PreviewPhase; readonly content: string; readonly error: string | null; /** The identity this content belongs to. Never inherited across a change. */ readonly owner: PreviewOwner; /** * ★★★ THE RUN THIS CONTENT CAME FROM — `null` when nothing is in flight. * * Content may only be written for the run currently in flight. That single * rule closes three separate ways the wrong text reached a user: * a snapshot still arriving from an interrupted turn overwriting the new * answer; a late terminal event ending a turn that was still streaming; and * an event from a previous account being relabelled as the current one's, * because a surface reads identity at DELIVERY time. */ readonly runId: string | null; /** * ★★ What the assistant has been DOING, not just what it has said. * * Carried so a surface can render progress — a step rail, a spinner per tool — * rather than only the prose. Without it the capability could express "the * answer, wider" and nothing else, which is why a "watch it build" surface * could not be based on it. Empty for a run that has called no tools, and * cleared by `start` so one run's activity never decorates the next. */ readonly steps: readonly AssistantToolStep[]; } export declare const NO_OWNER: PreviewOwner; export declare const IDLE_PREVIEW: PreviewState; /** The identity as it is NOW. Read at delivery; never cached. */ export declare function currentPreviewOwner(): PreviewOwner; export declare function sameOwner(a: PreviewOwner, b: PreviewOwner): boolean; /** * Did this run START under this identity? * * ★★★ BOTH HALVES, which is the whole reason `actorId` was added to * `AssistantPreviewRun`. Checking only the workspace left two accounts sharing * one workspace indistinguishable: a late `start` from the previous actor was * adopted under the new one, and every snapshot after it then matched the run id * and painted the previous actor's answer. That case was not closeable in an app * at all — the run carried no actor to compare. */ export declare function runBelongsTo(run: AssistantPreviewRun, owner: PreviewOwner): boolean; /** * The reducer. Pure, so it can be tested without React or a DOM. * * ★ `abandoned` is NOT an error. The user interrupted their own turn; showing a * failure for that blames them for their own decision. It keeps the text that * arrived and says it stopped. */ export declare function applyPreviewEvent(state: PreviewState, event: AssistantPreviewEvent, owner: PreviewOwner): PreviewState; /** * The store a product's reading view subscribes to. * * ★★ EXTERNAL, read through `useSyncExternalStore`. The controller handed to the * panel must stay STABLE across renders — the bridge deliberately does not reset * on controller identity — so a surface cannot hold its state in a closure the * controller captures. */ export interface AssistantPreviewSurface { subscribe(listener: () => void): () => void; getSnapshot(): PreviewState; /** * Reduce an event and publish. ★ The owner is derived HERE, once, so no * product can forget to do it or do it differently. */ deliver(event: AssistantPreviewEvent): void; /** Clear. Call when the identity changes. */ reset(): void; /** * Re-check the owner and NOTIFY if it changed. * * ★★★ THE NOTIFICATION PATH THAT `getSnapshot` IS NOT. `getSnapshot` clears * on read, which protects any reader that re-renders — but nothing makes it * re-render: `getWorkspaceId()`/`getActorId()` are imperative reads of the * URL, storage and the JWT, with no change event for the store to subscribe * to, and a surface component need not consume the auth context that did * change. So on an account switch with no preview event and no unrelated * render, the previous owner's answer stayed painted. * * ★ Products call this from the effect they already key on their auth / * workspace context. It is a no-op when the owner has not changed, which is * why it is safe to call on every such tick and why it is preferable to * `reset()` there — `reset()` would also wipe a live run of the SAME owner * whenever an unrelated part of that context ticked. * * @returns whether the owner had changed (and subscribers were notified). */ syncOwner(): boolean; } export declare function createAssistantPreviewSurface( /** * ★ Injectable so this can be tested without a DOM — `currentPreviewOwner()` * reads `window.location`, which a pure test run does not have. */ readOwner?: () => PreviewOwner): AssistantPreviewSurface; //# sourceMappingURL=surfaceState.d.ts.map