feat(a2ui): stream protocol messages in playground#2709
Conversation
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 Walkthrough<review_stack_artifact_start --> New toast notification system and refactored JSON payload display; integrates toast into preview and chat UIs and updates CSS.Adds `useCopyToast` hook and `CopyToast` component, styles, and wires preview/chat copy handlers to show success/error toasts with auto-dismiss.range_893bfd0d5780 range_3d656e361e99 range_ddfccdce82fd range_9dff286ec31f range_70bec9a51dc9 range_93de7d2dbcda range_a9851c617726 range_7e8ff1f93cb2 range_7efd2b46caaa range_8ee2bcb42a05 range_e5fc8ea08ebd ```mermaid sequenceDiagram participant PreviewPanel participant ChatPage participant useCopyToast participant CopyToast PreviewPanel->>useCopyToast: showCopyToast(ok) ChatPage->>useCopyToast: showCopyToast(ok) useCopyToast->>CopyToast: toast state CopyToast->>CopyToast: render aria-live and auto-dismiss ```Replaces CodeMirror rendering with `payloadToChunks`/`JsonPayloadViewer`, adds per-chunk copy controls, removes `payloadLabel`, and updates chat CSS/layout for chunked payloads.range_da6505e667d3 range_bbca3510ac65 range_df038220788d range_bac4dfbbc2a4 range_21177168b926 range_24f1c1ae3cfd range_8c3d3acaa5f4 range_8c4a43e7ed6d range_0aa03ca1dd27 range_24f1c1ae3cfdBuffers and flushes live/action messages to the Lynx renderer and coordinates live-message handling with the app-level message store and mock agent lifecycle.Adds refs for pending live/action batches, flush/schedule helpers with bounded retries, updates window.message handler to enqueue events, calls postRenderReady, and adds cleanup on unmount.range_dc6f3fbcb9d5 range_2a9b7cb68b65 range_9753fd222314 range_a71419e0e19d range_2339a200df97 range_2a078ad21b2a range_8334bb38c102Adds buffering for live messages in App, `pushLiveMessagesToStore` helper to normalize and push messages into an active MessageStore, registers `A2UI_LIVE_MESSAGES` listener to buffer or deliver, drains buffer during stream init, and coordinates mock-agent lifecycle.range_250f66460f0e range_04357d8a9823 range_73e4466d0541 range_6c7e602db545 range_c2071800e685 range_0aa03ca1dd27Uses `A2UI_LIVE_MESSAGES` for live updates when reusing render URL; initializes fresh renders with `messages: []` for live-action wiring and conditionally replays persisted preview messages.range_4795b94b68cc range_e7c3a5d79c4e range_ab45a5a1588f range_9b9ef9239899 range_996d00000000Updates SSE handling in chat, reconstructs persisted user actions, changes action streaming to a single streaming card, integrates copy handlers, and adjusts follow-to-bottom behavior.Extends `readA2UIResponse` to process `event: message` frames by normalizing payloads into message arrays and publishing them immediately when partial publishing is enabled.range_0c9b8140d409 range_5cc1543c6f91 range_0c9b8140d409Parses persisted `A2UI_USER_ACTION:` entries, emits forwarding status and action cards, and converts subsequent assistant output into agent-response and applied/ready status cards.range_1e987f035c69 range_bac4dfbbc2a4 range_1e987f035c69Refactors action streaming to insert/update one "Streaming RESPONSE…" card after the first non-empty delta, update it with normalized response messages, post `A2UI_ACTION_RESPONSE` to iframe using computed frame origin, record assistantContent, and finalize pending card with applied/UI-updated statuses.range_ae44545e5748 range_ceb30d800255 range_b5952389701c range_79dd954c001c range_126100000000Re-pins follow-to-bottom during streaming/layout/preview updates, adds MutationObserver to observe newly inserted chat rows, and keeps ResizeObserver setup for async rendering.range_a11961e115df range_42df924235f5 range_8c3d3acaa5f4 range_8c4a43e7ed6d range_8c3d3acaa5f4Wires `useCopyToast`, provides `handleCopyText` and `renderUrlRef`, injects `` into the page, and rewrites chat rows to use `JsonPayloadViewer` with copy-all buttons; generated output uses `finalMessages` as payload.range_035ef6df1c4e range_b192b687e3ac range_d57e3f15b40c range_3338723ff764 range_46f622c6cf07 range_21177168b926 range_24f1c1ae3cfd range_3338723ff764 range_4795b94b68ccAdds an incremental A2UI protocol message stream parser and a splitter for normalizing `updateComponents` into reachable snapshots.Defines protocol types and runtime validators, heuristics for image/text components, surface-id sniffing, referenced-id extraction and rewriting to placeholders, reachability snapshot builder, and the stateful `A2UIProtocolMessageStreamParser` plus `splitA2UIProtocolMessages` batch helper.range_e8585ae21e1f range_c5d81e13ba91 range_168141da8038 range_c0b184ec82c6 range_0b8b8ddaeca1 range_5f20ebc3b8b9 range_5e52b4b1f3d1 range_5f20ebc3b8b9 range_5e52b4b1f3d1Integrates the parser into SSE routes, post-processes validation/repair outputs through the splitter, and adds structured per-request logging and improved error handling.Action SSE route now constructs `A2UIProtocolMessageStreamParser`, parses chunks to emit `delta` and `message` events, resolves image URLs then splits messages, and logs lifecycle and repair/validation outcomes.range_e5e65a4fc122 range_6c27a782f95a range_9e918cbf3e61 range_53034580929d range_e9ee64b04ffb range_c3115feb4902 range_ceb811d9b5d6 range_fad0ba03c594Main SSE route constructs a protocol parser, accumulates parsed messages across chunks, emits `delta` and `message` correctly, applies image-resolution + splitter for validation/repair, and emits structured logs and final enqueue events.range_00749ad40bfe range_66954de63544 range_bf5949c33af4 range_ee2b6797e3d5 range_6aa6d118ee7f range_387f912b81ee range_662a86f7640e range_397a34943ce6 range_ee2b6797e3d5Smaller supporting updates: image source normalization, prompt/validation improvements, and client model gating.Removes hook-driven error fallback, adds `imageSourceFromServer` to trim/validate server image URLs, centralizes variant/className computation, and sets empty src when invalid.range_00907c069386 range_0c3ddbef8020 range_57b1694acd37Clarifies `updateDataModel` `"path"` placement in the A2UI prompt hard rules and gates client-provided `model` via `A2UI_ALLOW_CLIENT_OVERRIDE`.range_749fa8e43b69 range_77d9e576dc3d range_269b4d137c02 range_f8e7391df015 range_8cb3826658e7 range_d7e3972f0a83Adds exported validation debug interfaces and `getA2UIValidationDebugData(raw, errors)` to parse raw output, compute parsedType, and map validation errors to error/path/value entries with path resolution helpers.range_7efd2b46caaa range_215a932426f9 range_43978e7f171b<unassigned_ranges> 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
8d55e1f to
e616902
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/genui/a2ui-playground/src/pages/AIChatPage.css`:
- Line 811: Replace the deprecated CSS declaration "word-break: break-word;" in
AIChatPage.css with the modern overflow-wrap property to preserve wrapping
behavior; specifically locate the rule containing "word-break: break-word;" and
change it to "overflow-wrap: anywhere;" (or "overflow-wrap: break-word;" if you
prefer the older alias) so stylelint no longer flags the rule.
In `@packages/genui/a2ui-playground/src/pages/AIChatPage.tsx`:
- Around line 611-612: The persisted-action label builder only reads record.name
(producing 'unknown' for shapes like { action: { event: { name } } }); update
the logic that constructs the label (the code handling record / action) to check
for the nested name variants in order—e.g. record.name, record.action?.name,
then record.action?.event?.name—and use the first defined string value so
history cards correctly show the event name instead of 'unknown'.
In `@packages/genui/a2ui-playground/src/styles.css`:
- Line 80: Rename the keyframe identifier to kebab-case to satisfy stylelint:
change the `@keyframes` name currently referenced as copyToastIn to a kebab-case
equivalent (e.g., copy-toast-in) and update all uses of the animation property
(the line with animation: copyToastIn 160ms ease-out; and any other occurrences)
to the new kebab-case name so the `@keyframes` declaration and animation
references match.
In `@packages/genui/a2ui/src/catalog/Image/index.tsx`:
- Around line 31-38: isLoadableImageSource currently only accepts strings while
ImageProps.url can be string | { path: string }, so object-form URLs are treated
as non-loadable; update isLoadableImageSource to normalize the input by
extracting a string path when value is an object with a string 'path' property
(or otherwise fall back to existing behavior), i.e., handle both string and {
path: string } shapes inside isLoadableImageSource so callers (and the usages
referenced around the checks near the other occurrences) don't render an empty
placeholder when given object-form urls.
In `@packages/genui/server/agent/a2ui-stream-parser.ts`:
- Around line 138-217: The current logic in buildReachableComponentSnapshot +
replaceMissingChildRefs rewrites unseen child refs into newly created
placeholder IDs (loading_...), which drops or corrupts legitimate
disconnected/sibling updates; instead, when encountering a child ref id that is
not in seen (inside replaceMissingChildRefs and any place using
collectChildRefs), preserve the original string id (do not create a placeholder)
or seed seen with the existing surface state before reachability pruning so
reachable computation doesn't prune valid but currently-unseen components;
update replaceMissingChildRefs (and the visit/collect flow in
buildReachableComponentSnapshot) to only create placeholders for truly missing
refs when you have no external surface state to seed, and ensure refs are left
as raw IDs when they might refer to existing client-side components (so links to
loading_* are not introduced).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 11b241e2-009e-4577-9f6b-176913648350
📒 Files selected for processing (12)
packages/genui/a2ui-playground/lynx-src/a2ui/App.tsxpackages/genui/a2ui-playground/src/components/CopyToast.tsxpackages/genui/a2ui-playground/src/components/PreviewPanel.tsxpackages/genui/a2ui-playground/src/pages/AIChatPage.csspackages/genui/a2ui-playground/src/pages/AIChatPage.tsxpackages/genui/a2ui-playground/src/render.tsxpackages/genui/a2ui-playground/src/styles.csspackages/genui/a2ui/src/catalog/Image/index.tsxpackages/genui/server/agent/a2ui-stream-parser.tspackages/genui/server/app/a2ui/_shared.tspackages/genui/server/app/a2ui/action/stream/route.tspackages/genui/server/app/a2ui/stream/route.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx`:
- Around line 445-458: The live-message listener (useLynxGlobalEventListener ->
normalizeProtocolMessages -> createMessageStore -> agentRef/storeRef/setStore)
can apply updates that get clobbered by a later async initialization; introduce
a generation token (e.g., initVersionRef or initTokenRef) that you
increment/replace whenever the async bootstrap that initializes
agentRef/storeRef starts, capture that token in the listener closure and only
apply the normalized messages and mutate agentRef/storeRef/setStore if the
captured token still matches the current init token; update the async bootstrap
to set/renew the token at start and ignore its own completion if the token has
changed to prevent stale overwrites.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7c25522f-8f4a-4942-82dd-bd17a7739112
📒 Files selected for processing (12)
packages/genui/a2ui-playground/lynx-src/a2ui/App.tsxpackages/genui/a2ui-playground/src/components/CopyToast.tsxpackages/genui/a2ui-playground/src/components/PreviewPanel.tsxpackages/genui/a2ui-playground/src/pages/AIChatPage.csspackages/genui/a2ui-playground/src/pages/AIChatPage.tsxpackages/genui/a2ui-playground/src/render.tsxpackages/genui/a2ui-playground/src/styles.csspackages/genui/a2ui/src/catalog/Image/index.tsxpackages/genui/server/agent/a2ui-stream-parser.tspackages/genui/server/app/a2ui/_shared.tspackages/genui/server/app/a2ui/action/stream/route.tspackages/genui/server/app/a2ui/stream/route.ts
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will degrade performance by 16.35%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
Web Explorer#10286 Bundle Size — 903.53KiB (0%).dc6f009(current) vs 5fc1ca1 main#10280(baseline) Bundle metrics
Bundle size by type
|
| Current #10286 |
Baseline #10280 |
|
|---|---|---|
499.15KiB |
499.15KiB |
|
402.16KiB |
402.16KiB |
|
2.22KiB |
2.22KiB |
Bundle analysis report Branch Sherry-hue:feat/a2ui-protocol-st... Project dashboard
Generated by RelativeCI Documentation Report issue
React External#1824 Bundle Size — 699.5KiB (0%).dc6f009(current) vs 5fc1ca1 main#1819(baseline) Bundle metrics
|
| Current #1824 |
Baseline #1819 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
3 |
3 |
|
17 |
17 |
|
5 |
5 |
|
7.13% |
7.13% |
|
0 |
0 |
|
0 |
0 |
Bundle analysis report Branch Sherry-hue:feat/a2ui-protocol-st... Project dashboard
Generated by RelativeCI Documentation Report issue
React Example with Element Template#978 Bundle Size — 204.36KiB (0%).dc6f009(current) vs 5fc1ca1 main#972(baseline) Bundle metrics
|
| Current #978 |
Baseline #972 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
4 |
4 |
|
124 |
124 |
|
50 |
50 |
|
45.19% |
45.19% |
|
2 |
2 |
|
0 |
0 |
Bundle size by type no changes
| Current #978 |
Baseline #972 |
|
|---|---|---|
145.76KiB |
145.76KiB |
|
58.61KiB |
58.61KiB |
Bundle analysis report Branch Sherry-hue:feat/a2ui-protocol-st... Project dashboard
Generated by RelativeCI Documentation Report issue
React MTF Example#1843 Bundle Size — 208.94KiB (0%).dc6f009(current) vs 5fc1ca1 main#1838(baseline) Bundle metrics
|
| Current #1843 |
Baseline #1838 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
3 |
3 |
|
199 |
199 |
|
78 |
78 |
|
44.08% |
44.08% |
|
2 |
2 |
|
0 |
0 |
Bundle size by type no changes
| Current #1843 |
Baseline #1838 |
|
|---|---|---|
111.23KiB |
111.23KiB |
|
97.71KiB |
97.71KiB |
Bundle analysis report Branch Sherry-hue:feat/a2ui-protocol-st... Project dashboard
Generated by RelativeCI Documentation Report issue
React Example#8709 Bundle Size — 238KiB (0%).dc6f009(current) vs 5fc1ca1 main#8704(baseline) Bundle metrics
|
| Current #8709 |
Baseline #8704 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
4 |
4 |
|
204 |
204 |
|
81 |
81 |
|
44.59% |
44.59% |
|
2 |
2 |
|
0 |
0 |
Bundle size by type no changes
| Current #8709 |
Baseline #8704 |
|
|---|---|---|
145.76KiB |
145.76KiB |
|
92.24KiB |
92.24KiB |
Bundle analysis report Branch Sherry-hue:feat/a2ui-protocol-st... Project dashboard
Generated by RelativeCI Documentation Report issue
9cdc151 to
9847f51
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/genui/a2ui-playground/src/pages/AIChatPage.tsx (1)
596-603:⚠️ Potential issue | 🟠 Major | ⚡ Quick winEmit only the new suffix for streamed
messageevents.The stream routes now send cumulative
messagesarrays. Forwarding each batch verbatim makes append-only consumers replay earlier action-response messages on every chunk; in this PR that path ends up posting duplicateA2UI_ACTION_RESPONSEupdates into the Lynx preview. Please expose a delta here, or add a separate cumulative-vs-delta callback contract.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui-playground/src/pages/AIChatPage.tsx` around lines 596 - 603, When handling parsed.event === 'message' in the stream, don't forward the entire cumulative array returned by normalizeA2UIMessages; instead compute the delta suffix relative to the previously sent latestMessages and call onMessages with only the new items so consumers (e.g., A2UI_ACTION_RESPONSE handlers) don't replay earlier entries. Use publishPartialMessages as before, call normalizeA2UIMessages(parsed.data) to get messages, compare messages to the existing latestMessages (by length or id) to extract only the new tail, update latestMessages to the full cumulative array, and invoke onMessages with the suffix only when it contains items.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx`:
- Around line 382-390: The current pushLiveMessagesToStore callback
(pushLiveMessagesToStore) normalizes the full snapshot and calls
MessageStore.push for each message, which re-appends duplicates because
MessageStore.push is append-only; change this to replay the snapshot instead of
appending: either (A) replace the body to create a fresh BaseClient /
client.processor.processMessages(...) invocation with the normalized array so
the client reconstructs state from scratch, or (B) compute the suffix delta by
comparing the incoming normalized messages (from normalizeProtocolMessages)
against targetStore's existing messages and only call targetStore.push for the
new tail; update pushLiveMessagesToStore accordingly and ensure it uses the same
identity/format as client.processor.processMessages to avoid duplication.
In `@packages/genui/server/agent/a2ui-stream-parser.ts`:
- Around line 404-406: The current code treats an empty array from parser.push
as a failure and falls back to the original messages, undoing intentional
filtering; change the fallback to only occur when parser.push returns
null/undefined (i.e. a real parser failure). Update the return logic around
A2UIProtocolMessageStreamParser and parser.push in a2ui-stream-parser.ts to
return replayMessages when replayMessages is not null/undefined (even if it's an
empty array) and only return messages when replayMessages is null/undefined so
intentionally filtered empty replays are preserved.
---
Outside diff comments:
In `@packages/genui/a2ui-playground/src/pages/AIChatPage.tsx`:
- Around line 596-603: When handling parsed.event === 'message' in the stream,
don't forward the entire cumulative array returned by normalizeA2UIMessages;
instead compute the delta suffix relative to the previously sent latestMessages
and call onMessages with only the new items so consumers (e.g.,
A2UI_ACTION_RESPONSE handlers) don't replay earlier entries. Use
publishPartialMessages as before, call normalizeA2UIMessages(parsed.data) to get
messages, compare messages to the existing latestMessages (by length or id) to
extract only the new tail, update latestMessages to the full cumulative array,
and invoke onMessages with the suffix only when it contains items.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dfda8431-12d4-4340-bef7-4ca3f90721af
📒 Files selected for processing (8)
packages/genui/a2ui-playground/lynx-src/a2ui/App.tsxpackages/genui/a2ui-playground/src/pages/AIChatPage.csspackages/genui/a2ui-playground/src/pages/AIChatPage.tsxpackages/genui/a2ui-playground/src/render.tsxpackages/genui/a2ui/src/catalog/Image/index.tsxpackages/genui/server/agent/a2ui-stream-parser.tspackages/genui/server/app/a2ui/action/stream/route.tspackages/genui/server/app/a2ui/stream/route.ts
9847f51 to
3754557
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (6)
packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx (1)
382-388:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftReplay live snapshots through the processor instead of appending them to the current store.
A2UI_LIVE_MESSAGESis still applied viaMessageStore.push(). That path replays snapshot payloads as append-only updates, so earlier protocol messages can be re-added and the preview drifts from the latest server state. Route these updates throughBaseClient/client.processor.processMessages()(or rebuild the store before replaying) so each snapshot converges.#!/bin/bash set -euo pipefail app="packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx" page="packages/genui/a2ui-playground/src/pages/AIChatPage.tsx" echo "== App live replay path ==" sed -n '382,528p' "$app" | awk 'BEGIN{n=381} {print ++n "\t" $0}' echo echo "== Replay-related APIs referenced in App.tsx ==" rg -n "BaseClient|processor\.processMessages|pushLiveMessagesToStore|A2UI_LIVE_MESSAGES|targetStore\.push|currentStore\.push" "$app" echo echo "== Sender-side live message publication ==" rg -n "A2UI_LIVE_MESSAGES|publishPreviewMessages|postMessage" "$page"As per coding guidelines
Use BaseClient with client.processor.processMessages() to replay provided messages over time in the Lynx app.Also applies to: 455-468, 522-528
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx` around lines 382 - 388, pushLiveMessagesToStore currently normalizes incoming A2UI_LIVE_MESSAGES and calls targetStore.push(msg) which replays snapshot payloads as append-only updates; instead route those normalized messages through the standard replay path by calling the BaseClient processor (e.g. client.processor.processMessages(normalized, { store: targetStore })) or rebuild/replace the store before pushing so snapshots converge; update pushLiveMessagesToStore (and the similar blocks around lines referenced) to use client.processor.processMessages(...) rather than targetStore.push(...) and ensure the client instance (BaseClient) and correct store are passed into the call.packages/genui/a2ui/src/catalog/Image/index.tsx (1)
26-30:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle bound
{ path }URLs before filtering the source.
ImageProps.urlstill allows{ path: string }, butimageSourceFromServer()rejects every non-string input. Bound image URLs will now collapse tosrc=""and never render.🔧 Minimal fix
function imageSourceFromServer(value: unknown): string | undefined { - if (typeof value !== 'string') return undefined; - const src = value.trim(); - if (!src) return undefined; - return src; + const src = typeof value === 'string' + ? value + : (typeof value === 'object' + && value !== null + && typeof (value as { path?: unknown }).path === 'string' + ? (value as { path: string }).path + : undefined); + const trimmed = src?.trim(); + if (!trimmed) return undefined; + return trimmed; }Also applies to: 56-63
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui/src/catalog/Image/index.tsx` around lines 26 - 30, imageSourceFromServer currently rejects non-string inputs so bound URLs passed as objects like { path: string } collapse; update imageSourceFromServer to detect when value is an object with a string "path" property (e.g., typeof value === 'object' && value !== null && typeof (value as any).path === 'string'), extract that path string, then trim and validate it the same way as the existing string branch; apply the same change to the other similar helper at lines 56-63 (the second imageSourceFromServer/URL-normalization helper) so both functions accept bound { path } shapes and return undefined only for empty or invalid strings.packages/genui/a2ui-playground/src/pages/AIChatPage.tsx (2)
652-667:⚠️ Potential issue | 🟡 MinorPersisted action labels still miss nested event names.
Hydration still falls back straight to
'unknown'when the stored action is shaped like{ event: { name } }, so the new forwarding status and action card can still lose the label.Suggested fix
try { const parsed = JSON.parse(content.slice(prefix.length).trim()) as unknown; if (!parsed || typeof parsed !== 'object') return null; const action = (parsed as { action?: unknown }).action; if (!action || typeof action !== 'object') return null; const record = action as Record<string, unknown>; + const event = record.event; + const eventName = event && typeof event === 'object' + ? (event as { name?: unknown }).name + : undefined; return { action: record, - name: typeof record.name === 'string' ? record.name : 'unknown', + name: typeof record.name === 'string' + ? record.name + : (typeof eventName === 'string' ? eventName : 'unknown'), }; } catch {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui-playground/src/pages/AIChatPage.tsx` around lines 652 - 667, The parsePersistedUserAction function only reads record.name and falls back to 'unknown', so actions shaped like { event: { name: '...' } } are mis-labeled; update parsePersistedUserAction to also check for a nested name at record.event?.name (and ensure record.event is an object and record.event.name is a string) and use that value before falling back to 'unknown', keeping the same return shape and guarding with the existing type checks.
1165-1172:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winReject
postMessagetraffic that doesn’t originate from the preview iframeThe
windowmessagehandler forA2UI_RENDER_READY/A2UI_USER_ACTIONonly checkse.dataand never validatese.sourceore.origin, so any frame can trigger preview publication / action streaming.
parsePersistedUserAction()also derives the action name solely fromrecord.name; if the name is nested in the persisted payload it will fall back to'unknown'.Suggested guard
const handleMessage = (e: MessageEvent<unknown>) => { + const previewWindow = previewFrameRef.current?.contentWindow; + const expectedOrigin = renderUrlRef.current + ? targetOriginForFrame(renderUrlRef.current) + : window.location.origin; + if (e.source !== previewWindow || e.origin !== expectedOrigin) { + return; + } if (!e.data || typeof e.data !== 'object') return; const msg = e.data as Record<string, unknown>; if (msg.type === 'A2UI_RENDER_READY') { publishPreviewMessages(latestPreviewMessagesRef.current); return; } if (msg.type !== 'A2UI_USER_ACTION') return;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui-playground/src/pages/AIChatPage.tsx` around lines 1165 - 1172, The message handler handleMessage should reject postMessage events that don't come from the preview iframe by validating event.source and event.origin before acting: check that e.source === previewIframeRef.current?.contentWindow (or equivalent iframe window reference) and that e.origin matches the expected preview origin (derived from the preview URL/config) before calling publishPreviewMessages or handling A2UI_USER_ACTION; additionally, harden parsePersistedUserAction so it safely retrieves the action name (not only record.name) by walking the persisted payload for nested name fields and only falling back to 'unknown' if no name is found, and update any references to latestPreviewMessagesRef/current handling accordingly.packages/genui/server/agent/a2ui-stream-parser.ts (2)
404-406:⚠️ Potential issue | 🟠 MajorKeep an intentionally empty replay result.
parser.push()returning[]can mean “everything in this batch was intentionally filtered out,” not “parser failed.” Falling back tomessageshere re-sends the non-renderable updates this helper just removed.Minimal fix
export function splitA2UIProtocolMessages( messages: A2UIMessage[], ): A2UIMessage[] { const parser = new A2UIProtocolMessageStreamParser(); - const replayMessages = parser.push(JSON.stringify(messages)); - return replayMessages.length > 0 ? replayMessages : messages; + return parser.push(JSON.stringify(messages)); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/server/agent/a2ui-stream-parser.ts` around lines 404 - 406, The current helper calls A2UIProtocolMessageStreamParser().push(...) and if replayMessages is an empty array it falls back to returning the original messages, which re-sends items that were intentionally filtered; change the return logic in the function using A2UIProtocolMessageStreamParser so it always returns replayMessages (even when []), i.e. remove the fallback to `messages` and return the parser.push(...) result directly (referencing the parser variable, replayMessages and the original messages variable to locate the code).
189-196:⚠️ Potential issue | 🟠 MajorPreserve unknown refs and avoid first-node pruning for update-only batches.
This still rewrites every unseen child ref to
loading_*and still snapshots from the first seen component whenrootis absent. Update-only/action patches can legitimately reference existing client-side nodes outside the current chunk, so this can still drop sibling updates and corrupt live refs.Also applies to: 238-260
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/server/agent/a2ui-stream-parser.ts` around lines 189 - 196, The replaceRef logic currently converts any unseen child ref into a new placeholder (via createPlaceholderComponent and expectedPlaceholderComponent) and later code snapshots from the first seen component when root is absent; change it so unseen refs are preserved for update-only/action patches and when there's no root: detect the batch/patch mode (e.g., an isUpdateOnly or action flag/context) and if the patch is update-only or root is missing, return the original id instead of creating a placeholder or using expectedPlaceholderComponent, and avoid selecting a “first seen” component for snapshotting; apply the same guard to the similar block that uses expectedPlaceholderComponent/placeholders.set in the other occurrence so sibling updates referencing existing client-side nodes outside the chunk are not rewritten or pruned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/genui/server/agent/a2ui-validator.ts`:
- Around line 225-249: The debug lookup fails for validator messages like "Prop
root.children[0]..." because extractValidationErrorPath only handles "Schema
violation at ..." and valueAtPath treats "[0]" as part of a property name;
update the parsing so extractValidationErrorPath (used by
getA2UIValidationDebugData) recognizes the "Prop ..." pattern and splits paths
into tokens that convert bracket indices into numeric array indices (e.g.,
"root.children[0].name" -> ["root","children",0,"name"]), and ensure valueAtPath
accepts numeric indices when traversing arrays (or add a small normalizer that
converts the extracted path string into that token array before calling
valueAtPath). Apply the same fix for the other occurrence referenced (lines
~538-556).
- Around line 225-240: The function getA2UIValidationDebugData currently returns
the full raw model response in rawText; change it to avoid persisting full
outputs by making raw capture opt-in and/or returning a short redacted preview:
add an optional parameter (e.g., includeRaw?: boolean = false) to
getA2UIValidationDebugData and if includeRaw is true return rawText, otherwise
set rawText to a short preview (first N chars + ellipsis) or a redacted version
(masking sensitive patterns). Update callers (stream routes that log validation
failures) to explicitly pass includeRaw only when necessary. Keep
extractJsonArray logic and A2UIValidationDebugData shape consistent but ensure
rawText is either undefined, a redacted preview, or present only when includeRaw
is true.
---
Duplicate comments:
In `@packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx`:
- Around line 382-388: pushLiveMessagesToStore currently normalizes incoming
A2UI_LIVE_MESSAGES and calls targetStore.push(msg) which replays snapshot
payloads as append-only updates; instead route those normalized messages through
the standard replay path by calling the BaseClient processor (e.g.
client.processor.processMessages(normalized, { store: targetStore })) or
rebuild/replace the store before pushing so snapshots converge; update
pushLiveMessagesToStore (and the similar blocks around lines referenced) to use
client.processor.processMessages(...) rather than targetStore.push(...) and
ensure the client instance (BaseClient) and correct store are passed into the
call.
In `@packages/genui/a2ui-playground/src/pages/AIChatPage.tsx`:
- Around line 652-667: The parsePersistedUserAction function only reads
record.name and falls back to 'unknown', so actions shaped like { event: { name:
'...' } } are mis-labeled; update parsePersistedUserAction to also check for a
nested name at record.event?.name (and ensure record.event is an object and
record.event.name is a string) and use that value before falling back to
'unknown', keeping the same return shape and guarding with the existing type
checks.
- Around line 1165-1172: The message handler handleMessage should reject
postMessage events that don't come from the preview iframe by validating
event.source and event.origin before acting: check that e.source ===
previewIframeRef.current?.contentWindow (or equivalent iframe window reference)
and that e.origin matches the expected preview origin (derived from the preview
URL/config) before calling publishPreviewMessages or handling A2UI_USER_ACTION;
additionally, harden parsePersistedUserAction so it safely retrieves the action
name (not only record.name) by walking the persisted payload for nested name
fields and only falling back to 'unknown' if no name is found, and update any
references to latestPreviewMessagesRef/current handling accordingly.
In `@packages/genui/a2ui/src/catalog/Image/index.tsx`:
- Around line 26-30: imageSourceFromServer currently rejects non-string inputs
so bound URLs passed as objects like { path: string } collapse; update
imageSourceFromServer to detect when value is an object with a string "path"
property (e.g., typeof value === 'object' && value !== null && typeof (value as
any).path === 'string'), extract that path string, then trim and validate it the
same way as the existing string branch; apply the same change to the other
similar helper at lines 56-63 (the second
imageSourceFromServer/URL-normalization helper) so both functions accept bound {
path } shapes and return undefined only for empty or invalid strings.
In `@packages/genui/server/agent/a2ui-stream-parser.ts`:
- Around line 404-406: The current helper calls
A2UIProtocolMessageStreamParser().push(...) and if replayMessages is an empty
array it falls back to returning the original messages, which re-sends items
that were intentionally filtered; change the return logic in the function using
A2UIProtocolMessageStreamParser so it always returns replayMessages (even when
[]), i.e. remove the fallback to `messages` and return the parser.push(...)
result directly (referencing the parser variable, replayMessages and the
original messages variable to locate the code).
- Around line 189-196: The replaceRef logic currently converts any unseen child
ref into a new placeholder (via createPlaceholderComponent and
expectedPlaceholderComponent) and later code snapshots from the first seen
component when root is absent; change it so unseen refs are preserved for
update-only/action patches and when there's no root: detect the batch/patch mode
(e.g., an isUpdateOnly or action flag/context) and if the patch is update-only
or root is missing, return the original id instead of creating a placeholder or
using expectedPlaceholderComponent, and avoid selecting a “first seen” component
for snapshotting; apply the same guard to the similar block that uses
expectedPlaceholderComponent/placeholders.set in the other occurrence so sibling
updates referencing existing client-side nodes outside the chunk are not
rewritten or pruned.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dcb24c03-5f10-4021-9fc9-9628e482da22
📒 Files selected for processing (10)
packages/genui/a2ui-playground/lynx-src/a2ui/App.tsxpackages/genui/a2ui-playground/src/pages/AIChatPage.csspackages/genui/a2ui-playground/src/pages/AIChatPage.tsxpackages/genui/a2ui-playground/src/render.tsxpackages/genui/a2ui/src/catalog/Image/index.tsxpackages/genui/server/agent/a2ui-prompt.tspackages/genui/server/agent/a2ui-stream-parser.tspackages/genui/server/agent/a2ui-validator.tspackages/genui/server/app/a2ui/action/stream/route.tspackages/genui/server/app/a2ui/stream/route.ts
3754557 to
2eea637
Compare
2eea637 to
261e2b2
Compare
d51493b to
cfeaed7
Compare
cfeaed7 to
dc6f009
Compare
Summary by CodeRabbit
New Features
Bug Fixes
Style
Summary
/a2ui/streamand/a2ui/action/streamat protocol-message granularityValidation
pnpm -C packages/genui/a2ui-playground buildpnpm -C packages/genui/server exec tsc --noEmit --pretty falsepnpm -C packages/genui/a2ui exec tsc --project tsconfig.build.json --noEmit --pretty falseChecklist