refactor(a2ui): React renderer + headless <A2UI> component#2571
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:
📝 WalkthroughWalkthroughThis PR refactors A2UI from a resource-polling streaming model to a store-driven message-buffer architecture with React-based context rendering. It introduces MessageStore buffering, multi-listener MessageProcessor, immutable Resource snapshots, React hooks for binding and action dispatch, context-based provider/renderer, removes registry-based component wiring in favor of explicit tree-shakable exports, and adds agent example implementations (MockAgent, SseAgent) for the playground. ChangesStore & Message Processing Foundation
React Hooks & Binding
React Provider & Renderer
Catalog Tree-Shaking & Exports
Package API & Configuration
Playground & Example Agents
Test Coverage
Deprecated & Removed APIs
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
packages/genui/a2ui/test/catalog.test.ts (1)
110-111: 💤 Low valueHardcoded version
'0.9'is brittle.If
serializeCatalogbumps its version, this test breaks without being related to the change under test. Consider importing the version constant from the implementation or at least leaving an inline comment documenting where this value comes from.🤖 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/test/catalog.test.ts` around lines 110 - 111, Test uses a brittle hardcoded version string ('0.9') in assertions; update the test to reference the canonical version constant from the implementation (the value used by serializeCatalog) instead of hardcoding, e.g., import the version constant exported by the module that defines serializeCatalog or read it via the same export that serializeCatalog uses, and replace the literal '0.9' in the expect(out.version).toBe(...) assertion with that imported constant (or add an inline comment pointing to the implementing symbol if importing isn’t possible).packages/genui/a2ui/src/react/A2UIProvider.tsx (1)
34-36: ⚡ Quick winReturn type uses an inline import when
ReactNodeis already imported.
ReactNodeis already imported on Line 5, so the inlineimport('@lynx-js/react').ReactNodeis redundant.🔧 Proposed fix
export function A2UIProvider( props: ProviderProps, -): import('@lynx-js/react').ReactNode { +): ReactNode {🤖 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/react/A2UIProvider.tsx` around lines 34 - 36, The return type of A2UIProvider uses an inline import "import('@lynx-js/react').ReactNode" even though ReactNode is already imported; update the A2UIProvider signature to use the existing ReactNode type (i.e., change the return type to ReactNode) and remove the inline import usage so the function declaration simply reads A2UIProvider(props: ProviderProps): ReactNode {, ensuring the file-level import for ReactNode (from the top, where it's already imported) is used.packages/genui/a2ui/test/messageStore.test.ts (1)
30-35: 💤 Low valueTest name is slightly misleading — no mutations occur in this test.
The test name says "between mutations" but only checks two consecutive
getSnapshot()calls with no mutation in between. Consider renaming to'getSnapshot returns the same reference on repeated calls'or adding apush(A)between the two calls to actually validate post-mutation stability (i.e.,getSnapshot()after a mutation returns a new stable reference).🤖 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/test/messageStore.test.ts` around lines 30 - 35, The test name is misleading because no mutation happens; either rename the test to reflect repeated calls (e.g., "'getSnapshot returns the same reference on repeated calls'") or modify the test to perform a mutation between snapshots to assert post-mutation stability — call createMessageStore(), take firstSnapshot = store.getSnapshot(), perform a mutation via the store's mutation API (e.g., store.push(...) or store.addMessage(...)), then take secondSnapshot = store.getSnapshot() and assert expected referential behavior; locate the test using createMessageStore and getSnapshot to apply the change.packages/genui/a2ui/test/payloadNormalizer.test.ts (1)
55-74: ⚡ Quick winDocument the mutation of
activeSurfaceIdsin the function's JSDoc comment.The
prepareMessagesForProcessingfunction mutates itsactiveSurfaceIdsparameter by calling.delete()(line 180) and.add()(line 189) on it, but the JSDoc comment (lines 165–169) does not document this side-effect. Add a note like "@param activeSurfaceIds — mutated to track created/deleted surfaces" to clarify this out-parameter pattern, or alternatively, return the updated set from the function instead of mutating the input.🤖 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/test/payloadNormalizer.test.ts` around lines 55 - 74, The JSDoc for prepareMessagesForProcessing must document that the activeSurfaceIds parameter is mutated (it calls .delete() and .add() to track created/deleted surfaces); update the function comment to include a line such as "@param activeSurfaceIds — mutated to track created/deleted surfaces" describing this out-parameter side-effect, or alternatively change the function signature to return the updated Set (and update callers) instead of mutating the input; reference prepareMessagesForProcessing and the activeSurfaceIds parameter when making the change.packages/genui/a2ui/src/store/MessageProcessor.ts (1)
437-449: 🏗️ Heavy liftPass the surface in the
deleteSurfaceevent payload and delete first.
emitUpdatefires beforethis.surfaces.delete(surfaceId), so theA2UI.tsxlistener works only becauseproc.getOrCreateSurface(surfaceId)still returns the live surface. Two consequences:
- If the surface doesn't exist in
this.surfaces(e.g., adeleteSurfacearriving for an unknown/already-removedsurfaceId), the listener'sgetOrCreateSurfacesynthesizes a phantom surface, which the subsequentthis.surfaces.delete(surfaceId)then deletes. The deleteSurface event becomes a near-noop — the active resource never receives thedeleteSurfacesnapshot because the phantom'sresourcesmap is empty.- Listeners must keep the implicit ordering contract in mind, which is fragile.
Including the original surface in the payload lets you delete first and removes the listener's dependency on map state:
♻️ Proposed refactor
if ('deleteSurface' in message && message.deleteSurface) { const { surfaceId } = message.deleteSurface; const surface = this.surfaces.get(surfaceId); + this.surfaces.delete(surfaceId); + this.emitUpdate({ type: 'deleteSurface', surfaceId, + surface, targetId: surface?.rootComponentId ?? surfaceId, messageId: (message as { messageId?: string }).messageId, }); - - this.surfaces.delete(surfaceId); }In
A2UI.tsx, prefer the payloadsurfaceoverproc.getOrCreateSurface(surfaceId)for the deleteSurface branch so the listener no longer creates phantom surfaces.Based on learnings: "Update events (emitUpdate) should reflect the state after the mutation has been applied… Fix by applying the mutation (delete the surface) before emitting the update, or by passing a stable post-mutation snapshot to listeners."
🤖 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/store/MessageProcessor.ts` around lines 437 - 449, The deleteSurface handler currently emits an update before removing the surface from this.surfaces, causing listeners (e.g., A2UI.tsx using proc.getOrCreateSurface) to see the pre-delete map state or synthesize phantom surfaces; change the flow in the deleteSurface branch so you first retrieve and delete the surface from this.surfaces (use this.surfaces.get(surfaceId) then this.surfaces.delete(surfaceId)), and then call emitUpdate with a payload that includes the original surface object (e.g., include surface in the emitted payload along with surfaceId/targetId/messageId) so listeners receive a stable post-delete snapshot and no longer rely on proc.getOrCreateSurface ordering.
🤖 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/examples/io-sse/sseAgent.ts`:
- Around line 138-170: The flush loop can lose batches when succeed() calls
cleanup() which clears queue; update logic so pending batches are drained before
resolve: either remove queue.length = 0 from cleanup() and let flush() drain
naturally, or change succeed() to set settled = true but not clear queue and
then modify flush() to continue processing until queue is empty (e.g., loop
condition uses (queue.length > 0 || !settled) or ensure it drains while
queue.length > 0 regardless of settled), making sure cleanup() still removes
listeners/timeouts after drain; reference succeed(), cleanup(), flush(), queue
and MESSAGE_PROCESS_DELAY when applying the change.
In `@packages/genui/a2ui-playground/package.json`:
- Line 9: The current "dev" script only runs "rsbuild dev" and no longer starts
the Lynx dev bundle; update the package.json "dev" script to run the Lynx dev
bundle concurrently with the standard dev server (i.e., start "rsbuild dev" and
whatever script exposes the Lynx dev bundle, commonly "rsbuild dev:lynx" or
"dev:lynx") using a concurrent runner (e.g., concurrently or npm-run-all) so the
Lynx dev server runs alongside the web dev server and avoids pre-building the
Lynx bundle or producing stale snapshots; adjust the script to invoke both
"rsbuild dev" and the Lynx dev command concurrently and ensure any added tool is
in devDependencies.
In `@packages/genui/a2ui/src/index.ts`:
- Around line 7-13: Add a brief inline comment above the export block explaining
that A2UIRenderer is intentionally omitted from public exports because it is an
internal implementation detail of A2UI (see react/index.ts), so contributors do
not re-export it; reference the public symbols A2UI, NodeRenderer, useAction,
useDataBinding, and useResolvedProps and mention A2UIRenderer by name to make
the intent explicit.
---
Nitpick comments:
In `@packages/genui/a2ui/src/react/A2UIProvider.tsx`:
- Around line 34-36: The return type of A2UIProvider uses an inline import
"import('@lynx-js/react').ReactNode" even though ReactNode is already imported;
update the A2UIProvider signature to use the existing ReactNode type (i.e.,
change the return type to ReactNode) and remove the inline import usage so the
function declaration simply reads A2UIProvider(props: ProviderProps): ReactNode
{, ensuring the file-level import for ReactNode (from the top, where it's
already imported) is used.
In `@packages/genui/a2ui/src/store/MessageProcessor.ts`:
- Around line 437-449: The deleteSurface handler currently emits an update
before removing the surface from this.surfaces, causing listeners (e.g.,
A2UI.tsx using proc.getOrCreateSurface) to see the pre-delete map state or
synthesize phantom surfaces; change the flow in the deleteSurface branch so you
first retrieve and delete the surface from this.surfaces (use
this.surfaces.get(surfaceId) then this.surfaces.delete(surfaceId)), and then
call emitUpdate with a payload that includes the original surface object (e.g.,
include surface in the emitted payload along with surfaceId/targetId/messageId)
so listeners receive a stable post-delete snapshot and no longer rely on
proc.getOrCreateSurface ordering.
In `@packages/genui/a2ui/test/catalog.test.ts`:
- Around line 110-111: Test uses a brittle hardcoded version string ('0.9') in
assertions; update the test to reference the canonical version constant from the
implementation (the value used by serializeCatalog) instead of hardcoding, e.g.,
import the version constant exported by the module that defines serializeCatalog
or read it via the same export that serializeCatalog uses, and replace the
literal '0.9' in the expect(out.version).toBe(...) assertion with that imported
constant (or add an inline comment pointing to the implementing symbol if
importing isn’t possible).
In `@packages/genui/a2ui/test/messageStore.test.ts`:
- Around line 30-35: The test name is misleading because no mutation happens;
either rename the test to reflect repeated calls (e.g., "'getSnapshot returns
the same reference on repeated calls'") or modify the test to perform a mutation
between snapshots to assert post-mutation stability — call createMessageStore(),
take firstSnapshot = store.getSnapshot(), perform a mutation via the store's
mutation API (e.g., store.push(...) or store.addMessage(...)), then take
secondSnapshot = store.getSnapshot() and assert expected referential behavior;
locate the test using createMessageStore and getSnapshot to apply the change.
In `@packages/genui/a2ui/test/payloadNormalizer.test.ts`:
- Around line 55-74: The JSDoc for prepareMessagesForProcessing must document
that the activeSurfaceIds parameter is mutated (it calls .delete() and .add() to
track created/deleted surfaces); update the function comment to include a line
such as "@param activeSurfaceIds — mutated to track created/deleted surfaces"
describing this out-parameter side-effect, or alternatively change the function
signature to return the updated Set (and update callers) instead of mutating the
input; reference prepareMessagesForProcessing and the activeSurfaceIds parameter
when making the change.
🪄 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: 7524e074-1b6f-40a1-9445-fe6af09be659
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (58)
packages/genui/a2ui-playground/examples/README.mdpackages/genui/a2ui-playground/examples/io-mock/mockAgent.tspackages/genui/a2ui-playground/examples/io-sse/sseAgent.tspackages/genui/a2ui-playground/lynx-src/App.tsxpackages/genui/a2ui-playground/lynx-src/tsconfig.jsonpackages/genui/a2ui-playground/package.jsonpackages/genui/a2ui/package.jsonpackages/genui/a2ui/rstest.config.tspackages/genui/a2ui/src/catalog/Button.tspackages/genui/a2ui/src/catalog/Card.tspackages/genui/a2ui/src/catalog/CheckBox.tspackages/genui/a2ui/src/catalog/Column.tspackages/genui/a2ui/src/catalog/Divider.tspackages/genui/a2ui/src/catalog/Image.tspackages/genui/a2ui/src/catalog/List.tspackages/genui/a2ui/src/catalog/README.mdpackages/genui/a2ui/src/catalog/RadioGroup.tspackages/genui/a2ui/src/catalog/Row.tspackages/genui/a2ui/src/catalog/Text.tspackages/genui/a2ui/src/catalog/all.tspackages/genui/a2ui/src/catalog/index.tspackages/genui/a2ui/src/chat/Conversation.tsxpackages/genui/a2ui/src/chat/index.tspackages/genui/a2ui/src/chat/useLynxClient.tspackages/genui/a2ui/src/core/A2UIRender.tsxpackages/genui/a2ui/src/core/BaseClient.tspackages/genui/a2ui/src/core/ComponentRegistry.tspackages/genui/a2ui/src/core/index.tspackages/genui/a2ui/src/core/types.tspackages/genui/a2ui/src/core/useAction.tspackages/genui/a2ui/src/core/useDataBinding.tspackages/genui/a2ui/src/index.tspackages/genui/a2ui/src/react/A2UI.tsxpackages/genui/a2ui/src/react/A2UIProvider.tsxpackages/genui/a2ui/src/react/A2UIRenderer.tsxpackages/genui/a2ui/src/react/index.tspackages/genui/a2ui/src/react/useA2UIContext.tspackages/genui/a2ui/src/react/useAction.tspackages/genui/a2ui/src/react/useCatalog.tspackages/genui/a2ui/src/react/useDataBinding.tspackages/genui/a2ui/src/store/MessageProcessor.tspackages/genui/a2ui/src/store/MessageStore.tspackages/genui/a2ui/src/store/Resource.tspackages/genui/a2ui/src/store/SignalStore.tspackages/genui/a2ui/src/store/index.tspackages/genui/a2ui/src/store/payloadNormalizer.tspackages/genui/a2ui/src/store/types.tspackages/genui/a2ui/src/utils/ComponentRegistry.tspackages/genui/a2ui/src/utils/createResource.tspackages/genui/a2ui/src/utils/index.tspackages/genui/a2ui/test/catalog.test.tspackages/genui/a2ui/test/createResource.test.tspackages/genui/a2ui/test/messageStore.test.tspackages/genui/a2ui/test/payloadNormalizer.test.tspackages/genui/a2ui/test/processor.test.tspackages/genui/a2ui/tsconfig.build.jsonpackages/genui/a2ui/tsconfig.jsonpackages/genui/tsconfig.json
💤 Files with no reviewable changes (24)
- packages/genui/a2ui/src/core/types.ts
- packages/genui/a2ui/src/catalog/List.ts
- packages/genui/a2ui/src/chat/useLynxClient.ts
- packages/genui/a2ui/src/catalog/Divider.ts
- packages/genui/a2ui/src/utils/ComponentRegistry.ts
- packages/genui/a2ui/src/catalog/Text.ts
- packages/genui/a2ui/src/catalog/Image.ts
- packages/genui/a2ui/src/chat/index.ts
- packages/genui/a2ui/src/catalog/Row.ts
- packages/genui/a2ui/src/core/ComponentRegistry.ts
- packages/genui/a2ui/src/core/useAction.ts
- packages/genui/a2ui/src/catalog/Column.ts
- packages/genui/a2ui/src/utils/index.ts
- packages/genui/a2ui/src/catalog/all.ts
- packages/genui/a2ui/src/catalog/Button.ts
- packages/genui/a2ui/src/catalog/RadioGroup.ts
- packages/genui/a2ui/src/catalog/CheckBox.ts
- packages/genui/a2ui/src/catalog/Card.ts
- packages/genui/a2ui/src/core/index.ts
- packages/genui/a2ui/src/core/useDataBinding.ts
- packages/genui/a2ui/src/chat/Conversation.tsx
- packages/genui/a2ui/src/utils/createResource.ts
- packages/genui/a2ui/src/core/A2UIRender.tsx
- packages/genui/a2ui/src/core/BaseClient.ts
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will improve performance by 19.21%
Performance Changes
Comparing Footnotes
|
Web Explorer#9619 Bundle Size — 900.04KiB (0%).c60c802(current) vs 33b124f main#9602(baseline) Bundle metrics
Bundle size by type
|
| Current #9619 |
Baseline #9602 |
|
|---|---|---|
495.91KiB |
495.91KiB |
|
401.92KiB |
401.92KiB |
|
2.22KiB |
2.22KiB |
Bundle analysis report Branch PupilTong:claude/quirky-merkle-r... Project dashboard
Generated by RelativeCI Documentation Report issue
React Example with Element Template#312 Bundle Size — 197.79KiB (0%).c60c802(current) vs 33b124f main#295(baseline) Bundle metrics
|
| Current #312 |
Baseline #295 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
4 |
4 |
|
81 |
81 |
|
23 |
23 |
|
40.29% |
40.29% |
|
2 |
2 |
|
0 |
0 |
Bundle size by type no changes
| Current #312 |
Baseline #295 |
|
|---|---|---|
145.76KiB |
145.76KiB |
|
52.03KiB |
52.03KiB |
Bundle analysis report Branch PupilTong:claude/quirky-merkle-r... Project dashboard
Generated by RelativeCI Documentation Report issue
React Example#8046 Bundle Size — 235.77KiB (0%).c60c802(current) vs 33b124f main#8029(baseline) Bundle metrics
|
| Current #8046 |
Baseline #8029 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
4 |
4 |
|
197 |
197 |
|
80 |
80 |
|
44.85% |
44.85% |
|
2 |
2 |
|
0 |
0 |
Bundle size by type no changes
| Current #8046 |
Baseline #8029 |
|
|---|---|---|
145.76KiB |
145.76KiB |
|
90.01KiB |
90.01KiB |
Bundle analysis report Branch PupilTong:claude/quirky-merkle-r... Project dashboard
Generated by RelativeCI Documentation Report issue
React External#1160 Bundle Size — 690.27KiB (0%).c60c802(current) vs 33b124f main#1143(baseline) Bundle metrics
|
| Current #1160 |
Baseline #1143 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
3 |
3 |
|
17 |
17 |
|
5 |
5 |
|
8.59% |
8.59% |
|
0 |
0 |
|
0 |
0 |
Bundle analysis report Branch PupilTong:claude/quirky-merkle-r... Project dashboard
Generated by RelativeCI Documentation Report issue
React MTF Example#1177 Bundle Size — 206.6KiB (0%).c60c802(current) vs 33b124f main#1160(baseline) Bundle metrics
|
| Current #1177 |
Baseline #1160 |
|
|---|---|---|
0B |
0B |
|
0B |
0B |
|
0% |
0% |
|
0 |
0 |
|
3 |
3 |
|
192 |
192 |
|
77 |
77 |
|
44.36% |
44.36% |
|
2 |
2 |
|
0 |
0 |
Bundle size by type no changes
| Current #1177 |
Baseline #1160 |
|
|---|---|---|
111.23KiB |
111.23KiB |
|
95.37KiB |
95.37KiB |
Bundle analysis report Branch PupilTong:claude/quirky-merkle-r... Project dashboard
Generated by RelativeCI Documentation Report issue
## Summary
Foundational message-buffer + payload-normalization layer for the
headless A2UI renderer. Pure data logic, no React surface, no breaking
changes.
- `MessageStore` — append-only buffer of raw protocol messages with a
`useSyncExternalStore`-friendly `subscribe` / `getSnapshot` API.
- `payloadNormalizer` — turns arbitrary developer payloads (string,
structured object, `{ kind, data }` envelope, nested arrays) into a flat
`ServerToClientMessage[]`. Includes
`createFallbackMessagesFromPlainText`, `createTextCardMessages`, and
`prepareMessagesForProcessing` helpers used by IO transports.
Adds a per-package `rstest.config.ts` plus split `tsconfig.json` (lint +
tests) / `tsconfig.build.json` (emit) so tests run via `pnpm -F
@lynx-js/a2ui-reactlynx test` without polluting the emit `rootDir`.
This PR is **purely additive** — the existing `core/`, `chat/`, and
`utils/` paths (`BaseClient`, `A2UIRender`, the global
`componentRegistry`, the legacy `createResource` / `SignalStore`) keep
working untouched. The bigger types-divergent pieces
(`MessageProcessor`, the new `Resource` with `subscribe` / `getSnapshot`
/ explicit status, the new `SignalStore`) ship together with the React
renderer in
[#2571](#2571), since they
introduce a new `Surface` shape that the catalog components must migrate
to in the same PR.
## Test plan
- [ ] \`pnpm -F @lynx-js/a2ui-reactlynx test\` — 16/16 pass
(`MessageStore`, `payloadNormalizer`)
- [ ] \`pnpm -F @lynx-js/a2ui-reactlynx build\` — extractor still emits
the 10 catalog manifests
- [ ] Existing playground (which uses `core/`/`chat/` paths) still runs
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a message store and payload-normalization utilities for handling
and preparing runtime messages.
* **Tests**
* Added tests covering message store behavior, payload normalization,
message processing, and edge cases.
* **Chores**
* Integrated a test runner and dev-dependency, and updated TypeScript
build/test configurations and project references.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
6d0a83d to
2c893d4
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/src/store/MessageProcessor.ts`:
- Around line 441-446: The deleteSurface emit populates messageId optionally and
can emit undefined; update the emit in MessageProcessor (the deleteSurface path
that calls this.emitUpdate) to normalize the messageId the same way
beginRendering does by falling back to a stable id like `surface:${surfaceId}`
when (message as { messageId?: string }).messageId is missing or falsy; locate
the deleteSurface emission (variables: surfaceId, surface, message) and replace
the raw messageId with a normalized value before calling this.emitUpdate so
consumers always receive a non-undefined messageId.
- Around line 44-51: dispatch currently passes the same resolve function to
every subscriber in this.eventListeners, causing a race where the first caller
resolves the shared Promise; fix by switching to a deterministic pattern: either
(A) enforce a single active listener by only invoking the first callback in
this.eventListeners (if onEvent is meant to be single-subscriber), or (B)
implement fan-out with aggregation by mapping each listener callback to its own
Promise (or require listeners to return a Promise) and then return
Promise.all(...) of those per-listener Promises so dispatch resolves with an
array of results; update the onEvent contract/documentation accordingly
(referencing dispatch, this.eventListeners, and onEvent) and ensure tests
reflect the chosen behavior.
🪄 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: 7d27f021-660e-4bb3-b3f3-989e234eea35
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (50)
packages/genui/a2ui-playground/examples/README.mdpackages/genui/a2ui-playground/examples/io-mock/mockAgent.tspackages/genui/a2ui-playground/examples/io-sse/sseAgent.tspackages/genui/a2ui-playground/lynx-src/App.tsxpackages/genui/a2ui-playground/lynx-src/tsconfig.jsonpackages/genui/a2ui-playground/package.jsonpackages/genui/a2ui/package.jsonpackages/genui/a2ui/src/catalog/Button.tspackages/genui/a2ui/src/catalog/Card.tspackages/genui/a2ui/src/catalog/CheckBox.tspackages/genui/a2ui/src/catalog/Column.tspackages/genui/a2ui/src/catalog/Divider.tspackages/genui/a2ui/src/catalog/Image.tspackages/genui/a2ui/src/catalog/List.tspackages/genui/a2ui/src/catalog/README.mdpackages/genui/a2ui/src/catalog/RadioGroup.tspackages/genui/a2ui/src/catalog/Row.tspackages/genui/a2ui/src/catalog/Text.tspackages/genui/a2ui/src/catalog/all.tspackages/genui/a2ui/src/catalog/index.tspackages/genui/a2ui/src/chat/Conversation.tsxpackages/genui/a2ui/src/chat/index.tspackages/genui/a2ui/src/chat/useLynxClient.tspackages/genui/a2ui/src/core/A2UIRender.tsxpackages/genui/a2ui/src/core/BaseClient.tspackages/genui/a2ui/src/core/ComponentRegistry.tspackages/genui/a2ui/src/core/index.tspackages/genui/a2ui/src/core/types.tspackages/genui/a2ui/src/core/useAction.tspackages/genui/a2ui/src/core/useDataBinding.tspackages/genui/a2ui/src/index.tspackages/genui/a2ui/src/react/A2UI.tsxpackages/genui/a2ui/src/react/A2UIProvider.tsxpackages/genui/a2ui/src/react/A2UIRenderer.tsxpackages/genui/a2ui/src/react/index.tspackages/genui/a2ui/src/react/useA2UIContext.tspackages/genui/a2ui/src/react/useAction.tspackages/genui/a2ui/src/react/useCatalog.tspackages/genui/a2ui/src/react/useDataBinding.tspackages/genui/a2ui/src/store/MessageProcessor.tspackages/genui/a2ui/src/store/Resource.tspackages/genui/a2ui/src/store/SignalStore.tspackages/genui/a2ui/src/store/index.tspackages/genui/a2ui/src/store/types.tspackages/genui/a2ui/src/utils/ComponentRegistry.tspackages/genui/a2ui/src/utils/createResource.tspackages/genui/a2ui/src/utils/index.tspackages/genui/a2ui/test/catalog.test.tspackages/genui/a2ui/test/createResource.test.tspackages/genui/a2ui/test/processor.test.ts
💤 Files with no reviewable changes (24)
- packages/genui/a2ui/src/utils/ComponentRegistry.ts
- packages/genui/a2ui/src/catalog/Column.ts
- packages/genui/a2ui/src/catalog/Divider.ts
- packages/genui/a2ui/src/catalog/List.ts
- packages/genui/a2ui/src/catalog/Button.ts
- packages/genui/a2ui/src/catalog/Card.ts
- packages/genui/a2ui/src/catalog/Text.ts
- packages/genui/a2ui/src/catalog/all.ts
- packages/genui/a2ui/src/utils/createResource.ts
- packages/genui/a2ui/src/core/index.ts
- packages/genui/a2ui/src/catalog/Image.ts
- packages/genui/a2ui/src/core/A2UIRender.tsx
- packages/genui/a2ui/src/catalog/Row.ts
- packages/genui/a2ui/src/catalog/RadioGroup.ts
- packages/genui/a2ui/src/chat/index.ts
- packages/genui/a2ui/src/core/useDataBinding.ts
- packages/genui/a2ui/src/catalog/CheckBox.ts
- packages/genui/a2ui/src/chat/useLynxClient.ts
- packages/genui/a2ui/src/core/useAction.ts
- packages/genui/a2ui/src/core/ComponentRegistry.ts
- packages/genui/a2ui/src/core/BaseClient.ts
- packages/genui/a2ui/src/utils/index.ts
- packages/genui/a2ui/src/core/types.ts
- packages/genui/a2ui/src/chat/Conversation.tsx
✅ Files skipped from review due to trivial changes (7)
- packages/genui/a2ui-playground/package.json
- packages/genui/a2ui-playground/lynx-src/tsconfig.json
- packages/genui/a2ui-playground/examples/README.md
- packages/genui/a2ui/src/react/index.ts
- packages/genui/a2ui/src/react/A2UI.tsx
- packages/genui/a2ui/src/catalog/README.md
- packages/genui/a2ui/src/react/useDataBinding.ts
🚧 Files skipped from review as they are similar to previous changes (16)
- packages/genui/a2ui/src/react/useCatalog.ts
- packages/genui/a2ui/src/react/useA2UIContext.ts
- packages/genui/a2ui/src/react/A2UIProvider.tsx
- packages/genui/a2ui-playground/examples/io-mock/mockAgent.ts
- packages/genui/a2ui/test/processor.test.ts
- packages/genui/a2ui/src/catalog/index.ts
- packages/genui/a2ui/package.json
- packages/genui/a2ui-playground/examples/io-sse/sseAgent.ts
- packages/genui/a2ui/src/store/index.ts
- packages/genui/a2ui/src/store/types.ts
- packages/genui/a2ui/test/createResource.test.ts
- packages/genui/a2ui/src/react/useAction.ts
- packages/genui/a2ui/src/index.ts
- packages/genui/a2ui/src/react/A2UIRenderer.tsx
- packages/genui/a2ui/src/store/Resource.ts
- packages/genui/a2ui-playground/lynx-src/App.tsx
2c893d4 to
0940278
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
packages/genui/a2ui/src/react/useDataBinding.ts (1)
60-66: ⚡ Quick winCentralize and normalize binding path resolution.
The same path-joining logic is copied in three places, and it only prefixes segments. Relative bindings like
../fooor./foostay unnormalized, so reads and writes can drift if another layer expects canonical store keys.♻️ Suggested direction
+function resolveStorePath(path: string | undefined, dataContextPath?: string) { + if (!path) return undefined; + const raw = path.startsWith('/') + ? path + : `${dataContextPath ?? ''}/${path}`; + + const normalized: string[] = []; + for (const segment of raw.split('/')) { + if (!segment || segment === '.') continue; + if (segment === '..') { + normalized.pop(); + continue; + } + normalized.push(segment); + } + return `/${normalized.join('/')}`; +}Then reuse that helper for all three read/write call sites.
Also applies to: 125-127, 203-205
🤖 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/react/useDataBinding.ts` around lines 60 - 66, The path-joining logic is duplicated and doesn't normalize relative segments like "../" or "./"; create a single helper (e.g., normalizeBindingPath or resolveBindingPath) that accepts (path: string, dataContextPath?: string) and returns a canonical absolute-like path by: 1) prefixing dataContextPath when path doesn't start with '/', 2) collapsing "." and ".." segments and duplicate slashes, and 3) trimming any trailing slash except for root; then replace the three inlined blocks (the current join logic found around the path handling in useDataBinding.ts) with calls to this helper for both reads and writes so all callers use the same normalized key space.
🤖 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/src/catalog/README.md`:
- Around line 11-13: Update the README to warn that relying on bare component
names via `displayName ?? component.name` is unsafe in production because
toplevel name mangling during minification can remove `component.name`; advise
authors to set an explicit `displayName` on components or use the
manifest/manifest form when calling `defineCatalog` so protocol names remain
stable, and mention that `defineCatalog` will throw if both `displayName` and
`name` are absent (but only manifests in production builds), so include this
warning near the existing examples that reference `displayName ??
component.name`.
In `@packages/genui/a2ui/src/react/A2UIRenderer.tsx`:
- Around line 106-111: The current use of the nullish coalescing operator treats
an explicit null from renderFallback or renderError as “no override”, preventing
consumers from intentionally returning null; change both branches to call
renderFallback?.() and renderError?.(error), capture the result, and return the
result if it is not strictly undefined (result !== undefined), otherwise fall
back to the built-in UI (DefaultLoading or the default error text); update the
logic around renderFallback, renderError, DefaultLoading, and the error branch
in A2UIRenderer to use this undefined-check pattern.
In `@packages/genui/a2ui/src/react/useDataBinding.ts`:
- Around line 87-89: The currentValue ternary uses (signalValue ?? initialValue)
when path is truthy, which skips the caller-provided fallbackValue; change the
path branch in useDataBinding (the currentValue computation) to prefer
signalValue, then fallbackValue, then initialValue (e.g., signalValue ??
fallbackValue ?? initialValue) so unresolved bound paths use the provided
fallback.
In `@packages/genui/a2ui/src/store/MessageProcessor.ts`:
- Around line 56-79: dispatch() currently waits for each listener to call a
provided resolve callback, so one non-calling listener can block forever; change
the listener contract in MessageProcessor so listeners return a value or Promise
and make dispatch treat each cb(...) as Promise.resolve(returnedValue) and use
Promise.race([... , timeoutPromise]) per listener to enforce a per-listener
timeout; in practice update the loop that invokes eventListeners (in
MessageProcessor.dispatch / the tryResolve/cb invocation area) to call const p =
Promise.resolve(cb({ message })) then p.then(value => { if (!hasResponse) {
hasResponse = true; firstResponse = value; } }).finally(() => { settled += 1; if
(settled >= total) resolve(hasResponse ? firstResponse : []); }) and remove the
old resolve callback param (or keep but ignore for compatibility) so one stalled
listener no longer blocks dispatch.
- Around line 73-76: The dispatch() logic sets hasResponse and firstResponse on
any resolved value, so an empty payload (e.g., [] or '') can mark the "first
non-empty response" incorrectly; change the assignment so
hasResponse/firstResponse are only set when the resolved value is actually
non-empty—implement a simple truthy/non-empty check (e.g., value !==
null/undefined and for arrays length>0, for strings length>0, for objects
Object.keys(value).length>0) before setting hasResponse and firstResponse in the
dispatch handler (refer to dispatch(), hasResponse, firstResponse).
---
Nitpick comments:
In `@packages/genui/a2ui/src/react/useDataBinding.ts`:
- Around line 60-66: The path-joining logic is duplicated and doesn't normalize
relative segments like "../" or "./"; create a single helper (e.g.,
normalizeBindingPath or resolveBindingPath) that accepts (path: string,
dataContextPath?: string) and returns a canonical absolute-like path by: 1)
prefixing dataContextPath when path doesn't start with '/', 2) collapsing "."
and ".." segments and duplicate slashes, and 3) trimming any trailing slash
except for root; then replace the three inlined blocks (the current join logic
found around the path handling in useDataBinding.ts) with calls to this helper
for both reads and writes so all callers use the same normalized key space.
🪄 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: 08527a98-e6dd-44f3-a6d2-3b9404317da3
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (49)
packages/genui/a2ui-playground/examples/README.mdpackages/genui/a2ui-playground/examples/io-mock/mockAgent.tspackages/genui/a2ui-playground/examples/io-sse/sseAgent.tspackages/genui/a2ui-playground/lynx-src/App.tsxpackages/genui/a2ui-playground/lynx-src/tsconfig.jsonpackages/genui/a2ui/package.jsonpackages/genui/a2ui/src/catalog/Button.tspackages/genui/a2ui/src/catalog/Card.tspackages/genui/a2ui/src/catalog/CheckBox.tspackages/genui/a2ui/src/catalog/Column.tspackages/genui/a2ui/src/catalog/Divider.tspackages/genui/a2ui/src/catalog/Image.tspackages/genui/a2ui/src/catalog/List.tspackages/genui/a2ui/src/catalog/README.mdpackages/genui/a2ui/src/catalog/RadioGroup.tspackages/genui/a2ui/src/catalog/Row.tspackages/genui/a2ui/src/catalog/Text.tspackages/genui/a2ui/src/catalog/all.tspackages/genui/a2ui/src/catalog/index.tspackages/genui/a2ui/src/chat/Conversation.tsxpackages/genui/a2ui/src/chat/index.tspackages/genui/a2ui/src/chat/useLynxClient.tspackages/genui/a2ui/src/core/A2UIRender.tsxpackages/genui/a2ui/src/core/BaseClient.tspackages/genui/a2ui/src/core/ComponentRegistry.tspackages/genui/a2ui/src/core/index.tspackages/genui/a2ui/src/core/types.tspackages/genui/a2ui/src/core/useAction.tspackages/genui/a2ui/src/core/useDataBinding.tspackages/genui/a2ui/src/index.tspackages/genui/a2ui/src/react/A2UI.tsxpackages/genui/a2ui/src/react/A2UIProvider.tsxpackages/genui/a2ui/src/react/A2UIRenderer.tsxpackages/genui/a2ui/src/react/index.tspackages/genui/a2ui/src/react/useA2UIContext.tspackages/genui/a2ui/src/react/useAction.tspackages/genui/a2ui/src/react/useCatalog.tspackages/genui/a2ui/src/react/useDataBinding.tspackages/genui/a2ui/src/store/MessageProcessor.tspackages/genui/a2ui/src/store/Resource.tspackages/genui/a2ui/src/store/SignalStore.tspackages/genui/a2ui/src/store/index.tspackages/genui/a2ui/src/store/types.tspackages/genui/a2ui/src/utils/ComponentRegistry.tspackages/genui/a2ui/src/utils/createResource.tspackages/genui/a2ui/src/utils/index.tspackages/genui/a2ui/test/catalog.test.tspackages/genui/a2ui/test/createResource.test.tspackages/genui/a2ui/test/processor.test.ts
💤 Files with no reviewable changes (24)
- packages/genui/a2ui/src/catalog/Divider.ts
- packages/genui/a2ui/src/catalog/Column.ts
- packages/genui/a2ui/src/catalog/Row.ts
- packages/genui/a2ui/src/catalog/Button.ts
- packages/genui/a2ui/src/catalog/RadioGroup.ts
- packages/genui/a2ui/src/catalog/Card.ts
- packages/genui/a2ui/src/catalog/CheckBox.ts
- packages/genui/a2ui/src/chat/index.ts
- packages/genui/a2ui/src/catalog/Image.ts
- packages/genui/a2ui/src/catalog/all.ts
- packages/genui/a2ui/src/core/useAction.ts
- packages/genui/a2ui/src/utils/createResource.ts
- packages/genui/a2ui/src/core/index.ts
- packages/genui/a2ui/src/catalog/List.ts
- packages/genui/a2ui/src/core/types.ts
- packages/genui/a2ui/src/core/A2UIRender.tsx
- packages/genui/a2ui/src/utils/ComponentRegistry.ts
- packages/genui/a2ui/src/chat/Conversation.tsx
- packages/genui/a2ui/src/core/ComponentRegistry.ts
- packages/genui/a2ui/src/chat/useLynxClient.ts
- packages/genui/a2ui/src/core/useDataBinding.ts
- packages/genui/a2ui/src/catalog/Text.ts
- packages/genui/a2ui/src/utils/index.ts
- packages/genui/a2ui/src/core/BaseClient.ts
✅ Files skipped from review due to trivial changes (9)
- packages/genui/a2ui/src/react/useCatalog.ts
- packages/genui/a2ui/src/react/useA2UIContext.ts
- packages/genui/a2ui-playground/lynx-src/tsconfig.json
- packages/genui/a2ui-playground/examples/README.md
- packages/genui/a2ui/src/react/index.ts
- packages/genui/a2ui/src/react/A2UIProvider.tsx
- packages/genui/a2ui/src/index.ts
- packages/genui/a2ui-playground/examples/io-sse/sseAgent.ts
- packages/genui/a2ui/src/react/A2UI.tsx
🚧 Files skipped from review as they are similar to previous changes (11)
- packages/genui/a2ui-playground/examples/io-mock/mockAgent.ts
- packages/genui/a2ui/src/store/index.ts
- packages/genui/a2ui/src/catalog/index.ts
- packages/genui/a2ui/test/catalog.test.ts
- packages/genui/a2ui/test/processor.test.ts
- packages/genui/a2ui/src/store/Resource.ts
- packages/genui/a2ui/src/store/types.ts
- packages/genui/a2ui/src/react/useAction.ts
- packages/genui/a2ui-playground/lynx-src/App.tsx
- packages/genui/a2ui/package.json
- packages/genui/a2ui/test/createResource.test.ts
0940278 to
3b57ae5
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (2)
packages/genui/a2ui/src/react/useDataBinding.ts (1)
87-89:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
fallbackValueis still ignored on the bound-path branch.When
pathis provided but the signal has no resolved value yet, this returnsundefinedand never consults the caller-suppliedfallbackValue, so controlled props start empty even though the hook’s API advertises a fallback. This was raised on a prior commit and the code is unchanged.💡 Suggested fix
const currentValue = path - ? (signalValue ?? initialValue) + ? (signalValue ?? initialValue ?? fallbackValue) : (initialValue ?? fallbackValue);🤖 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/react/useDataBinding.ts` around lines 87 - 89, The bound-path branch for currentValue ignores fallbackValue; when path is truthy and signalValue is undefined you must fall back to initialValue and then to fallbackValue. Update the logic that computes currentValue (the ternary using path, signalValue, initialValue, fallbackValue) so the path-true expression uses signalValue ?? initialValue ?? fallbackValue (while keeping the existing path-false expression as initialValue ?? fallbackValue) to ensure the caller-supplied fallbackValue is respected.packages/genui/a2ui/src/react/A2UIRenderer.tsx (1)
106-111:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
??still preventsrenderFallback/renderErrorfrom intentionally returningnull.
renderFallback?.()andrenderError?.(error)typed to returnReactNodeallow consumers to suppress the built-in loading/error UI with an explicitnull, but??treatsnullas "no override" and falls through to<DefaultLoading>/ the error text. This was raised on a prior commit and the code is unchanged.💡 Suggested fix
- if (status === 'pending' && data === undefined) { - return renderFallback?.() ?? <DefaultLoading id={resource.id} />; - } - - if (status === 'error') { - return renderError?.(error) ?? <text>Error: {String(error)}</text>; - } + if (status === 'pending' && data === undefined) { + const fb = renderFallback?.(); + return fb !== undefined ? fb : <DefaultLoading id={resource.id} />; + } + + if (status === 'error') { + const er = renderError?.(error); + return er !== undefined ? er : <text>Error: {String(error)}</text>; + }🤖 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/react/A2UIRenderer.tsx` around lines 106 - 111, The current use of the nullish coalescing operator (??) causes explicit null returns from renderFallback or renderError to be ignored; update the logic in A2UIRenderer to call renderFallback?.() and renderError?.(error), store each result (e.g., fallbackNode and errorNode) and then check for !== undefined to decide whether to return the consumer-provided node (including null) or fall back to the built-ins (DefaultLoading with resource.id and the error text). Ensure you reference renderFallback, renderError, DefaultLoading, resource.id and error when making this change.
🤖 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.
Duplicate comments:
In `@packages/genui/a2ui/src/react/A2UIRenderer.tsx`:
- Around line 106-111: The current use of the nullish coalescing operator (??)
causes explicit null returns from renderFallback or renderError to be ignored;
update the logic in A2UIRenderer to call renderFallback?.() and
renderError?.(error), store each result (e.g., fallbackNode and errorNode) and
then check for !== undefined to decide whether to return the consumer-provided
node (including null) or fall back to the built-ins (DefaultLoading with
resource.id and the error text). Ensure you reference renderFallback,
renderError, DefaultLoading, resource.id and error when making this change.
In `@packages/genui/a2ui/src/react/useDataBinding.ts`:
- Around line 87-89: The bound-path branch for currentValue ignores
fallbackValue; when path is truthy and signalValue is undefined you must fall
back to initialValue and then to fallbackValue. Update the logic that computes
currentValue (the ternary using path, signalValue, initialValue, fallbackValue)
so the path-true expression uses signalValue ?? initialValue ?? fallbackValue
(while keeping the existing path-false expression as initialValue ??
fallbackValue) to ensure the caller-supplied fallbackValue is respected.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7e3392f2-47bc-4f03-a591-cbee2d8b5091
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (49)
packages/genui/a2ui-playground/examples/README.mdpackages/genui/a2ui-playground/examples/io-mock/mockAgent.tspackages/genui/a2ui-playground/examples/io-sse/sseAgent.tspackages/genui/a2ui-playground/lynx-src/App.tsxpackages/genui/a2ui-playground/lynx-src/tsconfig.jsonpackages/genui/a2ui/package.jsonpackages/genui/a2ui/src/catalog/Button.tspackages/genui/a2ui/src/catalog/Card.tspackages/genui/a2ui/src/catalog/CheckBox.tspackages/genui/a2ui/src/catalog/Column.tspackages/genui/a2ui/src/catalog/Divider.tspackages/genui/a2ui/src/catalog/Image.tspackages/genui/a2ui/src/catalog/List.tspackages/genui/a2ui/src/catalog/README.mdpackages/genui/a2ui/src/catalog/RadioGroup.tspackages/genui/a2ui/src/catalog/Row.tspackages/genui/a2ui/src/catalog/Text.tspackages/genui/a2ui/src/catalog/all.tspackages/genui/a2ui/src/catalog/index.tspackages/genui/a2ui/src/chat/Conversation.tsxpackages/genui/a2ui/src/chat/index.tspackages/genui/a2ui/src/chat/useLynxClient.tspackages/genui/a2ui/src/core/A2UIRender.tsxpackages/genui/a2ui/src/core/BaseClient.tspackages/genui/a2ui/src/core/ComponentRegistry.tspackages/genui/a2ui/src/core/index.tspackages/genui/a2ui/src/core/types.tspackages/genui/a2ui/src/core/useAction.tspackages/genui/a2ui/src/core/useDataBinding.tspackages/genui/a2ui/src/index.tspackages/genui/a2ui/src/react/A2UI.tsxpackages/genui/a2ui/src/react/A2UIProvider.tsxpackages/genui/a2ui/src/react/A2UIRenderer.tsxpackages/genui/a2ui/src/react/index.tspackages/genui/a2ui/src/react/useA2UIContext.tspackages/genui/a2ui/src/react/useAction.tspackages/genui/a2ui/src/react/useCatalog.tspackages/genui/a2ui/src/react/useDataBinding.tspackages/genui/a2ui/src/store/MessageProcessor.tspackages/genui/a2ui/src/store/Resource.tspackages/genui/a2ui/src/store/SignalStore.tspackages/genui/a2ui/src/store/index.tspackages/genui/a2ui/src/store/types.tspackages/genui/a2ui/src/utils/ComponentRegistry.tspackages/genui/a2ui/src/utils/createResource.tspackages/genui/a2ui/src/utils/index.tspackages/genui/a2ui/test/catalog.test.tspackages/genui/a2ui/test/createResource.test.tspackages/genui/a2ui/test/processor.test.ts
💤 Files with no reviewable changes (24)
- packages/genui/a2ui/src/catalog/Text.ts
- packages/genui/a2ui/src/catalog/RadioGroup.ts
- packages/genui/a2ui/src/catalog/Divider.ts
- packages/genui/a2ui/src/utils/index.ts
- packages/genui/a2ui/src/catalog/List.ts
- packages/genui/a2ui/src/catalog/Card.ts
- packages/genui/a2ui/src/chat/index.ts
- packages/genui/a2ui/src/catalog/Image.ts
- packages/genui/a2ui/src/core/types.ts
- packages/genui/a2ui/src/catalog/all.ts
- packages/genui/a2ui/src/catalog/Button.ts
- packages/genui/a2ui/src/core/ComponentRegistry.ts
- packages/genui/a2ui/src/catalog/Row.ts
- packages/genui/a2ui/src/core/useAction.ts
- packages/genui/a2ui/src/catalog/Column.ts
- packages/genui/a2ui/src/catalog/CheckBox.ts
- packages/genui/a2ui/src/core/index.ts
- packages/genui/a2ui/src/utils/ComponentRegistry.ts
- packages/genui/a2ui/src/core/useDataBinding.ts
- packages/genui/a2ui/src/core/A2UIRender.tsx
- packages/genui/a2ui/src/chat/Conversation.tsx
- packages/genui/a2ui/src/utils/createResource.ts
- packages/genui/a2ui/src/chat/useLynxClient.ts
- packages/genui/a2ui/src/core/BaseClient.ts
✅ Files skipped from review due to trivial changes (8)
- packages/genui/a2ui/src/react/useCatalog.ts
- packages/genui/a2ui-playground/lynx-src/tsconfig.json
- packages/genui/a2ui-playground/examples/README.md
- packages/genui/a2ui/src/react/useA2UIContext.ts
- packages/genui/a2ui-playground/examples/io-mock/mockAgent.ts
- packages/genui/a2ui/src/react/index.ts
- packages/genui/a2ui/test/catalog.test.ts
- packages/genui/a2ui/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (13)
- packages/genui/a2ui/src/store/index.ts
- packages/genui/a2ui/test/processor.test.ts
- packages/genui/a2ui/src/catalog/index.ts
- packages/genui/a2ui/src/store/MessageProcessor.ts
- packages/genui/a2ui-playground/examples/io-sse/sseAgent.ts
- packages/genui/a2ui/src/react/useAction.ts
- packages/genui/a2ui-playground/lynx-src/App.tsx
- packages/genui/a2ui/src/catalog/README.md
- packages/genui/a2ui/src/store/types.ts
- packages/genui/a2ui/package.json
- packages/genui/a2ui/src/react/A2UI.tsx
- packages/genui/a2ui/test/createResource.test.ts
- packages/genui/a2ui/src/store/Resource.ts
3b57ae5 to
8c6ecfa
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx (1)
239-293: ⚡ Quick winTwo small effect cleanups: drop the write-only
storeRef, and resetstoreon teardown.
storeRefis declared at Line 239, written at Line 278 and Line 290, but never read anywhere in the file — it's dead state. Either remove it, or actually use it (e.g., for the cleanup's stop bookkeeping if you intended to access the previous store).Cleanup nulls
agentRef.currentbut doesn't reset thestorestate. WheneffectiveDatachanges, the old<A2UI>keeps rendering against the previousMessageStoreuntil the newrun()finishes itsawait Promise.all(...)and callssetStore(next). In that window the user sees stale content and any clicks are silently dropped becauseonActionforwards toagentRef.current?.onAction(action)which is nownull. CallingsetStore(null)in cleanup makes the loading state honest and avoids the lost-action footgun.♻️ Proposed cleanup
- const storeRef = useRef<MessageStore | null>(null); const agentRef = useRef<ReturnType<typeof createMockAgent> | null>(null); const [store, setStore] = useState<MessageStore | null>(null); @@ agentRef.current?.stop(); - storeRef.current = next; agentRef.current = agent; setStore(next); @@ return () => { cancelled = true; agentRef.current?.stop(); - storeRef.current = null; agentRef.current = null; + setStore(null); };🤖 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 239 - 293, storeRef is write-only and unused, and the effect cleanup doesn't reset the React state store causing stale UI and dropped actions; remove the unused storeRef (delete the declaration storeRef and the assignments to storeRef.current in run and teardown) and update the effect cleanup to call setStore(null) (in addition to agentRef.current?.stop() and nulling agentRef) so the component shows the loading/empty state while the new run() loads; keep existing agent creation via createMessageStore() and agentRef handling (createMockAgent, agent.start/stop) intact.
🤖 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.
Nitpick comments:
In `@packages/genui/a2ui-playground/lynx-src/a2ui/App.tsx`:
- Around line 239-293: storeRef is write-only and unused, and the effect cleanup
doesn't reset the React state store causing stale UI and dropped actions; remove
the unused storeRef (delete the declaration storeRef and the assignments to
storeRef.current in run and teardown) and update the effect cleanup to call
setStore(null) (in addition to agentRef.current?.stop() and nulling agentRef) so
the component shows the loading/empty state while the new run() loads; keep
existing agent creation via createMessageStore() and agentRef handling
(createMockAgent, agent.start/stop) intact.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 05ad8bc4-f7a6-4540-8308-a9e2a5812f98
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (49)
packages/genui/a2ui-playground/examples/README.mdpackages/genui/a2ui-playground/examples/io-mock/mockAgent.tspackages/genui/a2ui-playground/examples/io-sse/sseAgent.tspackages/genui/a2ui-playground/lynx-src/a2ui/App.tsxpackages/genui/a2ui-playground/lynx-src/tsconfig.jsonpackages/genui/a2ui/package.jsonpackages/genui/a2ui/src/catalog/Button.tspackages/genui/a2ui/src/catalog/Card.tspackages/genui/a2ui/src/catalog/CheckBox.tspackages/genui/a2ui/src/catalog/Column.tspackages/genui/a2ui/src/catalog/Divider.tspackages/genui/a2ui/src/catalog/Image.tspackages/genui/a2ui/src/catalog/List.tspackages/genui/a2ui/src/catalog/README.mdpackages/genui/a2ui/src/catalog/RadioGroup.tspackages/genui/a2ui/src/catalog/Row.tspackages/genui/a2ui/src/catalog/Text.tspackages/genui/a2ui/src/catalog/all.tspackages/genui/a2ui/src/catalog/index.tspackages/genui/a2ui/src/chat/Conversation.tsxpackages/genui/a2ui/src/chat/index.tspackages/genui/a2ui/src/chat/useLynxClient.tspackages/genui/a2ui/src/core/A2UIRender.tsxpackages/genui/a2ui/src/core/BaseClient.tspackages/genui/a2ui/src/core/ComponentRegistry.tspackages/genui/a2ui/src/core/index.tspackages/genui/a2ui/src/core/types.tspackages/genui/a2ui/src/core/useAction.tspackages/genui/a2ui/src/core/useDataBinding.tspackages/genui/a2ui/src/index.tspackages/genui/a2ui/src/react/A2UI.tsxpackages/genui/a2ui/src/react/A2UIProvider.tsxpackages/genui/a2ui/src/react/A2UIRenderer.tsxpackages/genui/a2ui/src/react/index.tspackages/genui/a2ui/src/react/useA2UIContext.tspackages/genui/a2ui/src/react/useAction.tspackages/genui/a2ui/src/react/useCatalog.tspackages/genui/a2ui/src/react/useDataBinding.tspackages/genui/a2ui/src/store/MessageProcessor.tspackages/genui/a2ui/src/store/Resource.tspackages/genui/a2ui/src/store/SignalStore.tspackages/genui/a2ui/src/store/index.tspackages/genui/a2ui/src/store/types.tspackages/genui/a2ui/src/utils/ComponentRegistry.tspackages/genui/a2ui/src/utils/createResource.tspackages/genui/a2ui/src/utils/index.tspackages/genui/a2ui/test/catalog.test.tspackages/genui/a2ui/test/createResource.test.tspackages/genui/a2ui/test/processor.test.ts
💤 Files with no reviewable changes (24)
- packages/genui/a2ui/src/catalog/all.ts
- packages/genui/a2ui/src/catalog/Image.ts
- packages/genui/a2ui/src/catalog/Row.ts
- packages/genui/a2ui/src/chat/index.ts
- packages/genui/a2ui/src/core/ComponentRegistry.ts
- packages/genui/a2ui/src/catalog/Button.ts
- packages/genui/a2ui/src/catalog/List.ts
- packages/genui/a2ui/src/utils/index.ts
- packages/genui/a2ui/src/core/index.ts
- packages/genui/a2ui/src/utils/ComponentRegistry.ts
- packages/genui/a2ui/src/catalog/Column.ts
- packages/genui/a2ui/src/core/types.ts
- packages/genui/a2ui/src/catalog/Card.ts
- packages/genui/a2ui/src/catalog/CheckBox.ts
- packages/genui/a2ui/src/catalog/RadioGroup.ts
- packages/genui/a2ui/src/utils/createResource.ts
- packages/genui/a2ui/src/core/A2UIRender.tsx
- packages/genui/a2ui/src/core/useAction.ts
- packages/genui/a2ui/src/catalog/Text.ts
- packages/genui/a2ui/src/chat/useLynxClient.ts
- packages/genui/a2ui/src/catalog/Divider.ts
- packages/genui/a2ui/src/chat/Conversation.tsx
- packages/genui/a2ui/src/core/useDataBinding.ts
- packages/genui/a2ui/src/core/BaseClient.ts
✅ Files skipped from review due to trivial changes (3)
- packages/genui/a2ui-playground/examples/README.md
- packages/genui/a2ui-playground/lynx-src/tsconfig.json
- packages/genui/a2ui/test/catalog.test.ts
🚧 Files skipped from review as they are similar to previous changes (19)
- packages/genui/a2ui/src/react/A2UIProvider.tsx
- packages/genui/a2ui/src/react/index.ts
- packages/genui/a2ui/src/react/useA2UIContext.ts
- packages/genui/a2ui/src/react/useCatalog.ts
- packages/genui/a2ui/src/store/index.ts
- packages/genui/a2ui-playground/examples/io-mock/mockAgent.ts
- packages/genui/a2ui/src/store/Resource.ts
- packages/genui/a2ui-playground/examples/io-sse/sseAgent.ts
- packages/genui/a2ui/src/store/types.ts
- packages/genui/a2ui/src/react/A2UIRenderer.tsx
- packages/genui/a2ui/src/react/useAction.ts
- packages/genui/a2ui/test/createResource.test.ts
- packages/genui/a2ui/src/store/MessageProcessor.ts
- packages/genui/a2ui/src/index.ts
- packages/genui/a2ui/package.json
- packages/genui/a2ui/test/processor.test.ts
- packages/genui/a2ui/src/catalog/README.md
- packages/genui/a2ui/src/react/useDataBinding.ts
- packages/genui/a2ui/src/react/A2UI.tsx
8c6ecfa to
1b63b1c
Compare
c204e52 to
970ad4d
Compare
Build the public React surface on top of the message buffer + surface
processor introduced in the previous PR. The renderer is headless:
it ships no styles or chrome, and consumers wrap surfaces themselves.
- `<A2UI>`: all-in-one component that owns a `MessageProcessor` per
mount, subscribes to the developer's `MessageStore`, processes new
tail messages each render, and renders the most recent surface.
- `<A2UIRenderer>` / `NodeRenderer`: lower-level building blocks for
consumers that want manual control over surface lifecycle.
- `<A2UIProvider>`: internal context carrying the active processor
+ catalog map.
- `useAction` / `useDataBinding` / `useCatalog`: hooks the catalog
components reach for; resolve actions, data bindings, and catalog
lookups against the current provider.
Catalog migration:
- Built-in components (`Text`, `Button`, `Card`, `Column`, `List`,
`Row`, `CheckBox`, `RadioGroup`, `Image`, `Divider`) move from
`core/A2UIRender` + global `componentRegistry` to the new
`react/A2UIRenderer`. Side-effect re-exports (`<Name>.ts`,
`all.ts`) and the global registry are dropped — every consumer
composes via `defineCatalog([...])`.
- New public surface in `src/index.ts`; `package.json` exports
trimmed accordingly (`./core`, `./chat`, `./catalog/all` removed;
`./catalog/<Name>/catalog.json` subpaths kept for the manifest
imports).
Cleanup:
- Delete `src/core/` (BaseClient, A2UIRender, ComponentRegistry,
processor, types, useAction, useDataBinding) — replaced by the
new layered design.
- Delete `src/chat/` (Conversation, useLynxClient) — now lives as
an example pattern in the playground README, not as a package
export.
- Delete `src/utils/` (ComponentRegistry, SignalStore,
createResource) — replaced by the typed `src/store/` equivalents.
Playground:
- `lynx-src/App.tsx` switches from the old `BaseClient` +
`A2UIRender` flow to `<A2UI messageStore={...} catalogs={...}>`
with the per-component manifest tuple form.
- `examples/io-mock/` and `examples/io-sse/` show how to push raw
protocol messages into a `MessageStore` from a developer-owned IO
module (mock + SSE transports).
- `lynx-src/tsconfig.json` enables `resolveJsonModule` so the
catalog manifest imports resolve.
Stacks on top of `feat(a2ui): add headless message buffer and surface
processor`. Once that lands and main rebases here, the diff is just
the React layer + catalog migration + playground.
970ad4d to
c60c802
Compare
Summary
The React surface that consumes the message buffer + surface processor.
<A2UI>— all-in-one component owning aMessageProcessorper mount, subscribing to a developer-suppliedMessageStore, processing tail messages each render, rendering the most recent surface.<A2UIRenderer>/NodeRenderer— lower-level building blocks for consumers that want manual control over surface lifecycle.<A2UIProvider>+useAction/useDataBinding/useCatalog— internal context + hooks the catalog components reach for.Catalog migration:
Text,Button,Card,Column,List,Row,CheckBox,RadioGroup,Image,Divider) move fromcore/A2UIRender+ globalcomponentRegistryto the newreact/A2UIRenderer. Side-effect re-exports (<Name>.ts,all.ts) and the global registry are dropped — every consumer composes viadefineCatalog([...]).src/index.tsis the new public surface;package.jsonexports trimmed (./core,./chat,./catalog/allremoved).Cleanup: delete
src/core/,src/chat/,src/utils/(replaced by the new layered design).Playground:
lynx-src/App.tsxswitches fromBaseClient+A2UIRenderto<A2UI messageStore={...} catalogs={...}>with the per-component manifest tuple form.examples/io-mock/andexamples/io-sse/show how to push raw protocol messages into aMessageStorefrom a developer-owned IO module.lynx-src/tsconfig.jsonenablesresolveJsonModulefor the catalog manifest imports.Stacked on
PR-α: #2572 — feat(a2ui): add headless message buffer and surface processor.
The first commit on this branch is PR-α's content; once that lands and main is rebased into this branch, only the React-layer commit remains. Reviewers should focus on commit 2 (`refactor(a2ui): React renderer + headless component`) — that's the PR-β scope.
Test plan
Summary by CodeRabbit
New Features
A2UIReact component for rendering agent messages with flexible catalog compositionDocumentation