Skip to content

feat(a2ui): stream protocol messages in playground#2709

Merged
Sherry-hue merged 6 commits into
lynx-family:mainfrom
Sherry-hue:feat/a2ui-protocol-streaming
May 27, 2026
Merged

feat(a2ui): stream protocol messages in playground#2709
Sherry-hue merged 6 commits into
lynx-family:mainfrom
Sherry-hue:feat/a2ui-protocol-streaming

Conversation

@Sherry-hue
Copy link
Copy Markdown
Collaborator

@Sherry-hue Sherry-hue commented May 25, 2026

Summary by CodeRabbit

  • New Features

    • Buffered live preview delivery and batched live updates
    • Copy feedback toasts for URL/JSON copy actions
    • Chunked JSON payload viewer with per-block and copy-all controls
    • Validation debug output for returned model payloads
  • Bug Fixes

    • More reliable incremental streaming, parsing, and message ordering
    • Improved reconstruction of conversation/actions and applied-action status
    • More consistent image preview behavior for invalid sources
  • Style

    • Updated chat and toast styling for improved layout and readability

Review Change Stack

Summary

  • stream A2UI protocol messages from /a2ui/stream and /a2ui/action/stream at protocol-message granularity
  • update the A2UI playground to render streamed message chunks, live preview updates, action responses, and copy feedback consistently
  • avoid fallback image flashes for non-loadable Image sources

Validation

  • pnpm -C packages/genui/a2ui-playground build
  • pnpm -C packages/genui/server exec tsc --noEmit --pretty false
  • pnpm -C packages/genui/a2ui exec tsc --project tsconfig.build.json --noEmit --pretty false
  • pre-commit lint/format hooks

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).
  • Changeset added, and when a BREAKING CHANGE occurs, it needs to be clearly marked (or not required).

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 25, 2026

⚠️ No Changeset found

Latest commit: dc6f009

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

<review_stack_artifact_start -->
<stack_title>A2UI Streaming, Payload Display, and Live Message Updates</stack_title>
<stack_summary>Adds copy-to-clipboard toasts and a chunked JSON payload viewer, introduces an incremental A2UI protocol message parser and splitter, extends SSE routes to emit parsed messages, buffers/flushed live/action messages into the Lynx renderer, and wires iframe live-message handling into playground UI.</stack_summary>

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_24f1c1ae3cfd Buffers 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_8334bb38c102 Adds 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_0aa03ca1dd27 Uses `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_996d00000000 Updates 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_0c9b8140d409 Parses 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_1e987f035c69 Refactors 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_126100000000 Re-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_8c3d3acaa5f4 Wires `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_4795b94b68cc Adds 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_5e52b4b1f3d1 Integrates 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_fad0ba03c594 Main 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_ee2b6797e3d5 Smaller 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_57b1694acd37 Clarifies `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_d7e3972f0a83 Adds 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>
range_5e52b4b1f3d1
range_996d00000000
range_126100000000
range_479500000000
range_3338723ff764
range_53034580929d
range_ee2b6797e3d5
range_5f20ebc3b8b9
</unassigned_ranges>

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(a2ui): stream protocol messages in playground' directly and clearly summarizes the main objective of the changeset: streaming A2UI protocol messages in the playground component.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Sherry-hue Sherry-hue force-pushed the feat/a2ui-protocol-streaming branch from 8d55e1f to e616902 Compare May 25, 2026 12:02
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between bbfb26b and 8d55e1f.

📒 Files selected for processing (12)
  • packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx
  • packages/genui/a2ui-playground/src/components/CopyToast.tsx
  • packages/genui/a2ui-playground/src/components/PreviewPanel.tsx
  • packages/genui/a2ui-playground/src/pages/AIChatPage.css
  • packages/genui/a2ui-playground/src/pages/AIChatPage.tsx
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui-playground/src/styles.css
  • packages/genui/a2ui/src/catalog/Image/index.tsx
  • packages/genui/server/agent/a2ui-stream-parser.ts
  • packages/genui/server/app/a2ui/_shared.ts
  • packages/genui/server/app/a2ui/action/stream/route.ts
  • packages/genui/server/app/a2ui/stream/route.ts

Comment thread packages/genui/a2ui-playground/src/pages/AIChatPage.css Outdated
Comment thread packages/genui/a2ui-playground/src/pages/AIChatPage.tsx Outdated
Comment thread packages/genui/a2ui-playground/src/styles.css Outdated
Comment thread packages/genui/a2ui/src/catalog/Image/index.tsx Outdated
Comment thread packages/genui/server/agent/a2ui-stream-parser.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8d55e1f and e616902.

📒 Files selected for processing (12)
  • packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx
  • packages/genui/a2ui-playground/src/components/CopyToast.tsx
  • packages/genui/a2ui-playground/src/components/PreviewPanel.tsx
  • packages/genui/a2ui-playground/src/pages/AIChatPage.css
  • packages/genui/a2ui-playground/src/pages/AIChatPage.tsx
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui-playground/src/styles.css
  • packages/genui/a2ui/src/catalog/Image/index.tsx
  • packages/genui/server/agent/a2ui-stream-parser.ts
  • packages/genui/server/app/a2ui/_shared.ts
  • packages/genui/server/app/a2ui/action/stream/route.ts
  • packages/genui/server/app/a2ui/stream/route.ts

Comment thread packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx
@codecov
Copy link
Copy Markdown

codecov Bot commented May 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 25, 2026

Merging this PR will degrade performance by 16.35%

❌ 1 regressed benchmark
✅ 80 untouched benchmarks
⏩ 26 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
basic-performance-large-css 16.2 ms 19.4 ms -16.35%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing Sherry-hue:feat/a2ui-protocol-streaming (dc6f009) with main (5fc1ca1)

Open in CodSpeed

Footnotes

  1. 26 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 25, 2026

Web Explorer

#10286 Bundle Size — 903.53KiB (0%).

dc6f009(current) vs 5fc1ca1 main#10280(baseline)

Bundle metrics  Change 1 change
                 Current
#10286
     Baseline
#10280
No change  Initial JS 45.06KiB 45.06KiB
No change  Initial CSS 2.22KiB 2.22KiB
No change  Cache Invalidation 0% 0%
No change  Chunks 9 9
No change  Assets 11 11
Change  Modules 232(+0.87%) 230
No change  Duplicate Modules 11 11
No change  Duplicate Code 27.12% 27.12%
No change  Packages 10 10
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#10286
     Baseline
#10280
No change  JS 499.15KiB 499.15KiB
No change  Other 402.16KiB 402.16KiB
No change  CSS 2.22KiB 2.22KiB

Bundle analysis reportBranch Sherry-hue:feat/a2ui-protocol-st...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 25, 2026

React External

#1824 Bundle Size — 699.5KiB (0%).

dc6f009(current) vs 5fc1ca1 main#1819(baseline)

Bundle metrics  no changes
                 Current
#1824
     Baseline
#1819
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 17 17
No change  Duplicate Modules 5 5
No change  Duplicate Code 7.13% 7.13%
No change  Packages 0 0
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#1824
     Baseline
#1819
No change  Other 699.5KiB 699.5KiB

Bundle analysis reportBranch Sherry-hue:feat/a2ui-protocol-st...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 25, 2026

React Example with Element Template

#978 Bundle Size — 204.36KiB (0%).

dc6f009(current) vs 5fc1ca1 main#972(baseline)

Bundle metrics  no changes
                 Current
#978
     Baseline
#972
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 124 124
No change  Duplicate Modules 50 50
No change  Duplicate Code 45.19% 45.19%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#978
     Baseline
#972
No change  IMG 145.76KiB 145.76KiB
No change  Other 58.61KiB 58.61KiB

Bundle analysis reportBranch Sherry-hue:feat/a2ui-protocol-st...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 25, 2026

React MTF Example

#1843 Bundle Size — 208.94KiB (0%).

dc6f009(current) vs 5fc1ca1 main#1838(baseline)

Bundle metrics  no changes
                 Current
#1843
     Baseline
#1838
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 199 199
No change  Duplicate Modules 78 78
No change  Duplicate Code 44.08% 44.08%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#1843
     Baseline
#1838
No change  IMG 111.23KiB 111.23KiB
No change  Other 97.71KiB 97.71KiB

Bundle analysis reportBranch Sherry-hue:feat/a2ui-protocol-st...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 25, 2026

React Example

#8709 Bundle Size — 238KiB (0%).

dc6f009(current) vs 5fc1ca1 main#8704(baseline)

Bundle metrics  no changes
                 Current
#8709
     Baseline
#8704
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 204 204
No change  Duplicate Modules 81 81
No change  Duplicate Code 44.59% 44.59%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#8709
     Baseline
#8704
No change  IMG 145.76KiB 145.76KiB
No change  Other 92.24KiB 92.24KiB

Bundle analysis reportBranch Sherry-hue:feat/a2ui-protocol-st...Project dashboard


Generated by RelativeCIDocumentationReport issue

@Sherry-hue Sherry-hue force-pushed the feat/a2ui-protocol-streaming branch from 9cdc151 to 9847f51 Compare May 26, 2026 06:15
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Emit only the new suffix for streamed message events.

The stream routes now send cumulative messages arrays. Forwarding each batch verbatim makes append-only consumers replay earlier action-response messages on every chunk; in this PR that path ends up posting duplicate A2UI_ACTION_RESPONSE updates 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9cdc151 and 9847f51.

📒 Files selected for processing (8)
  • packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx
  • packages/genui/a2ui-playground/src/pages/AIChatPage.css
  • packages/genui/a2ui-playground/src/pages/AIChatPage.tsx
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui/src/catalog/Image/index.tsx
  • packages/genui/server/agent/a2ui-stream-parser.ts
  • packages/genui/server/app/a2ui/action/stream/route.ts
  • packages/genui/server/app/a2ui/stream/route.ts

Comment thread packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx
Comment thread packages/genui/server/agent/a2ui-stream-parser.ts Outdated
@Sherry-hue Sherry-hue force-pushed the feat/a2ui-protocol-streaming branch from 9847f51 to 3754557 Compare May 26, 2026 06:54
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (6)
packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx (1)

382-388: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replay live snapshots through the processor instead of appending them to the current store.

A2UI_LIVE_MESSAGES is still applied via MessageStore.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 through BaseClient / 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 win

Handle bound { path } URLs before filtering the source.

ImageProps.url still allows { path: string }, but imageSourceFromServer() rejects every non-string input. Bound image URLs will now collapse to src="" 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 | 🟡 Minor

Persisted 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 win

Reject postMessage traffic that doesn’t originate from the preview iframe

The window message handler for A2UI_RENDER_READY / A2UI_USER_ACTION only checks e.data and never validates e.source or e.origin, so any frame can trigger preview publication / action streaming.

  • parsePersistedUserAction() also derives the action name solely from record.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 | 🟠 Major

Keep an intentionally empty replay result.

parser.push() returning [] can mean “everything in this batch was intentionally filtered out,” not “parser failed.” Falling back to messages here 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 | 🟠 Major

Preserve 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 when root is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9847f51 and 3754557.

📒 Files selected for processing (10)
  • packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx
  • packages/genui/a2ui-playground/src/pages/AIChatPage.css
  • packages/genui/a2ui-playground/src/pages/AIChatPage.tsx
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui/src/catalog/Image/index.tsx
  • packages/genui/server/agent/a2ui-prompt.ts
  • packages/genui/server/agent/a2ui-stream-parser.ts
  • packages/genui/server/agent/a2ui-validator.ts
  • packages/genui/server/app/a2ui/action/stream/route.ts
  • packages/genui/server/app/a2ui/stream/route.ts

Comment thread packages/genui/server/agent/a2ui-validator.ts Outdated
Comment thread packages/genui/server/agent/a2ui-validator.ts
@Sherry-hue Sherry-hue force-pushed the feat/a2ui-protocol-streaming branch from 3754557 to 2eea637 Compare May 26, 2026 08:32
@Sherry-hue Sherry-hue force-pushed the feat/a2ui-protocol-streaming branch from 2eea637 to 261e2b2 Compare May 26, 2026 08:42
@Sherry-hue Sherry-hue force-pushed the feat/a2ui-protocol-streaming branch 2 times, most recently from d51493b to cfeaed7 Compare May 27, 2026 01:14
@Sherry-hue Sherry-hue force-pushed the feat/a2ui-protocol-streaming branch from cfeaed7 to dc6f009 Compare May 27, 2026 03:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants