@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.
Files changed (301) hide show
  1. package/README.md +15 -0
  2. package/dist/auth/bearer.d.ts +49 -0
  3. package/dist/auth/bearer.d.ts.map +1 -0
  4. package/dist/auth/bearer.js +159 -0
  5. package/dist/auth/bearer.js.map +1 -0
  6. package/dist/auth/oauth/authorize.d.ts +19 -0
  7. package/dist/auth/oauth/authorize.d.ts.map +1 -0
  8. package/dist/auth/oauth/authorize.js +111 -0
  9. package/dist/auth/oauth/authorize.js.map +1 -0
  10. package/dist/auth/oauth/config.d.ts +10 -0
  11. package/dist/auth/oauth/config.d.ts.map +1 -0
  12. package/dist/auth/oauth/config.js +16 -0
  13. package/dist/auth/oauth/config.js.map +1 -0
  14. package/dist/auth/oauth/dcr.d.ts +15 -0
  15. package/dist/auth/oauth/dcr.d.ts.map +1 -0
  16. package/dist/auth/oauth/dcr.js +38 -0
  17. package/dist/auth/oauth/dcr.js.map +1 -0
  18. package/dist/auth/oauth/jwt.d.ts +13 -0
  19. package/dist/auth/oauth/jwt.d.ts.map +1 -0
  20. package/dist/auth/oauth/jwt.js +46 -0
  21. package/dist/auth/oauth/jwt.js.map +1 -0
  22. package/dist/auth/oauth/metadata.d.ts +9 -0
  23. package/dist/auth/oauth/metadata.d.ts.map +1 -0
  24. package/dist/auth/oauth/metadata.js +19 -0
  25. package/dist/auth/oauth/metadata.js.map +1 -0
  26. package/dist/auth/oauth/router.d.ts +32 -0
  27. package/dist/auth/oauth/router.d.ts.map +1 -0
  28. package/dist/auth/oauth/router.js +159 -0
  29. package/dist/auth/oauth/router.js.map +1 -0
  30. package/dist/auth/oauth/store.d.ts +55 -0
  31. package/dist/auth/oauth/store.d.ts.map +1 -0
  32. package/dist/auth/oauth/store.js +63 -0
  33. package/dist/auth/oauth/store.js.map +1 -0
  34. package/dist/auth/oauth/token.d.ts +21 -0
  35. package/dist/auth/oauth/token.d.ts.map +1 -0
  36. package/dist/auth/oauth/token.js +77 -0
  37. package/dist/auth/oauth/token.js.map +1 -0
  38. package/dist/auth/oidc/group-policy.d.ts +43 -0
  39. package/dist/auth/oidc/group-policy.d.ts.map +1 -0
  40. package/dist/auth/oidc/group-policy.js +54 -0
  41. package/dist/auth/oidc/group-policy.js.map +1 -0
  42. package/dist/auth/oidc/verifier.d.ts +42 -0
  43. package/dist/auth/oidc/verifier.d.ts.map +1 -0
  44. package/dist/auth/oidc/verifier.js +145 -0
  45. package/dist/auth/oidc/verifier.js.map +1 -0
  46. package/dist/auth/token-cli.d.ts +9 -0
  47. package/dist/auth/token-cli.d.ts.map +1 -0
  48. package/dist/auth/token-cli.js +98 -0
  49. package/dist/auth/token-cli.js.map +1 -0
  50. package/dist/cli.d.ts +3 -0
  51. package/dist/cli.d.ts.map +1 -0
  52. package/dist/cli.js +149 -0
  53. package/dist/cli.js.map +1 -0
  54. package/dist/config/redact.d.ts +20 -0
  55. package/dist/config/redact.d.ts.map +1 -0
  56. package/dist/config/redact.js +77 -0
  57. package/dist/config/redact.js.map +1 -0
  58. package/dist/config/secrets.d.ts +100 -0
  59. package/dist/config/secrets.d.ts.map +1 -0
  60. package/dist/config/secrets.js +130 -0
  61. package/dist/config/secrets.js.map +1 -0
  62. package/dist/framework/html.d.ts +103 -0
  63. package/dist/framework/html.d.ts.map +1 -0
  64. package/dist/framework/html.js +271 -0
  65. package/dist/framework/html.js.map +1 -0
  66. package/dist/framework/interface.d.ts +130 -0
  67. package/dist/framework/interface.d.ts.map +1 -0
  68. package/dist/framework/interface.js +112 -0
  69. package/dist/framework/interface.js.map +1 -0
  70. package/dist/framework/react.d.ts +133 -0
  71. package/dist/framework/react.d.ts.map +1 -0
  72. package/dist/framework/react.js +243 -0
  73. package/dist/framework/react.js.map +1 -0
  74. package/dist/framework/vue.d.ts +99 -0
  75. package/dist/framework/vue.d.ts.map +1 -0
  76. package/dist/framework/vue.js +375 -0
  77. package/dist/framework/vue.js.map +1 -0
  78. package/dist/index.d.ts +3 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +3 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/llm/client.d.ts +81 -0
  83. package/dist/llm/client.d.ts.map +1 -0
  84. package/dist/llm/client.js +141 -0
  85. package/dist/llm/client.js.map +1 -0
  86. package/dist/llm/component-response.d.ts +111 -0
  87. package/dist/llm/component-response.d.ts.map +1 -0
  88. package/dist/llm/component-response.js +167 -0
  89. package/dist/llm/component-response.js.map +1 -0
  90. package/dist/llm/generated-files.d.ts +10 -0
  91. package/dist/llm/generated-files.d.ts.map +1 -0
  92. package/dist/llm/generated-files.js +26 -0
  93. package/dist/llm/generated-files.js.map +1 -0
  94. package/dist/llm/prompts/CHANGELOG.md +70 -0
  95. package/dist/llm/prompts/generate-component.system.md +99 -0
  96. package/dist/llm/prompts/refine-component.system.md +112 -0
  97. package/dist/llm/prompts.d.ts +66 -0
  98. package/dist/llm/prompts.d.ts.map +1 -0
  99. package/dist/llm/prompts.js +78 -0
  100. package/dist/llm/prompts.js.map +1 -0
  101. package/dist/llm/retry.d.ts +183 -0
  102. package/dist/llm/retry.d.ts.map +1 -0
  103. package/dist/llm/retry.js +475 -0
  104. package/dist/llm/retry.js.map +1 -0
  105. package/dist/llm/schema.d.ts +129 -0
  106. package/dist/llm/schema.d.ts.map +1 -0
  107. package/dist/llm/schema.js +147 -0
  108. package/dist/llm/schema.js.map +1 -0
  109. package/dist/llm/validate.d.ts +107 -0
  110. package/dist/llm/validate.d.ts.map +1 -0
  111. package/dist/llm/validate.js +174 -0
  112. package/dist/llm/validate.js.map +1 -0
  113. package/dist/manifest/compiler.d.ts +112 -0
  114. package/dist/manifest/compiler.d.ts.map +1 -0
  115. package/dist/manifest/compiler.js +306 -0
  116. package/dist/manifest/compiler.js.map +1 -0
  117. package/dist/manifest/ensure.d.ts +46 -0
  118. package/dist/manifest/ensure.d.ts.map +1 -0
  119. package/dist/manifest/ensure.js +49 -0
  120. package/dist/manifest/ensure.js.map +1 -0
  121. package/dist/manifest/index.d.ts +13 -0
  122. package/dist/manifest/index.d.ts.map +1 -0
  123. package/dist/manifest/index.js +13 -0
  124. package/dist/manifest/index.js.map +1 -0
  125. package/dist/middleware/plan-guard.d.ts +167 -0
  126. package/dist/middleware/plan-guard.d.ts.map +1 -0
  127. package/dist/middleware/plan-guard.js +190 -0
  128. package/dist/middleware/plan-guard.js.map +1 -0
  129. package/dist/plans/index.d.ts +71 -0
  130. package/dist/plans/index.d.ts.map +1 -0
  131. package/dist/plans/index.js +252 -0
  132. package/dist/plans/index.js.map +1 -0
  133. package/dist/schemas/component.schema.json +114 -0
  134. package/dist/server.d.ts +96 -0
  135. package/dist/server.d.ts.map +1 -0
  136. package/dist/server.js +258 -0
  137. package/dist/server.js.map +1 -0
  138. package/dist/store/empty-manifest.d.ts +58 -0
  139. package/dist/store/empty-manifest.d.ts.map +1 -0
  140. package/dist/store/empty-manifest.js +59 -0
  141. package/dist/store/empty-manifest.js.map +1 -0
  142. package/dist/store/git-host.d.ts +120 -0
  143. package/dist/store/git-host.d.ts.map +1 -0
  144. package/dist/store/git-host.js +796 -0
  145. package/dist/store/git-host.js.map +1 -0
  146. package/dist/store/index.d.ts +5 -0
  147. package/dist/store/index.d.ts.map +1 -0
  148. package/dist/store/index.js +4 -0
  149. package/dist/store/index.js.map +1 -0
  150. package/dist/store/interface.d.ts +322 -0
  151. package/dist/store/interface.d.ts.map +1 -0
  152. package/dist/store/interface.js +100 -0
  153. package/dist/store/interface.js.map +1 -0
  154. package/dist/store/kit-files.d.ts +88 -0
  155. package/dist/store/kit-files.d.ts.map +1 -0
  156. package/dist/store/kit-files.js +199 -0
  157. package/dist/store/kit-files.js.map +1 -0
  158. package/dist/store/local.d.ts +70 -0
  159. package/dist/store/local.d.ts.map +1 -0
  160. package/dist/store/local.js +651 -0
  161. package/dist/store/local.js.map +1 -0
  162. package/dist/store/manifest.d.ts +44 -0
  163. package/dist/store/manifest.d.ts.map +1 -0
  164. package/dist/store/manifest.js +116 -0
  165. package/dist/store/manifest.js.map +1 -0
  166. package/dist/store/viewer-assets.d.ts +20 -0
  167. package/dist/store/viewer-assets.d.ts.map +1 -0
  168. package/dist/store/viewer-assets.js +118 -0
  169. package/dist/store/viewer-assets.js.map +1 -0
  170. package/dist/sync/anchor.d.ts +125 -0
  171. package/dist/sync/anchor.d.ts.map +1 -0
  172. package/dist/sync/anchor.js +210 -0
  173. package/dist/sync/anchor.js.map +1 -0
  174. package/dist/sync/index.d.ts +15 -0
  175. package/dist/sync/index.d.ts.map +1 -0
  176. package/dist/sync/index.js +15 -0
  177. package/dist/sync/index.js.map +1 -0
  178. package/dist/sync/orchestrator.d.ts +160 -0
  179. package/dist/sync/orchestrator.d.ts.map +1 -0
  180. package/dist/sync/orchestrator.js +288 -0
  181. package/dist/sync/orchestrator.js.map +1 -0
  182. package/dist/tools/bind_kit.d.ts +29 -0
  183. package/dist/tools/bind_kit.d.ts.map +1 -0
  184. package/dist/tools/bind_kit.js +70 -0
  185. package/dist/tools/bind_kit.js.map +1 -0
  186. package/dist/tools/conjure.d.ts +141 -0
  187. package/dist/tools/conjure.d.ts.map +1 -0
  188. package/dist/tools/conjure.js +655 -0
  189. package/dist/tools/conjure.js.map +1 -0
  190. package/dist/tools/conjure_screen.d.ts +174 -0
  191. package/dist/tools/conjure_screen.d.ts.map +1 -0
  192. package/dist/tools/conjure_screen.js +458 -0
  193. package/dist/tools/conjure_screen.js.map +1 -0
  194. package/dist/tools/create_kit.d.ts +32 -0
  195. package/dist/tools/create_kit.d.ts.map +1 -0
  196. package/dist/tools/create_kit.js +131 -0
  197. package/dist/tools/create_kit.js.map +1 -0
  198. package/dist/tools/create_project.d.ts +267 -0
  199. package/dist/tools/create_project.d.ts.map +1 -0
  200. package/dist/tools/create_project.js +546 -0
  201. package/dist/tools/create_project.js.map +1 -0
  202. package/dist/tools/delete_files.d.ts +93 -0
  203. package/dist/tools/delete_files.d.ts.map +1 -0
  204. package/dist/tools/delete_files.js +276 -0
  205. package/dist/tools/delete_files.js.map +1 -0
  206. package/dist/tools/delete_project.d.ts +43 -0
  207. package/dist/tools/delete_project.d.ts.map +1 -0
  208. package/dist/tools/delete_project.js +118 -0
  209. package/dist/tools/delete_project.js.map +1 -0
  210. package/dist/tools/get_kit.d.ts +28 -0
  211. package/dist/tools/get_kit.d.ts.map +1 -0
  212. package/dist/tools/get_kit.js +95 -0
  213. package/dist/tools/get_kit.js.map +1 -0
  214. package/dist/tools/get_project.d.ts +37 -0
  215. package/dist/tools/get_project.d.ts.map +1 -0
  216. package/dist/tools/get_project.js +71 -0
  217. package/dist/tools/get_project.js.map +1 -0
  218. package/dist/tools/list_components.d.ts +42 -0
  219. package/dist/tools/list_components.d.ts.map +1 -0
  220. package/dist/tools/list_components.js +125 -0
  221. package/dist/tools/list_components.js.map +1 -0
  222. package/dist/tools/list_files.d.ts +32 -0
  223. package/dist/tools/list_files.d.ts.map +1 -0
  224. package/dist/tools/list_files.js +109 -0
  225. package/dist/tools/list_files.js.map +1 -0
  226. package/dist/tools/list_kits.d.ts +14 -0
  227. package/dist/tools/list_kits.d.ts.map +1 -0
  228. package/dist/tools/list_kits.js +41 -0
  229. package/dist/tools/list_kits.js.map +1 -0
  230. package/dist/tools/list_projects.d.ts +23 -0
  231. package/dist/tools/list_projects.d.ts.map +1 -0
  232. package/dist/tools/list_projects.js +66 -0
  233. package/dist/tools/list_projects.js.map +1 -0
  234. package/dist/tools/plan.d.ts +16 -0
  235. package/dist/tools/plan.d.ts.map +1 -0
  236. package/dist/tools/plan.js +146 -0
  237. package/dist/tools/plan.js.map +1 -0
  238. package/dist/tools/preview.d.ts +289 -0
  239. package/dist/tools/preview.d.ts.map +1 -0
  240. package/dist/tools/preview.js +672 -0
  241. package/dist/tools/preview.js.map +1 -0
  242. package/dist/tools/read_file.d.ts +40 -0
  243. package/dist/tools/read_file.d.ts.map +1 -0
  244. package/dist/tools/read_file.js +114 -0
  245. package/dist/tools/read_file.js.map +1 -0
  246. package/dist/tools/refine.d.ts +220 -0
  247. package/dist/tools/refine.d.ts.map +1 -0
  248. package/dist/tools/refine.js +501 -0
  249. package/dist/tools/refine.js.map +1 -0
  250. package/dist/tools/validate.d.ts +74 -0
  251. package/dist/tools/validate.d.ts.map +1 -0
  252. package/dist/tools/validate.js +224 -0
  253. package/dist/tools/validate.js.map +1 -0
  254. package/dist/tools/write_files.d.ts +158 -0
  255. package/dist/tools/write_files.d.ts.map +1 -0
  256. package/dist/tools/write_files.js +541 -0
  257. package/dist/tools/write_files.js.map +1 -0
  258. package/dist/transport.d.ts +77 -0
  259. package/dist/transport.d.ts.map +1 -0
  260. package/dist/transport.js +448 -0
  261. package/dist/transport.js.map +1 -0
  262. package/dist/ui/card-asset-broker.d.ts +42 -0
  263. package/dist/ui/card-asset-broker.d.ts.map +1 -0
  264. package/dist/ui/card-asset-broker.js +479 -0
  265. package/dist/ui/card-asset-broker.js.map +1 -0
  266. package/dist/ui/grid-resource.d.ts +252 -0
  267. package/dist/ui/grid-resource.d.ts.map +1 -0
  268. package/dist/ui/grid-resource.js +680 -0
  269. package/dist/ui/grid-resource.js.map +1 -0
  270. package/dist/ui/viewer-static/index.html +95 -0
  271. package/dist/ui/viewer-static/viewer.css +451 -0
  272. package/dist/ui/viewer-static/viewer.js +1405 -0
  273. package/dist/validate/full-scan.d.ts +87 -0
  274. package/dist/validate/full-scan.d.ts.map +1 -0
  275. package/dist/validate/full-scan.js +299 -0
  276. package/dist/validate/full-scan.js.map +1 -0
  277. package/dist/validate/index.d.ts +15 -0
  278. package/dist/validate/index.d.ts.map +1 -0
  279. package/dist/validate/index.js +18 -0
  280. package/dist/validate/index.js.map +1 -0
  281. package/dist/validate/marker.d.ts +89 -0
  282. package/dist/validate/marker.d.ts.map +1 -0
  283. package/dist/validate/marker.js +87 -0
  284. package/dist/validate/marker.js.map +1 -0
  285. package/dist/validate/phash.d.ts +135 -0
  286. package/dist/validate/phash.d.ts.map +1 -0
  287. package/dist/validate/phash.js +307 -0
  288. package/dist/validate/phash.js.map +1 -0
  289. package/dist/validate/render.d.ts +38 -0
  290. package/dist/validate/render.d.ts.map +1 -0
  291. package/dist/validate/render.js +155 -0
  292. package/dist/validate/render.js.map +1 -0
  293. package/dist/watch/index.d.ts +12 -0
  294. package/dist/watch/index.d.ts.map +1 -0
  295. package/dist/watch/index.js +12 -0
  296. package/dist/watch/index.js.map +1 -0
  297. package/dist/watch/watcher.d.ts +65 -0
  298. package/dist/watch/watcher.d.ts.map +1 -0
  299. package/dist/watch/watcher.js +238 -0
  300. package/dist/watch/watcher.js.map +1 -0
  301. 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
+ })();