Skip to content

feat: add playground native preview#2530

Merged
Sherry-hue merged 1 commit intolynx-family:mainfrom
Sherry-hue:feat/native-preview
Apr 29, 2026
Merged

feat: add playground native preview#2530
Sherry-hue merged 1 commit intolynx-family:mainfrom
Sherry-hue:feat/native-preview

Conversation

@Sherry-hue
Copy link
Copy Markdown
Collaborator

@Sherry-hue Sherry-hue commented Apr 27, 2026

Summary by CodeRabbit

  • New Features

    • Shareable dev-bundle flow with shortened bundle URL, QR, and copy-to-clipboard feedback.
    • In-memory payload service for creating short payload URLs and a dev endpoint to fetch the bundle URL.
    • URL-driven globalProps support for overriding demo data.
  • Improvements

    • Improved QR error reporting and quieter placeholder behavior.
    • Better QR/preview styling and more robust base64url encoding/decoding.
  • Chores

    • Dev start now builds the bundle before launching the dev server.
  • Removals

    • Two demo pages removed from the playground.

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).

Add a native preview QR code and pass the data to A2UIRender via globalProps:

image

@Sherry-hue Sherry-hue self-assigned this Apr 27, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 27, 2026

🦋 Changeset detected

Latest commit: c6d2b1e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 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

Adds JSON-first globalProps support and runtime merging, an in-memory dev payload store and dev-bundle URL endpoint, QR/share UI and style updates, base64url utility refactor, a small TypeScript CSS shim, build/dev script tweak, and removes two legacy page modules.

Changes

Cohort / File(s) Summary
Changeset
\.changeset/few-monkeys-juggle.md
Adds an empty changeset file containing only YAML frontmatter delimiters.
App & render integration
packages/genui/a2ui-playground/lynx-src/App.tsx, packages/genui/a2ui-playground/src/render.tsx
App now accepts globalProps, normalizes/merges it into init data (effectiveData) and uses it for loading; render.tsx parses globalProps from query, adds LynxViewElement.globalProps?: unknown, and sets lynxView.globalProps with precedence query → derived → {}.
Demos UI, QR & styles
packages/genui/a2ui-playground/src/pages/DemosPage.tsx, packages/genui/a2ui-playground/src/components/QrCode.tsx, packages/genui/a2ui-playground/src/styles.css
DemosPage fetches dev-bundle URL and swaps inline JSON for server-stored payload URLs; QR component gains onErrorChange, error state and hides when src empty; styles add QR layout, mono URL and copy UI.
Dev server & lynx dev plugin
packages/genui/a2ui-playground/rsbuild.config.ts, packages/genui/a2ui-playground/lynx.config.ts, packages/genui/a2ui-playground/package.json
rsbuild config exposes /__rspeedy_url with computed local bundle URL and sets server port from PORT; lynx config adds a dev plugin implementing in-memory /__a2ui_payload POST and /__a2ui/:id/... GET with TTL; dev npm script now runs build:lynx before rsbuild dev.
Utilities
packages/genui/a2ui-playground/src/utils/base64url.ts, packages/genui/a2ui-playground/src/utils/renderUrl.ts
Refactors base64url to use btoa/atob helpers, explicit padding handling and clearer errors; adds a clarifying comment about base64url-encoding of messages (no behavioral change).
Type shim
packages/genui/a2ui-playground/src/css.d.ts
Adds ambient module declaration declare module '*.css'; to allow CSS imports in TypeScript.
Removed pages
packages/genui/a2ui-playground/src/pages/Dynamic.tsx, packages/genui/a2ui-playground/src/pages/Static.tsx
Removes the DynamicPage and StaticPage modules and their exported components.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • gaoachao
  • HuJean
  • fzx2666-fz

Poem

🐇 I munched the JSON, hopped through query light,
I stitched props, stored payloads through the night.
A dev bundle bobbed, a QR blinked in cheer,
Copy, paste, hop—the preview’s near! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add playground native preview' directly aligns with the main objective of adding a native preview QR code feature to the playground, as evidenced by the new DemosPage QR functionality and the images showing mobile preview sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/genui/a2ui-playground/src/render.tsx (1)

119-155: ⚠️ Potential issue | 🟡 Minor

globalProps state is set once and never updated by postMessage.

globalProps is initialized from the URL query and stored with useState without a setter being used anywhere. On INIT_LYNX_VIEW postMessages (line 136), only initData is updated; the original query globalProps continues to take precedence in the effect at line 149.

If the parent iframe ever posts a fresh INIT_LYNX_VIEW after the initial query already supplied a globalProps, the lynx-view will keep the stale globalProps and ignore the freshly posted initData. Likely fine in the common path (one or the other is used), but worth confirming this matches the intended UX. If not, drop the precedence on stale state by using buildGlobalPropsFromInitData(initData) whenever initData came from a postMessage.

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

In `@packages/genui/a2ui-playground/src/render.tsx` around lines 119 - 155, The
bug is that globalProps is created via const [globalProps] = useState(...) and
never updated, so POSTed INIT_LYNX_VIEW messages (handled by handleMessage which
calls setInitData) cannot override the initial query globalProps; update the
component to use a mutable state for globalProps (const [globalProps,
setGlobalProps] = useState(...)) and in handleMessage (the MessageEvent handler
that detects isInitLynxViewMessage) also call setGlobalProps to either use any
globalProps from the incoming message or to
buildGlobalPropsFromInitData(e.data.data) (or null) so the effect that assigns
lynxView.globalProps will prefer the fresh posted payload rather than the stale
initial query value.
packages/genui/a2ui-playground/src/pages/DemosPage.tsx (1)

178-183: ⚠️ Potential issue | 🟠 Major

Effect at Line 178 will reset the user's scenario/edits when rspeedyDevUrl resolves.

doRender is wrapped in useCallback with rspeedyDevUrl in its deps (Line 175). The Lynx dev URL is fetched asynchronously by useRspeedyDevUrl, so its value flips from '' to a real URL ~one render after mount. When that happens, doRender's identity changes, and this effect re-fires with ALL_SCENARIOS[0] and its messages — overwriting any scenario the user has already selected via handleSelectScenario and any edits they made to customJson in the meantime.

Restrict the initial-render effect to mount only, or trigger the Lynx-dev-URL build separately so it doesn't piggyback on the "select first scenario" effect.

🐛 Suggested fix
-  useEffect(() => {
-    if (ALL_SCENARIOS[0]) {
-      const json = formatJson(ALL_SCENARIOS[0].messages);
-      doRender(json, ALL_SCENARIOS[0]);
-    }
-  }, [doRender]);
+  useEffect(() => {
+    if (ALL_SCENARIOS[0]) {
+      const json = formatJson(ALL_SCENARIOS[0].messages);
+      doRender(json, ALL_SCENARIOS[0]);
+    }
+    // Run only on mount; re-running when `doRender` changes (e.g. after
+    // `rspeedyDevUrl` resolves) would overwrite the user's current scenario/JSON.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  // When the rspeedy dev URL resolves later, re-build the Lynx dev URL
+  // for whatever the user is currently viewing.
+  useEffect(() => {
+    if (rspeedyDevUrl) {
+      doRender(customJson, currentScenario);
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [rspeedyDevUrl]);

(Adjust to the project's preferred eslint-disable convention.)

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

In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx` around lines 178 -
183, The effect that selects the initial scenario should run once on mount
instead of depending on doRender (which changes when rspeedyDevUrl resolves) —
change the useEffect([...], [doRender]) that references ALL_SCENARIOS and
doRender to run only on mount by using an empty dependency array and suppressing
the exhaustive-deps warning (e.g., add an eslint-disable-next-line
react-hooks/exhaustive-deps above this useEffect). This prevents
rspeedyDevUrl-driven identity changes to doRender from re-running the effect and
overwriting user selections/edits (handleSelectScenario, customJson); keep
doRender and its useCallback (with rspeedyDevUrl) intact so Lynx URL logic
remains untouched.
🧹 Nitpick comments (4)
packages/genui/a2ui-playground/lynx-src/App.tsx (1)

36-61: parseJsonLikeString falls back to returning the raw string.

When no parse attempt succeeds (after the initial parse and up to 3 URL-decode cycles), the function returns the original input string. Downstream, normalizeInitDataLike happily assigns this string to out.messages / out.actionMocks. The string then propagates into loadMessagesnormalizePayloadToMessages, which retries JSON.parse once more and silently returns [] on failure.

Net effect: a malformed messages/actionMocks value is silently swallowed with no error surfaced to the user, who only sees an empty UI. Consider returning undefined (or explicit error sentinel) from parseJsonLikeString on total failure so the caller can either skip the field or surface a parse error via setError.

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

In `@packages/genui/a2ui-playground/lynx-src/App.tsx` around lines 36 - 61,
parseJsonLikeString currently returns the original raw input when all
parse/URL-decode attempts fail, which lets malformed JSON silently propagate
into normalizeInitDataLike → out.messages/out.actionMocks and ultimately be
swallowed by normalizePayloadToMessages; change parseJsonLikeString to return
undefined instead of the raw string on total failure, and update callers
(notably normalizeInitDataLike and any code path into
loadMessages/normalizePayloadToMessages) to treat undefined as “no valid JSON” —
skip assigning the field and surface a parse error via setError (or another
explicit sentinel) so malformed payloads are not silently converted to empty
state.
packages/genui/a2ui-playground/rsbuild.config.ts (1)

11-36: Best-effort findLocalIp may pick the wrong interface.

findLocalIp returns the first non-internal IPv4 it encounters, iterating in Object.keys(networkInterfaces()) order. On dev machines with multiple interfaces (Wi-Fi + Ethernet + VPN + Docker/WSL bridges) the chosen interface is essentially nondeterministic and frequently not the one a phone on the same Wi-Fi can reach, breaking the QR-code-to-device flow silently.

Consider:

  • Allowing override via env (e.g. RSPEEDY_HOST / LYNX_DEV_HOST).
  • Falling back to req.headers.host (stripping port) so the URL inherits whatever host the developer is already using to reach the dev server.
♻️ Sketch using request host as fallback
-      (middlewares) => {
-        middlewares.unshift((req, res, next) => {
+      (middlewares) => {
+        middlewares.unshift((req, res, next) => {
           if (req.url?.startsWith('/__rspeedy_url')) {
-            const url = buildRspeedyBundleUrl();
+            const hostHeader = req.headers.host ?? '';
+            const reqHost = hostHeader.split(':')[0] || findLocalIp();
+            const url = `http://${reqHost}:${RSPEEDY_PORT}/main.lynx.js`;
             res.setHeader('Content-Type', 'application/json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-playground/rsbuild.config.ts` around lines 11 - 36,
findLocalIp currently picks the first non-internal IPv4 and
buildRspeedyBundleUrl always uses it, which can choose the wrong interface;
change buildRspeedyBundleUrl to first check for an explicit host override via
env vars (e.g. process.env.RSPEEDY_HOST or process.env.LYNX_DEV_HOST) and use
that if present, otherwise accept an optional host parameter (e.g. host?:
string) so callers can pass req.headers.host (strip any :port) as the preferred
runtime fallback, and only if neither env nor caller-provided host exist call
findLocalIp() and build the URL using that host + RSPEEDY_PORT/main.lynx.js
(ensure you strip any port when using req.headers.host and preserve RSPEEDY_PORT
when constructing the final URL).
packages/genui/a2ui-playground/package.json (1)

9-9: Verify the dev workflow with the new native-preview flow.

A couple of concerns about chaining pnpm run build:lynx && rsbuild dev:

  1. build:lynx produces a static lynx bundle once at startup. Subsequent edits to lynx-src/** won't be reflected in the served main.lynx.js until dev is restarted, which makes web-iframe preview stale during Lynx development.
  2. The new /__rspeedy_url middleware in rsbuild.config.ts returns http://<ip>:3000/main.lynx.js, which assumes an rspeedy dev server is running on port 3000. Just running pnpm dev does not start dev:lynx, so the native preview QR/URL surfaced from useRspeedyDevUrl will point at a port that is not actually serving anything unless the developer remembers to run dev:lynx in a second terminal.

Consider one of:

  • Running dev:lynx and rsbuild dev concurrently (e.g. via npm-run-all -p / concurrently) so both servers are live.
  • Updating README/AGENTS.md to document the two-process workflow explicitly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-playground/package.json` at line 9, The dev script chains
"build:lynx && rsbuild dev" which builds a static main.lynx.js once and leaves
the rsbuild dev server pointing to a port that may not actually serve the lynx
bundle; update the "dev" script in package.json to run the lynx dev server and
rsbuild dev concurrently (e.g., use npm-run-all -p or concurrently to start
"dev:lynx" alongside "rsbuild dev") so edits under lynx-src/** are live, and
also update rsbuild.config.ts middleware that returns "/__rspeedy_url" (and any
consumer like useRspeedyDevUrl) or the README/AGENTS.md to clearly document the
two-process requirement if you choose not to run both concurrently.
packages/genui/a2ui-playground/src/pages/DemosPage.tsx (1)

34-51: The Clipboard API is the modern standard, but the suggested async refactor requires call site changes.

document.execCommand('copy') is deprecated in favor of navigator.clipboard.writeText(). However, the suggested refactor changes the return type from boolean to Promise<boolean>, which would require the onClick handler at line 360 to become async. The current handler is synchronous and immediately reads the ok result to conditionally update state. Consider either:

  1. Making the onClick handler async: onClick={async () => { const ok = await copyToClipboard(full); if (ok) { ... } }}
  2. Or keeping a synchronous signature that attempts the modern API but returns immediately with a boolean (note: navigator.clipboard.writeText is inherently async, so this approach would need to fall back to execCommand synchronously).

The lint concern mentioned in the code comment about navigator being treated as Node-only is not applicable here—the ESLint config explicitly provides globals.browser for this file context.

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

In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx` around lines 34 - 51,
The current copyToClipboard function uses deprecated document.execCommand and
the refactor to navigator.clipboard.writeText must either propagate an async
result or retain a synchronous signature; update copyToClipboard to be async
(returning Promise<boolean>) and use navigator.clipboard.writeText(text) with
try/catch returning true/false, then change the caller (the onClick handler that
currently reads the boolean immediately) to be async and await
copyToClipboard(full) before updating state, or alternatively keep
copyToClipboard synchronous by attempting navigator.clipboard.writeText() when
available but falling back to the existing execCommand path and still returning
a boolean; locate the function copyToClipboard and the onClick handler that
reads the result and apply one of these two fixes consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/genui/a2ui-playground/lynx-src/App.tsx`:
- Around line 202-211: The unconditional console.info calls inside the useEffect
(referencing didLogRef, useEffect, and the variables globalProps,
globalPropsData, rawInitData, initData, effectiveData) should be gated so they
only run in development; wrap the logging behind a dev/debug check (e.g.
import.meta.env.DEV or process.env.NODE_ENV !== 'production') or remove them
entirely for production builds, keeping didLogRef logic but only invoking
console.info when the dev flag is true to avoid leaking large or user-authored
payloads in production.

In `@packages/genui/a2ui-playground/rsbuild.config.ts`:
- Line 9: Default RSPEEDY_PORT is causing frontend to point at port 3000 while
rsbuild dev may claim that port; change the default in RSPEEDY_PORT (symbol:
RSPEEDY_PORT) from 3000 to 3100 in rsbuild.config.ts and also set server.port:
3100 in lynx.config.ts so the /__rspeedy_url endpoint and the running rspeedy
dev server use the same non-conflicting port; update any environment docs or dev
scripts that rely on RSPEEDY_PORT to reflect 3100.

In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx`:
- Around line 138-170: The anonymous async IIFE that posts to
`${rspeedyDevUrl}/__a2ui_payload` can throw on network failure or during
res.json(); wrap the fetch + response parsing + subsequent URL mutation in a
try/catch, log or swallow the error, and return early on error so the code keeps
the original inline URL fallback; only proceed to mutate the URL and call
setLynxDevUrl(u.toString()) (and use lynxUrlSeqRef.current/seq check) when
fetch/res.json succeed and res.ok is true, and preserve existing checks for
typeof data.messagesUrl/data.actionMocksUrl and actionMocks before setting
search params.

In `@packages/genui/a2ui-playground/src/utils/renderUrl.ts`:
- Around line 17-21: The query now stores raw JSON via params.set('messages',
JSON.stringify(init.messages)) and params.set('actionMocks',
JSON.stringify(init.actionMocks)), which can blow up URL/QR length; change
renderUrl.ts to preserve compact base64url encoding for these payloads (or
implement a length-based fallback): when JSON.stringify(...) length (or
resulting encoded query length) exceeds a safe threshold (e.g., test with your
largest preset/QR limit), encode the payload using base64url and set a
flag/query param indicating base64 encoding, otherwise keep the plain JSON
encoding; update handling for both init.messages and init.actionMocks so the
consumer can detect and decode base64url when present (and verify with the
DemosPage.tsx QR flow).

---

Outside diff comments:
In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx`:
- Around line 178-183: The effect that selects the initial scenario should run
once on mount instead of depending on doRender (which changes when rspeedyDevUrl
resolves) — change the useEffect([...], [doRender]) that references
ALL_SCENARIOS and doRender to run only on mount by using an empty dependency
array and suppressing the exhaustive-deps warning (e.g., add an
eslint-disable-next-line react-hooks/exhaustive-deps above this useEffect). This
prevents rspeedyDevUrl-driven identity changes to doRender from re-running the
effect and overwriting user selections/edits (handleSelectScenario, customJson);
keep doRender and its useCallback (with rspeedyDevUrl) intact so Lynx URL logic
remains untouched.

In `@packages/genui/a2ui-playground/src/render.tsx`:
- Around line 119-155: The bug is that globalProps is created via const
[globalProps] = useState(...) and never updated, so POSTed INIT_LYNX_VIEW
messages (handled by handleMessage which calls setInitData) cannot override the
initial query globalProps; update the component to use a mutable state for
globalProps (const [globalProps, setGlobalProps] = useState(...)) and in
handleMessage (the MessageEvent handler that detects isInitLynxViewMessage) also
call setGlobalProps to either use any globalProps from the incoming message or
to buildGlobalPropsFromInitData(e.data.data) (or null) so the effect that
assigns lynxView.globalProps will prefer the fresh posted payload rather than
the stale initial query value.

---

Nitpick comments:
In `@packages/genui/a2ui-playground/lynx-src/App.tsx`:
- Around line 36-61: parseJsonLikeString currently returns the original raw
input when all parse/URL-decode attempts fail, which lets malformed JSON
silently propagate into normalizeInitDataLike → out.messages/out.actionMocks and
ultimately be swallowed by normalizePayloadToMessages; change
parseJsonLikeString to return undefined instead of the raw string on total
failure, and update callers (notably normalizeInitDataLike and any code path
into loadMessages/normalizePayloadToMessages) to treat undefined as “no valid
JSON” — skip assigning the field and surface a parse error via setError (or
another explicit sentinel) so malformed payloads are not silently converted to
empty state.

In `@packages/genui/a2ui-playground/package.json`:
- Line 9: The dev script chains "build:lynx && rsbuild dev" which builds a
static main.lynx.js once and leaves the rsbuild dev server pointing to a port
that may not actually serve the lynx bundle; update the "dev" script in
package.json to run the lynx dev server and rsbuild dev concurrently (e.g., use
npm-run-all -p or concurrently to start "dev:lynx" alongside "rsbuild dev") so
edits under lynx-src/** are live, and also update rsbuild.config.ts middleware
that returns "/__rspeedy_url" (and any consumer like useRspeedyDevUrl) or the
README/AGENTS.md to clearly document the two-process requirement if you choose
not to run both concurrently.

In `@packages/genui/a2ui-playground/rsbuild.config.ts`:
- Around line 11-36: findLocalIp currently picks the first non-internal IPv4 and
buildRspeedyBundleUrl always uses it, which can choose the wrong interface;
change buildRspeedyBundleUrl to first check for an explicit host override via
env vars (e.g. process.env.RSPEEDY_HOST or process.env.LYNX_DEV_HOST) and use
that if present, otherwise accept an optional host parameter (e.g. host?:
string) so callers can pass req.headers.host (strip any :port) as the preferred
runtime fallback, and only if neither env nor caller-provided host exist call
findLocalIp() and build the URL using that host + RSPEEDY_PORT/main.lynx.js
(ensure you strip any port when using req.headers.host and preserve RSPEEDY_PORT
when constructing the final URL).

In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx`:
- Around line 34-51: The current copyToClipboard function uses deprecated
document.execCommand and the refactor to navigator.clipboard.writeText must
either propagate an async result or retain a synchronous signature; update
copyToClipboard to be async (returning Promise<boolean>) and use
navigator.clipboard.writeText(text) with try/catch returning true/false, then
change the caller (the onClick handler that currently reads the boolean
immediately) to be async and await copyToClipboard(full) before updating state,
or alternatively keep copyToClipboard synchronous by attempting
navigator.clipboard.writeText() when available but falling back to the existing
execCommand path and still returning a boolean; locate the function
copyToClipboard and the onClick handler that reads the result and apply one of
these two fixes consistently.
🪄 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: 3c03c5ba-d3ba-45a3-9b72-f292c6edb04c

📥 Commits

Reviewing files that changed from the base of the PR and between 43353c6 and dea9f4a.

📒 Files selected for processing (10)
  • .changeset/few-monkeys-juggle.md
  • packages/genui/a2ui-playground/lynx-src/App.tsx
  • packages/genui/a2ui-playground/package.json
  • packages/genui/a2ui-playground/rsbuild.config.ts
  • packages/genui/a2ui-playground/src/css.d.ts
  • packages/genui/a2ui-playground/src/pages/DemosPage.tsx
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui-playground/src/styles.css
  • packages/genui/a2ui-playground/src/utils/base64url.ts
  • packages/genui/a2ui-playground/src/utils/renderUrl.ts
💤 Files with no reviewable changes (1)
  • packages/genui/a2ui-playground/src/utils/base64url.ts

Comment thread packages/genui/a2ui-playground/lynx-src/App.tsx Outdated
Comment thread packages/genui/a2ui-playground/rsbuild.config.ts Outdated
Comment thread packages/genui/a2ui-playground/src/pages/DemosPage.tsx Outdated
Comment thread packages/genui/a2ui-playground/src/utils/renderUrl.ts Outdated
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 27, 2026

Merging this PR will improve performance by 11.8%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 80 untouched benchmarks
⏩ 26 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
transform 1000 view elements 44.7 ms 40 ms +11.8%

Comparing Sherry-hue:feat/native-preview (c6d2b1e) with main (caadd3b)

Open in CodSpeed

Footnotes

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

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 27, 2026

React MTF Example

#844 Bundle Size — 196.58KiB (0%).

c6d2b1e(current) vs cf6e21c main#841(baseline)

Bundle metrics  no changes
                 Current
#844
     Baseline
#841
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 174 174
No change  Duplicate Modules 66 66
No change  Duplicate Code 44.04% 44.04%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#844
     Baseline
#841
No change  IMG 111.23KiB 111.23KiB
No change  Other 85.35KiB 85.35KiB

Bundle analysis reportBranch Sherry-hue:feat/native-previewProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 27, 2026

React External

#827 Bundle Size — 680.41KiB (0%).

c6d2b1e(current) vs cf6e21c main#824(baseline)

Bundle metrics  no changes
                 Current
#827
     Baseline
#824
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
#827
     Baseline
#824
No change  Other 680.41KiB 680.41KiB

Bundle analysis reportBranch Sherry-hue:feat/native-previewProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 27, 2026

React Example

#7712 Bundle Size — 225.43KiB (0%).

c6d2b1e(current) vs cf6e21c main#7709(baseline)

Bundle metrics  no changes
                 Current
#7712
     Baseline
#7709
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 180 180
No change  Duplicate Modules 69 69
No change  Duplicate Code 44.54% 44.54%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#7712
     Baseline
#7709
No change  IMG 145.76KiB 145.76KiB
No change  Other 79.67KiB 79.67KiB

Bundle analysis reportBranch Sherry-hue:feat/native-previewProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 27, 2026

Web Explorer

#9285 Bundle Size — 900.02KiB (0%).

c6d2b1e(current) vs cf6e21c main#9282(baseline)

Bundle metrics  Change 1 change
                 Current
#9285
     Baseline
#9282
No change  Initial JS 44.46KiB 44.46KiB
No change  Initial CSS 2.22KiB 2.22KiB
No change  Cache Invalidation 0% 0%
No change  Chunks 9 9
No change  Assets 11 11
Change  Modules 228(-0.44%) 229
No change  Duplicate Modules 11 11
No change  Duplicate Code 27.28% 27.28%
No change  Packages 10 10
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#9285
     Baseline
#9282
No change  JS 495.88KiB 495.88KiB
No change  Other 401.92KiB 401.92KiB
No change  CSS 2.22KiB 2.22KiB

Bundle analysis reportBranch Sherry-hue:feat/native-previewProject dashboard


Generated by RelativeCIDocumentationReport issue

@Sherry-hue Sherry-hue force-pushed the feat/native-preview branch from dea9f4a to 27049c8 Compare April 27, 2026 12:24
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/genui/a2ui-playground/src/components/QrCode.tsx (1)

31-64: Including onErrorChange in the effect deps is fragile for unstable callers.

If a future consumer passes an inline (e) => setX(e) (instead of the stable setLynxDevQrError used today), this effect will re-run on every parent render, re-invoking toDataURL for the same value and emitting a transient '' via onErrorChange?.('') at line 34 before the next encode resolves. Stash the latest callback in a ref and drop it from the deps to make the component robust.

♻️ Proposed refactor
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
@@
   const { value, size = 144, onErrorChange } = props;
   const [src, setSrc] = useState<string>('');
   const [error, setError] = useState<string>('');
+
+  const onErrorChangeRef = useRef(onErrorChange);
+  useEffect(() => {
+    onErrorChangeRef.current = onErrorChange;
+  }, [onErrorChange]);
@@
   useEffect(() => {
     let cancelled = false;
     setError('');
-    onErrorChange?.('');
+    onErrorChangeRef.current?.('');
@@
         if (!cancelled) {
           setSrc(url);
           setError('');
-          onErrorChange?.('');
+          onErrorChangeRef.current?.('');
         }
       } catch (e) {
         if (!cancelled) {
           setSrc('');
           const msg = e instanceof Error
             ? e.message
             : 'Failed to encode QR code';
           setError(msg);
-          onErrorChange?.(msg);
+          onErrorChangeRef.current?.(msg);
         }
       }
     })();
@@
     return () => {
       cancelled = true;
     };
-  }, [onErrorChange, options, value]);
+  }, [options, value]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-playground/src/components/QrCode.tsx` around lines 31 -
64, The effect is fragile because onErrorChange is in the dependency array; move
the latest onErrorChange into a ref (e.g., errorCbRef) and use that ref inside
the useEffect instead of the prop so the effect depends only on stable values
(value, options) and not on unstable caller functions; specifically, create a
ref updated whenever onErrorChange changes, call errorCbRef.current(...) in
place of onErrorChange?.(...) inside the async block and the error path, and
remove onErrorChange from the effect deps to avoid re-running toDataURL
unnecessarily.
packages/genui/a2ui-playground/lynx.config.ts (1)

21-31: Bound payloadStore and run GC on reads too.

gcPayloads() is only invoked from the POST branch (line 81), so if a long-lived dev session does many GETs and few POSTs, expired entries are not collected; conversely, a stream of POSTs within the TTL window can grow the map unboundedly. Consider also enforcing a soft max entry count to evict the oldest, and calling gcPayloads() on the GET branch as well.

♻️ Sketch
+const MAX_PAYLOAD_ENTRIES = 256;
+
 function gcPayloads(): void {
   const now = Date.now();
   for (const [id, p] of payloadStore) {
     if (now - p.createdAt > PAYLOAD_TTL_MS) {
       payloadStore.delete(id);
     }
   }
+  // Drop oldest if over cap (Map preserves insertion order).
+  while (payloadStore.size > MAX_PAYLOAD_ENTRIES) {
+    const oldest = payloadStore.keys().next().value;
+    if (oldest === undefined) break;
+    payloadStore.delete(oldest);
+  }
 }

…and call gcPayloads() near the top of the GET handler as well.

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

In `@packages/genui/a2ui-playground/lynx.config.ts` around lines 21 - 31, The
payload GC only runs on POST so expired entries can accumulate; update
gcPayloads, payloadStore and request handlers to run GC on reads too and impose
a soft max size eviction policy: 1) call gcPayloads() at the start of the GET
handler (where the GET branch is handled) as well as in the POST branch, 2) make
payloadStore and gcPayloads visible/shared where handlers run (ensure the Map
payloadStore and function gcPayloads() remain module-scoped/bound so both
branches can call them), and 3) augment gcPayloads() (or add a small helper) to
enforce a soft max entry count (e.g., if payloadStore.size > MAX_ENTRIES, evict
oldest entries by createdAt) so the Map cannot grow unboundedly; reference
payloadStore, gcPayloads(), PAYLOAD_TTL_MS and the GET/POST handler code paths
when making these changes.
packages/genui/a2ui-playground/src/render.tsx (1)

118-155: Confirm intent: URL globalProps permanently shadows post-mount INIT_LYNX_VIEW updates.

globalProps is captured once into state with no setter (line 125), and the effect prefers it over buildGlobalPropsFromInitData(initData) (lines 149-150). Consequently, when the URL contains globalProps, any subsequent INIT_LYNX_VIEW postMessage updates initData (and is forwarded to lynxView.initData) but lynxView.globalProps remains pinned to the URL’s original value — so the new messages/actionMocks won’t reach the Lynx app via globalProps. If postMessage updates are intended to refresh the payload, you’ll want to rebuild globalProps from the latest initData when no URL globalProps applies, or recompute on every effect run.

While here: since globalProps never changes after mount, useMemo reads more cleanly than useState with an unused setter.

♻️ One possible shape (recompute every run; URL still wins on first render)
-  const initial = useMemo(() => {
-    const initData = parseInitDataFromQuery();
-    const globalProps = parseGlobalPropsFromQuery();
-    return { initData, globalProps };
-  }, []);
-  const [initData, setInitData] = useState<InitData | null>(initial.initData);
-  const [globalProps] = useState<Record<string, unknown> | null>(
-    initial.globalProps,
-  );
+  const queryGlobalProps = useMemo(() => parseGlobalPropsFromQuery(), []);
+  const [initData, setInitData] = useState<InitData | null>(
+    () => parseInitDataFromQuery(),
+  );
@@
-    lynxView.initData = initData ?? {};
-    // Align with native: prefer `globalProps` as the channel for A2UI payload.
-    lynxView.globalProps = globalProps ?? buildGlobalPropsFromInitData(initData)
-      ?? {};
+    lynxView.initData = initData ?? {};
+    // Align with native: prefer `globalProps` as the channel for A2UI payload.
+    lynxView.globalProps = queryGlobalProps
+      ?? buildGlobalPropsFromInitData(initData)
+      ?? {};
@@
-  }, [globalProps, initData]);
+  }, [queryGlobalProps, initData]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-playground/src/render.tsx` around lines 118 - 155, The
code captures URL-derived globalProps once into state (globalProps from
parseGlobalPropsFromQuery in Render) which permanently overrides later
INIT_LYNX_VIEW postMessage updates; to fix, stop pinning it: compute a
persistent urlGlobalProps once (e.g. useMemo or a ref from
parseGlobalPropsFromQuery) but in the effect that writes to lynxView use
urlGlobalProps ?? buildGlobalPropsFromInitData(initData) ?? {} so
buildGlobalPropsFromInitData is evaluated on every initData change;
specifically, replace the useState([globalProps]) usage with a useMemo or ref
holding parseGlobalPropsFromQuery(), and update the effect that sets
lynxView.globalProps to prefer that memoized url value only if present,
otherwise recompute from buildGlobalPropsFromInitData(initData) so postMessage
updates to setInitData propagate into lynxView.globalProps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/genui/a2ui-playground/lynx.config.ts`:
- Around line 33-47: In readJsonBody, add a max-body-size guard and optional
Content-Type check: read req.headers['content-type'] and if present and not
matching 'application/json' reject immediately; while buffering chunks track
cumulative byte length and if it exceeds a safe limit (e.g. 1_048_576 bytes)
stop reading, remove listeners and reject with a clear error (and destroy the
socket or call req.destroy()), ensuring you reject with an Error and clean up
'data'/'end'/'error' listeners to avoid leaks; keep the rest of the JSON parse
flow intact so resolve/reject behavior of readJsonBody remains unchanged.

In `@packages/genui/a2ui-playground/src/render.tsx`:
- Around line 77-92: In parseGlobalPropsFromQuery, currently any parsed value
with typeof === 'object' (including arrays) is returned as a Record, so update
the validation to reject arrays and non-plain objects: after parsing in
parseGlobalPropsFromQuery, ensure parsed is non-null, typeof parsed ===
'object', Array.isArray(parsed) is false, and optionally confirm it's a plain
object (e.g., Object.getPrototypeOf(parsed) === Object.prototype) before casting
and returning; otherwise return null.

---

Nitpick comments:
In `@packages/genui/a2ui-playground/lynx.config.ts`:
- Around line 21-31: The payload GC only runs on POST so expired entries can
accumulate; update gcPayloads, payloadStore and request handlers to run GC on
reads too and impose a soft max size eviction policy: 1) call gcPayloads() at
the start of the GET handler (where the GET branch is handled) as well as in the
POST branch, 2) make payloadStore and gcPayloads visible/shared where handlers
run (ensure the Map payloadStore and function gcPayloads() remain
module-scoped/bound so both branches can call them), and 3) augment gcPayloads()
(or add a small helper) to enforce a soft max entry count (e.g., if
payloadStore.size > MAX_ENTRIES, evict oldest entries by createdAt) so the Map
cannot grow unboundedly; reference payloadStore, gcPayloads(), PAYLOAD_TTL_MS
and the GET/POST handler code paths when making these changes.

In `@packages/genui/a2ui-playground/src/components/QrCode.tsx`:
- Around line 31-64: The effect is fragile because onErrorChange is in the
dependency array; move the latest onErrorChange into a ref (e.g., errorCbRef)
and use that ref inside the useEffect instead of the prop so the effect depends
only on stable values (value, options) and not on unstable caller functions;
specifically, create a ref updated whenever onErrorChange changes, call
errorCbRef.current(...) in place of onErrorChange?.(...) inside the async block
and the error path, and remove onErrorChange from the effect deps to avoid
re-running toDataURL unnecessarily.

In `@packages/genui/a2ui-playground/src/render.tsx`:
- Around line 118-155: The code captures URL-derived globalProps once into state
(globalProps from parseGlobalPropsFromQuery in Render) which permanently
overrides later INIT_LYNX_VIEW postMessage updates; to fix, stop pinning it:
compute a persistent urlGlobalProps once (e.g. useMemo or a ref from
parseGlobalPropsFromQuery) but in the effect that writes to lynxView use
urlGlobalProps ?? buildGlobalPropsFromInitData(initData) ?? {} so
buildGlobalPropsFromInitData is evaluated on every initData change;
specifically, replace the useState([globalProps]) usage with a useMemo or ref
holding parseGlobalPropsFromQuery(), and update the effect that sets
lynxView.globalProps to prefer that memoized url value only if present,
otherwise recompute from buildGlobalPropsFromInitData(initData) so postMessage
updates to setInitData propagate into lynxView.globalProps.
🪄 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: d211c449-d0a6-4758-83da-2f9ac344fd03

📥 Commits

Reviewing files that changed from the base of the PR and between dea9f4a and 27049c8.

📒 Files selected for processing (14)
  • .changeset/few-monkeys-juggle.md
  • packages/genui/a2ui-playground/lynx-src/App.tsx
  • packages/genui/a2ui-playground/lynx.config.ts
  • packages/genui/a2ui-playground/package.json
  • packages/genui/a2ui-playground/rsbuild.config.ts
  • packages/genui/a2ui-playground/src/components/QrCode.tsx
  • packages/genui/a2ui-playground/src/css.d.ts
  • packages/genui/a2ui-playground/src/pages/DemosPage.tsx
  • packages/genui/a2ui-playground/src/pages/Dynamic.tsx
  • packages/genui/a2ui-playground/src/pages/Static.tsx
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui-playground/src/styles.css
  • packages/genui/a2ui-playground/src/utils/base64url.ts
  • packages/genui/a2ui-playground/src/utils/renderUrl.ts
💤 Files with no reviewable changes (3)
  • packages/genui/a2ui-playground/src/pages/Static.tsx
  • packages/genui/a2ui-playground/src/utils/base64url.ts
  • packages/genui/a2ui-playground/src/pages/Dynamic.tsx
✅ Files skipped from review due to trivial changes (3)
  • .changeset/few-monkeys-juggle.md
  • packages/genui/a2ui-playground/src/css.d.ts
  • packages/genui/a2ui-playground/package.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/genui/a2ui-playground/src/utils/renderUrl.ts
  • packages/genui/a2ui-playground/rsbuild.config.ts
  • packages/genui/a2ui-playground/src/pages/DemosPage.tsx
  • packages/genui/a2ui-playground/lynx-src/App.tsx

Comment thread packages/genui/a2ui-playground/lynx.config.ts
Comment thread packages/genui/a2ui-playground/src/render.tsx
@Sherry-hue Sherry-hue force-pushed the feat/native-preview branch from 27049c8 to 327be8c Compare April 27, 2026 12:49
PupilTong
PupilTong previously approved these changes Apr 27, 2026
@Sherry-hue Sherry-hue enabled auto-merge (squash) April 27, 2026 12:52
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.

♻️ Duplicate comments (1)
packages/genui/a2ui-playground/rsbuild.config.ts (1)

9-9: ⚠️ Potential issue | 🔴 Critical

PORT is reused for both the rsbuild server and the rspeedy bundle URL — they cannot share a port.

rsbuild dev (this config) and rspeedy dev (lynx.config.ts) run as separate processes/servers. Here, server.port: PORT binds rsbuild to PORT (default 3000), and buildRspeedyBundleUrl() returns http://<ip>:${PORT}/main.lynx.js — pointing the QR code//__rspeedy_url consumer at the rsbuild port, not the rspeedy port. When both default to 3000, rspeedy auto-increments to 3001/3002, and main.lynx.js will 404 from rsbuild while the actual bundle lives on the rspeedy port.

Use a distinct env var (e.g., RSPEEDY_PORT) for the bundle URL and ensure lynx.config.ts binds rspeedy to that same value, so the URL handed out by /__rspeedy_url actually resolves to the rspeedy dev server.

🛠️ Suggested change
-const PORT = Number(process.env.PORT ?? 3000);
+const PORT = Number(process.env.PORT ?? 3000);
+const RSPEEDY_PORT = Number(process.env.RSPEEDY_PORT ?? 3100);
@@
 function buildRspeedyBundleUrl(): string {
   const ip = findLocalIp();
-  return `http://${ip}:${PORT}/main.lynx.js`;
+  return `http://${ip}:${RSPEEDY_PORT}/main.lynx.js`;
 }

And in lynx.config.ts, set server.port: Number(process.env.RSPEEDY_PORT ?? 3100).

Also applies to: 33-36, 47-47

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

In `@packages/genui/a2ui-playground/rsbuild.config.ts` at line 9, The rsbuild
config reuses PORT for both its own server and the rspeedy bundle URL causing
wrong porting; change the bundle URL to use a separate env var (e.g.,
RSPEEDY_PORT) instead of PORT and update buildRspeedyBundleUrl() to interpolate
RSPEEDY_PORT, and then ensure lynx.config.ts sets server.port from the same
RSPEEDY_PORT (Number(process.env.RSPEEDY_PORT ?? 3100)) so the URL returned by
/__rspeedy_url points to the actual rspeedy dev server rather than the rsbuild
port.
🧹 Nitpick comments (3)
packages/genui/a2ui-playground/src/styles.css (2)

423-440: Add a visible focus style for .previewQrCopyBtn.

The copy button defines :hover but no :focus-visible state. Keyboard users will not get a discernible focus indicator (the default UA outline may also be suppressed by surrounding styles in some browsers). Mirroring the hover treatment on :focus-visible keeps the button keyboard-accessible.

♻️ Suggested addition
 .previewQrCopyBtn:hover {
   color: var(--geist-foreground);
   border-color: var(--geist-border-hover);
 }
+
+.previewQrCopyBtn:focus-visible {
+  color: var(--geist-foreground);
+  border-color: var(--geist-border-hover);
+  outline: 2px solid var(--geist-foreground);
+  outline-offset: 2px;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-playground/src/styles.css` around lines 423 - 440, The
.previewQrCopyBtn currently styles hover but lacks a keyboard focus style; add a
:focus-visible rule for .previewQrCopyBtn that mirrors the :hover treatment
(e.g., set color to var(--geist-foreground) and border-color to
var(--geist-border-hover)) so keyboard users get a clear visible indicator, and
ensure any existing outline suppression is handled appropriately to keep the
focus ring accessible when :focus-visible applies.

396-403: Remove unused .previewQrUrl class.

The .previewQrUrl rule (line 396) is not referenced anywhere in the codebase. The markup in DemosPage.tsx uses only .previewQrUrlRow and .previewQrUrlText for the dev-bundle URL row. Remove this unused CSS rule.

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

In `@packages/genui/a2ui-playground/src/styles.css` around lines 396 - 403, Remove
the unused CSS rule .previewQrUrl from
packages/genui/a2ui-playground/src/styles.css; locate the .previewQrUrl block
(the selector and its declarations) and delete it, leaving the existing
.previewQrUrlRow and .previewQrUrlText rules intact—also quickly grep for the
symbol previewQrUrl to confirm there are no remaining references before
committing.
packages/genui/a2ui-playground/rsbuild.config.ts (1)

15-21: Drop the unnecessary type assertion on list.

@types/node (v24.10.13) types networkInterfaces() as returning NodeJS.Dict<NetworkInterfaceInfo[]>, where NetworkInterfaceInfo is a discriminated union with family: "IPv4" | "IPv6" as literal string types. The inline as Array<{ ... }> cast is redundant, widens the family type to string | number, and forces the typeof net.family === 'string' check that becomes unnecessary with proper typing.

♻️ Proposed simplification
-    for (
-      const net of list as Array<{
-        address: string;
-        family: string | number;
-        internal: boolean;
-      }>
-    ) {
-      const family = typeof net.family === 'string'
-        ? net.family
-        : `IPv${net.family}`;
-      if (family === 'IPv4' && !net.internal) {
-        return net.address;
-      }
-    }
+    for (const net of list) {
+      if (net.family === 'IPv4' && !net.internal) {
+        return net.address;
+      }
+    }
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/genui/a2ui-playground/rsbuild.config.ts`:
- Line 9: The rsbuild config reuses PORT for both its own server and the rspeedy
bundle URL causing wrong porting; change the bundle URL to use a separate env
var (e.g., RSPEEDY_PORT) instead of PORT and update buildRspeedyBundleUrl() to
interpolate RSPEEDY_PORT, and then ensure lynx.config.ts sets server.port from
the same RSPEEDY_PORT (Number(process.env.RSPEEDY_PORT ?? 3100)) so the URL
returned by /__rspeedy_url points to the actual rspeedy dev server rather than
the rsbuild port.

---

Nitpick comments:
In `@packages/genui/a2ui-playground/src/styles.css`:
- Around line 423-440: The .previewQrCopyBtn currently styles hover but lacks a
keyboard focus style; add a :focus-visible rule for .previewQrCopyBtn that
mirrors the :hover treatment (e.g., set color to var(--geist-foreground) and
border-color to var(--geist-border-hover)) so keyboard users get a clear visible
indicator, and ensure any existing outline suppression is handled appropriately
to keep the focus ring accessible when :focus-visible applies.
- Around line 396-403: Remove the unused CSS rule .previewQrUrl from
packages/genui/a2ui-playground/src/styles.css; locate the .previewQrUrl block
(the selector and its declarations) and delete it, leaving the existing
.previewQrUrlRow and .previewQrUrlText rules intact—also quickly grep for the
symbol previewQrUrl to confirm there are no remaining references before
committing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 465a6bda-6978-4690-a0bc-1dd952dbe26d

📥 Commits

Reviewing files that changed from the base of the PR and between 27049c8 and 327be8c.

📒 Files selected for processing (14)
  • .changeset/few-monkeys-juggle.md
  • packages/genui/a2ui-playground/lynx-src/App.tsx
  • packages/genui/a2ui-playground/lynx.config.ts
  • packages/genui/a2ui-playground/package.json
  • packages/genui/a2ui-playground/rsbuild.config.ts
  • packages/genui/a2ui-playground/src/components/QrCode.tsx
  • packages/genui/a2ui-playground/src/css.d.ts
  • packages/genui/a2ui-playground/src/pages/DemosPage.tsx
  • packages/genui/a2ui-playground/src/pages/Dynamic.tsx
  • packages/genui/a2ui-playground/src/pages/Static.tsx
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui-playground/src/styles.css
  • packages/genui/a2ui-playground/src/utils/base64url.ts
  • packages/genui/a2ui-playground/src/utils/renderUrl.ts
💤 Files with no reviewable changes (2)
  • packages/genui/a2ui-playground/src/pages/Static.tsx
  • packages/genui/a2ui-playground/src/pages/Dynamic.tsx
✅ Files skipped from review due to trivial changes (3)
  • packages/genui/a2ui-playground/src/utils/renderUrl.ts
  • packages/genui/a2ui-playground/src/css.d.ts
  • .changeset/few-monkeys-juggle.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/genui/a2ui-playground/package.json
  • packages/genui/a2ui-playground/lynx-src/App.tsx
  • packages/genui/a2ui-playground/src/components/QrCode.tsx
  • packages/genui/a2ui-playground/lynx.config.ts
  • packages/genui/a2ui-playground/src/pages/DemosPage.tsx

Copy link
Copy Markdown
Collaborator

@Huxpro Huxpro left a comment

Choose a reason for hiding this comment

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

Review

1. __rspeedy_url returns the wrong port when the configured port is already in use

rsbuild.config.ts:27:

const PORT = Number(process.env.PORT ?? 3000);

This is evaluated at module load time, but rsbuild auto-increments the port if it's already taken (e.g. 3000 → 3001). The /__rspeedy_url endpoint then returns a URL with port 3000 while the server is actually on 3001 — native preview will fail to connect.

The actual listening port should be read from the server instance at request time rather than from the static config value.

2. Dead didLogRef effect in App.tsx

lynx-src/App.tsx:120-124:

const didLogRef = useRef(false);
useEffect(() => {
  if (didLogRef.current) return;
  didLogRef.current = true;
}, [effectiveData, globalProps, globalPropsData, initData, rawInitData]);

This sets a flag once and does nothing else — looks like debug logging was removed but the scaffolding was left behind. Should be deleted.

3. copyToClipboard uses deprecated document.execCommand('copy')

DemosPage.tsx:31-47: The comment says "some lint configs treat navigator as a Node-only API" but this code only runs in the browser. navigator.clipboard.writeText() is the standard API supported in all major browsers. document.execCommand('copy') is deprecated and will eventually be removed.

async function copyToClipboard(text: string): Promise<boolean> {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch {
    return false;
  }
}

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/genui/a2ui-playground/src/pages/DemosPage.tsx (1)

203-208: ⚠️ Potential issue | 🟠 Major

Don't reset the preview to the first preset when rspeedyDevUrl arrives.

Because doRender depends on rspeedyDevUrl, this effect reruns after /__rspeedy_url resolves and re-renders ALL_SCENARIOS[0]. If the user already picked another scenario or edited the JSON, their current preview gets overwritten. Make the bootstrap render one-shot, or rerender from customJson/currentScenario when only the dev-bundle URL changes.

Based on learnings: The a2ui-playground package must support two build targets: web via rsbuild/core (React DOM preview) and lynx via lynx-js/rspeedy (Lynx preview).

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

In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx` around lines 203 -
208, The effect that calls doRender currently reruns when doRender (and
transitively rspeedyDevUrl) changes, which resets the preview to
ALL_SCENARIOS[0]; change the effect so the bootstrap render is one-shot or only
runs when there is no user state: update the useEffect containing
ALL_SCENARIOS[0] to run on mount (empty deps) or add a guard that only calls
doRender when currentScenario is null/undefined and customJson is empty (i.e.,
user hasn't selected/edited), referencing the existing useEffect, ALL_SCENARIOS,
doRender, rspeedyDevUrl, customJson and currentScenario to locate and implement
the guard. Ensure subsequent changes to rspeedyDevUrl do not force re-render of
the initially chosen scenario unless user state indicates no prior selection.
🧹 Nitpick comments (1)
packages/genui/a2ui-playground/lynx-src/App.tsx (1)

202-206: Remove the empty didLogRef effect.

This hook no longer gates any logging or other side effect, so it only adds bootstrap noise and an unnecessary dependency list.

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

In `@packages/genui/a2ui-playground/lynx-src/App.tsx` around lines 202 - 206,
Remove the unused didLogRef and its accompanying useEffect: the const didLogRef
= useRef(false) and the useEffect(...) block that checks/sets didLogRef.current
should be deleted since it no longer gates any logging or side effects and only
adds an unnecessary dependency list (effectiveData, globalProps,
globalPropsData, initData, rawInitData); ensure no other code references
didLogRef before removing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/genui/a2ui-playground/lynx-src/App.tsx`:
- Around line 71-88: normalizeInitDataLike currently assigns messagesUrl and
actionMocksUrl directly, causing percent-encoded query params to remain encoded;
update normalizeInitDataLike to run messagesUrl and actionMocksUrl through a
decoding helper (e.g., decodeQueryParamValue that implements the same multi-pass
percent-decoding loop used by parseJsonLikeString) before assigning to
out.messagesUrl and out.actionMocksUrl so loadMessages() and loadActionMocks()
receive decoded URLs; keep the JSON-specific parsing steps out (only apply the
decoding loop).

In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx`:
- Around line 34-50: The temporary textarea created in copyToClipboard can leak
if document.execCommand throws; refactor copyToClipboard so the textarea
variable (ta) is declared outside the try block and removal
(document.body.removeChild(ta)) is moved into a finally block that runs whether
copy succeeds or fails; ensure you only attempt removal if ta was appended
(non-null) to avoid additional errors, and keep the function returning the copy
result (ok) or false on error.
- Around line 237-242: handleClear currently clears UI state but doesn't
invalidate any in-flight short-URL work; update handleClear to advance the
request sequence and clear the dev URL so any pending POST /__a2ui_payload
handlers fail the sequence check and cannot repopulate old QR/link state: inside
handleClear, increment or otherwise change lynxUrlSeqRef.current (e.g., ++ or
set to a new token) and call the setter to reset lynxDevUrl (e.g.,
setLynxDevUrl('')) in addition to the existing setCustomJson, setRenderUrl,
setRenderQrError, and setError calls.

---

Outside diff comments:
In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx`:
- Around line 203-208: The effect that calls doRender currently reruns when
doRender (and transitively rspeedyDevUrl) changes, which resets the preview to
ALL_SCENARIOS[0]; change the effect so the bootstrap render is one-shot or only
runs when there is no user state: update the useEffect containing
ALL_SCENARIOS[0] to run on mount (empty deps) or add a guard that only calls
doRender when currentScenario is null/undefined and customJson is empty (i.e.,
user hasn't selected/edited), referencing the existing useEffect, ALL_SCENARIOS,
doRender, rspeedyDevUrl, customJson and currentScenario to locate and implement
the guard. Ensure subsequent changes to rspeedyDevUrl do not force re-render of
the initially chosen scenario unless user state indicates no prior selection.

---

Nitpick comments:
In `@packages/genui/a2ui-playground/lynx-src/App.tsx`:
- Around line 202-206: Remove the unused didLogRef and its accompanying
useEffect: the const didLogRef = useRef(false) and the useEffect(...) block that
checks/sets didLogRef.current should be deleted since it no longer gates any
logging or side effects and only adds an unnecessary dependency list
(effectiveData, globalProps, globalPropsData, initData, rawInitData); ensure no
other code references didLogRef before removing.
🪄 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: f80d6f39-6849-4ad4-adad-9b1722d4c851

📥 Commits

Reviewing files that changed from the base of the PR and between 327be8c and 8cc78a2.

📒 Files selected for processing (14)
  • .changeset/few-monkeys-juggle.md
  • packages/genui/a2ui-playground/lynx-src/App.tsx
  • packages/genui/a2ui-playground/lynx.config.ts
  • packages/genui/a2ui-playground/package.json
  • packages/genui/a2ui-playground/rsbuild.config.ts
  • packages/genui/a2ui-playground/src/components/QrCode.tsx
  • packages/genui/a2ui-playground/src/css.d.ts
  • packages/genui/a2ui-playground/src/pages/DemosPage.tsx
  • packages/genui/a2ui-playground/src/pages/Dynamic.tsx
  • packages/genui/a2ui-playground/src/pages/Static.tsx
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui-playground/src/styles.css
  • packages/genui/a2ui-playground/src/utils/base64url.ts
  • packages/genui/a2ui-playground/src/utils/renderUrl.ts
💤 Files with no reviewable changes (2)
  • packages/genui/a2ui-playground/src/pages/Static.tsx
  • packages/genui/a2ui-playground/src/pages/Dynamic.tsx
✅ Files skipped from review due to trivial changes (4)
  • .changeset/few-monkeys-juggle.md
  • packages/genui/a2ui-playground/src/utils/renderUrl.ts
  • packages/genui/a2ui-playground/src/css.d.ts
  • packages/genui/a2ui-playground/rsbuild.config.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/genui/a2ui-playground/package.json
  • packages/genui/a2ui-playground/src/styles.css
  • packages/genui/a2ui-playground/lynx.config.ts
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui-playground/src/components/QrCode.tsx

Comment thread packages/genui/a2ui-playground/lynx-src/App.tsx
Comment thread packages/genui/a2ui-playground/src/pages/DemosPage.tsx Outdated
Comment thread packages/genui/a2ui-playground/src/pages/DemosPage.tsx
@Sherry-hue Sherry-hue force-pushed the feat/native-preview branch 2 times, most recently from 9867416 to ae5bc98 Compare April 29, 2026 05:37
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
packages/genui/a2ui-playground/src/pages/DemosPage.tsx (2)

34-50: ⚠️ Potential issue | 🟡 Minor

Always remove the temporary textarea.

If document.execCommand('copy') throws after append, the hidden textarea is never removed. Move cleanup into a finally (or use ta.remove()) so repeated copy failures do not leak DOM nodes.

Possible fix
 function copyToClipboard(text: string): boolean {
   // Prefer a compatibility path here: some lint configs treat `navigator` as a Node-only API.
+  const ta = document.createElement('textarea');
   try {
-    const ta = document.createElement('textarea');
     ta.value = text;
     ta.style.position = 'fixed';
     ta.style.left = '-9999px';
@@
     ta.focus();
     ta.select();
     const ok = document.execCommand('copy');
-    document.body.removeChild(ta);
     return ok;
   } catch {
     return false;
+  } finally {
+    ta.remove();
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx` around lines 34 - 50,
The copyToClipboard function can leak the temporary textarea if
document.execCommand('copy') throws; ensure the created element (ta) is always
removed by moving the cleanup into a finally block (or calling ta.remove() in
finally) so document.body.removeChild(ta) runs regardless of success or
exception; update the function body around the try/catch to guarantee ta is
removed even on errors while preserving the returned boolean from execCommand.

237-242: ⚠️ Potential issue | 🟠 Major

Clear should also invalidate the Lynx preview state.

This only resets the web preview. The old lynxDevUrl/copy state stays visible, and any in-flight POST /__a2ui_payload can still pass the current sequence check and repopulate stale QR data after Clear.

Possible fix
 const handleClear = useCallback(() => {
+  lynxUrlSeqRef.current += 1;
   setCustomJson('[]');
   setRenderUrl('');
+  setLynxDevUrl('');
   setRenderQrError('');
+  setLynxDevQrError('');
+  setLynxDevCopied(false);
   setError('');
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx` around lines 237 -
242, handleClear currently only resets web preview state (setCustomJson,
setRenderUrl, setRenderQrError, setError) but leaves Lynx preview/copy and the
payload sequence intact; update handleClear to also clear Lynx-specific state
(e.g., call the setter for lynxDevUrl and any copy flag like setLynxCopied) and
invalidate the in-flight POST acceptance token by bumping the payload sequence
(e.g., setPayloadSequence(s => s + 1) or setPayloadSequence(Date.now())) so any
late POST /__a2ui_payload responses are ignored and stale QR data cannot
repopulate the UI.
packages/genui/a2ui-playground/lynx.config.ts (1)

33-47: ⚠️ Potential issue | 🟡 Minor

Bound the buffered request body.

readJsonBody still buffers the entire request with no size or content-type guard. A single oversized or never-ending POST /__a2ui_payload can pin memory or stall the dev server; please reject non-JSON bodies early and cap the buffered bytes before concatenating.

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

In `@packages/genui/a2ui-playground/lynx.config.ts` around lines 33 - 47,
readJsonBody currently buffers the entire request with no content-type or size
checks; change readJsonBody(IncomingMessage) to first validate the Content-Type
header is application/json (reject early if not) and then track accumulated
bytes in the 'data' handler, rejecting the promise if a configurable
MAX_BODY_BYTES (e.g. 1MB) is exceeded before concatenating; on resolve or reject
remove all listeners ('data','end','error') to avoid leaks and ensure errors are
wrapped as Error instances as before.
packages/genui/a2ui-playground/lynx-src/App.tsx (1)

71-75: ⚠️ Potential issue | 🟠 Major

Decode messagesUrl and actionMocksUrl before storing them.

messages and actionMocks already go through the multi-pass decode path, but the URL fields are copied verbatim. DemosPage now sends these through query params on the Lynx dev URL, so native globalProps can leave them percent-encoded and fetch() ends up targeting the wrong string. Apply the same decode loop to these URL values before assigning them into out.

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

In `@packages/genui/a2ui-playground/lynx-src/App.tsx` around lines 71 - 75, The
URL fields messagesUrl and actionMocksUrl are copied verbatim into out but must
be percent-decoded just like messages and actionMocks; locate the multi-pass
decode logic used for messages/actionMocks and run messagesUrl and
actionMocksUrl through that same decode loop before assigning to out.messagesUrl
and out.actionMocksUrl so percent-encoded query params are correctly decoded for
fetch().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx`:
- Around line 99-100: The component currently discards the render QR error state
(const [, setRenderQrError]) so failures are recorded but never shown; change
this to keep the state value (e.g., const [renderQrError, setRenderQrError]) and
use it (alongside lynxDevQrError / setLynxDevQrError) to render a visible
fallback in the "View on Device" block whenever QrCode returns null or encoding
fails: set the error message inside the QrCode-creation/encode catch path and
render that message (or an explanatory placeholder/button) in place of the blank
area so users see why the QR couldn’t be generated. Ensure the same pattern is
applied to the other instance noted (lines ~352-358) where a state setter was
used without the corresponding value.

---

Duplicate comments:
In `@packages/genui/a2ui-playground/lynx-src/App.tsx`:
- Around line 71-75: The URL fields messagesUrl and actionMocksUrl are copied
verbatim into out but must be percent-decoded just like messages and
actionMocks; locate the multi-pass decode logic used for messages/actionMocks
and run messagesUrl and actionMocksUrl through that same decode loop before
assigning to out.messagesUrl and out.actionMocksUrl so percent-encoded query
params are correctly decoded for fetch().

In `@packages/genui/a2ui-playground/lynx.config.ts`:
- Around line 33-47: readJsonBody currently buffers the entire request with no
content-type or size checks; change readJsonBody(IncomingMessage) to first
validate the Content-Type header is application/json (reject early if not) and
then track accumulated bytes in the 'data' handler, rejecting the promise if a
configurable MAX_BODY_BYTES (e.g. 1MB) is exceeded before concatenating; on
resolve or reject remove all listeners ('data','end','error') to avoid leaks and
ensure errors are wrapped as Error instances as before.

In `@packages/genui/a2ui-playground/src/pages/DemosPage.tsx`:
- Around line 34-50: The copyToClipboard function can leak the temporary
textarea if document.execCommand('copy') throws; ensure the created element (ta)
is always removed by moving the cleanup into a finally block (or calling
ta.remove() in finally) so document.body.removeChild(ta) runs regardless of
success or exception; update the function body around the try/catch to guarantee
ta is removed even on errors while preserving the returned boolean from
execCommand.
- Around line 237-242: handleClear currently only resets web preview state
(setCustomJson, setRenderUrl, setRenderQrError, setError) but leaves Lynx
preview/copy and the payload sequence intact; update handleClear to also clear
Lynx-specific state (e.g., call the setter for lynxDevUrl and any copy flag like
setLynxCopied) and invalidate the in-flight POST acceptance token by bumping the
payload sequence (e.g., setPayloadSequence(s => s + 1) or
setPayloadSequence(Date.now())) so any late POST /__a2ui_payload responses are
ignored and stale QR data cannot repopulate the UI.
🪄 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: 28b01662-b86b-4130-aea7-3dac40e82a2e

📥 Commits

Reviewing files that changed from the base of the PR and between 8cc78a2 and ae5bc98.

📒 Files selected for processing (14)
  • .changeset/few-monkeys-juggle.md
  • packages/genui/a2ui-playground/lynx-src/App.tsx
  • packages/genui/a2ui-playground/lynx.config.ts
  • packages/genui/a2ui-playground/package.json
  • packages/genui/a2ui-playground/rsbuild.config.ts
  • packages/genui/a2ui-playground/src/components/QrCode.tsx
  • packages/genui/a2ui-playground/src/css.d.ts
  • packages/genui/a2ui-playground/src/pages/DemosPage.tsx
  • packages/genui/a2ui-playground/src/pages/Dynamic.tsx
  • packages/genui/a2ui-playground/src/pages/Static.tsx
  • packages/genui/a2ui-playground/src/render.tsx
  • packages/genui/a2ui-playground/src/styles.css
  • packages/genui/a2ui-playground/src/utils/base64url.ts
  • packages/genui/a2ui-playground/src/utils/renderUrl.ts
💤 Files with no reviewable changes (2)
  • packages/genui/a2ui-playground/src/pages/Dynamic.tsx
  • packages/genui/a2ui-playground/src/pages/Static.tsx
✅ Files skipped from review due to trivial changes (4)
  • .changeset/few-monkeys-juggle.md
  • packages/genui/a2ui-playground/src/utils/renderUrl.ts
  • packages/genui/a2ui-playground/src/css.d.ts
  • packages/genui/a2ui-playground/src/styles.css
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/genui/a2ui-playground/package.json
  • packages/genui/a2ui-playground/rsbuild.config.ts
  • packages/genui/a2ui-playground/src/render.tsx

Comment thread packages/genui/a2ui-playground/src/pages/DemosPage.tsx
@Sherry-hue Sherry-hue force-pushed the feat/native-preview branch from ae5bc98 to a814b39 Compare April 29, 2026 06:09
@Sherry-hue Sherry-hue force-pushed the feat/native-preview branch from a814b39 to c6d2b1e Compare April 29, 2026 06:29
@Sherry-hue
Copy link
Copy Markdown
Collaborator Author

Review

1. __rspeedy_url returns the wrong port when the configured port is already in use

rsbuild.config.ts:27:

const PORT = Number(process.env.PORT ?? 3000);

This is evaluated at module load time, but rsbuild auto-increments the port if it's already taken (e.g. 3000 → 3001). The /__rspeedy_url endpoint then returns a URL with port 3000 while the server is actually on 3001 — native preview will fail to connect.

The actual listening port should be read from the server instance at request time rather than from the static config value.

2. Dead didLogRef effect in App.tsx

lynx-src/App.tsx:120-124:

const didLogRef = useRef(false);
useEffect(() => {
  if (didLogRef.current) return;
  didLogRef.current = true;
}, [effectiveData, globalProps, globalPropsData, initData, rawInitData]);

This sets a flag once and does nothing else — looks like debug logging was removed but the scaffolding was left behind. Should be deleted.

3. copyToClipboard uses deprecated document.execCommand('copy')

DemosPage.tsx:31-47: The comment says "some lint configs treat navigator as a Node-only API" but this code only runs in the browser. navigator.clipboard.writeText() is the standard API supported in all major browsers. document.execCommand('copy') is deprecated and will eventually be removed.

async function copyToClipboard(text: string): Promise<boolean> {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch {
    return false;
  }
}

grate! These issues have been fixed.

@Sherry-hue Sherry-hue merged commit 29ba9ba into lynx-family:main Apr 29, 2026
125 of 134 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