perf(a2ui-playground): cap concurrent iframe mounts on showcase grid#2732
Conversation
Approach B: concurrency-capped, viewport-prioritized mount queue for the 43-card Showcase grid. Caps in-flight iframes to MAX_CONCURRENT=4 and uses IntersectionObserver to prioritize cards near the viewport.
The Showcase tab (DemosListPage) renders ~43 cards, each previously mounting an <iframe> immediately. Every iframe spawns a Web Worker, loads web-core + web-elements, fetches main.web.js, and boots a Lynx app running an A2UI render. ~43x of that on a single page made TTI unusable. Introduces a per-page MountQueue that holds at most MAX_CONCURRENT=4 "armed" cards. Cards register on mount and report their viewport priority (OFFSCREEN / NEAR / IN_VIEW) via two IntersectionObservers (rootMargin '50% 0px' for NEAR). The queue picks the top-K visible pending cards; armed cards get their preview iframe; the rest stay in the existing PreviewViewport empty state until promoted. A card frees its slot when the iframe posts A2UI_RENDER_READY (already sent by render.tsx). Fallback: iframe.onLoad + 5s safety timeout, so a failed iframe can't stall the queue. Tests: pure-JS MountQueue covered by 12 unit cases via @rstest/core. PreviewViewport itself is unchanged - all gating happens upstream via the src prop.
… requeue) - Theme/protocol changes now re-run through the mount queue via a new resetKey prop on MountQueueProvider, so toggling dark/light no longer reloads all 43 iframes at once. - Each card now overlays a centered "title + pulsing dots" placeholder on top of the iframe while it boots, fading out only when the iframe emits A2UI_RENDER_READY. This eliminates the white-iframe flash and also hides the pre-existing web-core LynxView race (enableJSDataProcessor undefined -> createIFrameRealm null contentDocument) that left a couple of cards blank - those cards now stay in the loading state rather than revealing a broken frame. - The safety timeout still releases the queue slot after 5s so the queue never stalls, but no longer sets rendered=true, keeping the overlay on top of slow-or-broken iframes (and still lifting it if the iframe eventually fires RENDER_READY). - Empty-state CSS (.previewEmpty) now fills its container and centers vertically, so the un-armed card no longer slumps to the top edge.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a concurrency-capped, viewport-prioritized mount queue to A2UI Playground that gates iframe mounts by priority, releases slots when iframes signal A2UI_RENDER_READY (or after a 5s fallback), integrates via a React provider/hook, wires ExampleCard in DemosListPage, adds styles, tests, and rstest infra. ChangesQueue Mount System
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR improves the A2UI playground Showcase grid’s responsiveness by throttling when preview iframes are allowed to mount, preventing dozens of lynx-view + worker boots from starting in parallel.
Changes:
- Add a priority-aware
MountQueueplus React provider/hook to cap concurrent iframe mounts (default 4). - Update
DemosListPagecards to gatePreviewViewport’ssrcvia the queue and show a loading overlay untilA2UI_RENDER_READY. - Add
@rstest/coretest coverage for the queue and wire uprstestconfig / script.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds lock entry for @rstest/core. |
| packages/genui/a2ui-playground/tsconfig.json | Includes rstest.config.ts in TS project inputs. |
| packages/genui/a2ui-playground/src/utils/mountQueue.ts | Implements the concurrency-capped, priority-based mount scheduler. |
| packages/genui/a2ui-playground/src/utils/mountQueue.test.ts | Adds unit tests validating queue behavior. |
| packages/genui/a2ui-playground/src/styles.css | Adds loading overlay styles and improves empty preview centering. |
| packages/genui/a2ui-playground/src/pages/DemosListPage.tsx | Integrates queue-driven iframe mounting + render-ready handling per card. |
| packages/genui/a2ui-playground/src/hooks/useMountQueue.tsx | Provides MountQueueProvider + useQueuedMount hook. |
| packages/genui/a2ui-playground/rstest.config.ts | Adds rstest config for this package. |
| packages/genui/a2ui-playground/package.json | Adds test script and @rstest/core dev dependency. |
| docs/superpowers/specs/2026-05-28-a2ui-showcase-bundle-loading-design.md | Adds design spec documenting the approach and test plan. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {EXTENDED_EXAMPLES.map((scenario) => ( | ||
| <ExampleCard | ||
| key={scenario.id} | ||
| scenario={scenario} | ||
| previewUrl={previewUrls.get(scenario.id)} | ||
| onOpen={handleOpenExample} | ||
| onKeyDown={handleCardKeyDown} | ||
| /> | ||
| ))} |
| Each card immediately mounts a `<PreviewViewport>` that contains an | ||
| `<iframe src="render.html?...&instant=1">`. Every iframe: | ||
|
|
||
| 1. Imports `@lynx-js/web-core/client` + `@lynx-js/web-elements/all`. | ||
| 2. Mounts `<lynx-view url="/main.web.js" thread-strategy="multi-thread" />`. | ||
| 3. Web-core spawns a **dedicated Web Worker** per iframe (multi-thread mode). | ||
| 4. Fetches/parses `main.web.js`, boots the Lynx app, replays the demo | ||
| messages with `instant=1`. |
| 1. `pnpm -C packages/genui/a2ui-playground build:lynx` (one-time). | ||
| 2. `PORT=5371 pnpm -C packages/genui/a2ui-playground dev` (unique port | ||
| avoids conflicting with other devs servers). | ||
| 3. Open `http://localhost:5371` → click "Examples" tab. | ||
| 4. DevTools → Network: at most 4 in-flight `main.web.js` requests at | ||
| any moment; rest queued/pending. |
| {OFFICIAL_EXAMPLES.map((scenario) => ( | ||
| <ExampleCard | ||
| key={scenario.id} | ||
| scenario={scenario} | ||
| previewUrl={previewUrls.get(scenario.id)} | ||
| badge='From A2UI Gallery' | ||
| onOpen={handleOpenExample} | ||
| onKeyDown={handleCardKeyDown} | ||
| /> | ||
| ))} |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/genui/a2ui-playground/src/styles.css (1)
1163-1178: 💤 Low valueStatic analysis: Keyframe names and keyword case.
Per Stylelint hints:
- Line 1167:
currentColorshould becurrentcolor(CSS keywords are case-insensitive, but the linter enforces lowercase).- Lines 1180, 1189: Keyframe names should use kebab-case (
card-preview-pulse,card-preview-dot) perkeyframes-name-pattern.Note: The existing codebase has mixed conventions (e.g.,
tagAppearat line 877 also uses camelCase), so this may be a broader cleanup.🔧 Suggested fixes for lint compliance
.cardPreviewLoadingDots span { width: 4px; height: 4px; border-radius: 50%; - background: currentColor; + background: currentcolor; opacity: 0.35; - animation: cardPreviewDot 1.2s ease-in-out infinite; + animation: card-preview-dot 1.2s ease-in-out infinite; } -@keyframes cardPreviewPulse { +@keyframes card-preview-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 0.85; } } -@keyframes cardPreviewDot { +@keyframes card-preview-dot { 0%, 100% { transform: translateY(0); opacity: 0.25; } 50% { transform: translateY(-2px); opacity: 0.9; } }Also update the animation reference in
.cardPreviewLoadingTitle(line 1155):- animation: cardPreviewPulse 1.8s ease-in-out infinite; + animation: card-preview-pulse 1.8s ease-in-out infinite;Also applies to: 1180-1198
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui-playground/src/styles.css` around lines 1163 - 1178, Update CSS to satisfy stylelint: change the color keyword currentColor to lowercase currentcolor in .cardPreviewLoadingDots, rename keyframe identifiers from camelCase (cardPreviewPulse, cardPreviewDot) to kebab-case (e.g., card-preview-pulse, card-preview-dot), and update any animation references that use the old names (including .cardPreviewLoadingTitle and .cardPreviewLoadingDots span rules) to use the new kebab-case names; ensure keyframes blocks themselves are renamed to match the new identifiers.packages/genui/a2ui-playground/src/pages/DemosListPage.tsx (1)
159-170: 💤 Low valueConsider validating message origin for defense-in-depth.
The existing pattern in
AIChatPage.tsxvalidatese.originbefore processing postMessages. While thee.sourcecheck here ensures the message comes from this card's iframe, adding origin validation would provide an extra layer of defense against potential cross-origin message spoofing.🛡️ Suggested origin validation
useEffect(() => { if (!armed) return; const handler = (e: MessageEvent) => { if (e.source !== iframeRef.current?.contentWindow) return; + // Optional: validate origin matches the expected render URL origin + // if (previewUrl && e.origin !== new URL(previewUrl, window.location.origin).origin) return; const data = e.data as { type?: string } | null; if (data && data.type === 'A2UI_RENDER_READY') { handleRenderReady(); } }; window.addEventListener('message', handler); return () => window.removeEventListener('message', handler); }, [armed, handleRenderReady]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui-playground/src/pages/DemosListPage.tsx` around lines 159 - 170, The message handler in useEffect for A2UI_RENDER_READY only checks e.source and should also validate e.origin for defense-in-depth: in the handler (function handler) check that iframeRef.current exists, derive the iframe's expected origin from iframeRef.current.src (e.g., new URL(iframeRef.current.src).origin) or compare against a configured allowlist, and return early if e.origin !== expectedOrigin before inspecting e.data and calling handleRenderReady; keep the existing e.source check and ensure the origin check is performed prior to processing the message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/specs/2026-05-28-a2ui-showcase-bundle-loading-design.md`:
- Around line 56-64: The fenced code block lacks a language tag which triggers
MD040; update the block fence surrounding the diagram (the section showing
DemosListPage → MountQueueProvider → ExampleCard → useQueuedMount →
PreviewViewport) to include a language identifier (e.g., "text") so the opening
fence becomes ```text and the closing fence remains ```, ensuring the snippet is
properly labeled without changing the diagram content.
In `@packages/genui/a2ui-playground/src/utils/mountQueue.ts`:
- Around line 46-48: The constructor for MountQueue should validate the
maxConcurrent input: in the constructor function (constructor) check that the
provided maxConcurrent is a finite integer > 0 (use Number.isInteger and
isFinite or coerce +Number and test), and if the value is invalid either throw a
descriptive TypeError or clamp to a safe default (e.g. 1); then assign the
validated value to this.maxConcurrent. Ensure you also guard against NaN and
fractional values so downstream logic that computes slots using
this.maxConcurrent behaves correctly.
---
Nitpick comments:
In `@packages/genui/a2ui-playground/src/pages/DemosListPage.tsx`:
- Around line 159-170: The message handler in useEffect for A2UI_RENDER_READY
only checks e.source and should also validate e.origin for defense-in-depth: in
the handler (function handler) check that iframeRef.current exists, derive the
iframe's expected origin from iframeRef.current.src (e.g., new
URL(iframeRef.current.src).origin) or compare against a configured allowlist,
and return early if e.origin !== expectedOrigin before inspecting e.data and
calling handleRenderReady; keep the existing e.source check and ensure the
origin check is performed prior to processing the message.
In `@packages/genui/a2ui-playground/src/styles.css`:
- Around line 1163-1178: Update CSS to satisfy stylelint: change the color
keyword currentColor to lowercase currentcolor in .cardPreviewLoadingDots,
rename keyframe identifiers from camelCase (cardPreviewPulse, cardPreviewDot) to
kebab-case (e.g., card-preview-pulse, card-preview-dot), and update any
animation references that use the old names (including .cardPreviewLoadingTitle
and .cardPreviewLoadingDots span rules) to use the new kebab-case names; ensure
keyframes blocks themselves are renamed to match the new identifiers.
🪄 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: d42eba45-ecbb-47a5-9c09-e1a2e1f8d2e6
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (9)
docs/superpowers/specs/2026-05-28-a2ui-showcase-bundle-loading-design.mdpackages/genui/a2ui-playground/package.jsonpackages/genui/a2ui-playground/rstest.config.tspackages/genui/a2ui-playground/src/hooks/useMountQueue.tsxpackages/genui/a2ui-playground/src/pages/DemosListPage.tsxpackages/genui/a2ui-playground/src/styles.csspackages/genui/a2ui-playground/src/utils/mountQueue.test.tspackages/genui/a2ui-playground/src/utils/mountQueue.tspackages/genui/a2ui-playground/tsconfig.json
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will improve performance by 9.58%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | transform 1000 view elements |
47.3 ms | 43.2 ms | +9.58% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing Huxpro:perf/a2ui-bundle-loading (86b6a9f) with main (f60716d)2
Footnotes
-
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. ↩
-
No successful run was found on
main(2d84c9f) during the generation of this report, so f60716d was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
- CSS: lowercase currentcolor, rename keyframes to kebab-case (card-preview-pulse, card-preview-dot) per stylelint rules - Docs: add `text` language tag to fenced code block (MD040) - mountQueue: guard constructor against negative/fractional maxConcurrent https://claude.ai/code/session_01QSss98Q6Zsz76uXcN4AtC1
Add `e.origin !== window.location.origin` guard alongside the existing `e.source` check so cross-origin frames can never trigger the render-ready flow, as suggested by CodeRabbit. https://claude.ai/code/session_014urpkMxFesXKwT8fGh5NfC
Addresses CodeRabbit docstring-coverage pre-merge check (was 0%). Each public method/constructor now has a one-line summary covering the non-obvious invariants (slot vs armed distinction, unsubscribe return value, etc.).
- Replace main.web.js → a2ui.web.js in design spec (problem description and manual test step); the playground has always used a2ui.web.js. - Rename CSS @Keyframes tagAppear → tag-appear (and its animation reference) to follow kebab-case naming convention.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
packages/genui/a2ui-playground/src/styles.css (3)
1051-1051: ⚡ Quick winUse kebab-case for container names.
CSS container names should follow kebab-case convention. Change
previewQrtopreview-qrat line 1051 and in the@containerquery at line 1067.♻️ Proposed fix
- container-name: previewQr; + container-name: preview-qr;-@container previewQr (max-width: 380px) { +@container preview-qr (max-width: 380px) {Also applies to: 1067-1067
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui-playground/src/styles.css` at line 1051, Change the CSS container name from camelCase to kebab-case: replace the container-name value "previewQr" with "preview-qr" where it's declared (the container-name property) and update the matching `@container` rule that currently references previewQr to use `@container` preview-qr instead so the container name usage is consistent.
736-736: ⚡ Quick winUse kebab-case for container names.
CSS container names should follow kebab-case convention. Change
previewPaneltopreview-panelat line 736 and in the@containerquery at line 768.♻️ Proposed fix
- container-name: previewPanel; + container-name: preview-panel;-@container previewPanel (max-width: 660px) { +@container preview-panel (max-width: 660px) {Also applies to: 768-768
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui-playground/src/styles.css` at line 736, The CSS uses a camelCase container name `previewPanel`; change it to kebab-case `preview-panel` by updating the container declaration `container-name: previewPanel;` to `container-name: preview-panel;` and update the matching `@container` query that references `previewPanel` to `@container preview-panel` (also search for any other occurrences of `previewPanel` and replace them with `preview-panel` to keep names consistent).
1236-1246: 💤 Low valueConsider modernizing the visually-hidden pattern.
The
clipproperty at line 1243 is deprecated in favor ofclip-path. While this pattern still works, consider using the modern equivalent for future compatibility.♻️ Proposed fix
.previewShareDescription { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; - clip: rect(0, 0, 0, 0); + clip-path: inset(50%); white-space: nowrap; border: 0; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/genui/a2ui-playground/src/styles.css` around lines 1236 - 1246, The visually-hidden CSS in the .previewShareDescription rule uses the deprecated clip property; update the rule to use the modern clip-path equivalent (e.g., clip-path: inset(50%); and include -webkit-clip-path for broader support) while keeping the existing positioning, size, overflow, white-space, padding, margin and border declarations so the element remains visually hidden but accessible; modify the .previewShareDescription selector to replace clip: rect(0, 0, 0, 0); with clip-path: inset(50%); (and -webkit-clip-path: inset(50%); as a fallback).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/genui/a2ui-playground/src/styles.css`:
- Line 1051: Change the CSS container name from camelCase to kebab-case: replace
the container-name value "previewQr" with "preview-qr" where it's declared (the
container-name property) and update the matching `@container` rule that currently
references previewQr to use `@container` preview-qr instead so the container name
usage is consistent.
- Line 736: The CSS uses a camelCase container name `previewPanel`; change it to
kebab-case `preview-panel` by updating the container declaration
`container-name: previewPanel;` to `container-name: preview-panel;` and update
the matching `@container` query that references `previewPanel` to `@container
preview-panel` (also search for any other occurrences of `previewPanel` and
replace them with `preview-panel` to keep names consistent).
- Around line 1236-1246: The visually-hidden CSS in the .previewShareDescription
rule uses the deprecated clip property; update the rule to use the modern
clip-path equivalent (e.g., clip-path: inset(50%); and include -webkit-clip-path
for broader support) while keeping the existing positioning, size, overflow,
white-space, padding, margin and border declarations so the element remains
visually hidden but accessible; modify the .previewShareDescription selector to
replace clip: rect(0, 0, 0, 0); with clip-path: inset(50%); (and
-webkit-clip-path: inset(50%); as a fallback).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: cff6d08b-a4fa-455b-8f82-31368f8e5622
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
docs/superpowers/specs/2026-05-28-a2ui-showcase-bundle-loading-design.mdpackages/genui/a2ui-playground/package.jsonpackages/genui/a2ui-playground/src/hooks/useMountQueue.tsxpackages/genui/a2ui-playground/src/pages/DemosListPage.tsxpackages/genui/a2ui-playground/src/styles.csspackages/genui/a2ui-playground/src/utils/mountQueue.ts
✅ Files skipped from review due to trivial changes (1)
- docs/superpowers/specs/2026-05-28-a2ui-showcase-bundle-loading-design.md
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/genui/a2ui-playground/package.json
- packages/genui/a2ui-playground/src/hooks/useMountQueue.tsx
- packages/genui/a2ui-playground/src/pages/DemosListPage.tsx
- packages/genui/a2ui-playground/src/utils/mountQueue.ts
…clip - Rename container-name `previewPanel` → `preview-panel` and matching `@container` query. - Rename container-name `previewQr` → `preview-qr` and matching `@container` query. - Replace deprecated `clip: rect(0, 0, 0, 0)` with modern `clip-path: inset(50%)` in the visually-hidden pattern. Addresses CodeRabbit review on 2026-05-28.
|
Actionable comments posted: 0 |
|
Actionable comments posted: 0 |
Summary
The A2UI playground's Examples / Showcase tab (
DemosListPage) renders a card grid with 43 demos — 8 playground examples + 35 A2UI Gallery entries. Each card previously mounted an<iframe src="render.html?...&instant=1">immediately, and every iframe:@lynx-js/web-core/client+@lynx-js/web-elements/all.<lynx-view url="/a2ui.web.js" thread-strategy="multi-thread" />.43× of that in parallel pinned the main thread and made the page borderline unresponsive. This PR keeps the per-card rendering pipeline exactly as it was, but throttles when each iframe is allowed to start.
How
MountQueue(pure JS, ~80 LOC) lives insrc/utils/mountQueue.ts. It holds at mostMAX_CONCURRENT = 4"armed" cards. Selection is priority-driven (OFFSCREEN < NEAR < IN_VIEW); ties broken by registration order.IntersectionObservers (one viewport, one withrootMargin: '50% 0px') to report its current priority to the queue.src→ the iframe mounts →render.tsxboots and posts the already-existingA2UI_RENDER_READYmessage. The card listens for that on its own iframe'scontentWindowand frees the queue slot.iframe.onLoadstarts a 5 s safety timer that releases the slot ifA2UI_RENDER_READYnever arrives, so the queue can't stall.CardPreviewLoadingoverlay (centered card title + pulsing dots,prefers-reduced-motion-aware) sits on top of the iframe untilRENDER_READYfires — eliminates the white-iframe flash between mount and first render, and hides a pre-existing web-coreLynxViewInstance.updateDatarace (enableJSDataProcessorundefined →createIFrameRealmnullcontentDocument) that left ~2 cards blank.resetKeyprop onMountQueueProvider, so flipping dark/light no longer reloads all 43 iframes at once.PreviewViewportandrender.tsxare unchanged — all gating happens via thesrcprop and the existing postMessage.Scope is
packages/genui/a2ui-playgroundonly. Noweb-core/lynx-viewchanges.Test plan
pnpm --filter a2ui-playground test— 12 / 12 unit cases pass forMountQueue(registration, priority, capacity, ties, sticky-armed, subscribe/unsubscribe, reset)PORT=5371 pnpm -C packages/genui/a2ui-playground dev→ openhttp://localhost:5371/→ Examplesa2ui.web.jsrequests at any momentSummary by CodeRabbit
New Features
User Experience
Documentation
Tests
Styles
Chores