Skip to content

chore: pick gen-ui exploration code#2471

Merged
PupilTong merged 13 commits intolynx-family:mainfrom
PupilTong:p/hw/pick-gen-ui
Apr 20, 2026
Merged

chore: pick gen-ui exploration code#2471
PupilTong merged 13 commits intolynx-family:mainfrom
PupilTong:p/hw/pick-gen-ui

Conversation

@PupilTong
Copy link
Copy Markdown
Collaborator

@PupilTong PupilTong commented Apr 17, 2026

Summary by CodeRabbit

  • New Features

    • New UI package offering A2UI rendering, streaming client, component registry, data binding, action handling, and utilities
    • Many catalog components added (buttons, cards, text, images, rows/columns/lists, divider, checkbox, radio group) plus a chat Conversation UI and client hook
  • Documentation

    • Initial package README and Apache-2.0 license added
  • Style

    • New theme tokens and comprehensive light/dark component styles
  • Chores

    • Project/tsconfig and workspace entries updated; CODEOWNERS entry added

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 Apr 17, 2026

⚠️ No Changeset found

Latest commit: 8e89ff2

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 Apr 17, 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

Walkthrough

Added a new package packages/genui/a2ui implementing an A2UI runtime: core types/processor/client/renderer, catalog components and styles, chat UI and hook, data-binding/action hooks, utility stores/registries, TypeScript configs, workspace entries, license, README, and CODEOWNERS.

Changes

Cohort / File(s) Summary
Package metadata & workspace
packages/genui/a2ui/LICENSE, packages/genui/a2ui/README.md, packages/genui/a2ui/package.json, packages/genui/a2ui/tsconfig.json, packages/genui/tsconfig.json, pnpm-workspace.yaml, tsconfig.json, CODEOWNERS
Added Apache-2.0 license, README, package manifest, TS project configs, workspace inclusion, and CODEOWNERS entry.
Core types & runtime
packages/genui/a2ui/src/core/types.ts, .../processor.ts, .../BaseClient.ts, .../ComponentRegistry.ts, .../index.ts
Introduced core A2UI types, MessageProcessor singleton, SSE BaseClient, component registry, and core barrel exports. Heavy protocol and processing logic in processor.ts and BaseClient.ts.
Renderer & node runtime
packages/genui/a2ui/src/core/A2UIRender.tsx
Added A2UIRender and NodeRenderer implementations that subscribe to resources, resolve components via registry, and render surface/component updates.
Hooks & integrations
packages/genui/a2ui/src/core/useAction.ts, packages/genui/a2ui/src/core/useDataBinding.ts
Added useAction (resolve and dispatch user actions) and useDataBinding/useResolvedProps (signal-backed bindings and resolved props setters).
Chat UI & client hook
packages/genui/a2ui/src/chat/Conversation.tsx, .../useLynxClient.ts, .../index.ts
Added Conversation component (controlled or URL-backed) and useLynxClient hook built on BaseClient to manage messages and streaming lifecycle.
Utilities
packages/genui/a2ui/src/utils/ComponentRegistry.ts, .../SignalStore.ts, .../createResource.ts, .../index.ts
New generic ComponentRegistry, SignalStore (cached signals + batch updates), createResource async resource helper, and utils barrel.
Catalog registration & aggregator
packages/genui/a2ui/src/catalog/*.ts, packages/genui/a2ui/src/catalog/all.ts, packages/genui/a2ui/src/catalog/index.ts
Per-component registration modules that register components to the registry and consolidated catalog entrypoints.
Catalog components — layout & composition
packages/genui/a2ui/src/catalog/Row/*, packages/genui/a2ui/src/catalog/Column/*, packages/genui/a2ui/src/catalog/Card/*
Added Row, Column, Card: JSON schemas, TSX implementations handling children/templates and dataContextPath, and CSS styles.
Catalog components — content & visuals
packages/genui/a2ui/src/catalog/Text/*, packages/genui/a2ui/src/catalog/Image/*, packages/genui/a2ui/src/catalog/Divider/*
Added Text, Image (with error fallback), Divider: schemas, components, and styles.
Catalog components — interactive
packages/genui/a2ui/src/catalog/Button/*, packages/genui/a2ui/src/catalog/CheckBox/*, packages/genui/a2ui/src/catalog/RadioGroup/*
Added Button, CheckBox, RadioGroup: schemas, components wired to actions/state, CSS, and registration files.
Catalog components — lists / data-driven
packages/genui/a2ui/src/catalog/List/*
Added List supporting static children or template-driven dynamic children with per-item dataContextPath and data-binding integration, plus styles.
Theming: Luna & Lunaris
packages/genui/a2ui/src/catalog/luna-styles/index.css, .../luna-*.css, .../lunaris-*.css
Added four theme CSS files defining design tokens and an index that imports them.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • Sherry-hue
  • HuJean
  • gaoachao

Poem

🐇 I hopped through code with whiskers bright,
Components nested, signals alight,
Messages streaming, themes in tune,
Buttons, lists, and cards in bloom,
A tiny rabbit cheers this night!

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'chore: pick gen-ui exploration code' accurately summarizes the main changeset, which adds a new @lynx-js/a2ui-reactlynx package with GenUI catalog components, chat utilities, and core infrastructure for A2UI rendering.

✏️ 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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 17, 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!

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: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (16)
packages/genui/a2ui/src/utils/createResource.ts-13-33 (1)

13-33: ⚠️ Potential issue | 🟡 Minor

'error' status is unreachable — no rejection path exposed.

reject is never captured from the Promise executor and there is no fail/error method on the returned Resource. The .catch branch (lines 29–33) and the case 'error' arm in read() therefore can never fire. Either:

  • expose a fail(err: unknown) method symmetric with complete, or
  • remove the unreachable error handling to avoid implying behavior that doesn't exist.

Given BaseClient consumes streamed processor events that can plausibly fail, exposing an explicit failure path is probably what you want.

🛠 Suggested fix: expose `fail`
 export interface Resource<T = unknown> {
   id: string;
   readonly completed: boolean;
   read: () => T;
   complete: (result: T) => void;
+  fail: (error: unknown) => void;
   onUpdate: (callback: (result: T) => void) => () => void;
   promise: Promise<T>;
 }

 export function createResource<T = unknown>(id: string): Resource<T> {
   let resolve: (value: T) => void;
-  const mockFn = new Promise<T>((_resolve) => {
-    resolve = _resolve;
-  });
+  let reject: (reason: unknown) => void;
+  const mockFn = new Promise<T>((_resolve, _reject) => {
+    resolve = _resolve;
+    reject = _reject;
+  });
@@
+    fail: (err: unknown) => {
+      if (status === 'pending') {
+        reject(err);
+      }
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/utils/createResource.ts` around lines 13 - 33, The
createResource function currently never exposes a rejection path so the 'error'
status and the .catch branch in the internal promise (and the 'error' arm in
read()) are unreachable; modify createResource to capture the Promise executor's
reject function (in addition to resolve) and add a fail(err: unknown) method
alongside complete that (1) calls the captured reject with err, (2) sets status
= 'error' and error = err, and (3) notifies any listeners of the failure as
appropriate so consumers (e.g., BaseClient) can trigger the .catch branch and
the read() 'error' case.
packages/genui/a2ui/src/catalog/List/catalog.json-38-46 (1)

38-46: ⚠️ Potential issue | 🟡 Minor

Schema declares align, but the component never reads it.

packages/genui/a2ui/src/catalog/List/index.tsx only destructures children, surface, dataContextPath, and direction from props — align is advertised in the catalog schema but has no effect. Either implement align (e.g., apply it via class name / style) in List/index.tsx, or remove it from the schema until it's wired up, otherwise consumers will set it expecting layout changes that never happen.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/List/catalog.json` around lines 38 - 46, The
catalog advertises an align prop but List/index.tsx does not use it; update the
List component to accept and apply the align prop (e.g., add align to the props
destructuring in List/index.tsx and translate it into a CSS class or inline
style on the root container to control cross-axis alignment) or remove the align
entry from packages/genui/a2ui/src/catalog/List/catalog.json; to fix, modify
List/index.tsx (where children, surface, dataContextPath, and direction are
destructured) to also destructure align and map its values
("start","center","end","stretch") to the appropriate className or style so the
advertised prop has effect.
packages/genui/a2ui/src/catalog/Row/catalog.json-31-41 (1)

31-41: ⚠️ Potential issue | 🟡 Minor

Same stretch mismatch as Column.

Row.justify enum contains "stretch", but the rendered class distribution-stretch has no rule in style.css (only distribution-start/center/end/spaceBetween/spaceAround/spaceEvenly are defined), and justify-content: stretch is not a valid flex value. Remove "stretch" from justify or add the missing CSS class to keep the schema and renderer consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Row/catalog.json` around lines 31 - 41,
Row.justify currently includes "stretch", but the renderer uses a CSS class
distribution-stretch which has no rule and justify-content: stretch is invalid;
remove "stretch" from the Row.justify enum in catalog.json so the schema matches
the defined classes
(distribution-start/center/end/spaceBetween/spaceAround/spaceEvenly) and avoid
producing an unsupported justify value, or alternatively add a valid CSS rule
named distribution-stretch in style.css that maps to a legal flex behavior if
you intentionally want a supported option; update either catalog.json (remove
"stretch") or style.css (add valid distribution-stretch rule) to keep schema and
renderer consistent.
packages/genui/a2ui/package.json-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

"private" should be a boolean, not a string.

"private": "true" is a string literal. npm/pnpm expect "private": true (boolean) to mark the package as unpublishable. The string form is currently truthy in most tooling, but it's not the documented contract and may not be honored consistently.

Proposed fix
-  "private": "true",
+  "private": true,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/package.json` at line 4, The package.json currently sets
the "private" field as a string ("private": "true"); change it to a boolean by
replacing the string value with the boolean true ("private": true) so npm/pnpm
correctly treat the package as unpublishable and conform to the documented
contract.
packages/genui/a2ui/src/catalog/Column/catalog.json-31-41 (1)

31-41: ⚠️ Potential issue | 🟡 Minor

justify enum includes stretch but no matching CSS class / valid mapping.

The Column component renders distribution-${justify} (see packages/genui/a2ui/src/catalog/Column/index.tsx), and style.css (lines 26-42) defines distribution-start/center/end/spaceBetween/spaceAround/spaceEvenly — there is no distribution-stretch rule. A schema-valid value of "stretch" will produce a class with no styling (and justify-content: stretch is not a standard flex value anyway). Either drop "stretch" from the enum or add the corresponding CSS rule.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Column/catalog.json` around lines 31 - 41,
The schema for the Column `justify` enum includes an unsupported value "stretch"
which produces a non-functional `distribution-stretch` class at render time (the
component builds `distribution-${justify}`); fix by either removing "stretch"
from the `justify` enum in catalog.json or by adding a corresponding CSS rule
for `.distribution-stretch` in the Column stylesheet to map to a valid flex
alignment (e.g., use `align-items: stretch` or an appropriate
`justify-content`/layout behavior) so that `distribution-stretch` has a defined
style; update whichever you choose consistently (catalog.json `justify` enum or
Column `style.css`) and ensure the Column component (index.tsx) continues to
generate `distribution-${justify}` classes without producing a no-op class.
packages/genui/a2ui/src/catalog/Column/index.tsx-22-27 (1)

22-27: ⚠️ Potential issue | 🟡 Minor

Schema permits justify: "stretch" but no matching CSS rule exists.

Column/catalog.json declares "stretch" in the justify enum, yet neither Column/style.css nor Row/style.css defines .distribution-stretch. If the server emits justify: "stretch", the rendered className distribution-stretch will be a no-op and justify-content will fall back to its initial value. Either drop "stretch" from the schema enum or add the corresponding CSS rule.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Column/index.tsx` around lines 22 - 27, The
schema allows justify: "stretch" but no CSS handles the generated class
distribution-stretch, so either remove "stretch" from the Column/catalog.json
justify enum or add a CSS rule for .distribution-stretch (and mirror in
Row/style.css if Rows can also receive "stretch") that sets the appropriate
justify-content value; update the Column component (the justify default/usage
and the className generation that produces `distribution-${justify}`) only if
you choose to change the schema so class names remain consistent with allowed
enum values.
packages/genui/a2ui/src/chat/useLynxClient.ts-33-66 (1)

33-66: ⚠️ Potential issue | 🟡 Minor

Client is created once but url changes are ignored.

getClient depends on url, but the cached clientRef.current is only initialized on first call — subsequent url changes will keep returning the original client bound to the original URL. If the hook is expected to track URL changes, recreate the client when url changes (e.g. useEffect to dispose/replace); otherwise consider documenting that url is captured once.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/chat/useLynxClient.ts` around lines 33 - 66,
getClient currently caches a BaseClient instance in clientRef.current on first
call and ignores later url changes; update the hook to recreate/dispose the
client when url changes by adding a useEffect that watches url (and keepHistory
if relevant), calls any cleanup method on clientRef.current (e.g.,
close/dispose) if present, clears or replaces clientRef.current, and reattaches
the onResourceCreated and onResponseComplete handlers to the new BaseClient so
getClient returns a client bound to the current url; ensure
getClient/useCallback and clientRef management reflect this lifecycle change.
packages/genui/a2ui/src/core/useAction.ts-34-40 (1)

34-40: ⚠️ Potential issue | 🟡 Minor

Falsy-but-valid signal values short-circuit the JSON parse path.

if (!raw) return raw; returns early for legitimate values like 0, false, and "". If the store stringifies primitives as JSON (e.g. "0", "false", '""'), those would skip JSON.parse and leak raw JSON strings to callers. Consider narrowing the guard to raw == null (or typeof raw !== 'string') so only truly absent values bypass the parse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/useAction.ts` around lines 34 - 40, The
early-return guard in useAction.ts incorrectly treats falsy-but-valid values as
absent; change the check around the local `raw` (from `if (!raw) return raw;`)
to only bypass parsing for truly missing/non-string values (e.g. `raw == null`
or `typeof raw !== 'string'`) so that legitimate stringified primitives (like
"0", "false", "" ) still go through `JSON.parse(raw)`; update the logic around
the `signal.value` -> `raw` handling and preserve the existing try/catch that
falls back to returning `raw` on parse errors.
packages/genui/a2ui/src/chat/Conversation.tsx-38-51 (1)

38-51: ⚠️ Potential issue | 🟡 Minor

useLynxClient is always invoked, even in controlled mode.

When callers supply messages/sendMessage, useLynxClient(url ?? '') still runs on every render, creating a BaseClient with an empty base URL on first use. That client is never used in controlled mode but it allocates state, sets up refs, and would attempt requests if accidentally invoked. Consider gating the hook (e.g. split into two sub-components) or making the client creation lazy in useLynxClient so a falsy/empty url doesn't construct a live client.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/chat/Conversation.tsx` around lines 38 - 51, The
Conversation component always calls useLynxClient(url ?? '') even in controlled
mode which creates an unused BaseClient; modify the component so the hook is
only invoked when uncontrolled (url truthy) — either gate the hook call by
moving the uncontrolled logic into a small UncontrolledConversation
sub-component that calls useLynxClient(url) or change useLynxClient to lazily
construct the BaseClient when a real non-empty url is provided; update
references to messages/sendMessage to use props when provided and fall back to
hookResult only when url is present (symbols: Conversation, useLynxClient, url,
messages, sendMessage).
packages/genui/a2ui/src/chat/Conversation.tsx-97-102 (1)

97-102: ⚠️ Potential issue | 🟡 Minor

Non-null assertion on optional resource can crash at runtime.

Message.resource is declared optional, but item.resource! (line 98) will throw if an agent message is pushed without a resource (e.g. early user-visible messages, or errors). Consider guarding:

-                  <A2UIRender
-                    resource={item.resource!}
-                    renderFallback={() => (
-                      <text className='loading-text'>Thinking...</text>
-                    )}
-                  />
+                  {item.resource
+                    ? (
+                      <A2UIRender
+                        resource={item.resource}
+                        renderFallback={() => (
+                          <text className='loading-text'>Thinking...</text>
+                        )}
+                      />
+                    )
+                    : <text className='loading-text'>Thinking...</text>}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/chat/Conversation.tsx` around lines 97 - 102, The
code uses a non-null assertion on the optional Message.resource (item.resource!)
when rendering A2UIRender which can crash if resource is undefined; change the
render so you only call A2UIRender when resource exists (e.g., guard with
if/item.resource conditional or conditional JSX: item.resource ? <A2UIRender
resource={item.resource} renderFallback={...}/> : <text
className='loading-text'>Thinking...</text>), or pass a safe value via optional
chaining and let the fallback handle it; reference symbols: A2UIRender,
Message.resource (item.resource) in Conversation.tsx.
packages/genui/a2ui/src/core/BaseClient.ts-358-410 (1)

358-410: ⚠️ Potential issue | 🟡 Minor

No cleanup path for the opened EventSource.

startStreaming constructs an EventSource but the returned object exposes no way to abort/close it. If the consumer drops the promise (component unmount, user cancels), the SSE connection continues until complete or error, holding a server slot and potentially mutating surface state after the UI has moved on. Consider returning an abort() / AbortController handle alongside startStreaming, and calling eventSource.close() on abort.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/BaseClient.ts` around lines 358 - 410,
startStreaming currently creates an EventSource (via EventSourceImpl and
eventSource) but never exposes a cleanup path; modify startStreaming to return
an object with an abort() method (or an AbortController) that calls
eventSource.close(), removes any attached listeners, and rejects/cleanup the
streaming promise so callers can cancel (e.g., on unmount). Ensure the returned
abort hook is wired into all places that resolve/complete the stream so
eventSource.close() is called on manual abort, on error, and on normal
completion to avoid leaking server connections or mutating state after
cancellation.
packages/genui/a2ui/src/core/BaseClient.ts-342-360 (1)

342-360: ⚠️ Potential issue | 🟡 Minor

User content is streamed over a GET query string.

buildSseParams places text (potentially long user input, userAction JSON context, session IDs) directly into the SSE URL. This has three downsides worth confirming are acceptable for this integration:

  1. Server access logs, proxies, and browser history retain arbitrary user content.
  2. The 2048-char warning (Line 392-395) only warns; it does not truncate or fall back to POST.
  3. sessionId containing auth material would leak through the same channels.

If this endpoint supports POST + SSE (e.g. via fetch streaming or a POST-then-redirect pattern), prefer that for anything user-generated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/BaseClient.ts` around lines 342 - 360, The
send() implementation streams user content in the SSE GET query (built by
buildSseParams) which can leak long or sensitive data; change
startStreaming()/send() to detect sensitive or oversized payloads (e.g.,
presence of sessionId or length > 2048) and fallback to a POST-based streaming
approach (use fetch POST with body and SSE-compatible response or
POST-then-redirect) instead of placing text/userAction/sessionId in the URL;
update buildSseParams to exclude sessionId and large text when called for GET
and ensure send() chooses GET only for safe/short, non-sensitive payloads, or
else switches to the POST streaming path.
packages/genui/a2ui/src/core/A2UIRender.tsx-129-131 (1)

129-131: ⚠️ Potential issue | 🟡 Minor

Error rendering double-prefixes "Error:".

String(new Error('boom')) yields "Error: boom", so the UI displays Error: Error: boom. Render error.message instead.

-  if (error) {
-    return <text>Error: {String(error)}</text>;
-  }
+  if (error) {
+    return <text>Error: {error.message}</text>;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/A2UIRender.tsx` around lines 129 - 131, The UI
prints duplicate "Error:" because A2UIRender.tsx uses String(error) inside the
Error branch; change the rendering in the error handling of the A2UIRender
component to use error.message (with a safe fallback for non-Error values), e.g.
render Error: {error?.message ?? String(error)} so Error objects show a single
"Error: <message>" while still handling other types.
packages/genui/a2ui/src/core/A2UIRender.tsx-81-123 (1)

81-123: ⚠️ Potential issue | 🟡 Minor

loading / error state is stale across resource changes.

When the resource prop changes, useState values persist from the previous resource. If the previous resource resolved (loading=false) and the new one is still pending, the effect will not short-circuit but the initial render will still show non-loading UI until setLoading fires. Similarly, a previous error will render the error UI briefly against the new resource.

Reset loading/error synchronously when resource changes — either by resetting inside the effect's first synchronous pass, or by deriving state from resource via a key/keyed reducer:

🛠️ Suggested change
   useEffect(() => {
     let active = true;
+    setError(null);
+    setLoading(!resource.completed);
 
     if (resource.completed) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/A2UIRender.tsx` around lines 81 - 123, When the
resource prop changes A2UIRender's effect must synchronously reset derived state
to avoid showing stale loading/error; at the start of the useEffect (before
async reads/promises) call setLoading(true) and setError(undefined) (and
optionally setData(undefined) if you want blank UI) so the initial render
reflects the new resource, then proceed with the existing
resource.read()/resource.promise logic and subscription (refer to useEffect,
resource, resource.read, resource.promise, resource.onUpdate, setLoading,
setError, setData, unsubscribe).
packages/genui/a2ui/src/core/BaseClient.ts-420-433 (1)

420-433: ⚠️ Potential issue | 🟡 Minor

Hard-coded 300 ms delay between message batches adds perceptible latency.

processQueue waits MESSAGE_PROCESS_DELAY (300 ms) between every batch, so a response with N SSE deltas takes at least N × 300 ms to render even when processing is instant. Consider making this configurable, using it only when animations need pacing, or using requestAnimationFrame / yielding to the event loop (await Promise.resolve()) instead of a fixed timeout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/BaseClient.ts` around lines 420 - 433,
processQueue currently enforces a hard-coded MESSAGE_PROCESS_DELAY between
batches causing N×300ms latency; change it to use a configurable delay and a
non-blocking default: add a new option/field (e.g., messageProcessDelay) passed
into the class or constructor, use await Promise.resolve() or
requestAnimationFrame() when messageProcessDelay is 0 or "yield" to just yield
to the event loop, and only call setTimeout with MESSAGE_PROCESS_DELAY when the
configured delay > 0 (or when an explicit "paceAnimations" flag is true); update
processQueue, MESSAGE_PROCESS_DELAY references, and any callers to use the new
option and keep using this.processor.processMessages(msgs) unchanged.
packages/genui/a2ui/src/core/useDataBinding.ts-118-124 (1)

118-124: ⚠️ Potential issue | 🟡 Minor

Effect re-creates on every render due to properties identity changes.

useResolvedProps is invoked from NodeRenderer with component (the full ComponentInstance) as properties (see A2UIRender.tsx Line 186). component is a React state value that is replaced with a fresh { ...dataMap['component'] } on every surface update, and anything that recreates NodeRenderer's parent produces a new object reference. Each render with a new reference tears down the signal effect subscription and re-runs resolveProperties synchronously, defeating the benefit of reactive tracking and causing extra renders.

Consider memoizing properties upstream or keying the effect on a stable signature (e.g. JSON.stringify(properties) or the set of bound paths) rather than referential identity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/useDataBinding.ts` around lines 118 - 124, The
effect in useDataBinding (the useEffect that creates the signal effect ->
setResolved(resolveProperties(...))) is being re-created on every render because
the `properties` object identity changes; fix by depending on a stable signature
instead of the raw object or by memoizing upstream: either memoize `properties`
in the caller (e.g. NodeRenderer/A2UIRender.tsx) so the same object reference is
reused, or change the dependency for that useEffect to a stable key derived from
`properties` (for example a JSON.stringify(properties) or a computed set of
bound paths) so the effect only re-runs when actual property content changes;
keep references to `surface` and `dataContextPath` as before and leave
`resolveProperties`, `setResolved`, and `effect` usage unchanged.
🧹 Nitpick comments (29)
packages/genui/a2ui/src/catalog/Text/catalog.json (1)

1-41: Minor: consider additionalProperties: false on the Text root.

The inner path-binding object enforces additionalProperties: false, but the Text schema itself does not, so unknown props (e.g. typos like varient) silently validate. If this catalog is meant to be authoritative for component prop validation, tightening the root to reject unknown properties would catch authoring mistakes early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Text/catalog.json` around lines 1 - 41, The
Text schema currently allows unknown root properties which lets typos like
"varient" pass; update the "Text" object in catalog.json to add
"additionalProperties": false at the same level as "properties" and "required"
so the root schema rejects unknown props (affects the "Text" schema that defines
"text" and "variant" and the nested path-binding object).
packages/genui/a2ui/src/utils/createResource.ts (1)

53-60: complete after success silently bypasses the resolved promise.

Once status === 'success', subsequent complete(res) calls update the closed-over result and notify listeners, but the promise has already resolved with the prior value and read() returns the new result synchronously. That asymmetry means consumers awaiting resource.promise see the first value, while consumers calling resource.read() (or subscribing via onUpdate) see later values — easy to misuse.

Worth either:

  • documenting "first complete resolves; later complete calls are streaming updates", or
  • splitting the API into complete(res) (terminal) and update(res) (streaming) for clarity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/utils/createResource.ts` around lines 53 - 60, The
current complete function (complete) conflates terminal resolution and ongoing
updates: when status === 'success' further complete(res) calls only update the
closed-over result and notify listeners (listeners, result) but do not resolve
the original promise (promise) again, causing inconsistent behavior between
await resource.promise and resource.read()/onUpdate subscribers. Fix by
splitting responsibilities: make complete(res) perform the one-time terminal
resolve of promise and set status to 'success', and add a new update(res) method
that sets result and notifies listeners without touching the promise; update any
call sites that intend streaming updates to use update, and update
read()/onUpdate to rely on result/listeners as before to preserve streaming
behavior.
packages/genui/a2ui/src/utils/SignalStore.ts (1)

21-26: Reference equality on update may miss object/array mutations.

s.value !== value will skip the assignment when callers pass back the same object reference with mutated internals (common when consumers do obj.field = x; store.update(path, obj)). For Preact signals this means subscribers won't be notified. If the contract is "value is always replaced by-reference for reactivity", consider documenting it; otherwise drop the equality guard and let Preact's own dedupe handle primitives.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/utils/SignalStore.ts` around lines 21 - 26, The
update method in SignalStore (update(path: string, value: unknown)) currently
uses reference equality (s.value !== value) which misses in-place mutations of
objects/arrays and prevents Preact signals from notifying subscribers; remove
the reference-equality guard so the assignment always runs (i.e., always set
s.value = value) and rely on Preact's own deduping for primitives, or
alternatively document that update requires callers to replace by-reference if
you intentionally keep the guard; locate the logic in the update function and
change it accordingly.
packages/genui/a2ui/src/catalog/List/style.css (1)

1-4: Hard-coded height: 400px on the list container.

A fixed pixel height on a generic catalog component will clip or leave dead space across surfaces with different layouts. Consider exposing height via a prop/CSS custom property, or letting the parent layout drive sizing (e.g., flex: 1 / height: 100%).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/List/style.css` around lines 1 - 4, The .list
CSS rule currently hard-codes height: 400px which breaks flexible layouts;
change .list to accept a CSS custom property and flexible sizing (for example
replace height: 400px with height: var(--list-height, auto); and add optional
flex behavior like flex: 1; or height: 100% so parent layouts can drive size),
and update any consuming components to set --list-height or rely on parent
flexbox sizing as needed.
packages/genui/a2ui/src/catalog/Card.ts (1)

8-8: Unsafe double cast pattern repeated across all catalog components.

The as unknown as ComponentRenderer cast appears in 10+ catalog files (Card, List, Row, Image, Divider, Column, Text, Button, RadioGroup, CheckBox, and others), all using identical code on line 8 or 10. This pattern defeats TypeScript's type checking and could mask runtime prop-shape bugs if component signatures diverge from what ComponentRenderer (which expects {id, surfaceId, surface, component}) actually receives.

Introduce a typed registration adapter (e.g., register<P>(name: string, component: ComponentType<P>)) that internally handles the cast once, eliminating the need for it at each catalog entry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Card.ts` at line 8, The catalog files use an
unsafe "as unknown as ComponentRenderer" double-cast on each component (e.g.,
Card) when calling componentRegistry.register; create a single typed adapter on
componentRegistry such as register<P>(name: string, component: ComponentType<P>)
that internally performs the one safe cast to ComponentRenderer, then replace
calls like componentRegistry.register('Card', Card as unknown as
ComponentRenderer) with componentRegistry.register('Card', Card) and remove the
double-cast from all catalog entries; touch the registry implementation
(componentRegistry.register) to accept the generic ComponentType and coerce once
to ComponentRenderer so callers (Card, List, Row, etc.) no longer bypass
TypeScript checks.
packages/genui/a2ui/src/catalog/Divider/style.css (1)

3-9: Nit: overflow: auto on a 1px divider looks unintended.

A divider with height: 1px (or width: 1px) has no content to scroll; overflow: auto can cause scrollbar artifacts on some renderers. Consider overflow: hidden (or removing it entirely).

Proposed tweak
 .divider {
   display: block;
   min-height: 0;
-  overflow: auto;
+  overflow: hidden;
   background-color: var(--rule);
   border: none;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Divider/style.css` around lines 3 - 9, The
.divider CSS uses overflow: auto which can produce scrollbar artifacts for a 1px
divider; update the .divider rule (class name ".divider") to remove overflow:
auto or replace it with overflow: hidden (or simply omit overflow entirely) so
the 1px/1px divider doesn't trigger scrollbars and remains visually stable.
packages/genui/a2ui/src/catalog/Image.ts (1)

8-8: Consider tightening ComponentRenderer to avoid as unknown as casts.

The double-cast is repeated across every catalog registrar (e.g., Text.ts, Image.ts). If ComponentRenderer were typed to accept function components taking GenericComponentProps, these casts could be removed, giving better type safety at registration sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Image.ts` at line 8, The catalog
registrations use unsafe double-casts like "Image as unknown as
ComponentRenderer"; update the ComponentRenderer type definition so it accepts
React function components that take GenericComponentProps (or a union that
includes FunctionComponent<GenericComponentProps>) and any necessary generics,
then remove the "as unknown as" casts at registration sites (e.g.,
componentRegistry.register('Image', Image) and similar in Text.ts). Locate and
change the ComponentRenderer type declaration (and any central registry typing)
to reference GenericComponentProps and re-run type checks to adjust callers if
needed.
packages/genui/a2ui/src/catalog/Text/index.tsx (2)

9-9: Optional: prefer a named type import over inline import('@lynx-js/react').ReactNode.

A top-level import type { ReactNode } from '@lynx-js/react' is more idiomatic and lets the return type be reused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Text/index.tsx` at line 9, Replace the inline
return type import with a top-level named type import: add a top-level line
importing the ReactNode type (import type { ReactNode } from '@lynx-js/react')
and update the component/function return annotation from
import('@lynx-js/react').ReactNode to simply ReactNode in the Text component (or
the exported function in this file) so the type can be reused and is more
idiomatic.

10-17: Minor: unsafe as string cast and misleading key on a single root element.

  • props['text'] is typed broadly; casting to string via {text as string} hides bugs when a non-string slips through (e.g., undefined or number from a server-driven payload). Prefer String(text ?? '') or a runtime guard.
  • key={id} on a non-list root has no effect; consider dropping it unless a parent expects it for reconciliation.
Proposed tweak
-  const id = props.id;
-  const text = props['text'];
+  const text = props['text'];
   const variant = props['variant'] as string | undefined ?? 'body';

   return (
-    <text key={id} className={`text-${variant}`}>
-      {text as string}
+    <text className={`text-${variant}`}>
+      {typeof text === 'string' ? text : String(text ?? '')}
     </text>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Text/index.tsx` around lines 10 - 17, Replace
the unsafe cast and the misleading key: remove key={id} from the root <text>
element and change the content rendering from {text as string} to a safe runtime
conversion such as {String(text ?? '')} (or validate typeof text === 'string'
and fallback to ''), referencing the id, text and variant variables in this file
(index.tsx) to locate the code to update.
packages/genui/a2ui/src/utils/ComponentRegistry.ts (1)

4-18: Duplicate of BaseComponentRegistry<T> in core/ComponentRegistry.ts.

This class has the exact same shape (Map<string, T> plus register / get / has) as BaseComponentRegistry<T> already defined in packages/genui/a2ui/src/core/ComponentRegistry.ts, and no catalog module imports the utils version — they all import from core/ComponentRegistry.js. Consider either deleting this file (and its export from utils/index.ts) or re-exporting BaseComponentRegistry from core to keep a single source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/utils/ComponentRegistry.ts` around lines 4 - 18, This
file defines ComponentRegistry<T> which is a duplicate of
BaseComponentRegistry<T> in core/ComponentRegistry.ts; remove the duplication by
deleting ComponentRegistry<T> and its export from utils/index.ts, or replace its
export with a re-export of BaseComponentRegistry from core (i.e., export {
BaseComponentRegistry as ComponentRegistry } from '.../core/ComponentRegistry')
so there is a single source of truth; update any imports relying on
ComponentRegistry to continue working via the core export if needed.
packages/genui/a2ui/src/catalog/luna-styles/lunaris-dark.css (1)

19-19: Nit: inconsistent hex casing.

#362E46 uses uppercase while every other hex value in this file is lowercase (#0d0d0d, #ff8ab5, etc.). Lowercase for consistency:

-  --secondary-2: `#362E46`;
+  --secondary-2: `#362e46`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/luna-styles/lunaris-dark.css` at line 19, The
CSS variable --secondary-2 currently uses an uppercase hex literal; change its
value from `#362E46` to lowercase `#362e46` so it matches the file's lowercase hex
casing convention and maintains consistency with other variables like --primary,
--background, etc.
packages/genui/a2ui/src/catalog/Button.ts (1)

8-8: Double cast hides a real type mismatch.

Button as unknown as ComponentRenderer bypasses TypeScript's type checker. ComponentRenderer = ComponentType<ComponentProps>, so if Button actually accepts GenericComponentProps (or similar), prefer making Button's prop type compatible with ComponentProps (e.g., extend it) or widen ComponentRenderer to a broader functional component shape, rather than silently casting through unknown. This same pattern appears in every catalog registration (Divider.ts, etc.); fixing the root types will eliminate the casts everywhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Button.ts` at line 8, The registration uses a
double-cast (Button as unknown as ComponentRenderer) which hides a real props
mismatch; update the Button component props to be assignable to ComponentProps
(e.g., have ButtonProps extend ComponentProps or map GenericComponentProps into
ComponentProps) or adjust the ComponentRenderer type to accept the broader
functional component shape, then remove the unsafe cast in
componentRegistry.register('Button', Button). Apply the same root-type fix for
other catalog entries (e.g., Divider) so registrations can pass types without
casting.
packages/genui/a2ui/src/catalog/Image/style.css (1)

1-38: Generic, unscoped class names risk collisions.

.icon, .avatar, .header, .smallFeature, .mediumFeature, .largeFeature are very common names and live in the global CSS namespace. Since a2ui-image is already used as the namespaced base class, consider either scoping the variants (e.g., .a2ui-image.icon, or .a2ui-image--icon) or renaming them with the a2ui-image- prefix to avoid clashing with consumer styles or other catalog components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Image/style.css` around lines 1 - 38, The CSS
uses generic global class names (.icon, .avatar, .header, .smallFeature,
.mediumFeature, .largeFeature) which can collide; update the stylesheet to scope
or prefix these with the component base (.a2ui-image) — e.g., convert to scoped
selectors like .a2ui-image.icon or BEM-style names like .a2ui-image--icon /
.a2ui-image--avatar / .a2ui-image--header / .a2ui-image--smallFeature /
.a2ui-image--mediumFeature / .a2ui-image--largeFeature and update any JSX/markup
using those classes to the new names so styles remain local to the Image
component.
packages/genui/a2ui/src/catalog/Button/style.css (1)

8-11: Optional: collapse padding to shorthand.

-  padding-left: 12px;
-  padding-right: 12px;
-  padding-top: 8px;
-  padding-bottom: 8px;
+  padding: 8px 12px;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Button/style.css` around lines 8 - 11,
Replace the four separate padding declarations in the CSS rule in
Button/style.css (the block containing padding-left, padding-right, padding-top,
padding-bottom) with a single shorthand padding property (e.g., padding: 8px
12px;) to collapse them into one concise declaration while preserving the same
vertical and horizontal spacing.
packages/genui/a2ui/src/catalog/Image/index.tsx (1)

21-23: Hardcoded external fallback image URL.

The fallback src points to a specific bytednsdoc.com asset. This couples the component to an external CDN (availability, offline use, brand) and isn't configurable per-theme or per-consumer. Consider making the fallback configurable via props/registry or bundling a local asset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Image/index.tsx` around lines 21 - 23, The
component currently hardcodes an external CDN fallback for image rendering
(finalSrc uses that bytednsdoc URL when hasError is true); change this to a
configurable fallback by adding a fallbackSrc prop (or pulling a theme/registry
value) with a sensible default (e.g., a bundled local asset imported into
Image/index.tsx), then update the finalSrc logic to use fallbackSrc when
hasError is true (fall back to url otherwise). Ensure the new prop is optional
with a default value and document/update any callers to pass a custom fallback
if needed.
packages/genui/a2ui/src/catalog/Card/style.css (1)

21-40: Unused Card variants — wire them up via a prop or drop them.

.card-outlined, .card-filled, and .card-ghost are defined here but Card/index.tsx hardcodes className='card card-elevated' (see packages/genui/a2ui/src/catalog/Card/index.tsx:25-27), so these three variants are currently dead CSS. Either expose a variant prop on Card and map it to the class, or remove the unused rules until needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Card/style.css` around lines 21 - 40, Card
has hardcoded className='card card-elevated' so the CSS rules .card-outlined,
.card-filled and .card-ghost are unused; update the Card component
(Card/index.tsx) to accept a variant prop (e.g.
'outlined'|'filled'|'ghost'|'elevated' with default 'elevated'), map that prop
to the corresponding CSS class name (card-outlined, card-filled, card-ghost, or
card-elevated), compose it with the base 'card' class and any incoming className
prop, and forward props as before so the three CSS variants become reachable;
alternatively, if you prefer not to add variants, remove those unused rules from
style.css.
packages/genui/a2ui/src/catalog/Divider/index.tsx (1)

6-13: DividerProps and ComponentProps import appear unused.

Divider is typed as (props: GenericComponentProps), so the locally declared DividerProps interface and the ComponentProps import on line 6 are not referenced. Either drop them or actually type the component with DividerProps for stronger typing of axis/component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Divider/index.tsx` around lines 6 - 13, The
file currently declares DividerProps and imports ComponentProps but the Divider
implementation uses GenericComponentProps, leaving both unused; either remove
the unused ComponentProps import and the local DividerProps interface, or
(preferred) apply the stronger typing by changing the Divider component
signature to use DividerProps (so the component param is typed as DividerProps
or DividerProps & GenericComponentProps if other generic fields are needed),
ensuring axis/component types are enforced and retaining the ComponentProps
import; update any related references and remove any now-unused imports.
packages/genui/a2ui/src/catalog/RadioGroup/style.css (1)

59-63: .label is a very generic global class.

Defining .label { font-size: 16px; color: var(--content); … } inside a component-scoped stylesheet that gets imported globally is risky — any other component in the app rendering an element with class="label" will inherit this typography. Consider scoping it (e.g. .radio-option .label or rename to .radio-option-label) to avoid cross-component bleed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/RadioGroup/style.css` around lines 59 - 63,
The global .label class in style.css can leak styles to other components; scope
or rename it to avoid collisions by changing the selector to a component-scoped
name (for example .radio-option .label or rename to .radio-option-label) and
update any corresponding markup that uses class="label" (e.g., radio option
rendering in the RadioGroup/RadioOption component) so the CSS selector and HTML
class names match; ensure the new selector preserves the existing declarations
(font-size, color, line-height).
packages/genui/a2ui/src/catalog/CheckBox/index.tsx (1)

11-18: CheckBoxProps is declared but never applied.

CheckBoxProps is exported but the component is typed with GenericComponentProps, so the more specific type provides no compile-time guarantees on component/dataContextPath. Either drop the interface or use it as the function parameter type.

Same shape (Props extends ComponentProps { component: v0_9.AnyComponent & { dataContextPath?: string } }) is duplicated in Row/index.tsx and Column/index.tsx; consider hoisting it into a shared types module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/CheckBox/index.tsx` around lines 11 - 18, The
export CheckBoxProps is unused because the CheckBox component is typed with
GenericComponentProps; change the CheckBox function signature to accept props:
CheckBoxProps instead of GenericComponentProps (so the component field and
optional dataContextPath are type-checked), or if you prefer not to specialize
here remove the unused CheckBoxProps export; additionally extract the repeated
type shape (Props extends ComponentProps { component: v0_9.AnyComponent & {
dataContextPath?: string } }) into a shared types module and import it into
CheckBox, Row, and Column to avoid duplication.
packages/genui/a2ui/src/catalog/Text/style.css (1)

60-74: Hard-coded price/link colors bypass the Luna theme.

#ff4d4f and #1890ff won't switch with luna-dark (or any future theme). Since the rest of the file routes through var(--content*), these two should also map to theme tokens (e.g., a --accent/--link token added to the Luna palettes) so dark mode renders correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Text/style.css` around lines 60 - 74, Replace
the hard-coded colors in the .text-price and .text-link rules with theme CSS
variables so they follow Luna palettes and respect luna-dark; specifically,
change color: `#ff4d4f` in .text-price to use a token like var(--accent, `#ff4d4f`)
and change color: `#1890ff` in .text-link to use var(--link, `#1890ff`) (or the
project’s existing --content-* naming if preferred), and ensure the Luna theme
palettes define these --accent / --link (or --content-accent / --content-link)
tokens for light and dark modes so the styles update with the theme.
packages/genui/a2ui/src/catalog/Row/style.css (1)

9-49: alignment-* / distribution-* are duplicated with Column/style.css.

The same ten utility classes are defined in both Row/style.css and Column/style.css. The rules are identical today, so behavior is fine, but any future tweak in one file will silently diverge from the other (and the import order will decide the winner). Move these shared utilities into luna-styles/ (or a dedicated layout.css) and import once.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Row/style.css` around lines 9 - 49, The
alignment-* (alignment-start, alignment-center, alignment-end,
alignment-stretch) and distribution-* (distribution-start, distribution-center,
distribution-end, distribution-spaceBetween, distribution-spaceAround,
distribution-spaceEvenly) utility classes are duplicated in Row/style.css and
Column/style.css; move these shared rules into a single shared stylesheet (e.g.,
luna-styles/layout.css or luna-styles/utilities.css), import that shared file
once (from your global styles entry or the Row/Column components as
appropriate), and remove the duplicated rules from Row/style.css and
Column/style.css so the classes are defined in one place and consumed via
import.
packages/genui/a2ui/src/core/BaseClient.ts (2)

18-21: randomId uses Math.random — collisions possible under load.

Using Date.now() + Math.random().slice(2,10) produces ~40 bits of entropy and is fine for local/dev use, but for production task IDs (which key this.resources/this.resolves), consider crypto.randomUUID() where available, falling back to the current implementation. Collisions here would cause a later request to overwrite an earlier resource mapping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/BaseClient.ts` around lines 18 - 21, randomId
currently uses Date.now() + Math.random() which has limited entropy and can
cause collisions that overwrite entries in this.resources / this.resolves;
update the randomId function to prefer crypto.randomUUID() when available
(browser or Node crypto) and fall back to the existing Date.now()+Math.random()
construction if not, so callers like the BaseClient resource/resolves keys get a
collision-resistant UUID without changing call sites.

372-381: Obfuscated EventSource lookup hurts readability.

const tsKey = 'Event' + 'Source';
const NativeES = g[tsKey] as ;

If this string concat is to evade a bundler/static analyzer, please leave a comment explaining why; otherwise use globalThis.EventSource directly — the split-string trick is surprising and looks like dead code to a reader.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/BaseClient.ts` around lines 372 - 381, The
concatenated string lookup (tsKey) and indirection (NativeES) make the
EventSource resolution in BaseClient.ts hard to read; replace the obfuscated
lookup with a direct reference to globalThis.EventSource when building
EventSourceImpl (i.e., remove tsKey/NativeES and use globalThis.EventSource or
window.EventSource explicitly), or if the split-string trick is intentional to
avoid bundler/static-analysis behavior, add a concise comment above tsKey
explaining that intent and why it's required; update references to
EventSourceImpl accordingly.
packages/genui/a2ui/src/core/A2UIRender.tsx (1)

39-41: Avoid inline import('…') type casts.

These import('@lynx-js/react').ReactNode uses inline-import the already-imported ReactNode type. You imported ReactNode on Line 5 — use it directly for readability and to avoid duplicating the type source.

-    const Component = componentRegistry.get(tag)! as unknown as (
-      props: Record<string, unknown>,
-    ) => import('@lynx-js/react').ReactNode;
+    const Component = componentRegistry.get(tag)! as unknown as (
+      props: Record<string, unknown>,
+    ) => ReactNode;

Same for Lines 64, 66, 166.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/A2UIRender.tsx` around lines 39 - 41, The code
uses inline import type casts like import('@lynx-js/react').ReactNode when
typing components retrieved from componentRegistry (e.g., the Component variable
in A2UIRender.tsx); replace those inline import('…') types with the
already-imported ReactNode type (remove import('…').ReactNode and use ReactNode
directly) and apply the same fix to the other occurrences referenced in this
file (the similar casts around the component retrieval and the two other spots
noted).
packages/genui/a2ui/src/core/processor.ts (3)

119-119: JSON.parse(JSON.stringify(...)) deep clone is fragile.

This silently drops undefined, functions, Date objects, and throws on cycles. For the bounded shape of ComponentInstance (JSON-serializable) it is probably fine today, but consider structuredClone(original) for a safer/faster alternative on modern runtimes, or a small explicit clone helper that preserves the documented ComponentInstance fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/processor.ts` at line 119, Replace the fragile
JSON.parse(JSON.stringify(original)) cloning in processor.ts (where cloned is
created as a ComponentInstance from original) with a safer clone: use
structuredClone(original) on modern runtimes, or introduce a small explicit
deepCloneComponentInstance helper that copies documented ComponentInstance
fields (preserving Dates, functions if needed, and handling cycles) and return
that result assigned to cloned; ensure the new implementation still
asserts/returns a ComponentInstance type and falls back to the explicit helper
when structuredClone is unavailable.

253-303: Child-template detection is brittle and hand-rolled.

The anyInstance['children'] shape check (Lines 269-280) inlines 12 lines of narrowing. Factor it into a small isTemplateRef(value): value is { componentId: string; path: string } helper — it's used again in updateDataModel (Line 369-371) and would make both call sites easier to audit.

Also, !Array.isArray(anyInstance['children']) combined with typeof … === 'object' && … !== null already implies non-array object; the explicit !Array.isArray check is redundant once typeof … === 'object' narrows, but keeping it for clarity is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/processor.ts` around lines 253 - 303, The
child-template detection logic is duplicated and brittle inside the
updateComponents block; extract the inline narrowing into a small type-guard
helper isTemplateRef(value): value is { componentId: string; path: string } and
use it from both the updateComponents loop (where you currently check
anyInstance['children']) and the updateDataModel call site; implement the helper
to safely check for non-null object shape and string-typed componentId and path,
then replace the 12-line inline check with a call to isTemplateRef and preserve
the same use of resolvePath, instance.__template assignment, and existing
casting/typing around children and dataContextPath so behavior is unchanged.

456-456: Module-level mutable singleton.

export const processor: MessageProcessor = new MessageProcessor() means every consumer of processor.ts shares state, which prevents multiple A2UI runtimes on the same page and complicates testing (state leaks across tests). Consider exposing a factory alongside the default singleton, or wiring the processor through React context so tests can substitute a fresh instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/processor.ts` at line 456, The module currently
exports a shared singleton (export const processor: MessageProcessor = new
MessageProcessor()) which leaks state; change the API to export a factory (e.g.,
export function createProcessor(): MessageProcessor) that returns new
MessageProcessor() and keep an optional default instance (export const processor
= createProcessor()) only for convenience; update consumers to accept a
processor via props or React context (provide a ProcessorContext) so tests can
inject createProcessor() instances and components can use the default singleton
when wiring is not provided. Ensure MessageProcessor remains unchanged and
update tests to call createProcessor() to get isolated instances.
packages/genui/a2ui/src/core/useDataBinding.ts (1)

96-104: Redundant else if / else branches.

Both branches assign result[key] = prop; — fold them into a single else, or drop the type-check branch entirely:

-    } else if (
-      typeof prop === 'string'
-      || typeof prop === 'number'
-      || typeof prop === 'boolean'
-    ) {
-      result[key] = prop;
-    } else {
-      result[key] = prop;
-    }
+    } else {
+      result[key] = prop;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/useDataBinding.ts` around lines 96 - 104, The
branches in useDataBinding.ts that check typeof prop and then assign result[key]
= prop in both the else-if and else are redundant; simplify by removing the
type-check branch and replace the entire conditional with a single assignment to
result[key] = prop (or collapse to one else block) so only one code path assigns
the value; update the block that references prop, result, and key accordingly.
packages/genui/a2ui/src/core/types.ts (1)

32-33: Nit: redundant | null with optional marker.

rootComponentId?: string | null means string | null | undefined. If null is semantically meaningful (processor.ts explicitly assigns null), keep it; otherwise simplify to rootComponentId?: string. Consumers currently mix both styles (A2UIRender.tsx line 141 uses !, treating it as definitely string).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/core/types.ts` around lines 32 - 33, rootComponentId
is declared as `rootComponentId?: string | null` which yields
string|null|undefined—either remove `| null` and make the type
`rootComponentId?: string` or, if `null` is semantically required, keep `| null`
and make consumers explicit; prefer removing `| null` here: change the type to
`rootComponentId?: string` and update the producer `processor.ts` to assign
undefined instead of null (or stop assigning null), and ensure consumers like
A2UIRender.tsx no longer rely on mixed null semantics.

Comment thread packages/genui/a2ui/package.json
Comment thread packages/genui/a2ui/src/catalog/List/index.tsx
Comment thread packages/genui/a2ui/src/chat/Conversation.tsx
Comment thread packages/genui/a2ui/src/chat/useLynxClient.ts
Comment thread packages/genui/a2ui/src/core/A2UIRender.tsx
Comment thread packages/genui/a2ui/src/core/processor.ts
Comment thread packages/genui/a2ui/src/core/processor.ts
Comment thread packages/genui/a2ui/src/core/useAction.ts
Comment thread packages/genui/a2ui/src/core/useDataBinding.ts
Comment thread packages/genui/a2ui/src/utils/SignalStore.ts Outdated
@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 17, 2026

React MTF Example

#549 Bundle Size — 193.94KiB (0%).

8e89ff2(current) vs 3118b10 main#540(baseline)

Bundle metrics  no changes
                 Current
#549
     Baseline
#540
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 173 173
No change  Duplicate Modules 66 66
No change  Duplicate Code 43.94% 43.94%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#549
     Baseline
#540
No change  IMG 111.23KiB 111.23KiB
No change  Other 82.71KiB 82.71KiB

Bundle analysis reportBranch PupilTong:p/hw/pick-gen-uiProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 17, 2026

React Example

#7416 Bundle Size — 223.33KiB (0%).

8e89ff2(current) vs 3118b10 main#7407(baseline)

Bundle metrics  no changes
                 Current
#7416
     Baseline
#7407
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 179 179
No change  Duplicate Modules 69 69
No change  Duplicate Code 44.48% 44.48%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#7416
     Baseline
#7407
No change  IMG 145.76KiB 145.76KiB
No change  Other 77.58KiB 77.58KiB

Bundle analysis reportBranch PupilTong:p/hw/pick-gen-uiProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 17, 2026

React External

#533 Bundle Size — 580.35KiB (0%).

8e89ff2(current) vs 3118b10 main#524(baseline)

Bundle metrics  no changes
                 Current
#533
     Baseline
#524
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 8.59% 8.59%
No change  Packages 0 0
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#533
     Baseline
#524
No change  Other 580.35KiB 580.35KiB

Bundle analysis reportBranch PupilTong:p/hw/pick-gen-uiProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 17, 2026

Web Explorer

#8990 Bundle Size — 898.09KiB (-0.03%).

8e89ff2(current) vs 3118b10 main#8981(baseline)

Bundle metrics  Change 3 changes
                 Current
#8990
     Baseline
#8981
No change  Initial JS 44.47KiB 44.47KiB
No change  Initial CSS 2.16KiB 2.16KiB
Change  Cache Invalidation 13.64% 0%
No change  Chunks 9 9
No change  Assets 11 11
Change  Modules 229(-0.87%) 231
No change  Duplicate Modules 11 11
Change  Duplicate Code 27.22%(+0.07%) 27.2%
No change  Packages 10 10
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Improvement 1 improvement
                 Current
#8990
     Baseline
#8981
Improvement  JS 494.3KiB (-0.05%) 494.55KiB
No change  Other 401.63KiB 401.63KiB
No change  CSS 2.16KiB 2.16KiB

Bundle analysis reportBranch PupilTong:p/hw/pick-gen-uiProject dashboard


Generated by RelativeCIDocumentationReport issue

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 17, 2026

Merging this PR will not alter performance

✅ 81 untouched benchmarks
⏩ 26 skipped benchmarks1


Comparing PupilTong:p/hw/pick-gen-ui (8e89ff2) with main (3118b10)

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.

Comment thread packages/genui/a2ui/src/catalog/Button/catalog.json
@PupilTong PupilTong self-assigned this Apr 17, 2026
@PupilTong PupilTong requested review from HuJean and fzx2666-fz April 17, 2026 10:17
Comment thread packages/genui/a2ui/src/utils/SignalStore.ts
Comment thread packages/genui/a2ui/src/catalog/luna-styles/index.css
@PupilTong PupilTong requested a review from HuJean April 17, 2026 16:04
HuJean
HuJean previously approved these changes Apr 20, 2026
HuJean
HuJean previously approved these changes Apr 20, 2026
@PupilTong PupilTong merged commit bd7166a into lynx-family:main Apr 20, 2026
76 of 79 checks passed
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.

3 participants