/** * The interactive bridge: one live screen behind the tree the renderer paints. * * A component screen ships BOTH halves — the flat tree of its first paint, and * the compiled source that can produce the next one. The served tree paints * immediately and the VM boots behind it, so an interactive screen is never * slower to appear than a static one; the moment the VM is up, a `{$handler}` * callback moves the screen: * * fire(handlerId, event) → { tree, intents } * tree → flattenTree → swapped in through the SAME walk the payload * took (validation, bindings, `$state`, outcomes all unchanged) * intents → each one through the renderer's existing `onAction` pipe, so * the guard, approvals and per-node outcome notices are the ones * already there — then `settle(intentId, outcome)` for whatever * the answer changes. * * A tool call that SUCCEEDED also makes the screen's own data stale — the * cancelled transfer is still in the list it was painted from — so the served * `queryPlan` re-runs down the same pipe and the answers are SUPPLIED to the * screen that is already standing. That is the whole refresh story: no generated * handler hand-patches a list it did not fetch, and nothing the person typed is * lost, because a supply re-renders the same component rather than booting a new * one. * * The same door answers the other half of a read. A `useQuery` whose input the * screen COMPUTES cannot be resolved before the screen renders, so the paint * NAMES what it wanted (`misses()`) and this file answers it — boot, ask, read, * supply, ask again, bounded at {@link MAX_SUPPLY_ROUNDS}. * * Four rules the shape encodes: * * 1. The screen never disappears. A VM that will not boot, a handler that * throws, a budget that runs out — the last good tree stays on screen and the * failure lands in the per-node outcome slot of the node that fired. * 2. One intent per handler at a time. Rapid-fire is the hole this closes: the * second click on a cancel button is dropped HERE, not merely greyed out in * the render that may not have happened yet. * 3. One refresh at a time. A second mutation that settles mid-refetch queues * ONE more cycle rather than racing a second set of reads against the first. * 4. The VM boots ONCE per screen — never again. The identity is the compiled source string, * not the payload object, because callers legitimately rebuild the payload * object every render — an object dep would re-boot the screen (and discard * everything the user typed) on every parent re-render. */ import type { Json, ToolOutcome } from "../../core/index.js"; import type { WalkTree } from "./renderer.js"; import { type ScreenInteractive } from "./screen-engine.js"; export interface ScreenBridge { /** The tree a handler moved the screen to; `null` while the served one stands. */ tree: WalkTree | null; /** Handler ids with an intent in flight — their control renders disabled. */ inFlight: ReadonlySet; fire(nodeId: string, handlerId: string, event?: unknown): void; /** * Re-read the plan and supply the answers to the standing screen — the same * cycle a changed action runs. Public for the one change that arrives from * OUTSIDE a press: an approval decided elsewhere, whose resumed call already * moved the data this screen is painted from (parked-approvals.ts). * * `fresh` boots a NEW screen on those answers instead of supplying them, which * discards everything the screen's own `useState` holds. Exactly one caller * wants that, and wants it for the state rather than the data: an approval that * was declined or that expired moved nothing, and the screen's latched * "Sending…" has no other way back. */ refresh(nodeId: string, fresh?: boolean): Promise; } export interface ScreenBridgeInput { /** Absent for every payload that is not a component screen. */ interactive: ScreenInteractive | undefined; /** The served tree: the swap keeps its queries, data and payload extras. */ base: WalkTree; catalog: readonly string[]; /** The viewer's wall, as `ScreenBoot` takes it (screen-engine.ts). Unset is * `"en-US"` in `"UTC"` — the engine's default, which is a server's wall. */ locale?: string; timeZone?: string; /** The renderer's own action pipe — never a second call path. */ runAction(nodeId: string, action: string, payload?: Json): Promise; /** Report a screen failure into a node's outcome slot. */ onFailure(nodeId: string, message: string): void; } export declare function useScreen(input: ScreenBridgeInput): ScreenBridge;