PR: #454: feat(agents): consume RenderDataFrame — componentResult kind, Zod guard, ref-persist [Story-1794] by @ankitmaheshwari-com
View PR →
Story-1794

App render-substrate: consume the broker's RenderDataFrame

Story-1791 shipped the component catalog (what a surface renders, bound to a real wire DTO) as schema + codegen only. This change is the app consumer: the dock-chat rail now takes the broker's RenderDataFrame ({component_id, query_ref, data, render_marker, state}, merged in functions #1793), validates the rows at the trust boundary against the bound component's prop-schema, and renders them — or, when the component is unknown or the rows are malformed, a text fallback that is never blank (RFD-AGT-004 §6 D11/D12, §7.6). No new .tsx catalog components yet — the substrate renders generically; the honest fallback / denied / empty paths are the proof.

What changed

One mapper at the rail boundary

renderFromWire joins the existing navigateFromWire / setViewFromWire / proposeOperationFromWire family. A new render.data case in reduceFrame sets RailState.render; it is reset on run.accepted like every other terminal intent.

Zod guard, then fallback — never crash, never blank

validateRenderRows looks the component_id up in the #1791 catalog and parses data with z.array(propSchema). Unknown id (D11) or a parse failure → a truncated JSON summary on fallbackText, rendered as text. Reuses the SAME GENERATED_PROP_SCHEMAS map check:catalog already validates cards against — one authority.

Persist a REFERENCE, not rows (HR-23)

The new componentResult StoredThreadMessage variant carries only {component_id, query_ref, capturedAt, renderCatalogVersion}. No row data reaches sessionStorage; replay RE-RUNS the bound intent (re-authorized server-side), so stale rows are never resurrected.

Explicit fallback branch, never default

ThreadItemView gets one explicit componentResult case → ComponentResultView, whose first branch is the explicit fallback (text summary). Denied → "no longer available"; empty → empty-state. The switch default stays only as the forward-compat unknown-KIND guard — it never catches an unknown component_id.

renderCatalogVersion on the reference

RENDER_CATALOG_VERSION is stamped on every persisted componentResult reference (AC4). There is NO client→brain version carrier: the generated DockChatContext is a STRICT envelope, and the app already falls back on an unknown component_id (D11), so the carrier is not needed for v1.

Generic render only (003 is later)

matter_table / matter_card bespoke .tsx are out of scope. Valid rows render as a token-styled grid keyed by DTO field names — enough to prove the substrate end to end without inventing a catalog component.

Where it lives

Verify

pnpm lint        # clean (1 pre-existing avatar.tsx warning, unrelated)
pnpm typecheck   # clean
pnpm test        # 882 passed
pnpm build       # green
git diff --check # clean
# NOTE: do NOT run pnpm codegen — import contracts from src/gen.

Reversible calls (stated for review): the render frame envelope type is assumed render.data with the RenderDataFrame fields flat on payload (consistent with run.failed); the exact string reconciles with the functions emission ticket and is forward-compatible (unknown frame types are ignored today). No client→brain version carrier is sent — the generated DockChatContext is a STRICT envelope, and D11 unknown-id fallback covers v1.

Deferred (re-scope, AGENT1 blocker-2): the reference-only persist is kept (security invariant satisfied), but replay RE-RUN on rehydrate — re-materializing rows after a refresh — is DEFERRED to a follow-on (coupled to live-rail re-invoke). A rehydrated reference stays an empty placeholder until then (marked with a ponytail: note in the dock).

Non-goal (RFD-AGT-004): no new .tsx catalog components (003-components), no functions/agents changes (separate emission tickets).

Files changed

15 file(s), +932 / -6 lines

Path Role + -
messages/en.jsonConfig+4-1
messages/hi.jsonConfig+4-1
src/components/agents/agent-chat-panel.render.test.tsxComponent+118-0
src/components/agents/agent-chat-panel.tsxComponent+45-1
src/components/agents/agent-dock-mount.render.test.tsxComponent+170-0
src/components/agents/agent-dock-mount.tsxComponent+50-2
src/components/agents/agent-thread-item.tsComponent+27-1
src/lib/agents/__tests__/agent-thread-store.test.tsLibrary+27-0
src/lib/agents/agent-thread-store.tsLibrary+18-0
src/lib/agents/atlas-rail.test.tsLibrary+121-0
src/lib/agents/atlas-rail.tsLibrary+104-0
src/lib/agents/dock-render-bridge.test.tsLibrary+111-0
src/lib/agents/dock-render-bridge.tsLibrary+79-0
src/lib/agents/use-dock-chat-rail.tsLibrary+1-0
src/lib/surface-registry/page-card/render-catalog.tsLibrary+53-0