@ambitresearch/genie 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/auth/bearer.d.ts +49 -0
- package/dist/auth/bearer.d.ts.map +1 -0
- package/dist/auth/bearer.js +159 -0
- package/dist/auth/bearer.js.map +1 -0
- package/dist/auth/oauth/authorize.d.ts +19 -0
- package/dist/auth/oauth/authorize.d.ts.map +1 -0
- package/dist/auth/oauth/authorize.js +111 -0
- package/dist/auth/oauth/authorize.js.map +1 -0
- package/dist/auth/oauth/config.d.ts +10 -0
- package/dist/auth/oauth/config.d.ts.map +1 -0
- package/dist/auth/oauth/config.js +16 -0
- package/dist/auth/oauth/config.js.map +1 -0
- package/dist/auth/oauth/dcr.d.ts +15 -0
- package/dist/auth/oauth/dcr.d.ts.map +1 -0
- package/dist/auth/oauth/dcr.js +38 -0
- package/dist/auth/oauth/dcr.js.map +1 -0
- package/dist/auth/oauth/jwt.d.ts +13 -0
- package/dist/auth/oauth/jwt.d.ts.map +1 -0
- package/dist/auth/oauth/jwt.js +46 -0
- package/dist/auth/oauth/jwt.js.map +1 -0
- package/dist/auth/oauth/metadata.d.ts +9 -0
- package/dist/auth/oauth/metadata.d.ts.map +1 -0
- package/dist/auth/oauth/metadata.js +19 -0
- package/dist/auth/oauth/metadata.js.map +1 -0
- package/dist/auth/oauth/router.d.ts +32 -0
- package/dist/auth/oauth/router.d.ts.map +1 -0
- package/dist/auth/oauth/router.js +159 -0
- package/dist/auth/oauth/router.js.map +1 -0
- package/dist/auth/oauth/store.d.ts +55 -0
- package/dist/auth/oauth/store.d.ts.map +1 -0
- package/dist/auth/oauth/store.js +63 -0
- package/dist/auth/oauth/store.js.map +1 -0
- package/dist/auth/oauth/token.d.ts +21 -0
- package/dist/auth/oauth/token.d.ts.map +1 -0
- package/dist/auth/oauth/token.js +77 -0
- package/dist/auth/oauth/token.js.map +1 -0
- package/dist/auth/oidc/group-policy.d.ts +43 -0
- package/dist/auth/oidc/group-policy.d.ts.map +1 -0
- package/dist/auth/oidc/group-policy.js +54 -0
- package/dist/auth/oidc/group-policy.js.map +1 -0
- package/dist/auth/oidc/verifier.d.ts +42 -0
- package/dist/auth/oidc/verifier.d.ts.map +1 -0
- package/dist/auth/oidc/verifier.js +145 -0
- package/dist/auth/oidc/verifier.js.map +1 -0
- package/dist/auth/token-cli.d.ts +9 -0
- package/dist/auth/token-cli.d.ts.map +1 -0
- package/dist/auth/token-cli.js +98 -0
- package/dist/auth/token-cli.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +149 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/redact.d.ts +20 -0
- package/dist/config/redact.d.ts.map +1 -0
- package/dist/config/redact.js +77 -0
- package/dist/config/redact.js.map +1 -0
- package/dist/config/secrets.d.ts +100 -0
- package/dist/config/secrets.d.ts.map +1 -0
- package/dist/config/secrets.js +130 -0
- package/dist/config/secrets.js.map +1 -0
- package/dist/framework/html.d.ts +103 -0
- package/dist/framework/html.d.ts.map +1 -0
- package/dist/framework/html.js +271 -0
- package/dist/framework/html.js.map +1 -0
- package/dist/framework/interface.d.ts +130 -0
- package/dist/framework/interface.d.ts.map +1 -0
- package/dist/framework/interface.js +112 -0
- package/dist/framework/interface.js.map +1 -0
- package/dist/framework/react.d.ts +133 -0
- package/dist/framework/react.d.ts.map +1 -0
- package/dist/framework/react.js +243 -0
- package/dist/framework/react.js.map +1 -0
- package/dist/framework/vue.d.ts +99 -0
- package/dist/framework/vue.d.ts.map +1 -0
- package/dist/framework/vue.js +375 -0
- package/dist/framework/vue.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/client.d.ts +81 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +141 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/component-response.d.ts +111 -0
- package/dist/llm/component-response.d.ts.map +1 -0
- package/dist/llm/component-response.js +167 -0
- package/dist/llm/component-response.js.map +1 -0
- package/dist/llm/generated-files.d.ts +10 -0
- package/dist/llm/generated-files.d.ts.map +1 -0
- package/dist/llm/generated-files.js +26 -0
- package/dist/llm/generated-files.js.map +1 -0
- package/dist/llm/prompts/CHANGELOG.md +70 -0
- package/dist/llm/prompts/generate-component.system.md +99 -0
- package/dist/llm/prompts/refine-component.system.md +112 -0
- package/dist/llm/prompts.d.ts +66 -0
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/llm/prompts.js +78 -0
- package/dist/llm/prompts.js.map +1 -0
- package/dist/llm/retry.d.ts +183 -0
- package/dist/llm/retry.d.ts.map +1 -0
- package/dist/llm/retry.js +475 -0
- package/dist/llm/retry.js.map +1 -0
- package/dist/llm/schema.d.ts +129 -0
- package/dist/llm/schema.d.ts.map +1 -0
- package/dist/llm/schema.js +147 -0
- package/dist/llm/schema.js.map +1 -0
- package/dist/llm/validate.d.ts +107 -0
- package/dist/llm/validate.d.ts.map +1 -0
- package/dist/llm/validate.js +174 -0
- package/dist/llm/validate.js.map +1 -0
- package/dist/manifest/compiler.d.ts +112 -0
- package/dist/manifest/compiler.d.ts.map +1 -0
- package/dist/manifest/compiler.js +306 -0
- package/dist/manifest/compiler.js.map +1 -0
- package/dist/manifest/ensure.d.ts +46 -0
- package/dist/manifest/ensure.d.ts.map +1 -0
- package/dist/manifest/ensure.js +49 -0
- package/dist/manifest/ensure.js.map +1 -0
- package/dist/manifest/index.d.ts +13 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +13 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/middleware/plan-guard.d.ts +167 -0
- package/dist/middleware/plan-guard.d.ts.map +1 -0
- package/dist/middleware/plan-guard.js +190 -0
- package/dist/middleware/plan-guard.js.map +1 -0
- package/dist/plans/index.d.ts +71 -0
- package/dist/plans/index.d.ts.map +1 -0
- package/dist/plans/index.js +252 -0
- package/dist/plans/index.js.map +1 -0
- package/dist/schemas/component.schema.json +114 -0
- package/dist/server.d.ts +96 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +258 -0
- package/dist/server.js.map +1 -0
- package/dist/store/empty-manifest.d.ts +58 -0
- package/dist/store/empty-manifest.d.ts.map +1 -0
- package/dist/store/empty-manifest.js +59 -0
- package/dist/store/empty-manifest.js.map +1 -0
- package/dist/store/git-host.d.ts +120 -0
- package/dist/store/git-host.d.ts.map +1 -0
- package/dist/store/git-host.js +796 -0
- package/dist/store/git-host.js.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +4 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/interface.d.ts +322 -0
- package/dist/store/interface.d.ts.map +1 -0
- package/dist/store/interface.js +100 -0
- package/dist/store/interface.js.map +1 -0
- package/dist/store/kit-files.d.ts +88 -0
- package/dist/store/kit-files.d.ts.map +1 -0
- package/dist/store/kit-files.js +199 -0
- package/dist/store/kit-files.js.map +1 -0
- package/dist/store/local.d.ts +70 -0
- package/dist/store/local.d.ts.map +1 -0
- package/dist/store/local.js +651 -0
- package/dist/store/local.js.map +1 -0
- package/dist/store/manifest.d.ts +44 -0
- package/dist/store/manifest.d.ts.map +1 -0
- package/dist/store/manifest.js +116 -0
- package/dist/store/manifest.js.map +1 -0
- package/dist/store/viewer-assets.d.ts +20 -0
- package/dist/store/viewer-assets.d.ts.map +1 -0
- package/dist/store/viewer-assets.js +118 -0
- package/dist/store/viewer-assets.js.map +1 -0
- package/dist/sync/anchor.d.ts +125 -0
- package/dist/sync/anchor.d.ts.map +1 -0
- package/dist/sync/anchor.js +210 -0
- package/dist/sync/anchor.js.map +1 -0
- package/dist/sync/index.d.ts +15 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +15 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/orchestrator.d.ts +160 -0
- package/dist/sync/orchestrator.d.ts.map +1 -0
- package/dist/sync/orchestrator.js +288 -0
- package/dist/sync/orchestrator.js.map +1 -0
- package/dist/tools/bind_kit.d.ts +29 -0
- package/dist/tools/bind_kit.d.ts.map +1 -0
- package/dist/tools/bind_kit.js +70 -0
- package/dist/tools/bind_kit.js.map +1 -0
- package/dist/tools/conjure.d.ts +141 -0
- package/dist/tools/conjure.d.ts.map +1 -0
- package/dist/tools/conjure.js +655 -0
- package/dist/tools/conjure.js.map +1 -0
- package/dist/tools/conjure_screen.d.ts +174 -0
- package/dist/tools/conjure_screen.d.ts.map +1 -0
- package/dist/tools/conjure_screen.js +458 -0
- package/dist/tools/conjure_screen.js.map +1 -0
- package/dist/tools/create_kit.d.ts +32 -0
- package/dist/tools/create_kit.d.ts.map +1 -0
- package/dist/tools/create_kit.js +131 -0
- package/dist/tools/create_kit.js.map +1 -0
- package/dist/tools/create_project.d.ts +267 -0
- package/dist/tools/create_project.d.ts.map +1 -0
- package/dist/tools/create_project.js +546 -0
- package/dist/tools/create_project.js.map +1 -0
- package/dist/tools/delete_files.d.ts +93 -0
- package/dist/tools/delete_files.d.ts.map +1 -0
- package/dist/tools/delete_files.js +276 -0
- package/dist/tools/delete_files.js.map +1 -0
- package/dist/tools/delete_project.d.ts +43 -0
- package/dist/tools/delete_project.d.ts.map +1 -0
- package/dist/tools/delete_project.js +118 -0
- package/dist/tools/delete_project.js.map +1 -0
- package/dist/tools/get_kit.d.ts +28 -0
- package/dist/tools/get_kit.d.ts.map +1 -0
- package/dist/tools/get_kit.js +95 -0
- package/dist/tools/get_kit.js.map +1 -0
- package/dist/tools/get_project.d.ts +37 -0
- package/dist/tools/get_project.d.ts.map +1 -0
- package/dist/tools/get_project.js +71 -0
- package/dist/tools/get_project.js.map +1 -0
- package/dist/tools/list_components.d.ts +42 -0
- package/dist/tools/list_components.d.ts.map +1 -0
- package/dist/tools/list_components.js +125 -0
- package/dist/tools/list_components.js.map +1 -0
- package/dist/tools/list_files.d.ts +32 -0
- package/dist/tools/list_files.d.ts.map +1 -0
- package/dist/tools/list_files.js +109 -0
- package/dist/tools/list_files.js.map +1 -0
- package/dist/tools/list_kits.d.ts +14 -0
- package/dist/tools/list_kits.d.ts.map +1 -0
- package/dist/tools/list_kits.js +41 -0
- package/dist/tools/list_kits.js.map +1 -0
- package/dist/tools/list_projects.d.ts +23 -0
- package/dist/tools/list_projects.d.ts.map +1 -0
- package/dist/tools/list_projects.js +66 -0
- package/dist/tools/list_projects.js.map +1 -0
- package/dist/tools/plan.d.ts +16 -0
- package/dist/tools/plan.d.ts.map +1 -0
- package/dist/tools/plan.js +146 -0
- package/dist/tools/plan.js.map +1 -0
- package/dist/tools/preview.d.ts +289 -0
- package/dist/tools/preview.d.ts.map +1 -0
- package/dist/tools/preview.js +672 -0
- package/dist/tools/preview.js.map +1 -0
- package/dist/tools/read_file.d.ts +40 -0
- package/dist/tools/read_file.d.ts.map +1 -0
- package/dist/tools/read_file.js +114 -0
- package/dist/tools/read_file.js.map +1 -0
- package/dist/tools/refine.d.ts +220 -0
- package/dist/tools/refine.d.ts.map +1 -0
- package/dist/tools/refine.js +501 -0
- package/dist/tools/refine.js.map +1 -0
- package/dist/tools/validate.d.ts +74 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +224 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/tools/write_files.d.ts +158 -0
- package/dist/tools/write_files.d.ts.map +1 -0
- package/dist/tools/write_files.js +541 -0
- package/dist/tools/write_files.js.map +1 -0
- package/dist/transport.d.ts +77 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +448 -0
- package/dist/transport.js.map +1 -0
- package/dist/ui/card-asset-broker.d.ts +42 -0
- package/dist/ui/card-asset-broker.d.ts.map +1 -0
- package/dist/ui/card-asset-broker.js +479 -0
- package/dist/ui/card-asset-broker.js.map +1 -0
- package/dist/ui/grid-resource.d.ts +252 -0
- package/dist/ui/grid-resource.d.ts.map +1 -0
- package/dist/ui/grid-resource.js +680 -0
- package/dist/ui/grid-resource.js.map +1 -0
- package/dist/ui/viewer-static/index.html +95 -0
- package/dist/ui/viewer-static/viewer.css +451 -0
- package/dist/ui/viewer-static/viewer.js +1405 -0
- package/dist/validate/full-scan.d.ts +87 -0
- package/dist/validate/full-scan.d.ts.map +1 -0
- package/dist/validate/full-scan.js +299 -0
- package/dist/validate/full-scan.js.map +1 -0
- package/dist/validate/index.d.ts +15 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +18 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate/marker.d.ts +89 -0
- package/dist/validate/marker.d.ts.map +1 -0
- package/dist/validate/marker.js +87 -0
- package/dist/validate/marker.js.map +1 -0
- package/dist/validate/phash.d.ts +135 -0
- package/dist/validate/phash.d.ts.map +1 -0
- package/dist/validate/phash.js +307 -0
- package/dist/validate/phash.js.map +1 -0
- package/dist/validate/render.d.ts +38 -0
- package/dist/validate/render.d.ts.map +1 -0
- package/dist/validate/render.js +155 -0
- package/dist/validate/render.js.map +1 -0
- package/dist/watch/index.d.ts +12 -0
- package/dist/watch/index.d.ts.map +1 -0
- package/dist/watch/index.js +12 -0
- package/dist/watch/index.js.map +1 -0
- package/dist/watch/watcher.d.ts +65 -0
- package/dist/watch/watcher.d.ts.map +1 -0
- package/dist/watch/watcher.js +238 -0
- package/dist/watch/watcher.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M4-03 (DRO-265) — genie preview viewer grid renderer.
|
|
3
|
+
*
|
|
4
|
+
* This is the browser-native script the Vite viewer (M4-02) and the
|
|
5
|
+
* `ui://genie/grid` MCP-Apps resource (M4-06) both boot into. It fetches the
|
|
6
|
+
* kit's compiled manifest, groups the cards by their `@genie` group, and
|
|
7
|
+
* renders each as a sandboxed, lazy-loaded `<iframe>` pointing at the
|
|
8
|
+
* component's `preview.html`.
|
|
9
|
+
*
|
|
10
|
+
* ── Classic script, NOT an ES module (DRO-749 fix) ─────────────────────────
|
|
11
|
+
* `index.html` loads this via `<script src="./viewer.js">` — no
|
|
12
|
+
* `type="module"`. It was briefly shipped as `type="module"` (the original
|
|
13
|
+
* M4-03/#164 merge); that broke the `file://` vehicle outright: verified
|
|
14
|
+
* empirically (headless Chromium, real `file://` navigation) that a module
|
|
15
|
+
* script's relative-path fetch is rejected — every `file://` document gets an
|
|
16
|
+
* opaque, distinct origin, so the ES module loader's same-origin check fails
|
|
17
|
+
* against it and the script never executes (console: "has been blocked by
|
|
18
|
+
* CORS policy"). Dynamic `import()` fails the same way. A classic script has
|
|
19
|
+
* no such restriction and runs identically under `file://`, the Vite dev
|
|
20
|
+
* server, and inside a sandboxed iframe — the only choice that actually
|
|
21
|
+
* satisfies RFC G-5 ("byte-identical across file:// / localhost / ui://",
|
|
22
|
+
* AGENTS.md hard rule 5). Modern syntax (`const`/`let`, arrow functions,
|
|
23
|
+
* template literals, optional chaining, async/await) is all still available
|
|
24
|
+
* outside a module — only `import`/`export` are off the table, and this file
|
|
25
|
+
* is the only script in the kit tree, so it needs neither.
|
|
26
|
+
*
|
|
27
|
+
* ── Manifest contract ───────────────────────────────────────────────────────
|
|
28
|
+
* The original DRO-265 issue body sketches `manifest.cards[]` with a
|
|
29
|
+
* structured `viewport:{width,height}`. That was the research-report sketch;
|
|
30
|
+
* the SHIPPED M3-03 compiler (`packages/server/src/manifest/compiler.ts`)
|
|
31
|
+
* instead emits:
|
|
32
|
+
*
|
|
33
|
+
* { version, name, generatedAt, groups: string[],
|
|
34
|
+
* components: [{ name, group, path, viewport, hash, lastModified }] }
|
|
35
|
+
*
|
|
36
|
+
* where `viewport` is the RAW marker string — either `"WxH"` (e.g. `"480x240"`)
|
|
37
|
+
* or a named token like `"desktop"` (kept opaque). `list_components`, a live P0
|
|
38
|
+
* tool, parses `components` and would throw on a `cards` key — so this viewer
|
|
39
|
+
* reads `components[]` and parses the string viewport itself. See the fixture
|
|
40
|
+
* at `packages/viewer/test/fixtures/kit/.genie/manifest.json`.
|
|
41
|
+
*
|
|
42
|
+
* ── Group order (DRO-749 fix) ────────────────────────────────────────────────
|
|
43
|
+
* `renderGrid` prefers the manifest's own `groups: string[]` for section
|
|
44
|
+
* order — the compiler already resolved `_groups.json` pinning server-side
|
|
45
|
+
* (`orderGroups` in compiler.ts). Previously this file derived order purely
|
|
46
|
+
* from first-seen-among-components, silently ignoring a pinned `groups[]`
|
|
47
|
+
* order (flagged in PR #164 review). `computeGroupOrder` ALWAYS appends any
|
|
48
|
+
* group actually present in the components that the declared list omitted
|
|
49
|
+
* (mirroring the server's own `orderGroups` remainder logic), so a partial
|
|
50
|
+
* or absent `groups[]` never silently drops a group's cards from the grid.
|
|
51
|
+
*
|
|
52
|
+
* ── Design: pure functions + guarded auto-boot ─────────────────────────────
|
|
53
|
+
* Every function takes its `document` (and `fetch`) as an argument and
|
|
54
|
+
* returns DOM rather than reaching for ambient globals, so `grid-renderer.
|
|
55
|
+
* test.ts` can drive the whole script inside a programmatic jsdom window (the
|
|
56
|
+
* same pattern as the server's `*-preview-host.test.ts`). Since a classic
|
|
57
|
+
* script cannot be `import`ed for its bindings, the pure helpers are exposed
|
|
58
|
+
* on `window.__genieViewerTestHooks` — but ONLY when that object already
|
|
59
|
+
* exists before this script runs (set up by the test harness via
|
|
60
|
+
* `window.eval`). Production pages never define it, so nothing is exposed
|
|
61
|
+
* and there is zero footprint on the shipped page.
|
|
62
|
+
*
|
|
63
|
+
* ── Security (defence in depth; hardened further in M4-07/DRO-269) ──────────
|
|
64
|
+
* Each preview iframe is `sandbox="allow-scripts"` with NO `allow-same-origin`:
|
|
65
|
+
* a compromised preview cannot reach the viewer's origin, cookies, or storage.
|
|
66
|
+
* `card.name` is written via `textContent` (never `innerHTML`), so a hostile
|
|
67
|
+
* component name cannot inject markup into the grid chrome.
|
|
68
|
+
*
|
|
69
|
+
* ── Accessibility (M4-09/DRO-271) ───────────────────────────────────────────
|
|
70
|
+
* Each card is a keyboard-operable `role="link"` (M4-09 AC3): `tabindex="0"`
|
|
71
|
+
* puts it in Tab order, an explicit `aria-label` gives it a clean accessible
|
|
72
|
+
* name (without one, a screen reader would concatenate the heading + group
|
|
73
|
+
* pill + viewport text with no separators — "Primary buttonsactions480x240"),
|
|
74
|
+
* and a `keydown`/`click` handler activates it (mirrors native `<a>`/`<button>`
|
|
75
|
+
* behaviour, which `role="link"` does NOT get for free — ARIA supplies
|
|
76
|
+
* semantics, never key handling). The card's own iframe is pulled OUT of Tab
|
|
77
|
+
* order (`tabindex="-1"`): a sandboxed iframe with no `allow-same-origin` is
|
|
78
|
+
* STILL natively focusable, so without this, Tab order would be
|
|
79
|
+
* search → card → iframe → card → iframe (M4-09 AC3 asks for
|
|
80
|
+
* search → card → card). Verified empirically against a real Chromium +
|
|
81
|
+
* axe-core run (`test/a11y.test.ts`) that this combination introduces no new
|
|
82
|
+
* violations and that `frame-title` (M4-09 AC5) still evaluates the iframe
|
|
83
|
+
* element correctly.
|
|
84
|
+
*/
|
|
85
|
+
(function () {
|
|
86
|
+
"use strict";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The kit-relative manifest URL. The AC sketch says `./manifest.json`, but
|
|
90
|
+
* the shipped compiler + M4-08 CLI (`MANIFEST_RELATIVE_PATH`) put it at
|
|
91
|
+
* `.genie/manifest.json`; the viewer fetches the real location.
|
|
92
|
+
*/
|
|
93
|
+
var MANIFEST_URL = ".genie/manifest.json";
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* DOM id of the inlined-manifest script the embedded `ui://genie/grid` tier
|
|
97
|
+
* (M4-06 / DRO-268) injects: a `<script type="application/json" id="manifest">`
|
|
98
|
+
* data island holding the compiled manifest. That tier's CSP is
|
|
99
|
+
* `default-src 'none'; … connect-src 'none'` — `fetch()` is blocked outright —
|
|
100
|
+
* so the manifest MUST travel inside the document and `boot` reads it from
|
|
101
|
+
* here instead of the network. The `file://` and localhost tiers carry NO such
|
|
102
|
+
* node, so they transparently keep the `fetch(MANIFEST_URL)` path — the one
|
|
103
|
+
* `viewer.js` stays byte-identical across all three vehicles (RFC G-5).
|
|
104
|
+
*/
|
|
105
|
+
var MANIFEST_ELEMENT_ID = "manifest";
|
|
106
|
+
var TOOL_RESULT_SHELL_META = "genie-tool-result-shell";
|
|
107
|
+
var TOOL_RESULT_EMBEDDED_MANIFEST_META_KEY = "genie/embeddedManifest";
|
|
108
|
+
var MCP_APP_PROTOCOL_VERSION = "2026-01-26";
|
|
109
|
+
var mcpAppRequestId = 0;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fallback card height (px) for a named/unparseable viewport (e.g. "desktop").
|
|
113
|
+
* A comfortable 16:10-ish default so a card without an explicit WxH still
|
|
114
|
+
* reserves a sensible preview area instead of collapsing to nothing.
|
|
115
|
+
*/
|
|
116
|
+
var DEFAULT_CARD_HEIGHT = 320;
|
|
117
|
+
|
|
118
|
+
var VIEWPORT_TOKEN_RE = /^(\d+)x(\d+)$/;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse a manifest `viewport` token into `{ width, height }`, or `null`
|
|
122
|
+
* when it is a named token ("desktop"), empty, absent, or otherwise not the
|
|
123
|
+
* strict `<digits>x<digits>` shape. Mirrors the server's `extractViewport`
|
|
124
|
+
* so the viewer and compiler agree on exactly which tokens are dimensional.
|
|
125
|
+
*
|
|
126
|
+
* @param {string=} token
|
|
127
|
+
* @returns {{ width: number, height: number } | null}
|
|
128
|
+
*/
|
|
129
|
+
function parseViewport(token) {
|
|
130
|
+
if (typeof token !== "string") return null;
|
|
131
|
+
var match = VIEWPORT_TOKEN_RE.exec(token.trim());
|
|
132
|
+
if (!match) return null;
|
|
133
|
+
var width = Number(match[1]);
|
|
134
|
+
var height = Number(match[2]);
|
|
135
|
+
// A zero (or non-positive) dimension is degenerate — treat it like a
|
|
136
|
+
// named token and fall back to the default height rather than render a
|
|
137
|
+
// 0-size, invisible iframe.
|
|
138
|
+
if (width <= 0 || height <= 0) return null;
|
|
139
|
+
return { width: width, height: height };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Bucket components by `group`, preserving first-seen group order (used as
|
|
144
|
+
* the fallback order when the manifest has no usable `groups[]` — see
|
|
145
|
+
* {@link computeGroupOrder}).
|
|
146
|
+
*
|
|
147
|
+
* @param {ReadonlyArray<object>} components
|
|
148
|
+
* @returns {Map<string, object[]>}
|
|
149
|
+
*/
|
|
150
|
+
function groupByGroup(components) {
|
|
151
|
+
var groups = new Map();
|
|
152
|
+
for (var i = 0; i < components.length; i++) {
|
|
153
|
+
var component = components[i];
|
|
154
|
+
var bucket = groups.get(component.group);
|
|
155
|
+
if (bucket) bucket.push(component);
|
|
156
|
+
else groups.set(component.group, [component]);
|
|
157
|
+
}
|
|
158
|
+
return groups;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Section display order (DRO-749 fix): prefer the manifest's own `groups`
|
|
163
|
+
* array — the compiler already resolved alphabetical-vs-`_groups.json`-
|
|
164
|
+
* pinned order server-side, so there is no reason to re-derive a
|
|
165
|
+
* (possibly different) order client-side — but ALWAYS append any group
|
|
166
|
+
* actually present in `grouped` that `declaredGroups` omitted, in
|
|
167
|
+
* first-seen order. Mirrors the server's own `orderGroups` "remainder"
|
|
168
|
+
* logic (`packages/server/src/manifest/compiler.ts`): "an incomplete pin
|
|
169
|
+
* list never silently drops a group." Without this, a valid-but-partial
|
|
170
|
+
* `groups[]` (e.g. a hand-edited or stale manifest listing only some of
|
|
171
|
+
* the groups `components[]` actually uses) would cause `renderGrid` to
|
|
172
|
+
* silently drop every component in an undeclared group — worse than the
|
|
173
|
+
* plain first-seen order this replaces. When `declaredGroups` is absent,
|
|
174
|
+
* empty, or entirely malformed, this degrades to pure first-seen order
|
|
175
|
+
* among `grouped`'s own keys (every group is then "remainder").
|
|
176
|
+
*
|
|
177
|
+
* @param {unknown} declaredGroups — `manifest.groups`, untrusted shape.
|
|
178
|
+
* @param {Map<string, object[]>} grouped
|
|
179
|
+
* @returns {string[]}
|
|
180
|
+
*/
|
|
181
|
+
function computeGroupOrder(declaredGroups, grouped) {
|
|
182
|
+
var order = [];
|
|
183
|
+
var seen = new Set();
|
|
184
|
+
if (Array.isArray(declaredGroups)) {
|
|
185
|
+
for (var i = 0; i < declaredGroups.length; i++) {
|
|
186
|
+
var g = declaredGroups[i];
|
|
187
|
+
if (typeof g === "string" && !seen.has(g)) {
|
|
188
|
+
seen.add(g);
|
|
189
|
+
order.push(g);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Remainder: any group actually present in `grouped` that the declared
|
|
194
|
+
// list didn't name (or the whole list was absent/empty/malformed) —
|
|
195
|
+
// appended in first-seen order, never dropped.
|
|
196
|
+
for (var key of grouped.keys()) {
|
|
197
|
+
if (!seen.has(key)) {
|
|
198
|
+
seen.add(key);
|
|
199
|
+
order.push(key);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return order;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Returns `value` trimmed, or `fallback` when it is missing, empty, or
|
|
207
|
+
* whitespace-only. Used for the two places M4-09 needs a GUARANTEED
|
|
208
|
+
* non-empty accessible name: the card's `aria-label` (axe-core's
|
|
209
|
+
* `link-name` rule flags a `role="link"` with no accessible name as a
|
|
210
|
+
* CRITICAL violation — and an empty string `aria-label=""` counts as "no
|
|
211
|
+
* name", it does NOT fall back to the element's text content) and the
|
|
212
|
+
* iframe's `title` (axe-core's `frame-title` rule, same "empty is not
|
|
213
|
+
* acceptable" contract). A card whose upstream manifest carries `name: ""`
|
|
214
|
+
* (schema-legal — `store/manifest.ts` only requires `z.string()`, not a
|
|
215
|
+
* non-empty one) must still render an accessible, non-violating card
|
|
216
|
+
* rather than silently produce an unnamed link/frame.
|
|
217
|
+
*
|
|
218
|
+
* @param {string=} value
|
|
219
|
+
* @param {string} fallback
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
function accessibleName(value, fallback) {
|
|
223
|
+
var trimmed = (value || "").trim();
|
|
224
|
+
return trimmed === "" ? fallback : trimmed;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Build one card element for a component: a header (name + group pill +
|
|
229
|
+
* viewport meta) and a sandboxed, lazy `<iframe>` preview.
|
|
230
|
+
*
|
|
231
|
+
* @param {Document} doc
|
|
232
|
+
* @param {object} card
|
|
233
|
+
* @returns {HTMLElement}
|
|
234
|
+
*/
|
|
235
|
+
function createCard(doc, card) {
|
|
236
|
+
var article = doc.createElement("article");
|
|
237
|
+
article.className = "ds-card";
|
|
238
|
+
// Lowercased once here so the search filter (AC5) is a plain substring
|
|
239
|
+
// test and never re-lowercases per keystroke.
|
|
240
|
+
article.setAttribute("data-name", (card.name || "").toLowerCase());
|
|
241
|
+
|
|
242
|
+
// M4-09 AC3 — keyboard-operable card: `tabindex="0"` puts it in Tab
|
|
243
|
+
// order, `role="link"` + an explicit `aria-label` give it a clean
|
|
244
|
+
// accessible name (see the module doc's "Accessibility" section —
|
|
245
|
+
// without the label, a screen reader concatenates the heading + group
|
|
246
|
+
// pill + viewport text with no separators), and Enter/click activate it
|
|
247
|
+
// (`role="link"` supplies semantics only, never key handling — unlike a
|
|
248
|
+
// real `<a>`, so the listener below is required, not decorative). There
|
|
249
|
+
// is no dedicated card-detail route yet (M4-05 leaves "per-card detail
|
|
250
|
+
// view" out of scope for v1), so the placeholder destination is the
|
|
251
|
+
// component's own preview: the one real, already-working URL a card
|
|
252
|
+
// carries.
|
|
253
|
+
article.setAttribute("tabindex", "0");
|
|
254
|
+
article.setAttribute("role", "link");
|
|
255
|
+
// `accessibleName` guards against axe-core's `link-name` (critical): an
|
|
256
|
+
// empty-string aria-label is worse than none (it suppresses the normal
|
|
257
|
+
// fall-back-to-content accessible-name computation), so an unnamed
|
|
258
|
+
// component still gets a real label rather than an empty one.
|
|
259
|
+
article.setAttribute("aria-label", accessibleName(card.name, "Untitled component"));
|
|
260
|
+
var openDetail = function () {
|
|
261
|
+
var view = doc.defaultView;
|
|
262
|
+
if (view && card.path) view.location.assign(card.path);
|
|
263
|
+
};
|
|
264
|
+
article.addEventListener("click", openDetail);
|
|
265
|
+
article.addEventListener("keydown", function (event) {
|
|
266
|
+
if (event.key === "Enter") {
|
|
267
|
+
// Prevent a default action (e.g. a native scroll-on-Enter in some
|
|
268
|
+
// ATs) before navigating — mirrors how a real `<a>` suppresses it
|
|
269
|
+
// too.
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
openDetail();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
var header = doc.createElement("header");
|
|
276
|
+
header.className = "ds-card__head";
|
|
277
|
+
|
|
278
|
+
var title = doc.createElement("h3");
|
|
279
|
+
title.className = "ds-card__name";
|
|
280
|
+
// textContent, never innerHTML — a hostile component name must not
|
|
281
|
+
// inject markup into the viewer chrome.
|
|
282
|
+
title.textContent = card.name || "";
|
|
283
|
+
header.appendChild(title);
|
|
284
|
+
|
|
285
|
+
var meta = doc.createElement("div");
|
|
286
|
+
meta.className = "ds-card__meta";
|
|
287
|
+
|
|
288
|
+
var group = doc.createElement("span");
|
|
289
|
+
group.className = "ds-card__group";
|
|
290
|
+
group.textContent = card.group || "";
|
|
291
|
+
meta.appendChild(group);
|
|
292
|
+
|
|
293
|
+
if (card.viewport) {
|
|
294
|
+
var vp = doc.createElement("span");
|
|
295
|
+
vp.className = "ds-card__viewport";
|
|
296
|
+
vp.textContent = card.viewport;
|
|
297
|
+
meta.appendChild(vp);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
header.appendChild(meta);
|
|
301
|
+
article.appendChild(header);
|
|
302
|
+
|
|
303
|
+
var frame = doc.createElement("div");
|
|
304
|
+
frame.className = "ds-card__frame";
|
|
305
|
+
|
|
306
|
+
var iframe = doc.createElement("iframe");
|
|
307
|
+
// AC3 — allow-scripts ONLY. No allow-same-origin: a compromised preview
|
|
308
|
+
// stays walled off from the viewer's origin (defence in depth; M4-07
|
|
309
|
+
// adds the full CSP layer).
|
|
310
|
+
iframe.setAttribute("sandbox", "allow-scripts");
|
|
311
|
+
// AC4 — never eagerly load offscreen previews.
|
|
312
|
+
iframe.setAttribute("loading", "lazy");
|
|
313
|
+
var cardSrc = card.path || "";
|
|
314
|
+
var cardIdentity = card.sourcePath || cardSrc;
|
|
315
|
+
iframe.setAttribute("src", cardSrc);
|
|
316
|
+
// M4-09 AC5 — the accessible name axe-core's `frame-title` rule checks
|
|
317
|
+
// for. `accessibleName` guards the same empty-string trap as the card's
|
|
318
|
+
// own aria-label above: `title=""` is indistinguishable from a missing
|
|
319
|
+
// title to `frame-title`, so a nameless component still gets a real
|
|
320
|
+
// fallback string.
|
|
321
|
+
iframe.setAttribute("title", accessibleName(card.name, "preview"));
|
|
322
|
+
// M4-09 AC3 — pull the iframe itself OUT of Tab order. A sandboxed
|
|
323
|
+
// iframe with no `allow-same-origin` is STILL natively focusable (the
|
|
324
|
+
// sandbox only restricts what the framed document can DO, not whether
|
|
325
|
+
// the frame element itself takes focus) — see the module doc's
|
|
326
|
+
// "Accessibility" section. Without this, Tab order would be
|
|
327
|
+
// search → card → iframe → card → iframe instead of the required
|
|
328
|
+
// search → card → card.
|
|
329
|
+
iframe.setAttribute("tabindex", "-1");
|
|
330
|
+
// M4-04 (DRO-266) — the canonical, kit-root-relative preview path, kept
|
|
331
|
+
// verbatim (never cache-busted) so the HMR bridge can match a
|
|
332
|
+
// `card.changed` message's `path` against exactly this attribute. The
|
|
333
|
+
// live `src` may later carry an `?__genie_hmr=N` cache-bust (see
|
|
334
|
+
// reloadIframeEl); `data-path` stays the stable identity.
|
|
335
|
+
iframe.setAttribute("data-path", cardIdentity);
|
|
336
|
+
// Embedded manifests replace `path` with an absolute/data transport URL.
|
|
337
|
+
// Keep that source separate from the kit-relative identity above so a host
|
|
338
|
+
// can target the card by sourcePath and replace its bytes safely.
|
|
339
|
+
iframe.setAttribute("data-src", cardSrc);
|
|
340
|
+
|
|
341
|
+
// AC2 — size from the viewport when it is a real WxH; otherwise reserve
|
|
342
|
+
// a sane default height and let CSS own the width (responsive column).
|
|
343
|
+
var size = parseViewport(card.viewport);
|
|
344
|
+
if (size) {
|
|
345
|
+
iframe.setAttribute("width", String(size.width));
|
|
346
|
+
iframe.setAttribute("height", String(size.height));
|
|
347
|
+
// Preserve the intrinsic aspect ratio as the column flexes.
|
|
348
|
+
iframe.style.aspectRatio = size.width + " / " + size.height;
|
|
349
|
+
} else {
|
|
350
|
+
iframe.setAttribute("height", String(DEFAULT_CARD_HEIGHT));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
frame.appendChild(iframe);
|
|
354
|
+
article.appendChild(frame);
|
|
355
|
+
|
|
356
|
+
return article;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Render the whole manifest into `grid`: one `<section>` per group
|
|
361
|
+
* (labelled, with a heading), each holding its cards, in the order
|
|
362
|
+
* {@link computeGroupOrder} resolves. An empty manifest renders a single
|
|
363
|
+
* visible empty state and zero iframes (AC6). Idempotent: clears any prior
|
|
364
|
+
* render first, so a re-render (e.g. future HMR, M4-04) never doubles
|
|
365
|
+
* cards.
|
|
366
|
+
*
|
|
367
|
+
* @param {Document} doc
|
|
368
|
+
* @param {HTMLElement} grid
|
|
369
|
+
* @param {object} manifest
|
|
370
|
+
*/
|
|
371
|
+
function renderGrid(doc, grid, manifest) {
|
|
372
|
+
grid.replaceChildren();
|
|
373
|
+
|
|
374
|
+
var components = (manifest && manifest.components) || [];
|
|
375
|
+
if (components.length === 0) {
|
|
376
|
+
var empty = doc.createElement("div");
|
|
377
|
+
empty.className = "ds-empty";
|
|
378
|
+
empty.textContent = "No components yet — generate one to see it here.";
|
|
379
|
+
grid.appendChild(empty);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
var grouped = groupByGroup(components);
|
|
384
|
+
var order = computeGroupOrder(manifest && manifest.groups, grouped);
|
|
385
|
+
|
|
386
|
+
for (var i = 0; i < order.length; i++) {
|
|
387
|
+
var groupName = order[i];
|
|
388
|
+
var cards = grouped.get(groupName);
|
|
389
|
+
// A declared-but-now-empty group (stale `groups[]` entry) is skipped —
|
|
390
|
+
// an empty section would render a heading over nothing.
|
|
391
|
+
if (!cards || cards.length === 0) continue;
|
|
392
|
+
|
|
393
|
+
var section = doc.createElement("section");
|
|
394
|
+
section.className = "ds-group";
|
|
395
|
+
section.setAttribute("data-group", groupName);
|
|
396
|
+
|
|
397
|
+
var heading = doc.createElement("h2");
|
|
398
|
+
heading.className = "ds-group__title";
|
|
399
|
+
heading.textContent = groupName;
|
|
400
|
+
section.appendChild(heading);
|
|
401
|
+
|
|
402
|
+
var row = doc.createElement("div");
|
|
403
|
+
row.className = "ds-grid";
|
|
404
|
+
for (var j = 0; j < cards.length; j++) {
|
|
405
|
+
row.appendChild(createCard(doc, cards[j]));
|
|
406
|
+
}
|
|
407
|
+
section.appendChild(row);
|
|
408
|
+
|
|
409
|
+
grid.appendChild(section);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function filterManifestBySearch(manifest, search) {
|
|
414
|
+
var Params =
|
|
415
|
+
typeof window !== "undefined" && typeof window.URLSearchParams === "function"
|
|
416
|
+
? window.URLSearchParams
|
|
417
|
+
: null;
|
|
418
|
+
if (!Params) return manifest;
|
|
419
|
+
var params = new Params(search || "");
|
|
420
|
+
var componentName = params.get("componentName");
|
|
421
|
+
var group = params.get("group");
|
|
422
|
+
if (!componentName && !group) return manifest;
|
|
423
|
+
|
|
424
|
+
var components = ((manifest && manifest.components) || []).filter(function (component) {
|
|
425
|
+
return (
|
|
426
|
+
(!componentName || component.name === componentName) &&
|
|
427
|
+
(!group || component.group === group)
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
var survivingGroups = new Set(
|
|
431
|
+
components.map(function (component) {
|
|
432
|
+
return component.group;
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
return {
|
|
436
|
+
...manifest,
|
|
437
|
+
groups: ((manifest && manifest.groups) || []).filter(function (groupName) {
|
|
438
|
+
return survivingGroups.has(groupName);
|
|
439
|
+
}),
|
|
440
|
+
components: components,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
var detachedShellHeaders = new WeakMap();
|
|
445
|
+
|
|
446
|
+
function restoreShellHeader(doc) {
|
|
447
|
+
var entry = detachedShellHeaders.get(doc);
|
|
448
|
+
if (!entry || !doc.body) return;
|
|
449
|
+
if (entry.nextSibling && entry.nextSibling.parentNode === doc.body) {
|
|
450
|
+
doc.body.insertBefore(entry.header, entry.nextSibling);
|
|
451
|
+
} else {
|
|
452
|
+
doc.body.appendChild(entry.header);
|
|
453
|
+
}
|
|
454
|
+
detachedShellHeaders.delete(doc);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function detachShellHeader(doc) {
|
|
458
|
+
var header = doc.querySelector("body > header");
|
|
459
|
+
if (!header) return;
|
|
460
|
+
detachedShellHeaders.set(doc, { header: header, nextSibling: header.nextSibling });
|
|
461
|
+
header.remove();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function renderToolResultError(doc, grid, detail) {
|
|
465
|
+
grid.replaceChildren();
|
|
466
|
+
var box = doc.createElement("div");
|
|
467
|
+
box.className = "ds-error";
|
|
468
|
+
box.textContent = detail;
|
|
469
|
+
grid.appendChild(box);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function renderToolResult(doc, grid, result) {
|
|
473
|
+
restoreShellHeader(doc);
|
|
474
|
+
var structured = result && result.structuredContent;
|
|
475
|
+
var metadata = result && result._meta;
|
|
476
|
+
var embeddedManifest =
|
|
477
|
+
metadata && metadata[TOOL_RESULT_EMBEDDED_MANIFEST_META_KEY] !== undefined
|
|
478
|
+
? metadata[TOOL_RESULT_EMBEDDED_MANIFEST_META_KEY]
|
|
479
|
+
: structured && structured.embeddedManifest;
|
|
480
|
+
if ((result && result.isError) || !structured) {
|
|
481
|
+
var messages = [];
|
|
482
|
+
var content = result && Array.isArray(result.content) ? result.content : [];
|
|
483
|
+
for (var i = 0; i < content.length; i++) {
|
|
484
|
+
if (content[i] && content[i].type === "text" && typeof content[i].text === "string") {
|
|
485
|
+
messages.push(content[i].text);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
renderToolResultError(doc, grid, messages.join("\n") || "Preview unavailable");
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
if (embeddedManifest && canRenderEmbeddedManifest(embeddedManifest)) {
|
|
492
|
+
renderGrid(doc, grid, embeddedManifest);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (typeof structured.embeddedError === "string" && structured.embeddedError) {
|
|
497
|
+
renderError(doc, grid, structured.embeddedError);
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (structured.locality !== "local" && embeddedManifest) {
|
|
502
|
+
renderError(
|
|
503
|
+
doc,
|
|
504
|
+
grid,
|
|
505
|
+
"remote previews require GENIE_PREVIEWS_BASE_URL so cards run on a declared origin",
|
|
506
|
+
);
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
var rawUrl = structured && structured.viewerUrl;
|
|
511
|
+
if (structured.locality !== "local" || typeof rawUrl !== "string") {
|
|
512
|
+
if (embeddedManifest) {
|
|
513
|
+
renderError(
|
|
514
|
+
doc,
|
|
515
|
+
grid,
|
|
516
|
+
"preview viewer unavailable; configure GENIE_PREVIEWS_BASE_URL for embedded cards",
|
|
517
|
+
);
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
renderToolResultError(doc, grid, "Preview unavailable");
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
var URLCtor = doc.defaultView && doc.defaultView.URL;
|
|
525
|
+
if (typeof URLCtor !== "function") {
|
|
526
|
+
renderToolResultError(doc, grid, "Preview unavailable");
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
var parsed;
|
|
530
|
+
try {
|
|
531
|
+
parsed = new URLCtor(rawUrl);
|
|
532
|
+
} catch {
|
|
533
|
+
renderToolResultError(doc, grid, "Preview unavailable");
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
537
|
+
renderToolResultError(doc, grid, "Preview unavailable");
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
var iframe = doc.createElement("iframe");
|
|
542
|
+
iframe.className = "ds-viewer-embed";
|
|
543
|
+
iframe.setAttribute("src", parsed.toString());
|
|
544
|
+
iframe.setAttribute("title", "genie component preview");
|
|
545
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
546
|
+
detachShellHeader(doc);
|
|
547
|
+
grid.replaceChildren(iframe);
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function canRenderEmbeddedManifest(manifest) {
|
|
552
|
+
if (!manifest || typeof manifest !== "object" || !Array.isArray(manifest.components)) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
var components = manifest.components;
|
|
556
|
+
return components.every(function (component) {
|
|
557
|
+
if (!component || typeof component.path !== "string") return false;
|
|
558
|
+
try {
|
|
559
|
+
var URLCtor =
|
|
560
|
+
typeof window !== "undefined" && typeof window.URL === "function" ? window.URL : null;
|
|
561
|
+
if (!URLCtor) return false;
|
|
562
|
+
var parsed = new URLCtor(component.path);
|
|
563
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
564
|
+
} catch {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function initMcpApp(doc, options) {
|
|
571
|
+
var opts = options || {};
|
|
572
|
+
var onTeardown = typeof opts.onTeardown === "function" ? opts.onTeardown : function () {};
|
|
573
|
+
var win = "win" in opts ? opts.win : typeof window !== "undefined" ? window : undefined;
|
|
574
|
+
if (
|
|
575
|
+
!win ||
|
|
576
|
+
!win.parent ||
|
|
577
|
+
win.parent === win ||
|
|
578
|
+
typeof win.addEventListener !== "function" ||
|
|
579
|
+
typeof win.parent.postMessage !== "function"
|
|
580
|
+
) {
|
|
581
|
+
return function () {};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
var host = win.parent;
|
|
585
|
+
var initializeId = ++mcpAppRequestId;
|
|
586
|
+
var resizeObserver = null;
|
|
587
|
+
var lastWidth = -1;
|
|
588
|
+
var lastHeight = -1;
|
|
589
|
+
var tornDown = false;
|
|
590
|
+
function post(message) {
|
|
591
|
+
host.postMessage(message, "*");
|
|
592
|
+
}
|
|
593
|
+
function notifySize() {
|
|
594
|
+
var root = doc.documentElement;
|
|
595
|
+
var body = doc.body;
|
|
596
|
+
var width = Math.ceil(Math.max(root?.scrollWidth || 0, body?.scrollWidth || 0));
|
|
597
|
+
var height = Math.ceil(Math.max(root?.scrollHeight || 0, body?.scrollHeight || 0));
|
|
598
|
+
if (width <= 0 || height <= 0 || (width === lastWidth && height === lastHeight)) return;
|
|
599
|
+
lastWidth = width;
|
|
600
|
+
lastHeight = height;
|
|
601
|
+
post({
|
|
602
|
+
jsonrpc: "2.0",
|
|
603
|
+
method: "ui/notifications/size-changed",
|
|
604
|
+
params: { width: width, height: height },
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
function observeSize() {
|
|
608
|
+
var ResizeObserverCtor = win.ResizeObserver;
|
|
609
|
+
if (typeof ResizeObserverCtor !== "function" || !doc.documentElement) return;
|
|
610
|
+
resizeObserver = new ResizeObserverCtor(notifySize);
|
|
611
|
+
resizeObserver.observe(doc.documentElement);
|
|
612
|
+
if (doc.body) resizeObserver.observe(doc.body);
|
|
613
|
+
}
|
|
614
|
+
function teardown() {
|
|
615
|
+
if (tornDown) return;
|
|
616
|
+
tornDown = true;
|
|
617
|
+
if (typeof win.removeEventListener === "function") {
|
|
618
|
+
win.removeEventListener("message", onMessage);
|
|
619
|
+
}
|
|
620
|
+
if (resizeObserver && typeof resizeObserver.disconnect === "function") {
|
|
621
|
+
resizeObserver.disconnect();
|
|
622
|
+
}
|
|
623
|
+
onTeardown();
|
|
624
|
+
}
|
|
625
|
+
function onMessage(event) {
|
|
626
|
+
if (tornDown) return;
|
|
627
|
+
if (!event || event.source !== host) return;
|
|
628
|
+
var data = event.data;
|
|
629
|
+
if (typeof data === "string") {
|
|
630
|
+
try {
|
|
631
|
+
data = JSON.parse(data);
|
|
632
|
+
} catch {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (!data || typeof data !== "object") return;
|
|
637
|
+
|
|
638
|
+
if (data.method === "ping" && "id" in data) {
|
|
639
|
+
post({ jsonrpc: "2.0", id: data.id, result: {} });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (data.method === "ui/resource-teardown" && "id" in data) {
|
|
643
|
+
post({ jsonrpc: "2.0", id: data.id, result: {} });
|
|
644
|
+
teardown();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (data.id === initializeId && data.result) {
|
|
648
|
+
post({ jsonrpc: "2.0", method: "ui/notifications/initialized" });
|
|
649
|
+
notifySize();
|
|
650
|
+
observeSize();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (data.method === "ui/notifications/tool-result") {
|
|
654
|
+
var grid = doc.getElementById("grid");
|
|
655
|
+
if (grid) {
|
|
656
|
+
renderToolResult(doc, grid, data.params);
|
|
657
|
+
notifySize();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
win.addEventListener("message", onMessage);
|
|
663
|
+
post({
|
|
664
|
+
jsonrpc: "2.0",
|
|
665
|
+
id: initializeId,
|
|
666
|
+
method: "ui/initialize",
|
|
667
|
+
params: {
|
|
668
|
+
protocolVersion: MCP_APP_PROTOCOL_VERSION,
|
|
669
|
+
appCapabilities: { availableDisplayModes: ["inline", "fullscreen"] },
|
|
670
|
+
appInfo: { name: "genie-preview-grid", version: "1.0.0" },
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
return teardown;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* AC5 — filter rendered cards by a case-insensitive substring of the
|
|
679
|
+
* component `name`. Hides non-matching cards, and hides a whole group
|
|
680
|
+
* section when none of its cards match (so an empty group header doesn't
|
|
681
|
+
* linger). An empty query reveals everything.
|
|
682
|
+
*
|
|
683
|
+
* @param {HTMLElement} grid
|
|
684
|
+
* @param {string} query
|
|
685
|
+
*/
|
|
686
|
+
function applyFilter(grid, query) {
|
|
687
|
+
var needle = (query || "").trim().toLowerCase();
|
|
688
|
+
|
|
689
|
+
var sections = grid.querySelectorAll("section.ds-group");
|
|
690
|
+
for (var s = 0; s < sections.length; s++) {
|
|
691
|
+
var section = sections[s];
|
|
692
|
+
var anyVisible = false;
|
|
693
|
+
var cards = section.querySelectorAll("[data-name]");
|
|
694
|
+
for (var c = 0; c < cards.length; c++) {
|
|
695
|
+
var card = cards[c];
|
|
696
|
+
var name = card.getAttribute("data-name") || "";
|
|
697
|
+
var match = needle === "" || name.indexOf(needle) !== -1;
|
|
698
|
+
card.hidden = !match;
|
|
699
|
+
if (match) anyVisible = true;
|
|
700
|
+
}
|
|
701
|
+
section.hidden = !anyVisible;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Render a visible error state in the grid (never throw out of `boot`) — a
|
|
707
|
+
* failed manifest fetch should tell the developer what to do, not blow up
|
|
708
|
+
* the page.
|
|
709
|
+
*
|
|
710
|
+
* @param {Document} doc
|
|
711
|
+
* @param {HTMLElement} grid
|
|
712
|
+
* @param {string} detail
|
|
713
|
+
*/
|
|
714
|
+
function renderError(doc, grid, detail) {
|
|
715
|
+
grid.replaceChildren();
|
|
716
|
+
var box = doc.createElement("div");
|
|
717
|
+
box.className = "ds-error";
|
|
718
|
+
box.textContent =
|
|
719
|
+
"Could not load the preview manifest (" +
|
|
720
|
+
detail +
|
|
721
|
+
"). Run the genie MCP server against this kit first.";
|
|
722
|
+
grid.appendChild(box);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ── HMR: per-card live refresh (M4-04 / DRO-266) ───────────────────────────
|
|
726
|
+
//
|
|
727
|
+
// Two transports, one pure dispatcher (`applyHmrMessage`):
|
|
728
|
+
// 1. A WebSocket on `/__genie_hmr` (AC1/AC2) — the primary channel on the
|
|
729
|
+
// Vite dev server (`http(s)://…`). The server plugin
|
|
730
|
+
// (`src/hmr-plugin.ts`) pushes `{event:"card.changed",path}` /
|
|
731
|
+
// `{event:"tokens.changed"}` off Vite's own file watcher.
|
|
732
|
+
// 2. `window` `postMessage` — the bridge for the EMBEDDED `ui://` tier,
|
|
733
|
+
// where the grid runs inside a host iframe under strict CSP
|
|
734
|
+
// (`default-src 'none'`, coordinated with DRO-269) that may forbid a
|
|
735
|
+
// direct WebSocket. A host forwards the same refresh signal as a
|
|
736
|
+
// message; we accept both the WS shape AND the research sketch's
|
|
737
|
+
// `{type:"refresh", id|path}` shape (M4-04 summary).
|
|
738
|
+
//
|
|
739
|
+
// Why src-reassignment, not `iframe.contentWindow.location.reload()` (which
|
|
740
|
+
// AC2 literally names): every preview iframe is `sandbox="allow-scripts"`
|
|
741
|
+
// with NO `allow-same-origin` (M4-03 AC3, a hard security rule) — so it has
|
|
742
|
+
// an opaque origin and touching `contentWindow.location` throws cross-origin.
|
|
743
|
+
// Reassigning `src` with a fresh cache-bust token is the cross-origin-safe
|
|
744
|
+
// equivalent with the identical observable outcome: ONLY that one iframe
|
|
745
|
+
// refetches its `preview.html` and reloads; the grid never re-renders and no
|
|
746
|
+
// sibling card reflows (AC3 — the sub-100 ms, one-card-only guarantee is
|
|
747
|
+
// structural, not a timing hack). `data-path` stays the stable identity the
|
|
748
|
+
// bridge matches on; the `?__genie_hmr=N` token rides only on the live `src`.
|
|
749
|
+
|
|
750
|
+
/** AC1's WebSocket endpoint path — must match `GENIE_HMR_PATH` server-side. */
|
|
751
|
+
var HMR_PATH = "/__genie_hmr";
|
|
752
|
+
|
|
753
|
+
/** Cache-bust query param appended to a reloaded iframe's live `src`. */
|
|
754
|
+
var HMR_CACHE_BUST_PARAM = "__genie_hmr";
|
|
755
|
+
|
|
756
|
+
/** AC4 — polling-fallback cadence when the WebSocket is unavailable. */
|
|
757
|
+
var HMR_POLL_INTERVAL_MS = 2000;
|
|
758
|
+
|
|
759
|
+
/** Monotonic cache-bust token source (never `Date.now`, so tests are pure). */
|
|
760
|
+
var hmrReloadToken = 0;
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Normalise a raw WS frame (a JSON string) or a `postMessage` payload (a
|
|
764
|
+
* string or already-parsed object) into an internal reload command, or
|
|
765
|
+
* `null` for anything unrecognised (so unrelated `postMessage`s from other
|
|
766
|
+
* libraries are silently ignored). Accepts both wire shapes:
|
|
767
|
+
* - `{ event: "card.changed", path }` → `{ kind: "card", path }` (WS, AC2)
|
|
768
|
+
* - `{ event: "tokens.changed" }` → `{ kind: "tokens" }` (WS, AC5)
|
|
769
|
+
* - `{ event: "manifest.changed" }` → `{ kind: "manifest" }` (WS, structural)
|
|
770
|
+
* - `{ type: "refresh", path, src? }` → `{ kind: "card", path, src? }` (postMessage)
|
|
771
|
+
* - `{ type: "refresh", id }` → `{ kind: "card", path:id }` (postMessage; `id` is the card path)
|
|
772
|
+
* - `{ type: "refresh" }` (no target) → `{ kind: "tokens" }` (refresh-all)
|
|
773
|
+
*
|
|
774
|
+
* @param {unknown} raw
|
|
775
|
+
* @returns {{ kind: "card", path: string, src?: string } | { kind: "tokens" } | { kind: "manifest" } | null}
|
|
776
|
+
*/
|
|
777
|
+
function normalizeHmrMessage(raw) {
|
|
778
|
+
var data = raw;
|
|
779
|
+
if (typeof data === "string") {
|
|
780
|
+
try {
|
|
781
|
+
data = JSON.parse(data);
|
|
782
|
+
} catch {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (!data || typeof data !== "object") return null;
|
|
787
|
+
|
|
788
|
+
if (data.event === "card.changed") {
|
|
789
|
+
if (typeof data.path !== "string" || !data.path) return null;
|
|
790
|
+
return typeof data.src === "string" && data.src
|
|
791
|
+
? { kind: "card", path: data.path, src: data.src }
|
|
792
|
+
: { kind: "card", path: data.path };
|
|
793
|
+
}
|
|
794
|
+
if (data.event === "tokens.changed") return { kind: "tokens" };
|
|
795
|
+
if (data.event === "manifest.changed") return { kind: "manifest" };
|
|
796
|
+
|
|
797
|
+
if (data.type === "refresh") {
|
|
798
|
+
var target = typeof data.path === "string" && data.path ? data.path : data.id;
|
|
799
|
+
if (typeof target === "string" && target) {
|
|
800
|
+
return typeof data.src === "string" && data.src
|
|
801
|
+
? { kind: "card", path: target, src: data.src }
|
|
802
|
+
: { kind: "card", path: target };
|
|
803
|
+
}
|
|
804
|
+
return { kind: "tokens" }; // a target-less refresh means "reload everything".
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Reassign one iframe's `src` to its stable `data-src` plus a fresh
|
|
811
|
+
* cache-bust token, or install `freshSrc` from the embedded host for a
|
|
812
|
+
* data-backed card. Returns `true` when a navigation was started.
|
|
813
|
+
*
|
|
814
|
+
* @param {HTMLIFrameElement} iframe
|
|
815
|
+
* @param {number|string} token
|
|
816
|
+
* @param {string=} freshSrc
|
|
817
|
+
* @returns {boolean}
|
|
818
|
+
*/
|
|
819
|
+
function reloadIframeEl(iframe, token, freshSrc) {
|
|
820
|
+
if (freshSrc) {
|
|
821
|
+
iframe.setAttribute("data-src", freshSrc);
|
|
822
|
+
iframe.setAttribute("src", freshSrc);
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
var src =
|
|
827
|
+
iframe.getAttribute("data-src") ||
|
|
828
|
+
iframe.getAttribute("src") ||
|
|
829
|
+
iframe.getAttribute("data-path");
|
|
830
|
+
if (!src || /^data:/i.test(src)) return false;
|
|
831
|
+
var sep = src.indexOf("?") === -1 ? "?" : "&";
|
|
832
|
+
iframe.setAttribute("src", src + sep + HMR_CACHE_BUST_PARAM + "=" + token);
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* AC2 — reload ONLY the card(s) whose `data-path` equals `path`. Iterates
|
|
838
|
+
* (rather than a `[data-path="…"]` selector) so a path with selector-special
|
|
839
|
+
* characters can't break matching. Returns how many iframes were reloaded.
|
|
840
|
+
*
|
|
841
|
+
* @param {HTMLElement} grid
|
|
842
|
+
* @param {string} path
|
|
843
|
+
* @param {number|string} token
|
|
844
|
+
* @param {string=} freshSrc
|
|
845
|
+
* @returns {number}
|
|
846
|
+
*/
|
|
847
|
+
function reloadCardByPath(grid, path, token, freshSrc) {
|
|
848
|
+
if (!grid || !path) return 0;
|
|
849
|
+
var iframes = grid.querySelectorAll("iframe[data-path]");
|
|
850
|
+
var n = 0;
|
|
851
|
+
for (var i = 0; i < iframes.length; i++) {
|
|
852
|
+
if (
|
|
853
|
+
iframes[i].getAttribute("data-path") === path &&
|
|
854
|
+
reloadIframeEl(iframes[i], token, freshSrc)
|
|
855
|
+
)
|
|
856
|
+
n++;
|
|
857
|
+
}
|
|
858
|
+
return n;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* AC5 — reload EVERY card iframe (a tokens/styles change repaints them all).
|
|
863
|
+
* One shared token for the batch is fine: each iframe has a distinct path, so
|
|
864
|
+
* the token only needs to differ from that iframe's previous `src`.
|
|
865
|
+
*
|
|
866
|
+
* @param {HTMLElement} grid
|
|
867
|
+
* @param {number|string} token
|
|
868
|
+
* @returns {number}
|
|
869
|
+
*/
|
|
870
|
+
function reloadAllCards(grid, token) {
|
|
871
|
+
if (!grid) return 0;
|
|
872
|
+
var iframes = grid.querySelectorAll("iframe[data-path]");
|
|
873
|
+
var n = 0;
|
|
874
|
+
for (var i = 0; i < iframes.length; i++) {
|
|
875
|
+
if (reloadIframeEl(iframes[i], token)) n++;
|
|
876
|
+
}
|
|
877
|
+
return n;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Pure dispatcher: normalise a message and apply it to the grid, returning
|
|
882
|
+
* the number of iframes reloaded (0 for an unrecognised or no-match message).
|
|
883
|
+
* A caller may pin `token` for determinism; otherwise a monotonic token is
|
|
884
|
+
* used so each dispatch actually changes every affected `src`.
|
|
885
|
+
*
|
|
886
|
+
* @param {HTMLElement} grid
|
|
887
|
+
* @param {unknown} message
|
|
888
|
+
* @param {number|string=} token
|
|
889
|
+
* @returns {number}
|
|
890
|
+
*/
|
|
891
|
+
function applyHmrMessage(grid, message, token) {
|
|
892
|
+
var cmd = normalizeHmrMessage(message);
|
|
893
|
+
if (!cmd) return 0;
|
|
894
|
+
if (cmd.kind === "manifest") return 0;
|
|
895
|
+
var t = token === undefined ? ++hmrReloadToken : token;
|
|
896
|
+
return cmd.kind === "card"
|
|
897
|
+
? reloadCardByPath(grid, cmd.path, t, cmd.src)
|
|
898
|
+
: reloadAllCards(grid, t);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* AC4 (polling fallback) — pure diff of two manifests: the kit-relative paths
|
|
903
|
+
* of components PRESENT in both whose `hash` changed. Structural and rendered
|
|
904
|
+
* metadata changes are detected separately by `manifestStructureChanged` and
|
|
905
|
+
* trigger a full re-render; this helper intentionally reports only in-place
|
|
906
|
+
* content edits. Never throws on a partial/absent manifest.
|
|
907
|
+
*
|
|
908
|
+
* @param {object} prev
|
|
909
|
+
* @param {object} next
|
|
910
|
+
* @returns {string[]}
|
|
911
|
+
*/
|
|
912
|
+
function diffManifestHashes(prev, next) {
|
|
913
|
+
var prevByPath = {};
|
|
914
|
+
var pc = (prev && prev.components) || [];
|
|
915
|
+
for (var i = 0; i < pc.length; i++) {
|
|
916
|
+
if (!pc[i]) continue;
|
|
917
|
+
var prevPath = pc[i].sourcePath || pc[i].path;
|
|
918
|
+
if (typeof prevPath === "string") prevByPath[prevPath] = pc[i].hash;
|
|
919
|
+
}
|
|
920
|
+
var changed = [];
|
|
921
|
+
var nc = (next && next.components) || [];
|
|
922
|
+
for (var j = 0; j < nc.length; j++) {
|
|
923
|
+
var comp = nc[j];
|
|
924
|
+
if (!comp) continue;
|
|
925
|
+
var nextPath = comp.sourcePath || comp.path;
|
|
926
|
+
if (typeof nextPath !== "string") continue;
|
|
927
|
+
if (
|
|
928
|
+
Object.prototype.hasOwnProperty.call(prevByPath, nextPath) &&
|
|
929
|
+
prevByPath[nextPath] !== comp.hash
|
|
930
|
+
) {
|
|
931
|
+
changed.push(nextPath);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return changed;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* True when component membership/order or declared group order changed.
|
|
939
|
+
* Content-only hash changes keep the lightweight per-card reload path.
|
|
940
|
+
*
|
|
941
|
+
* @param {object} prev
|
|
942
|
+
* @param {object} next
|
|
943
|
+
* @returns {boolean}
|
|
944
|
+
*/
|
|
945
|
+
function manifestStructureChanged(prev, next) {
|
|
946
|
+
function identity(manifest) {
|
|
947
|
+
var components = (manifest && manifest.components) || [];
|
|
948
|
+
var cards = [];
|
|
949
|
+
for (var i = 0; i < components.length; i++) {
|
|
950
|
+
var component = components[i] || {};
|
|
951
|
+
cards.push({
|
|
952
|
+
path: component.path || "",
|
|
953
|
+
sourcePath: component.sourcePath || "",
|
|
954
|
+
name: component.name || "",
|
|
955
|
+
group: component.group || "",
|
|
956
|
+
viewport: component.viewport || "",
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
return JSON.stringify({
|
|
960
|
+
groups: (manifest && manifest.groups) || [],
|
|
961
|
+
cards: cards,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
return identity(prev) !== identity(next);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** Re-render from a fresh manifest while preserving the active search query. */
|
|
968
|
+
function renderManifestUpdate(doc, grid, manifest) {
|
|
969
|
+
var searchQuery =
|
|
970
|
+
doc.defaultView && doc.defaultView.location && doc.defaultView.location.search;
|
|
971
|
+
renderGrid(doc, grid, filterManifestBySearch(manifest, searchQuery || ""));
|
|
972
|
+
var searchInput = doc.getElementById("q");
|
|
973
|
+
if (searchInput) applyFilter(grid, searchInput.value || "");
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* AC6 — increment the header's reload counter by `n` (a no-op when `n<=0` or
|
|
978
|
+
* the counter element is absent, e.g. the embedded shell). The count is
|
|
979
|
+
* mirrored in a `data-count` attribute so a test can read it without parsing
|
|
980
|
+
* display text.
|
|
981
|
+
*
|
|
982
|
+
* @param {Document} doc
|
|
983
|
+
* @param {number} n
|
|
984
|
+
* @returns {number} the new total
|
|
985
|
+
*/
|
|
986
|
+
function bumpReloadCounter(doc, n) {
|
|
987
|
+
var el = doc.getElementById("hmr-count");
|
|
988
|
+
if (!el || !(n > 0)) return el ? Number(el.getAttribute("data-count") || "0") : 0;
|
|
989
|
+
var next = Number(el.getAttribute("data-count") || "0") + n;
|
|
990
|
+
el.setAttribute("data-count", String(next));
|
|
991
|
+
el.textContent = String(next);
|
|
992
|
+
return next;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* The `ws(s)://…/__genie_hmr` URL for the current location, or `null` when
|
|
997
|
+
* there is no dev server to connect to — a `file://` open or an opaque/`ui://`
|
|
998
|
+
* embedded origin. That `null` is what makes the script byte-identical across
|
|
999
|
+
* vehicles (RFC G-5): the SAME `viewer.js` simply skips the live socket where
|
|
1000
|
+
* one can't exist and leans on the `postMessage` bridge instead.
|
|
1001
|
+
*
|
|
1002
|
+
* @param {Location|{protocol?:string,host?:string}} loc
|
|
1003
|
+
* @returns {string|null}
|
|
1004
|
+
*/
|
|
1005
|
+
function hmrSocketUrl(loc) {
|
|
1006
|
+
if (!loc || (loc.protocol !== "http:" && loc.protocol !== "https:") || !loc.host) return null;
|
|
1007
|
+
return (loc.protocol === "https:" ? "wss:" : "ws:") + "//" + loc.host + HMR_PATH;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Wire the live-refresh channels and return a teardown function. Everything
|
|
1012
|
+
* the browser touches is injectable so `hmr-client.test.ts` drives the whole
|
|
1013
|
+
* thing in jsdom with fakes — no real socket, no real timers, no network:
|
|
1014
|
+
*
|
|
1015
|
+
* - `win` — the window to bind `message`/`WebSocket` on (default `window`)
|
|
1016
|
+
* - `location` — used to derive the WS URL (default `win.location`)
|
|
1017
|
+
* - `WebSocketImpl` — the WebSocket constructor (default `win.WebSocket`)
|
|
1018
|
+
* - `fetchImpl` — manifest fetch for the poll fallback (default `win.fetch`)
|
|
1019
|
+
* - `setIntervalImpl` / `clearIntervalImpl` — poll timer seam (default `win`'s)
|
|
1020
|
+
* - `manifestUrl` — poll target (default `MANIFEST_URL`)
|
|
1021
|
+
* - `initialManifest`— baseline so the FIRST poll can already detect a change
|
|
1022
|
+
* - `pollIntervalMs` — cadence (default `HMR_POLL_INTERVAL_MS`)
|
|
1023
|
+
* - `parentOrigin` — optional trusted embedding-host origin; otherwise
|
|
1024
|
+
* derived from `document.referrer` when available
|
|
1025
|
+
*
|
|
1026
|
+
* The `postMessage` bridge is ALWAYS active (harmless where unused). The WS +
|
|
1027
|
+
* polling only engage when {@link hmrSocketUrl} resolves (a real dev server);
|
|
1028
|
+
* on `file://`/`ui://` there is nothing to poll, so we don't spin a timer
|
|
1029
|
+
* against a static snapshot.
|
|
1030
|
+
*
|
|
1031
|
+
* @param {Document} doc
|
|
1032
|
+
* @param {object=} options
|
|
1033
|
+
* @returns {() => void} teardown
|
|
1034
|
+
*/
|
|
1035
|
+
function initHmr(doc, options) {
|
|
1036
|
+
var opts = options || {};
|
|
1037
|
+
var grid = doc.getElementById("grid");
|
|
1038
|
+
if (!grid) return function () {};
|
|
1039
|
+
|
|
1040
|
+
// Resolve each injectable via an explicit "key in opts" check (not `||`), so
|
|
1041
|
+
// a test can DISABLE a capability by passing it as `undefined`/`null` — e.g.
|
|
1042
|
+
// `WebSocketImpl: undefined` to exercise the no-WebSocket polling path even
|
|
1043
|
+
// though the ambient jsdom `window` provides a real one. Production callers
|
|
1044
|
+
// omit the key entirely and get the ambient default.
|
|
1045
|
+
var win = "win" in opts ? opts.win : typeof window !== "undefined" ? window : undefined;
|
|
1046
|
+
var location = "location" in opts ? opts.location : win && win.location;
|
|
1047
|
+
var WebSocketImpl =
|
|
1048
|
+
"WebSocketImpl" in opts
|
|
1049
|
+
? opts.WebSocketImpl
|
|
1050
|
+
: (win && win.WebSocket) || (typeof WebSocket !== "undefined" ? WebSocket : undefined);
|
|
1051
|
+
var fetchImpl = "fetchImpl" in opts ? opts.fetchImpl : win && win.fetch;
|
|
1052
|
+
var setIntervalImpl =
|
|
1053
|
+
"setIntervalImpl" in opts
|
|
1054
|
+
? opts.setIntervalImpl
|
|
1055
|
+
: (win && win.setInterval) ||
|
|
1056
|
+
(typeof setInterval !== "undefined" ? setInterval : undefined);
|
|
1057
|
+
var clearIntervalImpl =
|
|
1058
|
+
"clearIntervalImpl" in opts
|
|
1059
|
+
? opts.clearIntervalImpl
|
|
1060
|
+
: (win && win.clearInterval) ||
|
|
1061
|
+
(typeof clearInterval !== "undefined" ? clearInterval : undefined);
|
|
1062
|
+
var manifestUrl = opts.manifestUrl || MANIFEST_URL;
|
|
1063
|
+
var pollIntervalMs = opts.pollIntervalMs || HMR_POLL_INTERVAL_MS;
|
|
1064
|
+
|
|
1065
|
+
var socket = null;
|
|
1066
|
+
var pollTimer = null;
|
|
1067
|
+
var lastManifest = opts.initialManifest || null;
|
|
1068
|
+
var pollInFlight = false;
|
|
1069
|
+
var manifestRefreshPending = false;
|
|
1070
|
+
var torn = false;
|
|
1071
|
+
|
|
1072
|
+
function applyFetchedManifest(next) {
|
|
1073
|
+
if (!next) return;
|
|
1074
|
+
if (!lastManifest || manifestStructureChanged(lastManifest, next)) {
|
|
1075
|
+
renderManifestUpdate(doc, grid, next);
|
|
1076
|
+
bumpReloadCounter(doc, 1);
|
|
1077
|
+
} else {
|
|
1078
|
+
var changed = diffManifestHashes(lastManifest, next);
|
|
1079
|
+
var total = 0;
|
|
1080
|
+
for (var i = 0; i < changed.length; i++) {
|
|
1081
|
+
total += reloadCardByPath(grid, changed[i], ++hmrReloadToken);
|
|
1082
|
+
}
|
|
1083
|
+
if (total > 0) bumpReloadCounter(doc, total);
|
|
1084
|
+
}
|
|
1085
|
+
lastManifest = next;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function finishManifestFetch() {
|
|
1089
|
+
pollInFlight = false;
|
|
1090
|
+
if (manifestRefreshPending && !torn) {
|
|
1091
|
+
manifestRefreshPending = false;
|
|
1092
|
+
fetchManifestUpdate();
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function fetchManifestUpdate() {
|
|
1097
|
+
if (torn || !fetchImpl) return;
|
|
1098
|
+
if (pollInFlight) {
|
|
1099
|
+
manifestRefreshPending = true;
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
pollInFlight = true;
|
|
1103
|
+
fetchImpl(manifestUrl)
|
|
1104
|
+
.then(function (res) {
|
|
1105
|
+
return res && res.ok ? res.json() : null;
|
|
1106
|
+
})
|
|
1107
|
+
.then(function (next) {
|
|
1108
|
+
if (torn || !next) return;
|
|
1109
|
+
applyFetchedManifest(next);
|
|
1110
|
+
})
|
|
1111
|
+
.catch(function () {
|
|
1112
|
+
// Keep the current grid; a later manifest event/poll can retry.
|
|
1113
|
+
})
|
|
1114
|
+
.then(finishManifestFetch);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** Apply any inbound message (WS or postMessage) and bump the counter. */
|
|
1118
|
+
function handle(rawData) {
|
|
1119
|
+
var command = normalizeHmrMessage(rawData);
|
|
1120
|
+
if (command && command.kind === "manifest") {
|
|
1121
|
+
fetchManifestUpdate();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
var reloaded = applyHmrMessage(grid, rawData);
|
|
1125
|
+
if (reloaded > 0) bumpReloadCounter(doc, reloaded);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ── Transport 2: the postMessage bridge (embedded ui:// tier) ────────────
|
|
1129
|
+
var parentOrigin = null;
|
|
1130
|
+
var configuredParentOrigin = "parentOrigin" in opts ? opts.parentOrigin : doc.referrer;
|
|
1131
|
+
var ParentURL = win && win.URL;
|
|
1132
|
+
if (configuredParentOrigin && typeof ParentURL === "function") {
|
|
1133
|
+
try {
|
|
1134
|
+
var parsedParentOrigin = new ParentURL(configuredParentOrigin).origin;
|
|
1135
|
+
if (parsedParentOrigin !== "null") parentOrigin = parsedParentOrigin;
|
|
1136
|
+
} catch {
|
|
1137
|
+
parentOrigin = null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function onMessage(event) {
|
|
1142
|
+
// Sandboxed cards can call parent.postMessage despite lacking
|
|
1143
|
+
// allow-same-origin. Only the embedding host may issue refresh commands.
|
|
1144
|
+
if (!event || !win || event.source !== win.parent) return;
|
|
1145
|
+
if (parentOrigin && event.origin !== parentOrigin) return;
|
|
1146
|
+
handle(event && "data" in event ? event.data : event);
|
|
1147
|
+
}
|
|
1148
|
+
if (win && typeof win.addEventListener === "function") {
|
|
1149
|
+
win.addEventListener("message", onMessage);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// ── AC4: polling fallback ────────────────────────────────────────────────
|
|
1153
|
+
function poll() {
|
|
1154
|
+
if (torn || pollInFlight || !fetchImpl) return;
|
|
1155
|
+
pollInFlight = true;
|
|
1156
|
+
fetchImpl(manifestUrl)
|
|
1157
|
+
.then(function (res) {
|
|
1158
|
+
return res && res.ok ? res.json() : null;
|
|
1159
|
+
})
|
|
1160
|
+
.then(function (next) {
|
|
1161
|
+
if (torn || !next) return;
|
|
1162
|
+
applyFetchedManifest(next);
|
|
1163
|
+
})
|
|
1164
|
+
.catch(function () {
|
|
1165
|
+
// A transient fetch failure must not kill the poll loop — try again
|
|
1166
|
+
// next tick.
|
|
1167
|
+
})
|
|
1168
|
+
.then(finishManifestFetch);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function startPolling() {
|
|
1172
|
+
if (torn || pollTimer || !setIntervalImpl || !fetchImpl) return;
|
|
1173
|
+
pollTimer = setIntervalImpl(poll, pollIntervalMs);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// ── Transport 1: the WebSocket (primary, dev-server only) ────────────────
|
|
1177
|
+
var url = hmrSocketUrl(location);
|
|
1178
|
+
if (url && WebSocketImpl) {
|
|
1179
|
+
try {
|
|
1180
|
+
socket = new WebSocketImpl(url);
|
|
1181
|
+
socket.onmessage = function (event) {
|
|
1182
|
+
handle(event && "data" in event ? event.data : event);
|
|
1183
|
+
};
|
|
1184
|
+
// A socket error or close (server gone, CSP block, network drop) falls
|
|
1185
|
+
// back to polling — but only once (guarded inside startPolling).
|
|
1186
|
+
socket.onerror = startPolling;
|
|
1187
|
+
socket.onclose = startPolling;
|
|
1188
|
+
} catch {
|
|
1189
|
+
// Constructing the socket threw (e.g. a CSP `connect-src` block) — go
|
|
1190
|
+
// straight to the polling fallback.
|
|
1191
|
+
startPolling();
|
|
1192
|
+
}
|
|
1193
|
+
} else if (url && !WebSocketImpl) {
|
|
1194
|
+
// A dev server is present but this environment has no WebSocket at all —
|
|
1195
|
+
// poll from the start.
|
|
1196
|
+
startPolling();
|
|
1197
|
+
}
|
|
1198
|
+
// else (url === null): file:// / ui:// — no dev server to reach; the
|
|
1199
|
+
// postMessage bridge above is the only live channel, by design.
|
|
1200
|
+
|
|
1201
|
+
return function teardown() {
|
|
1202
|
+
torn = true;
|
|
1203
|
+
if (win && typeof win.removeEventListener === "function") {
|
|
1204
|
+
win.removeEventListener("message", onMessage);
|
|
1205
|
+
}
|
|
1206
|
+
if (socket) {
|
|
1207
|
+
socket.onmessage = socket.onerror = socket.onclose = null;
|
|
1208
|
+
try {
|
|
1209
|
+
socket.close();
|
|
1210
|
+
} catch {
|
|
1211
|
+
/* already closed */
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (pollTimer && clearIntervalImpl) {
|
|
1215
|
+
clearIntervalImpl(pollTimer);
|
|
1216
|
+
pollTimer = null;
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Read the manifest inlined by the embedded `ui://genie/grid` tier (M4-06):
|
|
1223
|
+
* a `<script type="application/json" id="manifest">` data island whose text
|
|
1224
|
+
* content is the compiled manifest JSON. Returns the parsed object, or
|
|
1225
|
+
* `null` when there is no such node (the `file://` / localhost tiers, which
|
|
1226
|
+
* fetch instead) OR the node is present but not usable — wrong `type`, empty,
|
|
1227
|
+
* or malformed JSON. A `null` return is the caller's signal to fall back to
|
|
1228
|
+
* the network path; a malformed INLINE manifest deliberately degrades to that
|
|
1229
|
+
* same fallback rather than throwing, so a corrupt payload surfaces as the
|
|
1230
|
+
* normal error state, never an uncaught exception on the page.
|
|
1231
|
+
*
|
|
1232
|
+
* Reading `type` guards against picking up an unrelated `#manifest` element
|
|
1233
|
+
* and, more importantly, means only a genuine data block (never an executable
|
|
1234
|
+
* `<script>`) is ever parsed here.
|
|
1235
|
+
*
|
|
1236
|
+
* @param {Document} doc
|
|
1237
|
+
* @returns {object | null}
|
|
1238
|
+
*/
|
|
1239
|
+
function readInlineManifest(doc) {
|
|
1240
|
+
var el = doc.getElementById(MANIFEST_ELEMENT_ID);
|
|
1241
|
+
if (!el) return null;
|
|
1242
|
+
// Only a JSON data block counts — never an executable script.
|
|
1243
|
+
var type = (el.getAttribute && el.getAttribute("type")) || "";
|
|
1244
|
+
if (type.toLowerCase() !== "application/json") return null;
|
|
1245
|
+
var raw = el.textContent || "";
|
|
1246
|
+
if (raw.trim() === "") return null;
|
|
1247
|
+
try {
|
|
1248
|
+
return JSON.parse(raw);
|
|
1249
|
+
} catch {
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Wire the `#q` search box to live-filter the rendered grid (AC5). Shared by
|
|
1256
|
+
* both boot paths (inline + fetch) so the two vehicles behave identically.
|
|
1257
|
+
*
|
|
1258
|
+
* @param {Document} doc
|
|
1259
|
+
* @param {HTMLElement} grid
|
|
1260
|
+
*/
|
|
1261
|
+
function wireSearch(doc, grid) {
|
|
1262
|
+
var search = doc.getElementById("q");
|
|
1263
|
+
if (search) {
|
|
1264
|
+
search.addEventListener("input", function () {
|
|
1265
|
+
applyFilter(grid, search.value);
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Boot the viewer: obtain the manifest, render the grid, and wire the `#q`
|
|
1272
|
+
* search input to live-filter (AC5). Resolves (never rejects) so a caller
|
|
1273
|
+
* / the browser auto-boot can `await` it without an unhandled rejection;
|
|
1274
|
+
* on any failure it paints the error state instead.
|
|
1275
|
+
*
|
|
1276
|
+
* ── Manifest source: inline first, then fetch (M4-06 / DRO-268) ────────────
|
|
1277
|
+
* The embedded `ui://genie/grid` tier inlines the manifest into the document
|
|
1278
|
+
* (`<script type="application/json" id="manifest">`) because its CSP
|
|
1279
|
+
* (`connect-src 'none'`) blocks `fetch` entirely. So `boot` reads the inline
|
|
1280
|
+
* node FIRST and, when present, renders straight from it — issuing NO network
|
|
1281
|
+
* request. Only when there is no inline node (the `file://` / localhost tiers)
|
|
1282
|
+
* does it fall back to `fetch(MANIFEST_URL)`. This keeps `viewer.js`
|
|
1283
|
+
* byte-identical across all three vehicles (RFC G-5) while honouring each
|
|
1284
|
+
* tier's transport.
|
|
1285
|
+
*
|
|
1286
|
+
* @param {Document} doc
|
|
1287
|
+
* @param {typeof fetch} fetchImpl
|
|
1288
|
+
* @returns {Promise<void>}
|
|
1289
|
+
*/
|
|
1290
|
+
function boot(doc, fetchImpl) {
|
|
1291
|
+
var grid = doc.getElementById("grid");
|
|
1292
|
+
if (!grid) return Promise.resolve();
|
|
1293
|
+
|
|
1294
|
+
// Embedded tier: manifest is inlined; render it directly, never fetch.
|
|
1295
|
+
var inline = readInlineManifest(doc);
|
|
1296
|
+
if (inline !== null) {
|
|
1297
|
+
try {
|
|
1298
|
+
var inlineSearch = doc.defaultView && doc.defaultView.location;
|
|
1299
|
+
renderGrid(doc, grid, filterManifestBySearch(inline, inlineSearch?.search || ""));
|
|
1300
|
+
wireSearch(doc, grid);
|
|
1301
|
+
// M4-04 (DRO-266) — this tier is EXACTLY who the postMessage bridge
|
|
1302
|
+
// exists for (its strict CSP, connect-src 'none', blocks fetch AND a
|
|
1303
|
+
// direct WebSocket alike — see initHmr's own header). hmrSocketUrl
|
|
1304
|
+
// resolves to null here (no http(s) origin with a host — see its own
|
|
1305
|
+
// doc), so initHmr transparently skips the WS + polling paths and
|
|
1306
|
+
// wires ONLY the `message` listener: no network access is attempted,
|
|
1307
|
+
// satisfying the CSP without special-casing this branch. Omitting
|
|
1308
|
+
// this call (as an earlier revision did) left the bridge dead code in
|
|
1309
|
+
// the one tier it was built for. Best-effort, like the fetch path
|
|
1310
|
+
// below: a throw here must never take down an otherwise-good render.
|
|
1311
|
+
var teardownHmr = function () {};
|
|
1312
|
+
try {
|
|
1313
|
+
teardownHmr = initHmr(doc, { initialManifest: inline });
|
|
1314
|
+
} catch {
|
|
1315
|
+
/* live refresh is an enhancement, never a boot blocker */
|
|
1316
|
+
}
|
|
1317
|
+
if (doc.querySelector(`meta[name="${TOOL_RESULT_SHELL_META}"]`)) {
|
|
1318
|
+
initMcpApp(doc, { onTeardown: teardownHmr });
|
|
1319
|
+
}
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
var inlineDetail = err && err.message ? err.message : String(err);
|
|
1322
|
+
renderError(doc, grid, inlineDetail);
|
|
1323
|
+
}
|
|
1324
|
+
return Promise.resolve();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// file:// / localhost tiers: no inline node — fetch the manifest.
|
|
1328
|
+
return fetchImpl(MANIFEST_URL)
|
|
1329
|
+
.then(function (response) {
|
|
1330
|
+
if (!response.ok) {
|
|
1331
|
+
renderError(doc, grid, "HTTP " + response.status);
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
return response.json();
|
|
1335
|
+
})
|
|
1336
|
+
.then(function (manifest) {
|
|
1337
|
+
if (manifest === null) return; // error already rendered above
|
|
1338
|
+
var fetchedLocation = doc.defaultView && doc.defaultView.location;
|
|
1339
|
+
renderGrid(doc, grid, filterManifestBySearch(manifest, fetchedLocation?.search || ""));
|
|
1340
|
+
wireSearch(doc, grid);
|
|
1341
|
+
|
|
1342
|
+
// M4-04 (DRO-266) — engage live per-card refresh AFTER the grid exists,
|
|
1343
|
+
// handing the just-fetched manifest in as the polling baseline so the
|
|
1344
|
+
// fallback's very first tick can already spot a hash change. Best-effort:
|
|
1345
|
+
// if it throws (an exotic embed with no window at all), the static grid
|
|
1346
|
+
// still stands. The teardown fn is intentionally unused here — the
|
|
1347
|
+
// browser page lives until navigation; tests call `initHmr` directly and
|
|
1348
|
+
// own their own teardown.
|
|
1349
|
+
try {
|
|
1350
|
+
initHmr(doc, { initialManifest: manifest });
|
|
1351
|
+
} catch {
|
|
1352
|
+
/* live refresh is an enhancement, never a boot blocker */
|
|
1353
|
+
}
|
|
1354
|
+
})
|
|
1355
|
+
.catch(function (err) {
|
|
1356
|
+
var detail = err && err.message ? err.message : String(err);
|
|
1357
|
+
renderError(doc, grid, detail);
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// ── Browser auto-boot ─────────────────────────────────────────────────────
|
|
1362
|
+
// The ONLY side-effecting line. Guarded so evaluating this script under a
|
|
1363
|
+
// test harness that hasn't triggered a real navigation still behaves, and
|
|
1364
|
+
// so the auto-boot never fires twice. In the browser, `fetch` and
|
|
1365
|
+
// `document` are ambient globals.
|
|
1366
|
+
if (typeof document !== "undefined" && typeof fetch !== "undefined") {
|
|
1367
|
+
void boot(document, fetch);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Test-only seam — see file header. No-op (and no global write at all)
|
|
1371
|
+
// unless a test harness pre-defines the hook object before this script
|
|
1372
|
+
// runs.
|
|
1373
|
+
if (typeof window !== "undefined" && window.__genieViewerTestHooks) {
|
|
1374
|
+
window.__genieViewerTestHooks.MANIFEST_URL = MANIFEST_URL;
|
|
1375
|
+
window.__genieViewerTestHooks.DEFAULT_CARD_HEIGHT = DEFAULT_CARD_HEIGHT;
|
|
1376
|
+
window.__genieViewerTestHooks.parseViewport = parseViewport;
|
|
1377
|
+
window.__genieViewerTestHooks.groupByGroup = groupByGroup;
|
|
1378
|
+
window.__genieViewerTestHooks.computeGroupOrder = computeGroupOrder;
|
|
1379
|
+
window.__genieViewerTestHooks.accessibleName = accessibleName;
|
|
1380
|
+
window.__genieViewerTestHooks.createCard = createCard;
|
|
1381
|
+
window.__genieViewerTestHooks.renderGrid = renderGrid;
|
|
1382
|
+
window.__genieViewerTestHooks.filterManifestBySearch = filterManifestBySearch;
|
|
1383
|
+
window.__genieViewerTestHooks.renderToolResult = renderToolResult;
|
|
1384
|
+
window.__genieViewerTestHooks.initMcpApp = initMcpApp;
|
|
1385
|
+
window.__genieViewerTestHooks.applyFilter = applyFilter;
|
|
1386
|
+
window.__genieViewerTestHooks.readInlineManifest = readInlineManifest;
|
|
1387
|
+
window.__genieViewerTestHooks.wireSearch = wireSearch;
|
|
1388
|
+
window.__genieViewerTestHooks.MANIFEST_ELEMENT_ID = MANIFEST_ELEMENT_ID;
|
|
1389
|
+
window.__genieViewerTestHooks.boot = boot;
|
|
1390
|
+
// M4-04 (DRO-266) — HMR client seam.
|
|
1391
|
+
window.__genieViewerTestHooks.HMR_PATH = HMR_PATH;
|
|
1392
|
+
window.__genieViewerTestHooks.HMR_CACHE_BUST_PARAM = HMR_CACHE_BUST_PARAM;
|
|
1393
|
+
window.__genieViewerTestHooks.HMR_POLL_INTERVAL_MS = HMR_POLL_INTERVAL_MS;
|
|
1394
|
+
window.__genieViewerTestHooks.normalizeHmrMessage = normalizeHmrMessage;
|
|
1395
|
+
window.__genieViewerTestHooks.reloadCardByPath = reloadCardByPath;
|
|
1396
|
+
window.__genieViewerTestHooks.reloadAllCards = reloadAllCards;
|
|
1397
|
+
window.__genieViewerTestHooks.applyHmrMessage = applyHmrMessage;
|
|
1398
|
+
window.__genieViewerTestHooks.diffManifestHashes = diffManifestHashes;
|
|
1399
|
+
window.__genieViewerTestHooks.manifestStructureChanged = manifestStructureChanged;
|
|
1400
|
+
window.__genieViewerTestHooks.renderManifestUpdate = renderManifestUpdate;
|
|
1401
|
+
window.__genieViewerTestHooks.bumpReloadCounter = bumpReloadCounter;
|
|
1402
|
+
window.__genieViewerTestHooks.hmrSocketUrl = hmrSocketUrl;
|
|
1403
|
+
window.__genieViewerTestHooks.initHmr = initHmr;
|
|
1404
|
+
}
|
|
1405
|
+
})();
|