React chat generative UI preview
Render an airline dashboard inside a conversation using components your application owns. The agent supplies layout and data; the registry decides which React views those names can create.
The native React APIs on this page are experimental, private source-build packages. Run the repository example to use them; they are not an npm installation recipe.
Compose text and generated content
The application continues to use MessageList, Markdown, and tool observations for the conversation. For an admitted dashboard specification it adds RenderSpec with a closed registry and the matching state. Keeping those paths explicit lets ordinary text remain readable while generated content uses a controlled set of components. This example waits for a confirmed layout rather than promising Angular’s progressive rendering behavior.
The React conversation walkthrough connects this capability to requests, tools, UI state, and saved history.
Start a conversation
Choose Run, type a message and choose Send or press Enter. Show dashboard and Cancelled flights only seed the composer; they do not execute. The first Send creates the conversation. New conversation opens a blank local view and waits for Send before creating anything. Mounting, reading Docs or Code, and changing workspace modes make no thread requests.
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated consumer copies this internal example layout.
import { ExampleFooter, ExampleLayout } from '../../../../shared/react-example';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useAgent } from '@threadplane/react';
import {
ChatInput,
MessageList,
Reasoning,
ToolObservation,
type ChatInputHandle,
} from '@threadplane/react/chat';
import { Markdown } from '@threadplane/react/markdown';
import { RenderSpec, type RenderSpecProps } from '@threadplane/react/render';
import type { MessageRow } from '@threadplane/content/messages';
import {
createConnectedApplication,
type GenerativeUiConnection,
} from './connection';
import type { GenerativeUiSnapshot } from './application';
import { dashboardRegistry } from './dashboard-views';
function UnsupportedView() {
return <p role="alert">This dashboard view is unavailable.</p>;
}
const literal = (value: unknown) =>
typeof value === 'string' ? value : JSON.stringify(value, null, 2);
/** Layout authority belongs to the confirmed parent; data is shared by every retained layout. */
export function renderMessage(row: MessageRow, snapshot: GenerativeUiSnapshot) {
const surface = snapshot.surfaces.find((item) => item.messageId === row.id);
const notice = snapshot.notices.find((item) => item.messageId === row.id);
const calls = snapshot.toolCalls.filter((call) =>
row.message.toolCallIds?.includes(call.id)
);
const label =
row.role === 'user'
? 'You'
: row.role === 'assistant'
? 'Assistant'
: row.role === 'tool'
? 'Tool result'
: 'System';
return (
<article
aria-label={label}
className={`tp-chat-message tp-chat-message--${row.role}`}
>
<p className="tp-chat-message__role">{label}</p>
{row.role === 'assistant' && row.reasoning && (
<Reasoning snapshot={row.reasoning} />
)}
{surface ? null : row.role === 'tool' ? (
<details>
<summary>Server tool result</summary>
<pre>{row.message.content}</pre>
</details>
) : (
<Markdown snapshot={row.markdown} />
)}
{surface && (
<div data-dashboard-spec-owner={row.id}>
<RenderSpec
spec={surface.spec}
registry={dashboardRegistry}
state={snapshot.dashboard as RenderSpecProps['state']}
loading={snapshot.busy}
fallback={UnsupportedView}
/>
</div>
)}
{surface && (
<details>
<summary>Layout source</summary>
<pre>{row.message.content}</pre>
</details>
)}
{notice && <p role="status">{notice.text}</p>}
{calls.map((call) => (
<details key={call.id} className="tool-inspection">
<summary>
{call.name} · {call.status}
</summary>
<ToolObservation
name={call.name}
argumentsText={literal(call.args) ?? ''}
resultText={
call.status === 'complete'
? literal(call.result)
: call.status === 'error'
? call.error
: undefined
}
label={`Observed tool: ${call.status}`}
/>
</details>
))}
</article>
);
}
type Props = {
readonly connection: GenerativeUiConnection;
readonly onReady: () => void;
};
/** Replace the owner on connection changes without putting credentials in React keys. */
export function GenerativeUiDemo(props: Props) {
const fingerprint = JSON.stringify([
props.connection.apiUrl,
Object.entries(props.connection.headers).sort(([a], [b]) =>
a.localeCompare(b)
),
]);
const identity = useRef({ fingerprint, version: 0 });
if (identity.current.fingerprint !== fingerprint)
identity.current = { fingerprint, version: identity.current.version + 1 };
return <GenerativeUiOwner key={identity.current.version} {...props} />;
}
function GenerativeUiOwner({ connection, onReady }: Props) {
const [application] = useState(() => createConnectedApplication(connection));
const [draft, setDraft] = useState('');
const composer = useRef<ChatInputHandle>(null);
const resetButton = useRef<HTMLButtonElement>(null);
const focusAfterReset = useRef(false);
const snapshot = useAgent(application);
useEffect(() => {
if (!focusAfterReset.current || snapshot.busy) return;
focusAfterReset.current = false;
const document = resetButton.current?.ownerDocument;
if (
document &&
(document.activeElement === resetButton.current ||
document.activeElement === document.body)
) {
composer.current?.focus();
}
}, [snapshot.viewGeneration, snapshot.busy]);
useEffect(() => {
onReady();
}, [onReady]);
useEffect(
() => () => {
void application.dispose();
},
[application]
);
const renderRow = useCallback(
(row: MessageRow) => renderMessage(row, snapshot),
[snapshot]
);
const currentView = () =>
application.getSnapshot().viewGeneration === snapshot.viewGeneration;
const status = snapshot.busy
? snapshot.activity === 'creating'
? 'Creating conversation…'
: snapshot.activity === 'confirming'
? 'Confirming saved dashboard…'
: snapshot.activity === 'replacing'
? 'Releasing conversation…'
: 'Receiving response…'
: !snapshot.canSubmit
? 'Start a new conversation to continue.'
: snapshot.outcome === 'success'
? 'Response saved.'
: 'Ready.';
return (
<ExampleLayout className="dashboard-demo">
<header>
<div>
<h1>Chat generative UI</h1>
</div>
<button
ref={resetButton}
type="button"
onClick={() => {
if (currentView()) {
focusAfterReset.current = true;
setDraft('');
void application.newConversation();
}
}}
>
New conversation
</button>
</header>
<p>
Explore airline operations with fixture data. Confirmed dashboards stay
in the conversation and refresh together when a new update is saved.
</p>
{snapshot.rows.length === 0 && (
<div className="seed-prompts" aria-label="Example prompts">
{[
{
label: 'Show dashboard',
text: 'Show me a dashboard of airline operations.',
},
{
label: 'Cancelled flights',
text: 'Filter to only the cancelled flights.',
},
].map((prompt) => (
<button
type="button"
key={prompt.label}
disabled={snapshot.busy || !snapshot.canSubmit}
onClick={() => {
const current = application.getSnapshot();
if (currentView() && !current.busy && current.canSubmit) {
setDraft(prompt.text);
composer.current?.focus();
}
}}
>
{prompt.label}
</button>
))}
</div>
)}
<MessageList
key={`messages:${snapshot.viewGeneration}`}
rows={snapshot.rows}
renderMessage={renderRow}
label="Conversation"
/>
<ExampleFooter>
{snapshot.error && <p role="alert">{snapshot.error}</p>}
<ChatInput
ref={composer}
key={`composer:${snapshot.viewGeneration}`}
label="Message"
value={draft}
onValueChange={(text) => {
if (currentView()) setDraft(text);
}}
placeholder="Ask about airline operations…"
hint="Enter to send. Shift+Enter adds a new line."
busy={snapshot.busy}
disabled={snapshot.busy || !snapshot.canSubmit}
onStop={
snapshot.busy && snapshot.activity !== 'replacing'
? () => {
if (currentView()) void application.stop();
}
: undefined
}
onSubmit={(text) => {
const current = application.getSnapshot();
if (
!currentView() ||
current.busy ||
!current.canSubmit ||
!text.trim()
)
return false;
void application.submit(text);
return currentView() && application.getSnapshot().busy;
}}
/>
<p role="status">{status}</p>
</ExampleFooter>
</ExampleLayout>
);
}Confirm layouts and data together
Ask for an airline operations dashboard. The example uses fixture data. The shared graph writes layouts into assistant messages, persists its owned dashboard data and completion evidence, and still emits custom JSON Pointer state updates for Angular's progressive views.
The React preview admits no provisional dashboard UI. It checks the actual successful run and its exact saved checkpoint, then publishes the validated layout and dashboard data together. Every retained layout reads the same latest confirmed dashboard data. A data-only request such as Filter to only the cancelled flights refreshes all those layouts without creating another layout.
import type {
AgentSession,
CompleteOutcome,
Message,
ToolCall,
} from '@threadplane/core';
import {
createMessageContent,
type MessageRow,
} from '@threadplane/content/messages';
import { copyData, dataKey, type DashboardState } from './dashboard-data';
import {
createObservation,
messageKey,
type GenerativeUiState,
} from './observation';
import {
captureTerminal,
checkpointSource,
type Checkpoint,
type Surface,
type Notice,
} from './terminal';
export interface GenerativeUiSession extends Omit<AgentSession, 'getSnapshot'> {
getSnapshot(): GenerativeUiState;
load(options?: { readonly signal?: AbortSignal }): Promise<void>;
}
export interface GenerativeUiClient {
createThread(signal: AbortSignal): Promise<string>;
sessionFactory(threadId: string): GenerativeUiSession;
readCheckpoint(source: Checkpoint, signal: AbortSignal): Promise<unknown>;
}
export interface GenerativeUiSnapshot {
readonly threadId: string | null;
readonly rows: readonly MessageRow[];
readonly messages: readonly Message[];
readonly toolCalls: readonly ToolCall[];
readonly dashboard: DashboardState;
readonly surfaces: readonly Surface[];
readonly notices: readonly Notice[];
readonly activity:
| 'idle'
| 'creating'
| 'running'
| 'confirming'
| 'replacing';
readonly busy: boolean;
readonly canSubmit: boolean;
readonly outcome: CompleteOutcome | null;
readonly error: string | null;
readonly viewGeneration: number;
}
const empty = (): GenerativeUiSnapshot =>
Object.freeze({
threadId: null,
rows: Object.freeze([]),
messages: Object.freeze([]),
toolCalls: Object.freeze([]),
dashboard: Object.freeze({}),
surfaces: Object.freeze([]),
notices: Object.freeze([]),
activity: 'idle',
busy: false,
canSubmit: true,
outcome: null,
error: null,
viewGeneration: 0,
});
/** Own one lazy conversation and publish dashboard/layout authority atomically. */
export function createGenerativeUiApplication(options: GenerativeUiClient) {
type Owner = {
id: string;
session: GenerativeUiSession;
content: ReturnType<typeof createMessageContent>;
closed: boolean;
unsafe: boolean;
release?: () => void;
messages: readonly Message[];
tools: readonly ToolCall[];
observation?: ReturnType<typeof createObservation>;
saving: boolean;
generations: string[];
};
let snapshot = empty(),
owner: Owner | undefined,
operation: AbortController | undefined,
disposed = false,
disposal: Promise<void> | undefined;
const listeners = new Set<() => void>(),
cleanups = new Set<Promise<void>>();
function publish(update: Partial<GenerativeUiSnapshot>) {
if (disposed) return;
snapshot = Object.freeze({ ...snapshot, ...update });
for (const notify of [...listeners]) notify();
}
function bounded(work: () => void | Promise<void>) {
let timer: ReturnType<typeof setTimeout>;
return Promise.race([
Promise.resolve()
.then(work)
.catch(() => undefined),
new Promise<void>((resolve) => {
timer = setTimeout(resolve, 2000);
}),
]).finally(() => clearTimeout(timer));
}
function close(selected: Owner) {
if (selected.closed) return Promise.resolve();
selected.closed = true;
selected.release?.();
selected.content.dispose();
const cleanup = bounded(() => selected.session.dispose());
cleanups.add(cleanup);
void cleanup.then(() => cleanups.delete(cleanup));
return cleanup;
}
function detach() {
const previous = owner;
owner = undefined;
return previous ? close(previous) : Promise.resolve();
}
function blocked() {
publish({
canSubmit: false,
outcome: 'error',
error:
'This update could not be confirmed. The last confirmed dashboard is retained. Start a new conversation to continue.',
});
}
function read(
selected: Owner,
valid: () => boolean
): GenerativeUiState | undefined {
if (!valid()) return;
const source = selected.session.getSnapshot();
if (!valid()) return;
let state: GenerativeUiState;
try {
state = copyData(source) as GenerativeUiState;
if (selected.observation)
state = selected.observation.observe(state, selected.saving);
else if (
!state ||
state.error ||
state.status !== 'idle' ||
state.interrupts.length ||
state.subgraphs.length ||
state.messages.length !== selected.messages.length ||
state.messages.some(
(m, i) => messageKey(m) !== messageKey(selected.messages[i])
) ||
dataKey(state.toolCalls) !== dataKey(selected.tools)
)
throw new Error('Changed confirmed state');
} catch {
selected.unsafe = true;
if (valid()) blocked();
return;
}
if (!valid()) return;
const rows = selected.content.project(state);
if (!valid()) return;
publish({ rows, messages: state.messages, toolCalls: state.toolCalls });
if (!valid()) return;
if (selected.session.getSnapshot() !== source) {
selected.unsafe = true;
blocked();
return;
}
return state;
}
async function submit(text: string) {
if (disposed || snapshot.busy || !snapshot.canSubmit || !text.trim())
return false;
const admitted = new AbortController();
operation = admitted;
const current = () =>
!disposed && operation === admitted && !admitted.signal.aborted;
publish({
busy: true,
canSubmit: false,
activity: owner ? 'running' : 'creating',
outcome: null,
error: null,
});
try {
if (!current()) return false;
if (!owner) {
const id = await options.createThread(admitted.signal);
if (!current()) return false;
if (typeof id !== 'string' || !id)
throw new Error('Unconfirmed thread');
const session = options.sessionFactory(id),
selected: Owner = {
id,
session,
content: createMessageContent(),
closed: false,
unsafe: false,
messages: [],
tools: [],
saving: false,
generations: [],
};
if (!current()) {
void close(selected);
return false;
}
owner = selected;
const release = session.subscribe(() => {
if (owner === selected && !selected.closed && !disposed)
read(
selected,
() =>
owner === selected &&
!selected.closed &&
!disposed &&
!operation?.signal.aborted
);
});
selected.release = release;
if (selected.closed) release();
if (!current()) return false;
publish({ threadId: id, activity: 'running' });
}
const selected = owner,
valid = () => current() && owner === selected && !selected.closed;
if (!read(selected, valid) || !valid() || selected.unsafe) {
if (valid()) blocked();
return false;
}
selected.observation = createObservation(
text,
selected.messages,
selected.tools,
selected.generations
);
selected.saving = false;
if (!read(selected, valid) || !valid() || selected.unsafe) return false;
const outcome = await selected.session.submit(text, {
signal: admitted.signal,
});
if (!valid()) return false;
const live = read(selected, valid);
if (!valid()) return false;
if (
!live ||
selected.unsafe ||
outcome !== 'success' ||
live.status !== 'idle' ||
!selected.observation.human ||
live.messages.some((m) => m.delivery.phase !== 'complete')
) {
blocked();
return false;
}
publish({ activity: 'confirming' });
if (!valid()) return false;
selected.saving = true;
await selected.session.load({ signal: admitted.signal });
if (!valid()) return false;
const saved = read(selected, valid);
if (!valid()) return false;
const checkpoint =
saved && checkpointSource(saved.history?.[0]?.checkpoint, selected.id);
if (!saved || selected.unsafe || !checkpoint) {
blocked();
return false;
}
const raw = await options.readCheckpoint(checkpoint, admitted.signal);
if (!valid()) return false;
const latest = read(selected, valid);
if (!valid()) return false;
const confirmed =
latest &&
captureTerminal(
latest,
raw,
selected.id,
selected.observation,
snapshot.dashboard,
snapshot.surfaces
);
if (!confirmed || selected.unsafe) {
blocked();
return false;
}
selected.messages = confirmed.messages;
selected.tools = confirmed.tools;
const generation = selected.observation.human?.delivery.generation;
if (generation) selected.generations.push(generation);
selected.observation = undefined;
selected.saving = false;
publish({
dashboard: confirmed.dashboard,
surfaces: confirmed.surfaces,
notices: Object.freeze([...snapshot.notices, ...confirmed.notices]),
outcome: 'success',
canSubmit: true,
});
if (valid()) read(selected, valid);
return true;
} catch {
if (current()) blocked();
return false;
} finally {
if (current()) {
operation = undefined;
publish({ busy: false, activity: 'idle' });
}
}
}
async function stop() {
if (disposed || !operation) return;
operation.abort();
const admitted = new AbortController();
operation = admitted;
const selected = owner,
cleanup = detach();
if (!disposed && operation === admitted)
publish({
busy: true,
activity: 'replacing',
canSubmit: false,
outcome: 'aborted',
error: null,
});
await Promise.all([cleanup, bounded(() => selected?.session.stop())]);
if (!disposed && operation === admitted) {
operation = undefined;
publish({ busy: false, activity: 'idle' });
}
}
async function newConversation() {
if (disposed) return;
operation?.abort();
const admitted = new AbortController();
operation = admitted;
const cleanup = detach();
if (!disposed && operation === admitted)
publish({
...empty(),
viewGeneration: snapshot.viewGeneration + 1,
busy: true,
canSubmit: false,
activity: 'replacing',
});
await cleanup;
if (disposed || operation !== admitted) return;
operation = undefined;
publish({ busy: false, canSubmit: true, activity: 'idle' });
}
function dispose() {
if (disposal) return disposal;
disposed = true;
operation?.abort();
operation = undefined;
void detach();
listeners.clear();
disposal = Promise.all([...cleanups]).then(() => undefined);
return disposal;
}
return {
getSnapshot: () => snapshot,
subscribe(notify: () => void) {
if (disposed) return () => undefined;
listeners.add(notify);
return () => {
listeners.delete(notify);
};
},
submit,
stop,
newConversation,
dispose,
};
}import type { Message, ToolCall } from '@threadplane/core';
import type { RenderSpecData } from '@threadplane/react/render';
import {
copyData,
dataKey,
dashboardState,
plainRecord,
type DashboardState,
} from './dashboard-data';
import { dashboardSpec } from './dashboard-spec';
import { renderResult, toolData } from './tool-evidence';
import type { GenerativeUiState, createObservation } from './observation';
export interface Checkpoint {
readonly thread_id: string;
readonly checkpoint_ns: '';
readonly checkpoint_id: string;
readonly checkpoint_map?: Readonly<Record<string, string>>;
}
export interface Surface {
readonly messageId: string;
readonly spec: RenderSpecData;
}
export interface Notice {
readonly messageId: string;
readonly text: string;
}
export interface Confirmation {
readonly messages: readonly Message[];
readonly tools: readonly ToolCall[];
readonly dashboard: DashboardState;
readonly surfaces: readonly Surface[];
readonly notices: readonly Notice[];
}
export function checkpointSource(
input: unknown,
threadId: string
): Checkpoint | undefined {
const value = copyData(input);
if (
!plainRecord(value) ||
value.thread_id !== threadId ||
value.checkpoint_ns !== '' ||
typeof value.checkpoint_id !== 'string' ||
!value.checkpoint_id ||
Object.keys(value).some(
(k) =>
![
'thread_id',
'checkpoint_ns',
'checkpoint_id',
'checkpoint_map',
].includes(k)
)
)
return;
if (
value.checkpoint_map !== undefined &&
(!plainRecord(value.checkpoint_map) ||
Object.values(value.checkpoint_map).some((v) => typeof v !== 'string'))
)
return;
if (
plainRecord(value.checkpoint_map) &&
Object.entries(value.checkpoint_map).some(
([k, v]) => k !== '' || v !== value.checkpoint_id
)
)
return;
return value as unknown as Checkpoint;
}
const empty = (value: unknown) =>
value === undefined || (Array.isArray(value) && !value.length);
function clean(record: Record<string, unknown>) {
return (
(record.error === undefined || record.error === null) &&
['interrupts', '__interrupt__', 'subgraphs'].every((k) => empty(record[k]))
);
}
/** Confirm raw task-free checkpoint against the loaded projection and tool-derived data. */
export function captureTerminal(
input: GenerativeUiState,
checkpoint: unknown,
threadId: string,
observation: ReturnType<typeof createObservation>,
previous: DashboardState,
surfaces: readonly Surface[],
knownRunId?: string
): Confirmation | undefined {
try {
const state = copyData(input) as GenerativeUiState,
raw = copyData(checkpoint);
const requested = checkpointSource(
state.history?.[0]?.checkpoint,
threadId
);
if (
!requested ||
!plainRecord(raw) ||
dataKey(checkpointSource(raw.checkpoint, threadId)) !==
dataKey(requested) ||
!Array.isArray(raw.next) ||
raw.next.length ||
!Array.isArray(raw.tasks) ||
raw.tasks.length ||
!clean(raw) ||
state.status !== 'idle' ||
state.error ||
state.interrupts.length ||
state.subgraphs.length ||
state.history?.[0].next.length
)
return;
if (
knownRunId &&
((raw.run_id !== undefined && raw.run_id !== knownRunId) ||
(plainRecord(raw.metadata) &&
raw.metadata.run_id !== undefined &&
raw.metadata.run_id !== knownRunId))
)
return;
const values = raw.values;
if (
!plainRecord(values) ||
!clean(values) ||
!Array.isArray(values.messages)
)
return;
const projected = Object.fromEntries(
Object.entries(values).filter(
([k]) => k !== 'messages' && k !== '__interrupt__'
)
);
if (dataKey(projected) !== dataKey(state.values)) return;
const ms = state.messages,
ts = state.toolCalls;
if (
!observation.human ||
!ms.length ||
ms.at(-1)?.role !== 'assistant' ||
!ms.at(-1)?.content.trim() ||
values.completed_turn_id !== observation.human.id ||
values.completed_answer_id !== ms.at(-1)?.id ||
dataKey(values.completed_message_ids) !== dataKey(ms.map((m) => m.id)) ||
values.messages.length !== ms.length
)
return;
if (
ms.some(
(m) =>
m.delivery.phase !== 'complete' ||
m.delivery.outcome !== 'success' ||
m.delivery.generation !== m.id
) ||
ts.some((t) => t.status !== 'complete' || typeof t.result !== 'string')
)
return;
const calls = new Map(ts.map((t) => [t.id, t])),
pending = new Set<string>(),
seen = new Set<string>();
let dashboard: DashboardState = dashboardState(previous) ?? {};
const accepted = [...surfaces];
const notices: Notice[] = [];
for (const [index, m] of ms.entries()) {
const wire = values.messages[index];
if (
!plainRecord(wire) ||
!clean(wire) ||
wire.id !== m.id ||
wire.content !== m.content ||
wire.type !==
(m.role === 'user'
? 'human'
: m.role === 'assistant'
? 'ai'
: 'tool') ||
!empty(wire.invalid_tool_calls)
)
return;
if (
wire.additional_kwargs !== undefined &&
(!plainRecord(wire.additional_kwargs) ||
wire.additional_kwargs.function_call !== undefined ||
!empty(wire.additional_kwargs.tool_calls))
)
return;
const extra = plainRecord(wire.additional_kwargs)
? wire.additional_kwargs
: {};
const reasoning =
typeof wire.reasoning === 'string'
? wire.reasoning
: typeof extra.reasoning_content === 'string'
? extra.reasoning_content
: undefined;
if (
m.name !== (typeof wire.name === 'string' ? wire.name : undefined) ||
m.reasoning !== reasoning ||
m.citations?.length ||
[extra.citations, extra.sources].some(
(value) => Array.isArray(value) && value.length > 0
)
)
return;
if (m.role === 'assistant') {
const ids = m.toolCallIds ?? [],
rawCalls = wire.tool_calls ?? [];
if (
!Array.isArray(rawCalls) ||
rawCalls.length !== ids.length ||
pending.size
)
return;
for (const [i, id] of ids.entries()) {
const t = calls.get(id),
r = rawCalls[i];
if (
!t ||
seen.has(id) ||
!plainRecord(r) ||
r.id !== id ||
r.name !== t.name ||
dataKey(r.args) !== dataKey(t.args)
)
return;
pending.add(id);
seen.add(id);
}
} else if (m.role === 'tool') {
const id = m.toolCallId ?? '',
t = calls.get(id);
if (
!t ||
t.status !== 'complete' ||
!pending.delete(id) ||
m.name !== t.name ||
m.content !== t.result ||
wire.tool_call_id !== id ||
wire.name !== t.name ||
(wire.status !== undefined && wire.status !== 'success')
)
return;
if (index < observation.prefixLength) continue;
const parent = ms
.slice(0, index)
.find((p) => p.toolCallIds?.includes(id));
if (!parent) return;
if (t.name === 'render_spec') {
const spec = dashboardSpec(t.args);
if (!spec) return;
if (t.result === 'rendered') {
const wrap = observation.wraps.find((w) => w.callId === id);
if (
!wrap ||
wrap.parentId !== parent.id ||
wrap.resultId !== m.id ||
parent.content !== wrap.text
)
return;
accepted.push(Object.freeze({ messageId: parent.id, spec }));
} else {
if (!parent.content.trim() || !renderResult(t)) return;
notices.push(
Object.freeze({
messageId: parent.id,
text: 'This layout was not applied because the assistant already supplied prose. The tool result is available for inspection.',
})
);
}
} else {
const patch = toolData(t);
if (!patch) return;
dashboard = Object.freeze({ ...dashboard, ...patch });
}
} else if (
m.role !== 'user' ||
pending.size ||
!empty(wire.tool_calls) ||
wire.tool_call_id !== undefined
)
return;
}
const savedDashboard = dashboardState(values.dashboard);
if (
pending.size ||
seen.size !== ts.length ||
!savedDashboard ||
dataKey(savedDashboard) !== dataKey(dashboard)
)
return;
return Object.freeze({
messages: ms,
tools: ts,
dashboard: savedDashboard,
surfaces: Object.freeze(accepted),
notices: Object.freeze(notices),
});
} catch {
return undefined;
}
}A later structural layout adds another surface while earlier layouts remain in the conversation. A prose-only reply adds no surface. If the render call's parent already contains nonempty prose, the example preserves that prose, displays a layout no-op notice and keeps the raw tool result inspectable. Layout source and server tool observations remain available for inspection.
A closed, read-only dashboard catalog
The example owns six view types: dashboard_grid, container, stat_card, line_chart, bar_chart and data_grid. Its bounded spec policy accepts only the supported read-only layout and data bindings. Actions, A2UI and foreign view types are unsupported in this React example. These restrictions are local example policy; they do not expand the public renderer API.
import { Children, useId, type CSSProperties } from 'react';
import type {
ReactRenderRegistry,
RenderViewProps,
RenderValue,
} from '@threadplane/react/render';
const text = (value: RenderValue) =>
typeof value === 'string' || typeof value === 'number' ? String(value) : '';
function Waiting() {
return <p className="dashboard-empty">Waiting for dashboard data…</p>;
}
function DashboardGrid({ children }: RenderViewProps) {
return (
<div className="dashboard-grid" data-dashboard-view="dashboard_grid">
{children}
</div>
);
}
function Container({ props, children }: RenderViewProps) {
return (
<div
className="dashboard-container"
data-dashboard-view="container"
data-direction={props.direction === 'row' ? 'row' : 'column'}
style={
{
'--dashboard-cols': Math.max(1, Children.count(children)),
} as CSSProperties
}
>
{children}
</div>
);
}
function StatCard({ props }: RenderViewProps) {
return (
<section
className="dashboard-card"
data-dashboard-view="stat_card"
aria-label={text(props.label)}
>
<h3>{text(props.label)}</h3>
{props.value === undefined || props.value === null ? (
<Waiting />
) : (
<p className="dashboard-metric">{text(props.value)}</p>
)}
{props.delta !== null && props.delta !== undefined && (
<p
className="dashboard-delta"
data-trend={
text(props.delta).startsWith('+')
? 'up'
: /^[-−]/.test(text(props.delta))
? 'down'
: 'flat'
}
>
{text(props.delta)}
</p>
)}
</section>
);
}
type Point = Readonly<{ label: string; value: number }>;
function chartPoints(
value: RenderValue,
labelKey: string,
valueKey: string
): readonly Point[] | undefined {
if (!Array.isArray(value)) return;
return value.map((row) => {
const record = row as Readonly<Record<string, RenderValue>>;
return { label: text(record[labelKey]), value: Number(record[valueKey]) };
});
}
function Chart({
props,
kind,
}: RenderViewProps & { readonly kind: 'line' | 'bar' }) {
const gradientId = useId();
const title =
text(props.title) ||
(kind === 'line' ? 'On-time trend' : 'Flights by airline');
const points = chartPoints(
props.data,
kind === 'line' ? 'month' : 'airline',
kind === 'line' ? 'on_time_pct' : 'count'
);
const values = points?.map((point) => point.value) ?? [];
const rawMinimum = kind === 'line' && values.length ? Math.min(...values) : 0;
const rawMaximum =
kind === 'line' && values.length
? Math.max(...values)
: Math.max(1, ...values);
const domainPadding =
kind === 'line' && rawMinimum === rawMaximum
? Math.max(1, Math.abs(rawMinimum) * 0.05)
: 0;
const minimum = rawMinimum - domainPadding;
const maximum = rawMaximum + domainPadding;
const range = maximum - minimum || 1;
const padding =
kind === 'line'
? { top: 16, right: 32, bottom: 28, left: 64 }
: { top: 28, right: 16, bottom: 28, left: 16 };
const plotWidth = 400 - padding.left - padding.right;
const plotHeight = 220 - padding.top - padding.bottom;
const baseline = 220 - padding.bottom;
const x = (index: number) =>
(points?.length ?? 0) === 1
? padding.left + plotWidth / 2
: padding.left + (index * plotWidth) / ((points?.length ?? 0) - 1);
const y = (value: number) =>
padding.top + plotHeight - ((value - minimum) / range) * plotHeight;
const labelStep = Math.max(
1,
Math.ceil((((points?.length ?? 1) - 1) * 64) / plotWidth)
);
const labelCount =
kind === 'line'
? Math.min(4, Math.floor(((points?.length ?? 1) - 1) / labelStep) + 1)
: 4;
const labelIndices =
(points?.length ?? 0) <= labelCount
? points?.map((_, index) => index) ?? []
: Array.from({ length: labelCount }, (_, index) =>
Math.round((index * ((points?.length ?? 0) - 1)) / (labelCount - 1))
);
const numberLabel = (value: number) => {
const scale = Math.abs(value) >= 1000 && range >= 1000 ? 1000 : 1;
const precision = Math.min(
10,
Math.max(0, -Math.floor(Math.log10(range / (2 * scale))))
);
return `${(value / scale).toFixed(precision)}${scale === 1000 ? 'k' : ''}`;
};
const barGap = Math.min(12, plotWidth / (2 * (points?.length || 1)));
const barWidth =
(plotWidth - barGap * ((points?.length || 1) - 1)) / (points?.length || 1);
const barX = (index: number) => padding.left + index * (barWidth + barGap);
return (
<section
className="dashboard-card dashboard-chart"
data-dashboard-view={kind === 'line' ? 'line_chart' : 'bar_chart'}
>
<h3>{title}</h3>
{!points ? (
<Waiting />
) : points.length === 0 ? (
<p className="dashboard-empty">No chart data.</p>
) : (
<>
<svg viewBox="0 0 400 220" role="img" aria-label={title}>
{kind === 'line' ? (
<>
{[maximum, (minimum + maximum) / 2, minimum].map(
(value, index) => {
const tickY = padding.top + (index * plotHeight) / 2;
return (
<g key={index}>
<line
x1={padding.left}
y1={tickY}
x2={400 - padding.right}
y2={tickY}
className="chart-axis"
/>
<text
x={padding.left - 8}
y={tickY + 4}
textAnchor="end"
>
{numberLabel(value)}
</text>
</g>
);
}
)}
<path
d={`${points
.map(
(point, index) =>
`${index === 0 ? 'M' : 'L'} ${x(index)} ${y(
point.value
)}`
)
.join(' ')} L ${x(points.length - 1)} ${baseline} L ${x(
0
)} ${baseline} Z`}
fill={`url(#${gradientId})`}
/>
<polyline
points={points
.map((point, index) => `${x(index)},${y(point.value)}`)
.join(' ')}
className="chart-line"
/>
{labelIndices.map((index) => (
<text key={index} x={x(index)} y={214} textAnchor="middle">
{points[index].label}
</text>
))}
</>
) : (
<line
x1={padding.left}
y1={baseline}
x2={400 - padding.right}
y2={baseline}
className="chart-axis"
/>
)}
{points.map((point, index) =>
kind === 'line' ? (
<circle
key={index}
cx={x(index)}
cy={y(point.value)}
r="3"
className="chart-point"
>
<title>
{point.label}: {point.value}%
</title>
</circle>
) : (
<g key={index}>
<rect
x={barX(index)}
y={y(point.value)}
width={barWidth}
height={baseline - y(point.value)}
rx="3"
fill={`url(#${gradientId})`}
className="chart-bar"
>
<title>
{point.label}: {point.value}
</title>
</rect>
{barWidth >= 36 && (
<text
x={barX(index) + barWidth / 2}
y={y(point.value) - 6}
textAnchor="middle"
>
{point.value.toLocaleString()}
</text>
)}
{labelIndices.includes(index) && (
<text
x={barX(index) + barWidth / 2}
y={baseline + 16}
textAnchor="middle"
>
{point.label}
</text>
)}
</g>
)
)}
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="var(--dashboard-chart-color)"
stopOpacity={kind === 'line' ? 0.28 : 1}
/>
<stop
offset="100%"
stopColor="var(--dashboard-chart-color)"
stopOpacity={kind === 'line' ? 0 : 0.75}
/>
</linearGradient>
</defs>
</svg>
<details className="dashboard-chart-data">
<summary>View chart data</summary>
<table aria-label={`${title} data`}>
<thead>
<tr>
<th scope="col">{kind === 'line' ? 'Month' : 'Airline'}</th>
<th scope="col">
{kind === 'line' ? 'On time (%)' : 'Flights'}
</th>
</tr>
</thead>
<tbody>
{points.map((point, index) => (
<tr key={index}>
<th scope="row">{point.label}</th>
<td>{point.value}</td>
</tr>
))}
</tbody>
</table>
</details>
</>
)}
</section>
);
}
function LineChart(props: RenderViewProps) {
return <Chart {...props} kind="line" />;
}
function BarChart(props: RenderViewProps) {
return <Chart {...props} kind="bar" />;
}
const columnLabels: Readonly<Record<string, string>> = {
flight_number: 'Flight',
type: 'Disruption',
minutes: 'Minutes',
route: 'Route',
date: 'Date',
};
function DataGrid({ props }: RenderViewProps) {
const title = text(props.title) || 'Recent disruptions';
const rows = Array.isArray(props.rows) ? props.rows : undefined;
const columns = (props.columns ?? []) as readonly string[];
return (
<section className="dashboard-card" data-dashboard-view="data_grid">
<h3>{title}</h3>
{!rows ? (
<Waiting />
) : rows.length === 0 ? (
<p className="dashboard-empty">No disruptions.</p>
) : (
<div
className="dashboard-table-scroll"
tabIndex={0}
role="region"
aria-label={`${title} table`}
>
<table aria-label={title}>
<thead>
<tr>
{columns.map((column) => (
<th key={column} scope="col">
{columnLabels[column]}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={index}>
{columns.map((column) => (
<td key={column}>
{text(
(row as Readonly<Record<string, RenderValue>>)[column]
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
);
}
/** Only the six validated, read-only authored dashboard views are registered. */
export const dashboardRegistry: ReactRenderRegistry = Object.freeze({
dashboard_grid: DashboardGrid,
container: Container,
stat_card: StatCard,
line_chart: LineChart,
bar_chart: BarChart,
data_grid: DataGrid,
});import type {
RenderElementData,
RenderSpecData,
} from '@threadplane/react/render';
import type { PlainValue } from '@threadplane/core';
import {
arrayData,
copyData,
dashboardState,
dashboardText,
plainRecord,
} from './dashboard-data';
const metricKeys = ['on_time', 'flights_today', 'avg_delay', 'load_factor'];
const disruptionKeys = ['flight_number', 'type', 'minutes', 'route', 'date'];
const safeKey = (value: unknown): value is string =>
dashboardText(value) &&
value.length <= 80 &&
!['__proto__', 'prototype', 'constructor'].includes(value);
const finite = (value: unknown) =>
typeof value === 'number' && Number.isFinite(value);
function binding(value: unknown, paths: readonly string[]) {
return (
plainRecord(value) &&
Object.keys(value).length === 1 &&
typeof value.$state === 'string' &&
paths.includes(value.$state)
);
}
function scalar(value: unknown, suffix: 'value' | 'delta') {
return (
value === null ||
dashboardText(value) ||
(suffix === 'value' && finite(value)) ||
binding(
value,
metricKeys.map((key) => `/${key}/${suffix}`)
)
);
}
function data(
value: unknown,
key: 'on_time_trend' | 'flights_by_airline' | 'recent_disruptions'
) {
return (
value === null ||
binding(value, [`/${key}`]) ||
(Array.isArray(value) && dashboardState({ [key]: value }) !== undefined)
);
}
function columns(value: unknown) {
const items = arrayData(value);
return (
!!items &&
items.length > 0 &&
items.length <= disruptionKeys.length &&
items.every(
(key) => typeof key === 'string' && disruptionKeys.includes(key)
) &&
new Set(items).size === items.length
);
}
type PropRule = (value: unknown) => boolean;
interface ViewSchema {
readonly required: readonly string[];
readonly props: Readonly<Record<string, PropRule>>;
readonly children: boolean;
}
const catalog: Readonly<Record<string, ViewSchema>> = Object.freeze({
dashboard_grid: { required: [], props: {}, children: true },
container: {
required: [],
props: { direction: (value) => value === 'row' || value === 'column' },
children: true,
},
stat_card: {
required: ['label'],
props: {
label: dashboardText,
value: (value) => scalar(value, 'value'),
delta: (value) => scalar(value, 'delta'),
},
children: false,
},
line_chart: {
required: ['data', 'xKey', 'yKey'],
props: {
title: dashboardText,
data: (value) => data(value, 'on_time_trend'),
xKey: (value) => value === 'month',
yKey: (value) => value === 'on_time_pct',
},
children: false,
},
bar_chart: {
required: ['data', 'labelKey', 'valueKey'],
props: {
title: dashboardText,
data: (value) => data(value, 'flights_by_airline'),
labelKey: (value) => value === 'airline',
valueKey: (value) => value === 'count',
},
children: false,
},
data_grid: {
required: ['rows', 'columns'],
props: {
title: dashboardText,
rows: (value) => data(value, 'recent_disruptions'),
columns,
},
children: false,
},
});
function own(value: unknown): PlainValue {
if (Array.isArray(value)) {
const items = arrayData(value);
if (!items) throw new Error('Unsupported dashboard array');
return Object.freeze(items.map(own));
}
if (plainRecord(value))
return Object.freeze(
Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, own(item)])
)
);
return value as PlainValue;
}
function children(value: unknown): readonly string[] | undefined {
const items = arrayData(value);
if (!items || !items.every(safeKey) || new Set(items).size !== items.length)
return;
return Object.freeze([...items]);
}
/** Validate and own the existing authored catalog before presenting it. */
export function dashboardSpec(value: unknown): RenderSpecData | undefined {
try {
value = copyData(value);
if (
!plainRecord(value) ||
Object.keys(value).length !== 2 ||
!safeKey(value.root) ||
!plainRecord(value.elements) ||
!Object.hasOwn(value.elements, value.root)
)
return;
const entries = Object.entries(value.elements);
if (entries.length === 0 || entries.length > 100) return;
const elements: Record<string, RenderElementData> = {};
for (const [key, element] of entries) {
if (
!safeKey(key) ||
!plainRecord(element) ||
Object.keys(element).some(
(name) => !['type', 'props', 'children'].includes(name)
) ||
typeof element.type !== 'string' ||
!Object.hasOwn(catalog, element.type)
)
return;
const schema = catalog[element.type],
props = element.props === undefined ? {} : element.props;
if (
!plainRecord(props) ||
!schema.required.every((name) => Object.hasOwn(props, name)) ||
Object.keys(props).some(
(name) =>
!Object.hasOwn(schema.props, name) ||
!schema.props[name](props[name])
)
)
return;
const childKeys =
element.children === undefined ? undefined : children(element.children);
if (
element.children !== undefined &&
(!childKeys || (!schema.children && childKeys.length))
)
return;
elements[key] = Object.freeze({
type: element.type,
props: own(props) as Readonly<Record<string, PlainValue>>,
...(childKeys ? { children: childKeys } : {}),
});
}
const visited = new Set<string>(),
visiting = new Set<string>();
function visit(key: string): boolean {
if (!Object.hasOwn(elements, key) || visiting.has(key)) return false;
if (visited.has(key)) return false;
visiting.add(key);
for (const child of elements[key].children ?? [])
if (!visit(child)) return false;
visiting.delete(key);
visited.add(key);
return true;
}
if (!visit(value.root) || visited.size !== entries.length) return;
return Object.freeze({
root: value.root,
elements: Object.freeze(elements),
});
} catch {
return undefined;
}
}export type MetricData = {
readonly value: string | number;
readonly delta: string | null;
};
export interface DashboardState {
readonly on_time?: MetricData | null;
readonly flights_today?: MetricData | null;
readonly avg_delay?: MetricData | null;
readonly load_factor?: MetricData | null;
readonly on_time_trend?:
| readonly Readonly<{ month: string; on_time_pct: number }>[]
| null;
readonly flights_by_airline?:
| readonly Readonly<{ airline: string; count: number }>[]
| null;
readonly recent_disruptions?:
| readonly Readonly<{
flight_number: string;
type: string;
minutes: number;
route: string;
date: string;
}>[]
| null;
}
export function plainRecord(value: unknown): value is Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return (
(prototype === Object.prototype || prototype === null) &&
Object.getOwnPropertySymbols(value).length === 0 &&
Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) =>
Object.hasOwn(descriptor, 'value')
)
);
}
export function dashboardText(value: unknown): value is string {
return typeof value === 'string' && value.length > 0 && value.length <= 512;
}
/** Capture dense ordinary arrays without invoking caller-owned iteration. */
export function arrayData(value: unknown): readonly unknown[] | undefined {
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype)
return;
const descriptors = Object.getOwnPropertyDescriptors(
value
) as unknown as Record<string, PropertyDescriptor>;
const length = descriptors.length.value;
if (
!Number.isInteger(length) ||
length < 0 ||
length > 100 ||
Reflect.ownKeys(descriptors).length !== length + 1
)
return;
const items: unknown[] = [];
for (let index = 0; index < length; index++) {
const descriptor = descriptors[String(index)];
if (!descriptor || !Object.hasOwn(descriptor, 'value')) return;
items.push(descriptor.value);
}
return Object.freeze(items);
}
function finite(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value);
}
function exact(value: Record<string, unknown>, keys: readonly string[]) {
return (
Object.keys(value).length === keys.length &&
keys.every((key) => Object.hasOwn(value, key))
);
}
function metric(value: unknown): MetricData | undefined {
if (
!plainRecord(value) ||
!exact(value, ['value', 'delta']) ||
(!dashboardText(value.value) && !finite(value.value)) ||
(value.delta !== null && !dashboardText(value.delta))
)
return;
return Object.freeze({ value: value.value, delta: value.delta });
}
type FieldSchema = Readonly<Record<string, (value: unknown) => boolean>>;
function rows(
value: unknown,
schema: FieldSchema
): readonly Readonly<Record<string, unknown>>[] | undefined {
const items = arrayData(value);
if (!items) return;
const keys = Object.keys(schema),
result: Readonly<Record<string, unknown>>[] = [];
for (const item of items) {
if (
!plainRecord(item) ||
!exact(item, keys) ||
!keys.every((key) => schema[key](item[key]))
)
return;
result.push(
Object.freeze(Object.fromEntries(keys.map((key) => [key, item[key]])))
);
}
return Object.freeze(result);
}
const nonnegative = (value: unknown) => finite(value) && value >= 0;
const percentage = (value: unknown) =>
finite(value) && value >= 0 && value <= 100;
const fields = Object.freeze({
on_time: metric,
flights_today: metric,
avg_delay: metric,
load_factor: metric,
on_time_trend: (value: unknown) =>
rows(value, { month: dashboardText, on_time_pct: percentage }),
flights_by_airline: (value: unknown) =>
rows(value, { airline: dashboardText, count: nonnegative }),
recent_disruptions: (value: unknown) =>
rows(value, {
flight_number: dashboardText,
type: dashboardText,
minutes: nonnegative,
route: dashboardText,
date: dashboardText,
}),
});
/** Captures exactly the seven owned dashboard slots. */
export function dashboardState(state: unknown): DashboardState | undefined {
try {
state = copyData(state);
if (
!plainRecord(state) ||
Object.keys(state).some((key) => !Object.hasOwn(fields, key))
)
return;
const selected: Record<string, unknown> = {};
for (const [key, capture] of Object.entries(fields)) {
if (!Object.hasOwn(state, key)) continue;
const value = state[key] === null ? null : capture(state[key]);
if (value === undefined) return;
selected[key] = value;
}
return Object.freeze(selected) as DashboardState;
} catch {
return undefined;
}
}
/** Bounded descriptor-only capture: never retain mutable input or invoke getters. */
export function copyData(value: unknown): unknown {
const parents = new Set<object>();
let nodes = 0,
characters = 0;
function capture(item: unknown, depth: number): unknown {
if (++nodes > 20000 || depth > 32) throw new Error('Data exceeds budget');
if (typeof item === 'string') {
characters += item.length;
if (item.length > 100000 || characters > 1000000)
throw new Error('Text exceeds budget');
return item;
}
if (item === null || item === undefined || typeof item === 'boolean')
return item;
if (typeof item === 'number' && Number.isFinite(item)) return item;
if (
typeof item !== 'object' ||
parents.has(item) ||
Object.getOwnPropertySymbols(item).length
)
throw new Error('Unsupported data');
const array = Array.isArray(item),
prototype = Object.getPrototypeOf(item);
if (
array
? prototype !== Array.prototype
: prototype !== Object.prototype && prototype !== null
)
throw new Error('Unsupported prototype');
const descriptors = Object.getOwnPropertyDescriptors(item);
const length = array ? descriptors['length'].value : 0;
if (
array &&
(!Number.isInteger(length) ||
length > 10000 ||
Object.keys(descriptors).length !== length + 1)
)
throw new Error('Unsupported array');
const keys = array
? Array.from({ length }, (_, i) => String(i))
: Object.keys(descriptors).sort();
if (keys.length > 10000) throw new Error('Object exceeds budget');
const result = array ? [] : Object.create(null);
parents.add(item);
for (const key of keys) {
characters += key.length;
if (key.length > 100000 || characters > 1000000)
throw new Error('Keys exceed budget');
const descriptor = descriptors[key];
if (!descriptor || !Object.hasOwn(descriptor, 'value'))
throw new Error('Unsupported descriptor');
Object.defineProperty(result, key, {
value: capture(descriptor.value, depth + 1),
enumerable: true,
});
}
parents.delete(item);
return Object.freeze(result);
}
return capture(value, 0);
}
export const dataKey = (value: unknown): string =>
JSON.stringify(copyData(value));
export function parseData(text: unknown): unknown {
if (typeof text !== 'string' || text.length > 100000) return undefined;
try {
return copyData(JSON.parse(text));
} catch {
return undefined;
}
}Stop and recover
Stop or an uncertain execution requires New conversation before continuing. There is no automatic replay, retry or resume. The application withholds unconfirmed changes and prevents late results from restoring authority.
import type { AgentSnapshot, Message, ToolCall } from '@threadplane/core';
import { copyData, dataKey } from './dashboard-data';
import { renderResult, validToolArgs } from './tool-evidence';
export type GenerativeUiState = AgentSnapshot & {
readonly values?: Readonly<Record<string, unknown>>;
readonly history?: readonly {
readonly checkpoint: unknown;
readonly next: readonly string[];
}[];
readonly interrupts: readonly unknown[];
readonly subgraphs: readonly unknown[];
};
export function messageKey(message: Message): string {
return dataKey(
Object.fromEntries(
Object.entries(message).filter(([key]) => key !== 'delivery')
)
);
}
export interface WrapEvidence {
readonly callId: string;
readonly parentId: string;
readonly resultId: string;
readonly text: string;
}
/** A turn owns only append-only evidence plus one correlated render transfer. */
export function createObservation(
text: string,
messages: readonly Message[] = [],
tools: readonly ToolCall[] = [],
previousGenerations: readonly string[] = []
) {
const base = copyData(messages) as readonly Message[],
baseTools = copyData(tools) as readonly ToolCall[];
let previous = base,
previousTools = baseTools,
generation: string | undefined,
human: Message | undefined;
let live: GenerativeUiState | undefined;
const wraps = new Map<string, WrapEvidence>();
const fail = (): never => {
throw new Error('Unconfirmed turn evidence');
};
function observe(input: GenerativeUiState, saved = false): GenerativeUiState {
const state = copyData(input) as GenerativeUiState;
if (
!state ||
state.error ||
state.status === 'error' ||
!Array.isArray(state.messages) ||
!Array.isArray(state.toolCalls) ||
!Array.isArray(state.interrupts) ||
!Array.isArray(state.subgraphs) ||
state.interrupts.length ||
state.subgraphs.length
)
fail();
const ms = state.messages,
ts = state.toolCalls;
if (
new Set(ms.map((m) => m.id)).size !== ms.length ||
new Set(ts.map((t) => t.id)).size !== ts.length ||
ms.length < previous.length ||
ts.length < previousTools.length
)
fail();
if (
base.some((m, i) => !ms[i] || dataKey(m) !== dataKey(ms[i])) ||
baseTools.some((t, i) => dataKey(t) !== dataKey(ts[i]))
)
fail();
if (
previous.some((m, i) => m.id !== ms[i].id) ||
previousTools.some((t, i) => t.id !== ts[i].id)
)
fail();
const suffix = ms.slice(base.length);
if (suffix.length) {
const first = suffix[0];
if (
first.role !== 'user' ||
first.content !== text ||
first.delivery.phase !== 'complete' ||
suffix.slice(1).some((m) => m.role === 'user')
)
fail();
if (human && messageKey(first) !== messageKey(human)) fail();
human ??= first;
generation ??= first.delivery.generation;
if (
!generation ||
previousGenerations.includes(generation) ||
base.some((m) => m.delivery.generation === generation)
)
fail();
}
for (const m of ms) {
if (
!m.id ||
typeof m.content !== 'string' ||
!['user', 'assistant', 'tool'].includes(m.role) ||
!m.delivery ||
!['streaming', 'complete'].includes(m.delivery.phase) ||
(m.delivery.phase === 'complete' && m.delivery.outcome !== 'success') ||
(m.role !== 'assistant' && m.toolCallIds?.length) ||
(m.role !== 'tool' && m.toolCallId !== undefined) ||
(m.role === 'tool' && !m.toolCallId)
)
fail();
}
if (
suffix.some((m) => m.delivery.generation !== (saved ? m.id : generation))
)
fail();
for (const t of ts)
if (
!t.id ||
!validToolArgs(t) ||
!['pending', 'complete'].includes(t.status)
)
fail();
// Capture raw result and the empty owning assistant before either wrapped projection arrives.
for (const t of ts.slice(baseTools.length)) {
if (
t.name !== 'render_spec' ||
t.status !== 'complete' ||
wraps.has(t.id)
)
continue;
const parent = ms.find(
(m) => m.role === 'assistant' && m.toolCallIds?.includes(t.id)
);
const result = ms.find((m) => m.role === 'tool' && m.toolCallId === t.id);
const raw = renderResult(t);
if (
parent &&
result &&
!parent.content.trim() &&
raw &&
result.content === t.result &&
result.name === t.name
)
wraps.set(
t.id,
Object.freeze({
callId: t.id,
parentId: parent.id,
resultId: result.id,
text: raw,
})
);
}
for (const [i, old] of previous.entries()) {
const m = ms[i];
const wrap = [...wraps.values()].find(
(w) => w.parentId === m.id || w.resultId === m.id
);
// Raw tool completion grants one content transfer even when its parent
// still streams. That grant cannot survive intervening prose or regress.
if (old.delivery.phase !== 'complete' && wrap?.parentId !== m.id)
continue;
if (old.delivery.phase === 'complete' && m.delivery.phase !== 'complete')
fail();
if (messageKey(old) === messageKey(m)) continue;
if (!wrap || messageKey({ ...old, content: m.content }) !== messageKey(m))
return fail();
if (
m.id === wrap.parentId
? old.content.trim() !== '' || m.content !== wrap.text
: renderResult({
id: wrap.callId,
name: 'render_spec',
args: ts.find((t) => t.id === wrap.callId)?.args,
status: 'complete',
result: old.content,
}) !== wrap.text || m.content !== 'rendered'
)
fail();
}
for (const [i, old] of previousTools.entries()) {
const t = ts[i];
if (
old.name !== t.name ||
(old.status === 'complete' && dataKey(old.args) !== dataKey(t.args))
)
fail();
if (old.status !== 'complete' || dataKey(old) === dataKey(t)) continue;
const wrap = wraps.get(t.id);
if (
!wrap ||
renderResult(old) !== wrap.text ||
t.status !== 'complete' ||
t.result !== 'rendered' ||
dataKey({ ...old, result: 'rendered' }) !== dataKey(t)
)
fail();
}
for (const m of suffix) {
if (
m.role === 'tool' &&
m.content === 'rendered' &&
m.name === 'render_spec' &&
!wraps.has(m.toolCallId ?? '')
)
fail();
}
if (
saved &&
(!live ||
dataKey(live.values) !== dataKey(state.values) ||
live.messages.length !== ms.length ||
live.messages.some((m, i) => messageKey(m) !== messageKey(ms[i])))
)
fail();
previous = ms;
previousTools = ts;
if (!saved) live = state;
return state;
}
return {
observe,
get human() {
return human;
},
get wraps(): readonly WrapEvidence[] {
return Object.freeze([...wraps.values()]);
},
get prefixLength() {
return base.length;
},
};
}import type { ToolCall } from '@threadplane/core';
import {
copyData,
dashboardState,
dashboardText,
plainRecord,
parseData,
dataKey,
type DashboardState,
} from './dashboard-data';
function argumentsFor(
call: Pick<ToolCall, 'name' | 'args'>
): Record<string, unknown> | undefined {
const args = copyData(call.args),
name = call.name;
if (!plainRecord(args)) return;
if (name === 'render_spec') return args;
if (
![
'query_airline_kpis',
'query_on_time_trend',
'query_flights_by_airline',
'query_recent_disruptions',
].includes(name)
)
return;
const allowed =
name === 'query_airline_kpis'
? []
: name === 'query_on_time_trend'
? ['months']
: name === 'query_flights_by_airline'
? ['airlines']
: ['limit', 'type'];
if (Object.keys(args).some((key) => !allowed.includes(key))) return;
for (const key of ['months', 'limit'])
if (
Object.hasOwn(args, key) &&
(typeof args[key] !== 'number' ||
!Number.isInteger(args[key]) ||
args[key] < 1 ||
args[key] > 100)
)
return;
if (
Object.hasOwn(args, 'airlines') &&
args.airlines !== null &&
(!Array.isArray(args.airlines) ||
args.airlines.length > 100 ||
!args.airlines.every(dashboardText))
)
return;
if (
Object.hasOwn(args, 'type') &&
args.type !== null &&
!dashboardText(args.type)
)
return;
return args;
}
export function validToolArgs(tool: Pick<ToolCall, 'name' | 'args'>): boolean {
try {
return argumentsFor(tool) !== undefined;
} catch {
return false;
}
}
/** Match the graph's narrowly defined fenced result transfer. */
export function renderResult(tool: ToolCall): string | undefined {
if (
tool.name !== 'render_spec' ||
tool.status !== 'complete' ||
typeof tool.result !== 'string'
)
return;
let text = tool.result.trim();
if (text.startsWith('```'))
text = text
.split('\n')
.filter((line) => !line.startsWith('```'))
.join('\n')
.trim();
const parsed = parseData(text);
return parsed !== undefined && dataKey(parsed) === dataKey(tool.args)
? text
: undefined;
}
export function toolData(tool: ToolCall): DashboardState | undefined {
try {
return captureToolData(tool);
} catch {
return undefined;
}
}
function captureToolData(tool: ToolCall): DashboardState | undefined {
const args = argumentsFor(tool),
name = tool.name;
if (!args || name === 'render_spec' || tool.status !== 'complete') return;
const value = parseData(tool.result);
if (name === 'query_airline_kpis') {
const keys = ['on_time', 'flights_today', 'avg_delay', 'load_factor'];
return plainRecord(value) &&
Object.keys(value).length === keys.length &&
keys.every((key) => Object.hasOwn(value, key) && value[key] !== null) &&
dashboardState(value) !== undefined
? dashboardState(value)
: undefined;
}
const key =
name === 'query_on_time_trend'
? 'on_time_trend'
: name === 'query_flights_by_airline'
? 'flights_by_airline'
: 'recent_disruptions';
if (!Array.isArray(value) || dashboardState({ [key]: value }) === undefined)
return undefined;
if (
key === 'on_time_trend' &&
typeof args.months === 'number' &&
value.length > args.months
)
return undefined;
const airlines = args.airlines;
if (
key === 'flights_by_airline' &&
Array.isArray(airlines) &&
airlines.length > 0 &&
value.some((row) => !airlines.includes(row.airline))
)
return undefined;
if (
key === 'recent_disruptions' &&
((typeof args.limit === 'number' && value.length > args.limit) ||
(typeof args.type === 'string' &&
value.some((row) => row.type !== args.type)))
)
return undefined;
return dashboardState({ [key]: value });
}Runtime settings select one configured target and header owner for thread creation, streams and checkpoint reads, with SDK retries disabled. Docs, Code and Run select the same native implementation. Mode navigation preserves the mounted frame and unsent draft; reloading clears local state.
import { Client } from '@langchain/langgraph-sdk';
import { createSession } from '@threadplane/langgraph';
import {
createGenerativeUiApplication,
type GenerativeUiClient,
type GenerativeUiSession,
} from './application';
import { copyData } from './dashboard-data';
import { checkpointSource, type Checkpoint } from './terminal';
// eslint-disable-next-line @nx/enforce-module-boundaries -- Internal bridge is copied by the isolated build.
import type { RuntimeBridgeConfiguration } from '../../../../../libs/cockpit-runtime-bridge/src/index';
export interface GenerativeUiConnection {
readonly apiUrl: string;
readonly headers: Record<string, string>;
}
/** Resolve the shared proxy or the explicitly authored developer target. */
export function generativeUiConnection(
configuration: RuntimeBridgeConfiguration
): {
apiUrl: string;
headers: Record<string, string>;
} {
if (
configuration.status === 'error' ||
(configuration.status === 'configured' &&
configuration.target.kind === 'ag-ui')
)
throw new Error('Runtime configuration failed.');
const target =
configuration.status === 'configured' &&
configuration.target.kind === 'langsmith'
? configuration.target
: null;
return {
apiUrl: target?.apiUrl ?? new URL('/api', window.location.origin).href,
headers: target ? { 'x-api-key': target.apiKey } : {},
};
}
/** Connect only this page's confirmed IDs, with retries disabled for owned requests. */
export function createGenerativeUiClient(
connection: GenerativeUiConnection
): GenerativeUiClient {
connection = Object.freeze({
apiUrl: connection.apiUrl,
headers: Object.freeze({ ...connection.headers }),
});
const known = new Set<string>();
const client = new Client({
apiUrl: connection.apiUrl,
apiKey: null,
defaultHeaders: connection.headers,
callerOptions: { maxRetries: 0 },
});
return {
async createThread(signal) {
try {
if (signal.aborted) throw new Error('Aborted');
const threadId = crypto.randomUUID();
const created = await client.threads.create({
threadId,
ifExists: 'raise',
signal,
});
if (signal.aborted || created.thread_id !== threadId)
throw new Error('Unconfirmed');
known.add(threadId);
return threadId;
} catch {
throw new Error('Conversation creation was not confirmed.');
}
},
sessionFactory(threadId) {
if (!known.has(threadId)) throw new Error('Unknown conversation.');
let session: ReturnType<typeof createSession> | undefined;
try {
session = createSession({
assistantId: 'c-generative-ui',
threadId,
apiUrl: connection.apiUrl,
clientOptions: { maxRetries: 0, defaultHeaders: connection.headers },
});
const selected = session;
const state = selected.getSnapshot();
if (
!state ||
!Array.isArray(state.messages) ||
!Array.isArray(state.toolCalls) ||
!Array.isArray(state.interrupts) ||
!Array.isArray(state.subgraphs) ||
![
'getSnapshot',
'subscribe',
'submit',
'load',
'stop',
'dispose',
].every(
(method) =>
typeof selected[method as keyof typeof selected] === 'function'
)
)
throw new Error('Rejected session');
return session as GenerativeUiSession;
} catch {
try {
void Promise.resolve(session?.dispose()).catch(() => undefined);
} catch {
/* Rejected sessions cannot be installed. */
}
throw new Error('Runtime session is unavailable.');
}
},
async readCheckpoint(requested, signal) {
try {
const captured = copyData(requested) as Checkpoint;
const source = checkpointSource(captured, captured.thread_id);
if (!source || !known.has(source.thread_id) || signal.aborted)
throw new Error('Unavailable');
// SDK declares checkpoint_map required-but-possibly-undefined. Preserve its absence on the wire.
const state = await client.threads.getState(
source.thread_id,
source as Parameters<typeof client.threads.getState>[1],
{ signal }
);
if (signal.aborted) throw new Error('Aborted');
return state;
} catch {
throw new Error('Checkpoint preview is unavailable.');
}
},
};
}
export function createConnectedApplication(connection: GenerativeUiConnection) {
return createGenerativeUiApplication(createGenerativeUiClient(connection));
}Build from source
From a Threadplane checkout with dependencies installed:
npx nx build cockpit-chat-generative-ui-react
npx nx e2e cockpit-chat-generative-ui-reactBrowser verification uses the real SDK and compiled Python graph with deterministic providers and real saved checkpoints. Verification endpoints are excluded from public output.