@jxsuite/studio 5.2.0 → 5.4.0

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 (398) hide show
  1. package/data/css-meta.json +28 -27
  2. package/dist/chunks/{cf-account-picker-h4dab9bq.js → cf-account-picker-3f08rry6.js} +21 -18
  3. package/dist/chunks/{cf-settings-adg74qsk.js → cf-settings-m79r7p5y.js} +4 -4
  4. package/dist/chunks/{components-5y78e05d.js → components-xm826zjw.js} +3 -3
  5. package/dist/chunks/{content-source-sgg1sv6n.js → content-source-jt7rbcbc.js} +22 -22
  6. package/dist/chunks/{convert-to-repeater-mf1cxgfn.js → convert-to-repeater-vg38bq99.js} +22 -22
  7. package/dist/chunks/{editor-3qhbg51x.js → editor-ddy019sn.js} +2 -2
  8. package/dist/chunks/entry-commands-64nb2a9a.js +42 -0
  9. package/dist/chunks/{extension-sections-c6zwzbre.js → extension-sections-r73an5s6.js} +31 -30
  10. package/dist/chunks/{file-ops-vrpq1jjk.js → file-ops-3xan8mnj.js} +21 -21
  11. package/dist/chunks/{files-msaf95y6.js → files-egsb0ry8.js} +30 -29
  12. package/dist/chunks/{format-host-f64fnesp.js → format-host-mym8qnd0.js} +3 -3
  13. package/dist/chunks/{fs-events-3y6y0nrm.js → fs-events-z2gg8qjt.js} +4 -4
  14. package/dist/chunks/{git-panel-bv5e22j0.js → git-panel-a05atk6q.js} +20 -20
  15. package/dist/chunks/{grid-view-1p6hsyjx.css → grid-view-3g1h2re8.css} +956 -139
  16. package/dist/chunks/{grid-view-paj67nr5.js → grid-view-qaqer9kr.js} +24 -24
  17. package/dist/chunks/{icons-yy10wk9r.js → icons-drxq9q10.js} +8 -2
  18. package/dist/chunks/{iframe-host-dh5np8vk.js → iframe-host-psetcv20.js} +11 -11
  19. package/dist/chunks/{index-n4j7qxyr.js → index-9yjae3hz.js} +4 -4
  20. package/dist/chunks/{javascript-4abzjcy9.css → javascript-bc9z461d.css} +688 -54
  21. package/dist/chunks/{javascript-xvrfrpkb.js → javascript-hr5sg57n.js} +2 -2
  22. package/dist/chunks/{jsonMode-vvmpmk34.css → jsonMode-qqd9nm1t.css} +1120 -303
  23. package/dist/chunks/{jsonMode-tyx2kj6p.js → jsonMode-zs88v0wr.js} +4 -4
  24. package/dist/chunks/{jx-validate-j0wxk72w.js → jx-validate-epp2z3s3.js} +2 -2
  25. package/dist/chunks/layers-panel-83a16ytx.js +41 -0
  26. package/dist/chunks/{layers-1vcfdbet.js → layers-s6mc01ry.js} +3 -3
  27. package/dist/chunks/{listbox-6v2ac08j.js → listbox-b7jxnhq1.js} +19 -3
  28. package/dist/chunks/{media-picker-p49nhqhs.js → media-picker-nhn6rv4w.js} +11 -11
  29. package/dist/chunks/{menu-qht48vnc.js → menu-8j4qxjrp.js} +4 -4
  30. package/dist/chunks/{monaco-binding-sw6en4dw.js → monaco-binding-e8d9wzyk.js} +3 -3
  31. package/dist/chunks/{monaco-buffer-pykwbm92.js → monaco-buffer-jaq1t9zj.js} +2 -2
  32. package/dist/chunks/{monaco-setup-8vbyqnhr.css → monaco-setup-ey9cjcm3.css} +954 -137
  33. package/dist/chunks/{monaco-setup-8r62fj6y.js → monaco-setup-gsjdysze.js} +7 -7
  34. package/dist/chunks/{platform-ts9cg1a4.js → platform-fnchmhy7.js} +5 -3
  35. package/dist/chunks/publish-panel-ak6hxfma.js +27 -0
  36. package/dist/chunks/quick-search-c63mkt3b.js +47 -0
  37. package/dist/chunks/{references-h69xp9zz.js → references-f7bdv3z5.js} +3 -3
  38. package/dist/chunks/right-panel-t5n8wttg.js +45 -0
  39. package/dist/chunks/{split-g7nm1rgb.js → split-4cz4b6ef.js} +14 -3
  40. package/dist/chunks/{studio-sar8g54p.js → studio-01269reb.js} +8 -8
  41. package/dist/chunks/{studio-rnyf3f8e.js → studio-0a4gncx0.js} +3 -3
  42. package/dist/chunks/{studio-e1f24eff.js → studio-0d4p4wxj.js} +6778 -2993
  43. package/dist/chunks/{studio-4wbb9ct0.js → studio-1tejxvba.js} +3006 -1855
  44. package/dist/chunks/{studio-p6pmwkvh.js → studio-3nprwbd9.js} +147 -28
  45. package/dist/chunks/{studio-4c041r8y.js → studio-40z39dme.js} +3 -3
  46. package/dist/chunks/{studio-68fydsrj.js → studio-482eeazk.js} +20 -20
  47. package/dist/chunks/{studio-y4f9fdge.js → studio-4998spet.js} +10 -9
  48. package/dist/chunks/{studio-nq5yf5mp.js → studio-557bmn0x.js} +2 -2
  49. package/dist/chunks/{studio-hdc2jt3z.js → studio-5w26wza5.js} +3 -3
  50. package/dist/chunks/{studio-6nzaktvy.js → studio-6pwn9j8c.js} +6 -3
  51. package/dist/chunks/{studio-wnsyffg1.js → studio-6vg0x5fz.js} +3 -2
  52. package/dist/chunks/{studio-vn5hske5.js → studio-7d3c1m59.js} +12 -5
  53. package/dist/chunks/{studio-28vs5k2y.js → studio-9vsvz7w7.js} +7 -7
  54. package/dist/chunks/{studio-t3adrr56.js → studio-9wm0xzpf.js} +19 -19
  55. package/dist/chunks/{studio-38emantv.js → studio-a5h5j33a.js} +19 -5
  56. package/dist/chunks/{studio-ympe0n7a.js → studio-ag3z6shb.js} +10 -4
  57. package/dist/chunks/studio-aj9drheq.js +177 -0
  58. package/dist/chunks/{studio-vt0tj32j.js → studio-apv9x70c.js} +189 -130
  59. package/dist/chunks/{studio-c04nzbby.js → studio-b9q3znz8.js} +2 -2
  60. package/dist/chunks/{studio-2mdbbc76.js → studio-bh47w0cy.js} +9 -10
  61. package/dist/chunks/{studio-ez11dmdh.js → studio-dgdm6vvt.js} +264 -52
  62. package/dist/chunks/{studio-5nz00bhh.js → studio-dwdcr2e8.js} +4 -4
  63. package/dist/chunks/{studio-nqgawfks.js → studio-fbcyz234.js} +146 -139
  64. package/dist/chunks/{studio-kcgff4r0.js → studio-fgmfksz8.js} +2 -2
  65. package/dist/chunks/{studio-x72j58vw.js → studio-fqpkpq1t.js} +7 -4
  66. package/dist/chunks/{studio-15f3kxqe.js → studio-fy320dfc.js} +62 -68
  67. package/dist/chunks/{studio-zjjde71a.js → studio-gjj4bbfx.js} +2 -2
  68. package/dist/chunks/{studio-66arbpc2.js → studio-jfhxy8h3.js} +16 -15
  69. package/dist/chunks/{studio-d0300636.js → studio-ks2zzth2.js} +17 -2
  70. package/dist/chunks/{studio-c07s5ckd.js → studio-kwewmhhq.js} +4 -4
  71. package/dist/chunks/{studio-01jeck2y.js → studio-m58xn8at.js} +26 -25
  72. package/dist/chunks/{studio-mpzwze9x.js → studio-mfkh1q8d.js} +17 -17
  73. package/dist/chunks/{studio-etvx9s2g.js → studio-mrh0a92f.js} +17 -17
  74. package/dist/chunks/{studio-qa7fzxan.js → studio-mt79tn56.js} +3 -3
  75. package/dist/chunks/{studio-hhskq2mr.js → studio-ne593z1j.js} +39 -32
  76. package/dist/chunks/{studio-h4nvnjvz.js → studio-ptfcxkz5.js} +418 -232
  77. package/dist/chunks/{studio-4xmjcrg3.js → studio-qxye31h7.js} +7 -7
  78. package/dist/chunks/{studio-a1pswps3.js → studio-rav5bpmp.js} +81 -59
  79. package/dist/chunks/{studio-ncn9e189.js → studio-rwmmbrth.js} +6 -6
  80. package/dist/chunks/{studio-en82c639.js → studio-sceg8ws0.js} +8 -8
  81. package/dist/chunks/{studio-wjpgxywa.js → studio-sjas5b3a.js} +19 -13
  82. package/dist/chunks/{studio-4r14yzyy.js → studio-vd67b66d.js} +3 -3
  83. package/dist/chunks/{studio-14d9tq4d.js → studio-w475cbf9.js} +10 -10
  84. package/dist/chunks/{studio-ctm7bhx5.js → studio-w7gpcvfj.js} +9 -9
  85. package/dist/chunks/{studio-9j6sfeba.js → studio-xrbfdxgk.js} +7 -7
  86. package/dist/chunks/{studio-x10fp4tp.js → studio-y49b6mjv.js} +35 -35
  87. package/dist/chunks/{studio-5t7ena5z.js → studio-ygvr9e4q.js} +10 -10
  88. package/dist/chunks/{studio-z2smx5ce.js → studio-yxmmjy2a.js} +9 -6
  89. package/dist/chunks/{tsMode-jy4m86rv.js → tsMode-ay4c78ne.js} +4 -4
  90. package/dist/chunks/{tsMode-hnxxb73h.css → tsMode-c72wyz27.css} +954 -137
  91. package/dist/chunks/{workspace-bnw54cha.js → workspace-b9k2frks.js} +5 -5
  92. package/dist/chunks/{ws-client-ny4zc51r.js → ws-client-erze4czt.js} +356 -20
  93. package/dist/codicon.ttf +0 -0
  94. package/dist/iframe-entry.js +20 -18
  95. package/dist/studio.css +956 -139
  96. package/dist/studio.js +1511 -851
  97. package/dist/workers/editor.worker.js +196 -107
  98. package/dist/workers/json.worker.js +196 -107
  99. package/dist/workers/ts.worker.js +196 -107
  100. package/package.json +18 -17
  101. package/src/about/about-modal.ts +4 -3
  102. package/src/browse/library-commands.ts +15 -13
  103. package/src/browse/library-pane.ts +2 -2
  104. package/src/canvas/canvas-render.ts +97 -83
  105. package/src/canvas/canvas-surface.ts +2 -2
  106. package/src/canvas/canvas-utils.ts +42 -32
  107. package/src/canvas/diff-toolbar.ts +21 -2
  108. package/src/canvas/diff-view.ts +30 -6
  109. package/src/canvas/iframe-entry.ts +2 -2
  110. package/src/canvas/iframe-host.ts +5 -5
  111. package/src/canvas/iframe-interaction.ts +7 -6
  112. package/src/canvas/iframe-overlay.ts +1 -1
  113. package/src/canvas/iframe-protocol.ts +6 -5
  114. package/src/canvas/iframe-render.ts +3 -3
  115. package/src/canvas/popover-state.ts +1 -1
  116. package/src/canvas/stage-scrollbar.ts +112 -0
  117. package/src/canvas/surface-registry.ts +9 -2
  118. package/src/collab/collab-commands.ts +8 -6
  119. package/src/collab/collab-session.ts +4 -4
  120. package/src/collab/collab-state.ts +4 -4
  121. package/src/collab/presence-chips.ts +3 -3
  122. package/src/commands/app-commands.ts +7 -7
  123. package/src/commands/budget.ts +17 -16
  124. package/src/commands/command-args.ts +10 -9
  125. package/src/commands/context.ts +20 -10
  126. package/src/commands/defaults.ts +30 -29
  127. package/src/commands/levels.ts +8 -6
  128. package/src/commands/live-context.ts +12 -10
  129. package/src/commands/reference.ts +5 -5
  130. package/src/commands/registry.ts +2 -2
  131. package/src/content/draft-state.ts +9 -8
  132. package/src/content/entry-commands.ts +16 -14
  133. package/src/content/entry-editor.ts +2 -2
  134. package/src/content/entry-fields.ts +1 -1
  135. package/src/editor/context-menu.ts +4 -4
  136. package/src/editor/convert-to-component.ts +6 -5
  137. package/src/editor/inline-edit.ts +3 -3
  138. package/src/editor/shortcuts.ts +88 -20
  139. package/src/editor/slash-menu.ts +11 -10
  140. package/src/files/elements.ts +4 -5
  141. package/src/files/file-ops.ts +1 -1
  142. package/src/files/files.ts +21 -17
  143. package/src/files/serialize-document.ts +1 -1
  144. package/src/format/convert-file.ts +5 -5
  145. package/src/github/github-auth-status.ts +107 -0
  146. package/src/github/github-auth.ts +18 -82
  147. package/src/github/github-publish.ts +7 -6
  148. package/src/grid/cell-editors.ts +8 -7
  149. package/src/grid/grid-layout.ts +1 -1
  150. package/src/grid/grid-open.ts +6 -5
  151. package/src/grid/grid-panel.ts +13 -12
  152. package/src/grid/redirects-grid.ts +14 -11
  153. package/src/grid/redirects.ts +10 -5
  154. package/src/grid/sources/content-source.ts +10 -9
  155. package/src/hosting/document.ts +28 -1
  156. package/src/hosting/stage.ts +15 -4
  157. package/src/i18n/i18n-commands.ts +9 -7
  158. package/src/new-project/location-fields.ts +16 -3
  159. package/src/new-project/new-project-modal.ts +23 -7
  160. package/src/page-params.ts +6 -6
  161. package/src/panels/activity-panel.ts +11 -10
  162. package/src/panels/ai-chat/chat-view.ts +27 -14
  163. package/src/panels/ai-chat/composer.ts +10 -3
  164. package/src/panels/ai-panel.ts +87 -29
  165. package/src/panels/block-action-bar.ts +59 -33
  166. package/src/panels/bottom-dock.ts +22 -22
  167. package/src/panels/chat-panel.ts +40 -5
  168. package/src/panels/data-explorer.ts +9 -9
  169. package/src/panels/data-grid.ts +1 -1
  170. package/src/panels/editors.ts +8 -8
  171. package/src/panels/elements-panel.ts +7 -8
  172. package/src/panels/empty-state.ts +1 -1
  173. package/src/panels/events-panel.ts +13 -12
  174. package/src/panels/formula-workspace.ts +8 -5
  175. package/src/panels/frontmatter-fields.ts +4 -1
  176. package/src/panels/frontmatter-panel.ts +18 -18
  177. package/src/panels/git-panel.ts +19 -13
  178. package/src/panels/head-panel.ts +9 -10
  179. package/src/panels/i18n-panel.ts +6 -4
  180. package/src/panels/imports-panel.ts +1 -1
  181. package/src/panels/jump-bar.ts +20 -18
  182. package/src/panels/layers-panel.ts +19 -17
  183. package/src/panels/left-panel.ts +3 -3
  184. package/src/panels/navigator-panels.ts +8 -8
  185. package/src/panels/overlays.ts +1 -1
  186. package/src/panels/pane-context.ts +56 -28
  187. package/src/panels/pane-grid.ts +9 -8
  188. package/src/panels/panel-registry.ts +47 -19
  189. package/src/panels/problems-panel.ts +11 -12
  190. package/src/panels/properties-panel.ts +21 -20
  191. package/src/panels/provenance.ts +3 -3
  192. package/src/panels/quick-search.ts +15 -11
  193. package/src/panels/right-panel.ts +4 -3
  194. package/src/panels/seo-modal.ts +5 -4
  195. package/src/panels/settings-menu.ts +9 -9
  196. package/src/panels/settings-pane.ts +3 -3
  197. package/src/panels/signals-panel.ts +16 -9
  198. package/src/panels/statement-editor.ts +12 -9
  199. package/src/panels/style-panel.ts +45 -41
  200. package/src/panels/stylebook-panel.ts +0 -1
  201. package/src/panels/tab-strip.ts +26 -23
  202. package/src/platform-errors.ts +2 -2
  203. package/src/platform.ts +44 -4
  204. package/src/platforms/cloud.ts +11 -1
  205. package/src/platforms/default-platform.ts +141 -4
  206. package/src/platforms/devserver.ts +23 -3
  207. package/src/publish/deploy-checklist.ts +11 -10
  208. package/src/publish/publish-commands.ts +9 -8
  209. package/src/services/a11y-report.ts +1 -1
  210. package/src/services/ai-ask.ts +39 -16
  211. package/src/services/ai-command-tools.ts +28 -14
  212. package/src/services/ai-import-tools.ts +43 -29
  213. package/src/services/ai-project-tools.ts +20 -38
  214. package/src/services/ai-settings.ts +55 -2
  215. package/src/services/ai-tools.ts +69 -58
  216. package/src/services/ai-write-report.ts +4 -4
  217. package/src/services/ai-writes.ts +74 -55
  218. package/src/services/automation.ts +12 -12
  219. package/src/services/clock.ts +2 -1
  220. package/src/services/connection.ts +2 -2
  221. package/src/services/context-manager.ts +69 -6
  222. package/src/services/directory-picker.ts +93 -27
  223. package/src/services/document-assistant.ts +86 -17
  224. package/src/services/gated-registry.ts +4 -3
  225. package/src/services/idle.ts +5 -4
  226. package/src/services/live-surfaces.ts +2 -1
  227. package/src/services/module-preloads.ts +67 -0
  228. package/src/services/notify.ts +9 -7
  229. package/src/services/popover-report.ts +3 -3
  230. package/src/services/preview-eval.ts +1 -1
  231. package/src/services/project-adoption.ts +34 -1
  232. package/src/services/project-path.ts +53 -0
  233. package/src/services/settings/kernel.ts +13 -2
  234. package/src/services/tool-executor.ts +135 -30
  235. package/src/services/tool-outcomes.ts +143 -0
  236. package/src/settings/contexts-section.ts +8 -8
  237. package/src/settings/contributed-section.ts +6 -6
  238. package/src/settings/css-vars-editor.ts +6 -5
  239. package/src/settings/extension-commands.ts +19 -18
  240. package/src/settings/extension-sections.ts +7 -7
  241. package/src/settings/extensions-section.ts +3 -3
  242. package/src/settings/general-settings.ts +21 -18
  243. package/src/settings/preferences-accounts.ts +3 -3
  244. package/src/settings/preferences-dialog.ts +9 -8
  245. package/src/settings/preferences-sections.ts +1 -1
  246. package/src/settings/project-sections.ts +2 -2
  247. package/src/settings/section-registry.ts +7 -6
  248. package/src/settings/settings-document.ts +41 -43
  249. package/src/shell/tree.ts +29 -0
  250. package/src/shell.ts +63 -55
  251. package/src/site-context.ts +1 -1
  252. package/src/store.ts +2 -2
  253. package/src/studio.ts +52 -28
  254. package/src/style/live-preview.ts +8 -8
  255. package/src/style/project-styles.ts +8 -9
  256. package/src/style/token-ref.ts +2 -2
  257. package/src/surfaces/about.json +6 -2
  258. package/src/surfaces/add-repo.json +2 -2
  259. package/src/surfaces/add-repo.ts +2 -2
  260. package/src/surfaces/ai-chat.json +14 -6
  261. package/src/surfaces/ai-chat.ts +4 -2
  262. package/src/surfaces/ai-credentials-form.json +53 -49
  263. package/src/surfaces/ai-credentials-form.ts +18 -4
  264. package/src/surfaces/ai-managed-connect.json +4 -4
  265. package/src/surfaces/block-action-bar.json +4 -4
  266. package/src/surfaces/block-action-bar.ts +3 -3
  267. package/src/surfaces/boot-failure.json +228 -0
  268. package/src/surfaces/boot-failure.ts +156 -0
  269. package/src/surfaces/bottom-dock.json +1 -1
  270. package/src/surfaces/canvas-stage.json +9 -40
  271. package/src/surfaces/canvas-stage.ts +9 -9
  272. package/src/surfaces/cf-account-picker.json +11 -8
  273. package/src/surfaces/commandbar.ts +11 -10
  274. package/src/surfaces/convert-repeater.ts +3 -2
  275. package/src/surfaces/dialog.json +7 -1
  276. package/src/surfaces/dialog.ts +2 -2
  277. package/src/surfaces/doc-header.json +6 -13
  278. package/src/surfaces/doc-header.ts +4 -5
  279. package/src/surfaces/entry-editor.json +1 -1
  280. package/src/surfaces/entry-editor.ts +2 -1
  281. package/src/surfaces/expression-editor.json +5 -5
  282. package/src/surfaces/files-panel.json +4 -3
  283. package/src/surfaces/files-panel.ts +1 -1
  284. package/src/surfaces/formula-palette.json +10 -9
  285. package/src/surfaces/formula-palette.ts +11 -7
  286. package/src/surfaces/git-panel.json +167 -109
  287. package/src/surfaces/git-panel.ts +2 -2
  288. package/src/surfaces/github-auth.json +2 -1
  289. package/src/surfaces/github-publish.ts +3 -3
  290. package/src/surfaces/grid-open.json +1 -1
  291. package/src/surfaces/grid-panel.json +2 -2
  292. package/src/surfaces/grid-views.json +1 -1
  293. package/src/surfaces/inspector-dock.json +2 -2
  294. package/src/surfaces/jump-bar.json +1 -1
  295. package/src/surfaces/jump-bar.ts +1 -1
  296. package/src/surfaces/library-pane.ts +2 -1
  297. package/src/surfaces/logic-panel.json +7 -7
  298. package/src/surfaces/logic-panel.ts +6 -4
  299. package/src/surfaces/logic-workspace.json +2 -2
  300. package/src/surfaces/logic-workspace.ts +2 -2
  301. package/src/surfaces/media-browser.ts +3 -3
  302. package/src/surfaces/media-pane.ts +2 -1
  303. package/src/surfaces/menu.ts +5 -5
  304. package/src/surfaces/new-project.json +1 -1
  305. package/src/surfaces/palette.json +8 -7
  306. package/src/surfaces/pane-context.json +86 -57
  307. package/src/surfaces/pane-context.ts +2 -1
  308. package/src/surfaces/pane-grid.json +3 -3
  309. package/src/surfaces/panel-activity.json +2 -1
  310. package/src/surfaces/panel-deploy-checklist.json +1 -1
  311. package/src/surfaces/panel-elements.ts +2 -1
  312. package/src/surfaces/panel-i18n.ts +2 -1
  313. package/src/surfaces/panel-outline.json +1 -1
  314. package/src/surfaces/panel-outline.ts +4 -3
  315. package/src/surfaces/panel-page.json +2 -2
  316. package/src/surfaces/panel-page.ts +4 -1
  317. package/src/surfaces/panel-problems.json +4 -3
  318. package/src/surfaces/panel-problems.ts +2 -2
  319. package/src/surfaces/panel-signals.json +23 -8
  320. package/src/surfaces/panel-signals.ts +5 -1
  321. package/src/surfaces/panel-stylebook-layers.json +1 -1
  322. package/src/surfaces/panel-stylebook-layers.ts +2 -2
  323. package/src/surfaces/preferences.json +21 -8
  324. package/src/surfaces/preferences.ts +9 -1
  325. package/src/surfaces/properties-panel.json +4 -4
  326. package/src/surfaces/properties-panel.ts +2 -2
  327. package/src/surfaces/publish.json +1 -1
  328. package/src/surfaces/publish.ts +2 -1
  329. package/src/surfaces/push-plan.json +4 -3
  330. package/src/surfaces/rail.json +2 -2
  331. package/src/surfaces/rail.ts +10 -4
  332. package/src/surfaces/reference-field.json +1 -1
  333. package/src/surfaces/schema-form.json +2 -2
  334. package/src/surfaces/schema-form.ts +1 -1
  335. package/src/surfaces/seo.json +1 -1
  336. package/src/surfaces/seo.ts +2 -2
  337. package/src/surfaces/settings-contributed.ts +2 -1
  338. package/src/surfaces/settings-css-vars.json +4 -4
  339. package/src/surfaces/settings-deploy.json +4 -4
  340. package/src/surfaces/settings-deploy.ts +2 -1
  341. package/src/surfaces/settings-extensions.json +6 -6
  342. package/src/surfaces/settings-extensions.ts +6 -2
  343. package/src/surfaces/settings-locales.json +4 -4
  344. package/src/surfaces/settings-overview.ts +2 -1
  345. package/src/surfaces/settings-pane.ts +3 -2
  346. package/src/surfaces/settings-rawjson.ts +1 -1
  347. package/src/surfaces/slash-menu.ts +4 -4
  348. package/src/surfaces/statements.json +3 -3
  349. package/src/surfaces/statusbar.ts +23 -23
  350. package/src/surfaces/style-panel.json +9 -5
  351. package/src/surfaces/style-panel.ts +3 -3
  352. package/src/surfaces/tab-strip.json +2 -2
  353. package/src/surfaces/target-line.json +1 -1
  354. package/src/surfaces/target-line.ts +12 -10
  355. package/src/surfaces/welcome.json +7 -2
  356. package/src/tabs/doc-op-apply.ts +5 -5
  357. package/src/tabs/patch-ops.ts +3 -3
  358. package/src/tabs/project-config.ts +10 -10
  359. package/src/tabs/selection.ts +1 -1
  360. package/src/tabs/tab.ts +13 -14
  361. package/src/tabs/transact.ts +18 -24
  362. package/src/types.ts +27 -3
  363. package/src/ui/ai-credentials-form.ts +74 -16
  364. package/src/ui/ai-managed-connect.ts +68 -19
  365. package/src/ui/color-selector.ts +1 -1
  366. package/src/ui/expression-editor.ts +3 -3
  367. package/src/ui/form-controls.ts +6 -5
  368. package/src/ui/formula-catalog.ts +4 -4
  369. package/src/ui/formula-chips.ts +2 -2
  370. package/src/ui/layers.ts +20 -18
  371. package/src/ui/panel-resize.ts +8 -28
  372. package/src/ui/popover-surface.ts +2 -2
  373. package/src/ui/progress-modal.ts +3 -3
  374. package/src/ui/regions.ts +14 -9
  375. package/src/ui/schema-form.ts +10 -9
  376. package/src/ui/timing.ts +9 -7
  377. package/src/ui/value-source.ts +4 -4
  378. package/src/ui/virtual-window.ts +58 -5
  379. package/src/utils/base64.ts +20 -1
  380. package/src/utils/canvas-media.ts +1 -1
  381. package/src/utils/studio-utils.ts +0 -31
  382. package/src/view.ts +4 -3
  383. package/src/workspace/pane-derive.ts +14 -13
  384. package/src/workspace/session.ts +7 -6
  385. package/src/workspace/workspace.ts +49 -39
  386. package/styles/canvas.css +1 -27
  387. package/styles/inspector.css +4 -3
  388. package/styles/overlays.css +1 -1
  389. package/styles/tokens.css +17 -1
  390. package/styles/tokens.json +6 -1
  391. package/dist/chunks/entry-commands-w9qsxa6q.js +0 -42
  392. package/dist/chunks/layers-panel-fksbdg4q.js +0 -40
  393. package/dist/chunks/publish-panel-vz23wwmg.js +0 -27
  394. package/dist/chunks/quick-search-e9rp74m8.js +0 -46
  395. package/dist/chunks/right-panel-j97t2yvy.js +0 -45
  396. package/dist/chunks/studio-727xh0e3.js +0 -76
  397. package/src/files/json-layout.ts +0 -474
  398. package/src/services/ai-turn-signal.ts +0 -70
package/dist/studio.js CHANGED
@@ -22,6 +22,19 @@ import {
22
22
  hasElementSelection,
23
23
  emptyContext,
24
24
  keyScopeStack,
25
+ toolSuccess,
26
+ toolError,
27
+ createToolDefinition,
28
+ createSessionFacts,
29
+ createToolContext,
30
+ linkCallSignal,
31
+ createToolRegistry,
32
+ openTurnLedger,
33
+ fileTurn,
34
+ writesForTurn,
35
+ summarizeWrites,
36
+ resetAiWrites,
37
+ turnAnchor,
25
38
  PROJECT_CONFIG_PATH,
26
39
  editorKindOf,
27
40
  editorKindsOf,
@@ -46,6 +59,8 @@ import {
46
59
  mutateUpdateProperty,
47
60
  mutateUpdateStyle,
48
61
  mutateUpdateAttribute,
62
+ mutateAddDef,
63
+ mutateRemoveDef,
49
64
  mutateUpdateProp,
50
65
  mutateUpdateFrontmatter,
51
66
  collabState,
@@ -97,7 +112,7 @@ import {
97
112
  activateTab2,
98
113
  moveTabToPane2,
99
114
  registerTabCommands2
100
- } from "./chunks/studio-ez11dmdh.js";
115
+ } from "./chunks/studio-dgdm6vvt.js";
101
116
  import {
102
117
  LEVELS,
103
118
  CATEGORIES,
@@ -108,7 +123,7 @@ import {
108
123
  isPanelVisible,
109
124
  railGroups,
110
125
  railPanelSet
111
- } from "./chunks/studio-x72j58vw.js";
126
+ } from "./chunks/studio-fqpkpq1t.js";
112
127
  import {
113
128
  LIVE_PREVIEW,
114
129
  getNodeAtPath2,
@@ -141,17 +156,19 @@ import {
141
156
  clearSettings,
142
157
  hydrateSettings,
143
158
  watchRemoteSettings
144
- } from "./chunks/studio-z2smx5ce.js";
159
+ } from "./chunks/studio-yxmmjy2a.js";
145
160
  import {
146
161
  registerPlatform2,
162
+ launcherSignal2,
147
163
  getPlatform2,
148
164
  hasPlatform2
149
- } from "./chunks/studio-6nzaktvy.js";
165
+ } from "./chunks/studio-6pwn9j8c.js";
150
166
  import {
151
167
  effectScope,
152
168
  effect,
153
169
  reactive,
154
170
  toRaw,
171
+ shallowRef,
155
172
  announce,
156
173
  now,
157
174
  problems2,
@@ -178,7 +195,7 @@ import {
178
195
  bufferMovedOn2,
179
196
  commitTabBuffers2,
180
197
  tabBufferUnsaved2
181
- } from "./chunks/studio-wnsyffg1.js";
198
+ } from "./chunks/studio-6vg0x5fz.js";
182
199
  import {
183
200
  loadMonaco,
184
201
  loadedMonaco,
@@ -192,7 +209,7 @@ import {
192
209
  formatByName2,
193
210
  formatForPath2,
194
211
  formatSerialize2
195
- } from "./chunks/studio-28vs5k2y.js";
212
+ } from "./chunks/studio-9vsvz7w7.js";
196
213
  import {
197
214
  parseJsonDocument,
198
215
  serializeJson,
@@ -234,14 +251,14 @@ import {
234
251
  toggleActivityTab,
235
252
  resetProjectShell,
236
253
  registerShellViewCommands
237
- } from "./chunks/studio-mpzwze9x.js";
254
+ } from "./chunks/studio-mfkh1q8d.js";
238
255
  import {
239
256
  componentMetaFrom,
240
257
  componentRegistry2,
241
258
  loadComponentRegistry2,
242
259
  noteComponentSaved2,
243
260
  buildComponentInstance2
244
- } from "./chunks/studio-zjjde71a.js";
261
+ } from "./chunks/studio-gjj4bbfx.js";
245
262
  import {
246
263
  tabOfPane,
247
264
  derivationOfPane,
@@ -352,7 +369,7 @@ import {
352
369
  postApplyFormat2,
353
370
  postOpenSlash2,
354
371
  commitActiveEditSession2
355
- } from "./chunks/studio-etvx9s2g.js";
372
+ } from "./chunks/studio-mrh0a92f.js";
356
373
  import {
357
374
  PUBLIC_DIR,
358
375
  setSkipServerFunctions,
@@ -366,13 +383,14 @@ import {
366
383
  showModal,
367
384
  close2,
368
385
  splitModifiersOf,
386
+ trackOf,
369
387
  attrs,
370
388
  tagOf,
371
389
  labelOf,
372
390
  KIT_BASE,
373
391
  setActiveRegistry,
374
392
  activeRegistry
375
- } from "./chunks/studio-p6pmwkvh.js";
393
+ } from "./chunks/studio-3nprwbd9.js";
376
394
  import {
377
395
  DERIVE_PRESETS,
378
396
  PRESET_LABELS,
@@ -402,7 +420,7 @@ import {
402
420
  defBadgeLabel,
403
421
  mountSignalsPanel,
404
422
  registerSignalsCommands
405
- } from "./chunks/studio-a1pswps3.js";
423
+ } from "./chunks/studio-rav5bpmp.js";
406
424
  import {
407
425
  mountsUsing,
408
426
  mountsOf,
@@ -428,7 +446,7 @@ import {
428
446
  showConfirmDialog2,
429
447
  showSaveDiscardDialog2,
430
448
  showPromptDialog2
431
- } from "./chunks/studio-ympe0n7a.js";
449
+ } from "./chunks/studio-ag3z6shb.js";
432
450
  import {
433
451
  html3,
434
452
  nothing2,
@@ -444,11 +462,11 @@ import {
444
462
  uploadAssets,
445
463
  mediaSiteUrl,
446
464
  previewFileSrc
447
- } from "./chunks/studio-en82c639.js";
448
- import"./chunks/studio-5nz00bhh.js";
465
+ } from "./chunks/studio-sceg8ws0.js";
466
+ import"./chunks/studio-dwdcr2e8.js";
449
467
  import {
450
468
  invalidateUsages2
451
- } from "./chunks/studio-nq5yf5mp.js";
469
+ } from "./chunks/studio-557bmn0x.js";
452
470
  import {
453
471
  hydrateProjectList,
454
472
  getProjectList,
@@ -470,11 +488,7 @@ import {
470
488
  registerSourceControlCommands2,
471
489
  cleanupGitPanel2,
472
490
  registerGitPanel2
473
- } from "./chunks/studio-vt0tj32j.js";
474
- import"./chunks/studio-4r14yzyy.js";
475
- import {
476
- openMenu2
477
- } from "./chunks/studio-qa7fzxan.js";
491
+ } from "./chunks/studio-apv9x70c.js";
478
492
  import {
479
493
  parseGridTabId,
480
494
  gridTabLabel,
@@ -500,22 +514,26 @@ import {
500
514
  exportFile2,
501
515
  confirmFileDelete2,
502
516
  renamePromptMessage2
503
- } from "./chunks/studio-x10fp4tp.js";
517
+ } from "./chunks/studio-y49b6mjv.js";
518
+ import"./chunks/studio-vd67b66d.js";
519
+ import {
520
+ openMenu2
521
+ } from "./chunks/studio-mt79tn56.js";
504
522
  import {
505
523
  platformSupportsPublish,
506
524
  latestDeployment,
507
525
  currentDeploy,
508
526
  noteDeployment
509
- } from "./chunks/studio-9j6sfeba.js";
527
+ } from "./chunks/studio-xrbfdxgk.js";
510
528
  import {
511
529
  resetIgnoreCache,
512
530
  startFsSync2
513
- } from "./chunks/studio-4c041r8y.js";
531
+ } from "./chunks/studio-40z39dme.js";
514
532
  import {
515
533
  beginActivity,
516
534
  registerActivityPanel,
517
535
  showProgressModal
518
- } from "./chunks/studio-y4f9fdge.js";
536
+ } from "./chunks/studio-4998spet.js";
519
537
  import {
520
538
  _defineProperty,
521
539
  combine,
@@ -544,12 +562,15 @@ import {
544
562
  createLibrarySource,
545
563
  openLibraryTab,
546
564
  registerGridCommands
547
- } from "./chunks/studio-t3adrr56.js";
565
+ } from "./chunks/studio-9wm0xzpf.js";
548
566
  import {
549
567
  DRAFT_FIELD,
550
568
  isDraftEntry
551
- } from "./chunks/studio-5t7ena5z.js";
552
- import"./chunks/studio-wjpgxywa.js";
569
+ } from "./chunks/studio-ygvr9e4q.js";
570
+ import"./chunks/studio-sjas5b3a.js";
571
+ import {
572
+ computeWindow
573
+ } from "./chunks/studio-a5h5j33a.js";
553
574
  import {
554
575
  ensureDependenciesInstalled,
555
576
  convertTargets,
@@ -573,11 +594,11 @@ import {
573
594
  reloadCleanTab2,
574
595
  reloadFileInTab2,
575
596
  registerFilesPanel2
576
- } from "./chunks/studio-01jeck2y.js";
597
+ } from "./chunks/studio-m58xn8at.js";
577
598
  import {
578
599
  REDIRECT_STATUSES,
579
600
  referenceTarget
580
- } from "./chunks/studio-ctm7bhx5.js";
601
+ } from "./chunks/studio-w7gpcvfj.js";
581
602
  import"./chunks/studio-tjjk7jvk.js";
582
603
  import {
583
604
  FIELD_TYPES,
@@ -593,7 +614,7 @@ import {
593
614
  renderEntryMode,
594
615
  createEntry2,
595
616
  registerContentCommands2
596
- } from "./chunks/studio-68fydsrj.js";
617
+ } from "./chunks/studio-482eeazk.js";
597
618
  import {
598
619
  notifySettingsDocument2,
599
620
  onSettingsDocumentChanged2,
@@ -623,11 +644,11 @@ import {
623
644
  renderStylebookMode,
624
645
  selectStylebookTag,
625
646
  defaultCommands
626
- } from "./chunks/studio-2mdbbc76.js";
647
+ } from "./chunks/studio-bh47w0cy.js";
627
648
  import {
628
649
  openPopoverSurface,
629
650
  invalidateMediaCache2
630
- } from "./chunks/studio-ncn9e189.js";
651
+ } from "./chunks/studio-rwmmbrth.js";
631
652
  import {
632
653
  findEnclosingRepeater,
633
654
  convertToComponent,
@@ -670,11 +691,11 @@ import {
670
691
  liveElementCommands,
671
692
  dismissContextMenu,
672
693
  showContextMenu
673
- } from "./chunks/studio-hhskq2mr.js";
694
+ } from "./chunks/studio-ne593z1j.js";
674
695
  import {
675
696
  initQuickSearch2,
676
697
  openQuickSearch2
677
- } from "./chunks/studio-66arbpc2.js";
698
+ } from "./chunks/studio-jfhxy8h3.js";
678
699
  import {
679
700
  initCssData,
680
701
  registerStyleCommands,
@@ -682,19 +703,20 @@ import {
682
703
  render2,
683
704
  inspectorTab2,
684
705
  setInspectorTab2
685
- } from "./chunks/studio-15f3kxqe.js";
706
+ } from "./chunks/studio-fy320dfc.js";
686
707
  import {
687
708
  validateDoc2,
688
709
  validateProjectConfig2
689
- } from "./chunks/studio-kcgff4r0.js";
710
+ } from "./chunks/studio-fgmfksz8.js";
690
711
  import {
691
712
  getOpenAiKey,
692
713
  setOpenAiKey,
693
- hasOpenAiKey,
694
714
  getBaseUrl,
695
715
  storedModel,
696
716
  setModel,
697
717
  saveAiProvider,
718
+ aiProviderDiffers,
719
+ storedEquals,
698
720
  aiConnection,
699
721
  resetModelCache,
700
722
  cachedModels,
@@ -708,8 +730,8 @@ import {
708
730
  proxyModelsErrorMessage,
709
731
  preferredModel,
710
732
  fetchAvailableModels
711
- } from "./chunks/studio-vn5hske5.js";
712
- import"./chunks/studio-c04nzbby.js";
733
+ } from "./chunks/studio-7d3c1m59.js";
734
+ import"./chunks/studio-b9q3znz8.js";
713
735
  import {
714
736
  platformBrokersCf,
715
737
  refreshCfConnection,
@@ -719,13 +741,13 @@ import {
719
741
  revokeAccount,
720
742
  onCredentialsChanged,
721
743
  notifyCredentialsChanged
722
- } from "./chunks/studio-4xmjcrg3.js";
744
+ } from "./chunks/studio-qxye31h7.js";
723
745
  import {
724
746
  seedPublishConnected2
725
- } from "./chunks/studio-14d9tq4d.js";
747
+ } from "./chunks/studio-w475cbf9.js";
726
748
  import {
727
749
  selectContributedEntry
728
- } from "./chunks/studio-sar8g54p.js";
750
+ } from "./chunks/studio-01269reb.js";
729
751
  import {
730
752
  deepEqual2,
731
753
  matchChildren
@@ -744,13 +766,10 @@ import {
744
766
  applySavedView,
745
767
  deleteSavedView
746
768
  } from "./chunks/studio-1tbjfqv4.js";
747
- import {
748
- computeWindow
749
- } from "./chunks/studio-38emantv.js";
750
769
  import {
751
770
  entryCollections,
752
771
  collectionOfPath
753
- } from "./chunks/studio-hdc2jt3z.js";
772
+ } from "./chunks/studio-5w26wza5.js";
754
773
  import"./chunks/studio-q0a3yk4n.js";
755
774
  import {
756
775
  resolveAssetRef,
@@ -758,7 +777,7 @@ import {
758
777
  assetContextFor,
759
778
  previewAssetSrc,
760
779
  hostAssetDeclarations
761
- } from "./chunks/studio-c07s5ckd.js";
780
+ } from "./chunks/studio-kwewmhhq.js";
762
781
  import {
763
782
  rectOf,
764
783
  elementAtPoint
@@ -773,7 +792,7 @@ import {
773
792
  __preload,
774
793
  __chunks
775
794
  } from "./chunks/studio-5te5tqng.js";
776
- __chunks(import.meta.url,["gn6jycfj","3y6y0nrm","vz23wwmg","reg9eevt","h69xp9zz","42gk8ha1","bnw54cha","pykwbm92","ts9cg1a4","1vcfdbet","m244w9nh","sw6en4dw","5y78e05d","h4dab9bq","ny4zc51r","adg74qsk","5pq32t0q","vrpq1jjk","dh5np8vk","paj67nr5","p49nhqhs","j0wxk72w","f64fnesp","msaf95y6","c6zwzbre","3ntvq6cg","yxa81f45","ez11dmdh","x72j58vw","m1p9b6c3","z2smx5ce","6nzaktvy","d8z5bajd","wnsyffg1","28vs5k2y","mpzwze9x","zjjde71a","etvx9s2g","p6pmwkvh","a1pswps3","ympe0n7a","1k325pwf","en82c639","5nz00bhh","nq5yf5mp","vt0tj32j","4r14yzyy","qa7fzxan","x10fp4tp","9j6sfeba","4c041r8y","y4f9fdge","esrxgwh6","t171hxhe","t3adrr56","5t7ena5z","wjpgxywa","01jeck2y","ctm7bhx5","tjjk7jvk","68fydsrj","0hqbpbd0","vhv5tgxm","2mdbbc76","ncn9e189","hhskq2mr","66arbpc2","15f3kxqe","kcgff4r0","vn5hske5","c04nzbby","4xmjcrg3","14d9tq4d","sar8g54p","wqpngpsk","98b3z07n","1tbjfqv4","38emantv","hdc2jt3z","q0a3yk4n","c07s5ckd","rs76k67r","r6xmmtfh","5te5tqng","rnyf3f8e","h4nvnjvz","n4j7qxyr","727xh0e3","3qhbg51x","8r62fj6y","e9rp74m8","nj7tnjfn","j97t2yvy","bv5e22j0","qht48vnc","w9qsxa6q","sgg1sv6n","mf1cxgfn","fksbdg4q","g79n01v9","mdmyx6s2","d0300636","e1f24eff","tyx2kj6p","xvrfrpkb","4wbb9ct0","7e11jpa9","nqgawfks","jy4m86rv"],[["./studio.js",25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83],["./chunks/fs-events-3y6y0nrm.js",50,44,29,31,26,25],["./chunks/publish-panel-vz23wwmg.js",72,69,70,49,35,30,27,33,29,34,31,40,41,38,26,25,32],["./chunks/parse-reg9eevt.js",26,25],["./chunks/references-h69xp9zz.js",44,31,26,25],["./chunks/notify-42gk8ha1.js",32],["./chunks/workspace-bnw54cha.js",27,33,29,34,31,25,32],["./chunks/monaco-buffer-pykwbm92.js",33,32],["./chunks/platform-ts9cg1a4.js",31],["./chunks/layers-1vcfdbet.js",40,41,38,26,25,32],["./chunks/lit-html-m244w9nh.js",41],["./chunks/monaco-binding-sw6en4dw.js",84,85],["./chunks/components-5y78e05d.js",36,29,31,25],["./chunks/cf-account-picker-h4dab9bq.js",31,25,26,32,41,40,38,56,30,70,69,71],["./chunks/ws-client-ny4zc51r.js",85],["./chunks/cf-settings-adg74qsk.js",70,30,31,32],["./chunks/section-registry-5pq32t0q.js",61],["./chunks/file-ops-vrpq1jjk.js",48,50,79,46,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,47,40,41,38,26,25,32],["./chunks/iframe-host-dh5np8vk.js",37,35,36,30,80,27,33,29,52,34,31,38,26,25,32],["./chunks/grid-view-paj67nr5.js",31,34,25,29,27,41,40,38,32,26,64,33,42,43,55,50,44,49,35,30,51,28,48,46,47,37,36,52,53,76,79,80,81],["./chunks/media-picker-p49nhqhs.js",64,43,42,80,27,33,29,34,31,40,41,38,26,25,32],["./chunks/jx-validate-j0wxk72w.js",68],["./chunks/format-host-f64fnesp.js",34,31],["./chunks/files-msaf95y6.js",57,45,56,53,43,78,54,55,48,50,79,46,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,47,40,41,38,26,25,32],["./chunks/extension-sections-c6zwzbre.js",31,34,61,25,26,32,33,29,27,38,40,58,41,47,59,68,73,54,50,44,49,35,30,51,28,48,55,42,46,37,36,52,79,80],["./chunks/studio-3ntvq6cg.js"],["./chunks/studio-yxa81f45.js"],["./chunks/studio-ez11dmdh.js",32,33,31,34,25,29,87,83],["./chunks/studio-x72j58vw.js",27,29,35],["./chunks/studio-m1p9b6c3.js",25],["./chunks/studio-z2smx5ce.js",31,32],["./chunks/studio-6nzaktvy.js"],["./chunks/studio-d8z5bajd.js"],["./chunks/studio-wnsyffg1.js",32],["./chunks/studio-28vs5k2y.js",31,83],["./chunks/studio-mpzwze9x.js",32,31,34,29,27,30,26,83],["./chunks/studio-zjjde71a.js",31,29],["./chunks/studio-etvx9s2g.js",31,25,29,32,33,34,27,35,36,38,52,80,81,82],["./chunks/studio-p6pmwkvh.js",25,32,83],["./chunks/studio-a1pswps3.js",31,25,29,36,37,32,33,34,27,38,35,41,40,28,46,58,52,47,64,44,87,81,83],["./chunks/studio-ympe0n7a.js",41,38,32,26],["./chunks/studio-1k325pwf.js"],["./chunks/studio-en82c639.js",26,31,32,29,27,38,80],["./chunks/studio-5nz00bhh.js",31,38,42],["./chunks/studio-nq5yf5mp.js",31,26],["./chunks/studio-vt0tj32j.js",41,26,31,34,29,27,32,42,40,38,43,35,28,56,51,37,53,81,83],["./chunks/studio-4r14yzyy.js",31,25,29,34],["./chunks/studio-qa7fzxan.js",38,40,32,81],["./chunks/studio-x10fp4tp.js",31,25,26,44,32,34,29,27,42,38,40,47,35,49,37,50,51,79],["./chunks/studio-9j6sfeba.js",31,29,32,35,38,40],["./chunks/studio-4c041r8y.js",31,44,29],["./chunks/studio-y4f9fdge.js",41,38,49,32,35,40,28,44],["./chunks/studio-esrxgwh6.js",83],["./chunks/studio-t171hxhe.js"],["./chunks/studio-t3adrr56.js",32,31,34,29,27,40,26,50,48,55,42],["./chunks/studio-5t7ena5z.js",31,29,32,34,27,50,48,83],["./chunks/studio-wjpgxywa.js",26,32,40,38],["./chunks/studio-01jeck2y.js",41,26,27,40,38,32,29,31,34,50,44,35,30,51,28,48,36,52,45,42,54,47,77,78,83],["./chunks/studio-ctm7bhx5.js",38,27,25,32,40,47,87],["./chunks/studio-tjjk7jvk.js"],["./chunks/studio-68fydsrj.js",34,32,29,27,26,38,40,58,55,35,48,78,87,83],["./chunks/studio-0hqbpbd0.js"],["./chunks/studio-vhv5tgxm.js"],["./chunks/studio-2mdbbc76.js",29,38,40,32,27,35,36,37,83],["./chunks/studio-ncn9e189.js",31,29,40,32,42,43,80,81],["./chunks/studio-hhskq2mr.js",41,25,29,32,33,31,27,35,28,36,38,40,63,26,37,52,39,47,44,77,79,81,83],["./chunks/studio-66arbpc2.js",31,29,34,27,40,38,32,57],["./chunks/studio-15f3kxqe.js",25,29,32,27,38,39,40,58,37,47,35,44,63,87],["./chunks/studio-kcgff4r0.js",101,83],["./chunks/studio-vn5hske5.js",31,30],["./chunks/studio-c04nzbby.js",30],["./chunks/studio-4xmjcrg3.js",56,31,70,69,83],["./chunks/studio-14d9tq4d.js",38,49,31,29,32,40,70,69,83],["./chunks/studio-sar8g54p.js",26,31,32,29,40,58,68,54,37,35],["./chunks/studio-wqpngpsk.js"],["./chunks/studio-98b3z07n.js"],["./chunks/studio-1tbjfqv4.js"],["./chunks/studio-38emantv.js",81],["./chunks/studio-hdc2jt3z.js",29,27,55],["./chunks/studio-q0a3yk4n.js"],["./chunks/studio-c07s5ckd.js",31,27,38,29],["./chunks/studio-rs76k67r.js"],["./chunks/studio-r6xmmtfh.js"],["./chunks/studio-5te5tqng.js"],["./chunks/studio-rnyf3f8e.js",85],["./chunks/studio-h4nvnjvz.js"],["./chunks/index-n4j7qxyr.js",84,85,74,87],["./chunks/studio-727xh0e3.js"],["./chunks/editor-3qhbg51x.js",102],["./chunks/monaco-setup-8r62fj6y.js",102,105,59,62,101,82,83],["./chunks/quick-search-e9rp74m8.js",66,57,45,56,53,43,78,54,55,48,50,79,46,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,47,40,41,38,26,25,32],["./chunks/adapter-nj7tnjfn.js",52],["./chunks/right-panel-j97t2yvy.js",67,63,39,64,43,58,46,28,44,37,35,36,30,42,80,27,33,29,52,34,31,47,40,41,38,26,25,32,59],["./chunks/git-panel-bv5e22j0.js",45,56,53,43,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,40,41,38,26,25,32],["./chunks/menu-qht48vnc.js",47,40,41,38,26,25,32],["./chunks/entry-commands-w9qsxa6q.js",60,78,58,55,48,50,79,46,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,47,40,41,38,26,25,32,59],["./chunks/content-source-sgg1sv6n.js",55,48,50,79,46,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,47,40,41,38,26,25,32],["./chunks/convert-to-repeater-mf1cxgfn.js",25,29,32,33,31,34,27,41,40,38,26,37,39,58,59,28,30,35,36,52,46,47,64,42,43,44,80],["./chunks/layers-panel-fksbdg4q.js",65,63,39,64,43,58,79,46,28,44,37,35,36,30,42,80,27,33,29,52,34,31,47,40,41,38,26,25,32,59],["./chunks/2020-g79n01v9.js",106,83],["./chunks/index-mdmyx6s2.js",106,83],["./chunks/studio-d0300636.js"],["./chunks/studio-e1f24eff.js"],["./chunks/jsonMode-tyx2kj6p.js",102,107,105],["./chunks/javascript-xvrfrpkb.js",102],["./chunks/studio-4wbb9ct0.js",102,83],["./chunks/studio-7e11jpa9.js",83],["./chunks/studio-nqgawfks.js",102,105],["./chunks/tsMode-jy4m86rv.js",102,107,105]],0);
795
+ __chunks(import.meta.url,["eq7zkzt2","z2gg8qjt","ak6hxfma","reg9eevt","f7bdv3z5","42gk8ha1","b9k2frks","jaq1t9zj","fnchmhy7","s6mc01ry","m244w9nh","e8d9wzyk","xm826zjw","3f08rry6","erze4czt","m79r7p5y","5pq32t0q","3xan8mnj","psetcv20","qaqer9kr","nhn6rv4w","epp2z3s3","mym8qnd0","egsb0ry8","r73an5s6","3ntvq6cg","yxa81f45","dgdm6vvt","fqpkpq1t","m1p9b6c3","yxmmjy2a","6pwn9j8c","d8z5bajd","6vg0x5fz","9vsvz7w7","mfkh1q8d","gjj4bbfx","mrh0a92f","3nprwbd9","rav5bpmp","ag3z6shb","1k325pwf","sceg8ws0","dwdcr2e8","557bmn0x","apv9x70c","y49b6mjv","vd67b66d","mt79tn56","xrbfdxgk","40z39dme","4998spet","esrxgwh6","t171hxhe","9wm0xzpf","ygvr9e4q","sjas5b3a","a5h5j33a","m58xn8at","w7gpcvfj","tjjk7jvk","482eeazk","0hqbpbd0","vhv5tgxm","bh47w0cy","rwmmbrth","ne593z1j","jfhxy8h3","fy320dfc","fgmfksz8","7d3c1m59","b9q3znz8","qxye31h7","w475cbf9","01269reb","wqpngpsk","98b3z07n","1tbjfqv4","5w26wza5","q0a3yk4n","kwewmhhq","rs76k67r","r6xmmtfh","5te5tqng","0a4gncx0","ptfcxkz5","9yjae3hz","aj9drheq","ddy019sn","gsjdysze","c63mkt3b","nj7tnjfn","t5n8wttg","a05atk6q","8j4qxjrp","64nb2a9a","jt7rbcbc","vg38bq99","83a16ytx","g79n01v9","mdmyx6s2","ks2zzth2","0d4p4wxj","zs88v0wr","hr5sg57n","1tejxvba","7e11jpa9","fbcyz234","ay4c78ne"],[["./studio.js",25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83],["./chunks/fs-events-z2gg8qjt.js",50,44,29,31,26,25],["./chunks/publish-panel-ak6hxfma.js",73,70,71,49,35,30,27,33,29,34,31,40,41,38,26,25,32],["./chunks/parse-reg9eevt.js",26,25],["./chunks/references-f7bdv3z5.js",44,31,26,25],["./chunks/notify-42gk8ha1.js",32],["./chunks/workspace-b9k2frks.js",27,33,29,34,31,25,32],["./chunks/monaco-buffer-jaq1t9zj.js",33,32],["./chunks/platform-fnchmhy7.js",31],["./chunks/layers-s6mc01ry.js",40,41,38,26,25,32],["./chunks/lit-html-m244w9nh.js",41],["./chunks/monaco-binding-e8d9wzyk.js",84,85],["./chunks/components-xm826zjw.js",36,29,31,25],["./chunks/cf-account-picker-3f08rry6.js",31,25,26,32,41,40,38,56,30,71,70,72],["./chunks/ws-client-erze4czt.js",85],["./chunks/cf-settings-m79r7p5y.js",71,30,31,32],["./chunks/section-registry-5pq32t0q.js",62],["./chunks/file-ops-3xan8mnj.js",46,50,79,47,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,48,40,41,38,26,25,32],["./chunks/iframe-host-psetcv20.js",37,35,36,30,80,27,33,29,52,34,31,38,26,25,32],["./chunks/grid-view-qaqer9kr.js",31,34,25,29,27,41,40,38,32,26,65,33,42,43,55,50,44,49,35,30,51,28,46,47,48,37,36,52,53,77,79,80,81],["./chunks/media-picker-nhn6rv4w.js",65,43,42,80,27,33,29,34,31,40,41,38,26,25,32],["./chunks/jx-validate-epp2z3s3.js",69],["./chunks/format-host-mym8qnd0.js",34,31],["./chunks/files-egsb0ry8.js",58,57,45,56,53,43,78,54,55,46,50,79,47,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,48,40,41,38,26,25,32],["./chunks/extension-sections-r73an5s6.js",31,34,62,25,26,32,33,27,29,38,40,59,41,48,60,69,74,54,50,44,49,35,30,51,28,46,55,42,47,37,36,52,79,80],["./chunks/studio-3ntvq6cg.js"],["./chunks/studio-yxa81f45.js"],["./chunks/studio-dgdm6vvt.js",32,33,31,34,25,29,87,83],["./chunks/studio-fqpkpq1t.js",27,29,35],["./chunks/studio-m1p9b6c3.js",25],["./chunks/studio-yxmmjy2a.js",31,32],["./chunks/studio-6pwn9j8c.js"],["./chunks/studio-d8z5bajd.js"],["./chunks/studio-6vg0x5fz.js",32],["./chunks/studio-9vsvz7w7.js",31,83],["./chunks/studio-mfkh1q8d.js",32,27,31,34,29,30,26,83],["./chunks/studio-gjj4bbfx.js",31,29],["./chunks/studio-mrh0a92f.js",31,25,29,32,33,27,34,35,36,38,52,80,81,82],["./chunks/studio-3nprwbd9.js",25,32,83],["./chunks/studio-rav5bpmp.js",31,25,29,36,37,32,33,27,34,38,35,41,40,28,47,59,52,48,65,44,87,81,83],["./chunks/studio-ag3z6shb.js",41,38,32,26],["./chunks/studio-1k325pwf.js"],["./chunks/studio-sceg8ws0.js",26,31,32,27,29,38,80],["./chunks/studio-dwdcr2e8.js",31,38,42],["./chunks/studio-557bmn0x.js",31,26],["./chunks/studio-apv9x70c.js",41,26,31,34,29,27,32,42,40,38,43,35,28,56,51,37,53,81,83],["./chunks/studio-y49b6mjv.js",31,25,26,44,32,27,34,29,42,38,40,48,35,49,37,50,51,79],["./chunks/studio-vd67b66d.js",31,25,29,34],["./chunks/studio-mt79tn56.js",38,40,32,81],["./chunks/studio-xrbfdxgk.js",31,29,32,35,38,40],["./chunks/studio-40z39dme.js",31,44,29],["./chunks/studio-4998spet.js",41,38,49,32,35,40,28,44],["./chunks/studio-esrxgwh6.js",83],["./chunks/studio-t171hxhe.js"],["./chunks/studio-9wm0xzpf.js",32,27,31,34,29,40,26,50,46,55,42],["./chunks/studio-ygvr9e4q.js",31,29,32,27,34,50,46,83],["./chunks/studio-sjas5b3a.js",26,32,40,38],["./chunks/studio-a5h5j33a.js",81],["./chunks/studio-m58xn8at.js",41,26,27,40,38,32,29,31,34,50,44,35,30,51,28,46,36,52,45,42,54,48,57,78,83],["./chunks/studio-w7gpcvfj.js",38,27,25,32,40,48,87],["./chunks/studio-tjjk7jvk.js"],["./chunks/studio-482eeazk.js",34,32,27,29,26,38,40,59,55,35,46,78,87,83],["./chunks/studio-0hqbpbd0.js"],["./chunks/studio-vhv5tgxm.js"],["./chunks/studio-bh47w0cy.js",29,38,40,32,27,35,36,37,83],["./chunks/studio-rwmmbrth.js",31,29,40,32,42,43,80,81],["./chunks/studio-ne593z1j.js",41,25,29,32,33,27,31,35,28,36,38,40,64,26,37,52,39,48,44,57,79,81,83],["./chunks/studio-jfhxy8h3.js",31,29,34,27,40,38,32,58],["./chunks/studio-fy320dfc.js",25,29,32,27,38,39,40,59,37,48,35,44,64,87],["./chunks/studio-fgmfksz8.js",101,83],["./chunks/studio-7d3c1m59.js",31,30],["./chunks/studio-b9q3znz8.js",30],["./chunks/studio-qxye31h7.js",56,31,71,70,83],["./chunks/studio-w475cbf9.js",38,49,31,29,32,40,71,70,83],["./chunks/studio-01269reb.js",26,31,32,29,40,59,69,54,37,35],["./chunks/studio-wqpngpsk.js"],["./chunks/studio-98b3z07n.js"],["./chunks/studio-1tbjfqv4.js"],["./chunks/studio-5w26wza5.js",29,27,55],["./chunks/studio-q0a3yk4n.js"],["./chunks/studio-kwewmhhq.js",31,27,38,29],["./chunks/studio-rs76k67r.js"],["./chunks/studio-r6xmmtfh.js"],["./chunks/studio-5te5tqng.js"],["./chunks/studio-0a4gncx0.js",85],["./chunks/studio-ptfcxkz5.js"],["./chunks/index-9yjae3hz.js",84,85,75,87],["./chunks/studio-aj9drheq.js"],["./chunks/editor-ddy019sn.js",102],["./chunks/monaco-setup-gsjdysze.js",102,105,60,63,101,82,83],["./chunks/quick-search-c63mkt3b.js",67,58,57,45,56,53,43,78,54,55,46,50,79,47,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,48,40,41,38,26,25,32],["./chunks/adapter-nj7tnjfn.js",52],["./chunks/right-panel-t5n8wttg.js",68,64,39,65,43,59,47,28,44,37,35,36,30,42,80,27,33,29,52,34,31,48,40,41,38,26,25,32,60],["./chunks/git-panel-a05atk6q.js",45,56,53,43,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,40,41,38,26,25,32],["./chunks/menu-8j4qxjrp.js",48,40,41,38,26,25,32],["./chunks/entry-commands-64nb2a9a.js",61,78,59,55,46,50,79,47,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,48,40,41,38,26,25,32,60],["./chunks/content-source-jt7rbcbc.js",55,46,50,79,47,51,49,28,44,37,35,36,30,42,80,27,33,29,52,34,31,48,40,41,38,26,25,32],["./chunks/convert-to-repeater-vg38bq99.js",25,29,32,33,27,31,34,41,40,38,26,37,39,59,60,28,30,35,36,52,47,48,65,42,43,44,80],["./chunks/layers-panel-83a16ytx.js",66,64,39,65,57,43,59,79,47,28,44,37,35,36,30,42,80,27,33,29,52,34,31,48,40,41,38,26,25,32,60],["./chunks/2020-g79n01v9.js",106,83],["./chunks/index-mdmyx6s2.js",106,83],["./chunks/studio-ks2zzth2.js"],["./chunks/studio-0d4p4wxj.js"],["./chunks/jsonMode-zs88v0wr.js",102,107,105],["./chunks/javascript-hr5sg57n.js",102],["./chunks/studio-1tejxvba.js",102,83],["./chunks/studio-7e11jpa9.js",83],["./chunks/studio-fbcyz234.js",102,105],["./chunks/tsMode-ay4c78ne.js",102,107,105]],0);
777
796
 
778
797
  // ../../node_modules/ms/index.js
779
798
  var require_ms = __commonJS(function(exports, module) {
@@ -1307,6 +1326,41 @@ var require_extend = __commonJS(function(exports, module) {
1307
1326
  };
1308
1327
  });
1309
1328
 
1329
+ // src/services/module-preloads.ts
1330
+ var QUEUED = [];
1331
+ var frame = null;
1332
+ var originalAppend = null;
1333
+ function flush() {
1334
+ frame = null;
1335
+ const singleOrigin = originalAppend;
1336
+ if (!singleOrigin) {
1337
+ return;
1338
+ }
1339
+ for (const link of QUEUED.splice(0)) {
1340
+ singleOrigin.call(document.head, link);
1341
+ }
1342
+ }
1343
+ function appendChildPatched(node) {
1344
+ if (node instanceof HTMLLinkElement && node.rel === "modulepreload") {
1345
+ QUEUED.push(node);
1346
+ if (frame === null) {
1347
+ frame = requestAnimationFrame(flush);
1348
+ }
1349
+ return node;
1350
+ }
1351
+ return originalAppend ? originalAppend(node) : node;
1352
+ }
1353
+ function installModulePreloadBatcher() {
1354
+ const { head } = document;
1355
+ if (!head || head.__modulePreloadsBatched) {
1356
+ return;
1357
+ }
1358
+ const patched = head;
1359
+ originalAppend = Node.prototype.appendChild.bind(head);
1360
+ patched.__modulePreloadsBatched = true;
1361
+ head.appendChild = appendChildPatched;
1362
+ }
1363
+
1310
1364
  // src/canvas/diff-view.ts
1311
1365
  var _records = new Map;
1312
1366
  function recordFor(paneId) {
@@ -1349,6 +1403,9 @@ function stepDiff(paneId, delta) {
1349
1403
  function diffViewOf(paneId) {
1350
1404
  return _records.get(paneId)?.view ?? "visual";
1351
1405
  }
1406
+ function diffShowsCode(paneId, renderable) {
1407
+ return !renderable || diffViewOf(paneId) === "code";
1408
+ }
1352
1409
  function setDiffView(paneId, view) {
1353
1410
  recordFor(paneId).view = view;
1354
1411
  }
@@ -1693,9 +1750,14 @@ var _repaint = () => {};
1693
1750
  function setDiffRepaint(repaint) {
1694
1751
  _repaint = repaint;
1695
1752
  }
1753
+ var _chromeRepaint = null;
1754
+ function setDiffChromeRepaint(repaint) {
1755
+ _chromeRepaint = repaint;
1756
+ }
1696
1757
  function repaintDiffStage(paneId) {
1697
1758
  surfaceForPane(paneId).prevCanvasMode = null;
1698
1759
  _repaint(paneId);
1760
+ _chromeRepaint?.();
1699
1761
  }
1700
1762
  function renderDiffToolbar(paneId) {
1701
1763
  const bar = _bars.get(paneId);
@@ -1743,7 +1805,7 @@ async function stepDiffAndReveal(paneId, delta) {
1743
1805
  if (!step) {
1744
1806
  return;
1745
1807
  }
1746
- const { hostForCanvas, measureInCanvas, revealCanvasPathIn } = await (__preload("dh5np8vk"), import("./chunks/iframe-host-dh5np8vk.js"));
1808
+ const { hostForCanvas, measureInCanvas, revealCanvasPathIn } = await (__preload("psetcv20"), import("./chunks/iframe-host-psetcv20.js"));
1747
1809
  const measured = await Promise.all([
1748
1810
  step.originalPath && original ? measureInCanvas(original, step.originalPath) : null,
1749
1811
  step.currentPath && current ? measureInCanvas(current, step.currentPath) : null
@@ -1961,6 +2023,37 @@ function buildChangeMap(a, b, opts = {}) {
1961
2023
  };
1962
2024
  }
1963
2025
 
2026
+ // src/canvas/stage-scrollbar.ts
2027
+ var STAGE_SCROLLBAR_VAR = "--stage-scrollbar-w";
2028
+ function scrollbarWidthOf(scroller) {
2029
+ return Math.max(0, scroller.offsetWidth - scroller.clientWidth);
2030
+ }
2031
+ function cellOf(surface) {
2032
+ return surface.wrap?.closest(PANE_SELECTOR) ?? document.documentElement;
2033
+ }
2034
+ function trackStageScrollbar(surface, scroller) {
2035
+ if (surface.stageScrollbar?.scroller === scroller) {
2036
+ return;
2037
+ }
2038
+ releaseStageScrollbar(surface);
2039
+ if (!scroller) {
2040
+ return;
2041
+ }
2042
+ const cell = cellOf(surface);
2043
+ const write = () => {
2044
+ cell.style.setProperty(STAGE_SCROLLBAR_VAR, `${scrollbarWidthOf(scroller)}px`);
2045
+ };
2046
+ const observer = new ResizeObserver(write);
2047
+ observer.observe(scroller);
2048
+ surface.stageScrollbar = { observer, scroller };
2049
+ write();
2050
+ }
2051
+ function releaseStageScrollbar(surface) {
2052
+ surface.stageScrollbar?.observer.disconnect();
2053
+ surface.stageScrollbar = null;
2054
+ cellOf(surface).style.setProperty(STAGE_SCROLLBAR_VAR, "0px");
2055
+ }
2056
+
1964
2057
  // src/collab/monaco-cursors.ts
1965
2058
  function cssString(value) {
1966
2059
  return `"${value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`)}"`;
@@ -2011,7 +2104,7 @@ function loadGridEngine() {
2011
2104
  if (_engine) {
2012
2105
  return Promise.resolve(_engine);
2013
2106
  }
2014
- _loading ??= (__preload("paj67nr5"), import("./chunks/grid-view-paj67nr5.js")).then((engine) => {
2107
+ _loading ??= (__preload("qaqer9kr"), import("./chunks/grid-view-qaqer9kr.js")).then((engine) => {
2015
2108
  _engine = engine;
2016
2109
  return engine;
2017
2110
  });
@@ -2030,7 +2123,7 @@ var grid_panel_default = {
2030
2123
  part: "grid"
2031
2124
  },
2032
2125
  style: {
2033
- $description: "The frame's own layout and the engine's theme, and nothing that was a class. The toolbar is a single row that never wraps: the chrome budget (§12.2) caps what may stand in it, and the count is the only thing pinned to the far end.",
2126
+ $description: "The frame's own layout and the engine's theme, and nothing that was a class. The toolbar is a single row that never wraps: the chrome budget (studio-ui-guidelines.md §12.2) caps what may stand in it, and the count is the only thing pinned to the far end.",
2034
2127
  display: "flex",
2035
2128
  flexDirection: "column",
2036
2129
  height: "100%",
@@ -2482,7 +2575,7 @@ var grid_panel_default = {
2482
2575
  textContent: "${state.viewLabel}"
2483
2576
  }
2484
2577
  ],
2485
- $description: "One control for the four facets a saved view is made of, because they are only ever adjusted together (§12.2). Its label is the applied view's name, so the toolbar says what is on screen before the panel is opened."
2578
+ $description: "One control for the four facets a saved view is made of, because they are only ever adjusted together (studio-ui-guidelines.md §12.2). Its label is the applied view's name, so the toolbar says what is on screen before the panel is opened."
2486
2579
  },
2487
2580
  {
2488
2581
  tagName: "jx-textfield",
@@ -2818,7 +2911,7 @@ function mountGridPanelSurface(host, view, actions) {
2818
2911
  var grid_views_default = {
2819
2912
  $schema: "https://jxsuite.com/schema/v1",
2820
2913
  $id: "StudioGridViews",
2821
- $description: "The grid's View panel: the saved views, and the four facets one of them is made of — columns, sort, grouping, and the filter the toolbar box holds. ONE control rather than four, because the chrome budget caps what may stand in a toolbar (§12.2) and these four are only ever adjusted together. There is no Apply button: every edit writes the working layout and applies immediately, so the panel is never showing something the grid is not already doing.",
2914
+ $description: "The grid's View panel: the saved views, and the four facets one of them is made of — columns, sort, grouping, and the filter the toolbar box holds. ONE control rather than four, because the chrome budget caps what may stand in a toolbar (studio-ui-guidelines.md §12.2) and these four are only ever adjusted together. There is no Apply button: every edit writes the working layout and applies immediately, so the panel is never showing something the grid is not already doing.",
2822
2915
  tagName: "jx-popover",
2823
2916
  attributes: {
2824
2917
  part: "views-panel"
@@ -6985,10 +7078,10 @@ var add_repo_default = {
6985
7078
  color: "var(--danger)"
6986
7079
  },
6987
7080
  '& [part="access"]': {
6988
- $description: "The listing is bounded by what the Jx Suite GitHub App was granted, so the dialog always offers a way out of that boundary rather than only saying the list is short.",
7081
+ $description: "The listing is bounded by what the Jx Suite GitHub App was granted, so the dialog always offers a way out of that boundary rather than only saying the list is short. `center`, not `baseline`: the `Refresh` beside the note is a small kit button, and a small kit control is layout-contained so its outset hit area adds nothing to a scrolling ancestor's overflow, which leaves it with no baseline to export; a baseline row would synthesise one from its bottom edge and drop the note and the links onto it. surface-baseline-rows.test.ts holds the rule for every surface.",
6989
7082
  display: "flex",
6990
7083
  flexWrap: "wrap",
6991
- alignItems: "baseline",
7084
+ alignItems: "center",
6992
7085
  gap: "var(--jx-space-2)",
6993
7086
  paddingTop: "var(--jx-space-3)",
6994
7087
  borderTop: "1px solid var(--border)",
@@ -7642,10 +7735,15 @@ var welcome_default = {
7642
7735
  letterSpacing: "0.3px"
7643
7736
  },
7644
7737
  '& [part="section-header"]': {
7738
+ $description: "`center`, not `baseline`: `Clear all` is a small kit button, and a small kit control is layout-contained so its outset hit area adds nothing to a scrolling ancestor's overflow, which leaves it with no baseline to export. A baseline row synthesised one from the button's bottom edge, which stood the header 33px tall with the title 10px under the button's label. The gap under the header is the HEADER's, because centring aligns MARGIN boxes: while the h2 kept its own 8px block-end margin its margin box was 26px, exactly the header's height, so `center` had nothing to move and left the h2 flush at the top with the button centred 3.6px below its baseline — a fix that measured right and still looked wrong. With the margin moved out, the two boxes are the only things in the line and they centre on each other.",
7645
7739
  display: "flex",
7646
- alignItems: "baseline",
7740
+ alignItems: "center",
7647
7741
  justifyContent: "space-between",
7648
- gap: "var(--jx-space-4)"
7742
+ gap: "var(--jx-space-4)",
7743
+ marginBottom: "var(--jx-space-3)"
7744
+ },
7745
+ '& [part="section-header"] [part="section-title"]': {
7746
+ marginBlockEnd: "0"
7649
7747
  },
7650
7748
  '& [part="actions"]': {
7651
7749
  display: "flex",
@@ -8824,7 +8922,7 @@ var tab_strip_default = {
8824
8922
  lineHeight: "1"
8825
8923
  },
8826
8924
  '& [part="pill"]': {
8827
- $description: '`.entry-pill` — the draft pill (§7.6), in `jx-tab`\'s status slot. It is ON THE CHIP and not only inside the entry editor because the mistake it prevents, publishing something you believed was private, is made while glancing at a row of tabs. "Published" is deliberately as visible as "Draft": a state you can only see one half of is a state you cannot check. The two Spectrum colours it used to name are `--jx-signal` against `--jx-bg`, which flips with the theme where a fixed yellow and a fixed black did not.',
8925
+ $description: '`.entry-pill` — the draft pill (site-architecture.md §7.6), in `jx-tab`\'s status slot. It is ON THE CHIP and not only inside the entry editor because the mistake it prevents, publishing something you believed was private, is made while glancing at a row of tabs. "Published" is deliberately as visible as "Draft": a state you can only see one half of is a state you cannot check. The two Spectrum colours it used to name are `--jx-signal` against `--jx-bg`, which flips with the theme where a fixed yellow and a fixed black did not.',
8828
8926
  padding: "0 5px",
8829
8927
  borderRadius: "var(--radius)",
8830
8928
  background: "var(--bg)",
@@ -9057,7 +9155,7 @@ var tab_strip_default = {
9057
9155
  textContent: "${$map.item.pill}"
9058
9156
  },
9059
9157
  {
9060
- $description: "The pin, in the actions slot. `title` is the whole accessible name (§10): the glyph is hidden, so a matching `aria-label` would announce it twice. `tabindex` follows the tab's own selectedness, exactly as the close button's does — the strip is ONE stop in the tab order, and a control inside every chip that took the platform's default would make fifteen.",
9158
+ $description: "The pin, in the actions slot. `title` is the whole accessible name (studio-ui-guidelines.md §10): the glyph is hidden, so a matching `aria-label` would announce it twice. `tabindex` follows the tab's own selectedness, exactly as the close button's does — the strip is ONE stop in the tab order, and a control inside every chip that took the platform's default would make fifteen.",
9061
9159
  tagName: "button",
9062
9160
  attributes: {
9063
9161
  slot: "actions",
@@ -10217,7 +10315,7 @@ var panel_page_default = {
10217
10315
  padding: "0 var(--jx-space-2)"
10218
10316
  },
10219
10317
  '& [part="row"]': {
10220
- $description: "§4.1's standard form row, stacked: the label sits above a control that takes the whole width. `min-width: 0` is what keeps it honest in a 240px dock (§4.6) — a field refuses to shrink below its own min-content without it.",
10318
+ $description: "studio-ui-guidelines.md §4.1's standard form row, stacked: the label sits above a control that takes the whole width. `min-width: 0` is what keeps it honest in a 240px dock (studio-ui-guidelines.md §4.6) — a field refuses to shrink below its own min-content without it.",
10221
10319
  display: "flex",
10222
10320
  flexDirection: "column",
10223
10321
  alignItems: "stretch",
@@ -10233,7 +10331,7 @@ var panel_page_default = {
10233
10331
  color: "var(--fg-dim)"
10234
10332
  },
10235
10333
  '& [part="chip"]': {
10236
- $description: "§4.2's set dot: a 6px accent dot, drawn only where the value is set on this document, that clears it when pressed. A real `button` — it is pressable, and it is the row's only keyboard route to clearing — stripped of the chrome a button brings so the row still reads as a label with a dot beside it.",
10334
+ $description: "studio-ui-guidelines.md §4.2's set dot: a 6px accent dot, drawn only where the value is set on this document, that clears it when pressed. A real `button` — it is pressable, and it is the row's only keyboard route to clearing — stripped of the chrome a button brings so the row still reads as a label with a dot beside it.",
10237
10335
  width: "6px",
10238
10336
  height: "6px",
10239
10337
  padding: "0",
@@ -11901,7 +11999,7 @@ function removeEntry(key) {
11901
11999
  }
11902
12000
  var _mediaPicker = null;
11903
12001
  function mediaPicker() {
11904
- _mediaPicker ??= (__preload("p49nhqhs"), import("./chunks/media-picker-p49nhqhs.js"));
12002
+ _mediaPicker ??= (__preload("nhn6rv4w"), import("./chunks/media-picker-nhn6rv4w.js"));
11905
12003
  return _mediaPicker;
11906
12004
  }
11907
12005
  var ACTIONS = {
@@ -12004,28 +12102,21 @@ function registerPagePanel() {
12004
12102
  var doc_header_default = {
12005
12103
  $schema: "https://jxsuite.com/schema/v1",
12006
12104
  $id: "StudioDocHeader",
12007
- $description: "The Document Header card (§3.2 ⑧) — the artefact's own header, drawn IN the stage, once per pane. A bar naming the collection and the page's route, a column of rows, the door to Search appearance and a disclosure listing the raw head tags. ONE row vocabulary: Title, the Layout picker and every schema-driven frontmatter field are the same `row`, discriminated by `kind`, because they differ in what they COMMIT rather than in what they look like — and what they commit is `panels/frontmatter-panel.ts`'s decision, reached by the row's `key`. Nothing here knows what a content collection is, which document it is drawing, or that a page has a route: the flow hands over rows whose every field is already a string and this draws them.",
12105
+ $description: "The Document Header card (`docs/studio/editing/frontmatter.md`) — the artefact's own header, drawn IN the stage, docked above Edit's page, once per pane. A bar naming the collection and the page's route, a column of rows, the door to Search appearance and a disclosure listing the raw head tags. ONE row vocabulary: Title, the Layout picker and every schema-driven frontmatter field are the same `row`, discriminated by `kind`, because they differ in what they COMMIT rather than in what they look like — and what they commit is `panels/frontmatter-panel.ts`'s decision, reached by the row's `key`. Nothing here knows what a content collection is, which document it is drawing, or that a page has a route: the flow hands over rows whose every field is already a string and this draws them.",
12008
12106
  tagName: "section",
12009
12107
  attributes: {
12010
12108
  part: "card",
12011
12109
  "aria-label": "Document header",
12012
- "data-jx-region": "${state.region}",
12013
- "data-placement": "${state.placement}"
12110
+ "data-jx-region": "${state.region}"
12014
12111
  },
12015
12112
  style: {
12016
- $description: "The twelve `.doc-header*` rules from `styles/canvas.css` plus the row vocabulary the card used to borrow from `styles/inspector.css` (`.style-row`, `.style-row-label`, `.set-dot`) and `styles/overlays.css` (`.media-picker`, `.media-picker-thumb`). The borrowed ones are TRANSCRIBED rather than shared: the card is a document now and a class it does not emit cannot style it, and the three lit panels that still draw a `.style-row` go on owning those rules until they convert. Every `--spectrum-font-size-*` reference is re-pointed at the kit's scale — a document mounted in a pane's stage is outside `<sp-theme>`, so each of them would silently have taken its hard-coded fallback and stopped following the theme. And the PLACEMENT is an attribute rather than an ancestor class: `.doc-header-host.pinned .doc-header` reached in from the stage's own host, which a scoped style block cannot answer, so the stage's two placements arrive as `data-placement` and the card keys on itself.",
12113
+ $description: "The twelve `.doc-header*` rules from `styles/canvas.css` plus the row vocabulary the card used to borrow from `styles/inspector.css` (`.style-row`, `.style-row-label`, `.set-dot`) and `styles/overlays.css` (`.media-picker`, `.media-picker-thumb`). The borrowed ones are TRANSCRIBED rather than shared: the card is a document now and a class it does not emit cannot style it, and the three lit panels that still draw a `.style-row` go on owning those rules until they convert. Every `--spectrum-font-size-*` reference is re-pointed at the kit's scale — a document mounted in a pane's stage is outside `<sp-theme>`, so each of them would silently have taken its hard-coded fallback and stopped following the theme. And the card has ONE shape, because it has one place: Edit docks it above the page's scroller as a band across the pane, so it is a band rather than a box. It keeps only the bottom edge that separates it from the page, because a box's border and radius would draw a card edge across a full-width strip. It used to have two placements, arriving as a `data-placement` the card keyed on, and the band look was the Design one; the attribute was read before the runtime had written it, so the band never drew.",
12017
12114
  display: "flex",
12018
12115
  flexDirection: "column",
12019
12116
  padding: "8px 12px 10px",
12020
12117
  background: "var(--bg-panel)",
12021
- border: "1px solid var(--border)",
12022
- borderRadius: "var(--radius)",
12118
+ borderBottom: "1px solid var(--border)",
12023
12119
  fontFamily: "var(--jx-font-sans)",
12024
- '&[data-placement="pinned"]': {
12025
- $description: "Design draws the artboards under a pan/zoom transform, so the card is pinned above the surface at 1:1 rather than sitting in the document's own column. Pinned it is a band, not a box: it spans the stage, so the box's border and radius would draw a card edge across a full-width strip.",
12026
- borderWidth: "0 0 1px",
12027
- borderRadius: "0"
12028
- },
12029
12120
  '& [part="bar"]': {
12030
12121
  display: "flex",
12031
12122
  alignItems: "baseline",
@@ -12051,7 +12142,7 @@ var doc_header_default = {
12051
12142
  gap: "2px"
12052
12143
  },
12053
12144
  '& [part="row"]': {
12054
- $description: "§4.1's standard form row, stacked: the label sits above a control that takes the whole width. `min-width: 0` is what keeps it honest at a narrow stage (§4.6) — a field refuses to shrink below its own min-content without it, and the card is drawn inside a pane that can be half a window wide.",
12145
+ $description: "studio-ui-guidelines.md §4.1's standard form row, stacked: the label sits above a control that takes the whole width. `min-width: 0` is what keeps it honest at a narrow stage (studio-ui-guidelines.md §4.6) — a field refuses to shrink below its own min-content without it, and the card is drawn inside a pane that can be half a window wide.",
12055
12146
  display: "flex",
12056
12147
  flexDirection: "column",
12057
12148
  alignItems: "stretch",
@@ -12067,7 +12158,7 @@ var doc_header_default = {
12067
12158
  color: "var(--fg-dim)"
12068
12159
  },
12069
12160
  '& [part="chip"]': {
12070
- $description: "§4.2's set dot: a 6px accent dot, drawn only where the value is set here, that clears the value when it is pressed. It is a real `button` — it is pressable, and it is the row's only keyboard route to clearing — stripped of the chrome a button brings with it so the row still reads as a label with a dot beside it.",
12161
+ $description: "studio-ui-guidelines.md §4.2's set dot: a 6px accent dot, drawn only where the value is set here, that clears the value when it is pressed. It is a real `button` — it is pressable, and it is the row's only keyboard route to clearing — stripped of the chrome a button brings with it so the row still reads as a label with a dot beside it.",
12071
12162
  width: "6px",
12072
12163
  height: "6px",
12073
12164
  padding: "0",
@@ -12745,7 +12836,6 @@ var doc_header_default = {
12745
12836
  registerSurface("doc-header", doc_header_default);
12746
12837
  function project9(scope, view) {
12747
12838
  scope.region = view.region;
12748
- scope.placement = view.placement;
12749
12839
  scope.collection = view.collection;
12750
12840
  scope.route = view.route;
12751
12841
  scope.hasRoute = view.hasRoute;
@@ -12759,7 +12849,6 @@ function mountDocHeaderSurface(host, view, actions) {
12759
12849
  ...actions,
12760
12850
  collection: "Document",
12761
12851
  hasRoute: false,
12762
- placement: "in-column",
12763
12852
  rawEntries: [],
12764
12853
  rawOpen: false,
12765
12854
  rawState: "empty",
@@ -12914,7 +13003,6 @@ function viewFor(tab, paneId, card) {
12914
13003
  return {
12915
13004
  collection: collection ? collection.name : "Document",
12916
13005
  hasRoute: route !== null,
12917
- placement: card.el.dataset["placement"] === "pinned" ? "pinned" : "in-column",
12918
13006
  rawEntries: raw,
12919
13007
  rawOpen: _rawOpen.has(tab.id),
12920
13008
  rawState: raw.length === 0 ? "empty" : "list",
@@ -13090,7 +13178,7 @@ function makeActions(card) {
13090
13178
  }
13091
13179
  var _mediaPicker2 = null;
13092
13180
  function mediaPicker2() {
13093
- _mediaPicker2 ??= (__preload("p49nhqhs"), import("./chunks/media-picker-p49nhqhs.js"));
13181
+ _mediaPicker2 ??= (__preload("nhn6rv4w"), import("./chunks/media-picker-nhn6rv4w.js"));
13094
13182
  return _mediaPicker2;
13095
13183
  }
13096
13184
  // src/surfaces/canvas-stage.json
@@ -13103,7 +13191,7 @@ var canvas_stage_default = {
13103
13191
  part: "stage"
13104
13192
  },
13105
13193
  style: {
13106
- $description: "`styles/canvas.css`'s `.panzoom-wrap`, `.canvas-panel*`, `.source-*`, `.doc-header-host`, `.content-edit-*`, `.edit-width-handle`, `.preview-stage` and `.diff-code-*` rules, keyed on `part`. Two selectors reach FOREIGN DOM and say so where they stand: `.doc-header` is the card's own root inside its host, and `.is-resizing` is written by `canvas/edit-width-drag.ts` onto the column this document draws — the same shape `grid-panel.json` uses for Tabulator's theme. The width handles are `jx-split` elements, which paint their own hover, drag and focus states; this sheet says only where they stand and how wide they are.",
13194
+ $description: "`styles/canvas.css`'s `.panzoom-wrap`, `.canvas-panel*`, `.source-*`, `.content-edit-*`, `.edit-width-handle`, `.preview-stage` and `.diff-code-*` rules, keyed on `part`, plus the Document Header card's host. One selector reaches FOREIGN DOM and says so where it stands: `.is-resizing` is written by `canvas/edit-width-drag.ts` onto the column this document draws — the same shape `grid-panel.json` uses for Tabulator's theme. The width handles are `jx-split` elements, which paint their own hover, drag and focus states; this sheet says only where they stand and how wide they are.",
13107
13195
  display: "contents",
13108
13196
  '& [part="panzoom"]': {
13109
13197
  $description: "The transformed surface. `applyTransform` writes `transform` on it imperatively, so only the origin is declared here. `--boards-inset` is the Project Styles bar's clearance: that bar floats over the stage, so the boards start below it.",
@@ -13193,21 +13281,12 @@ var canvas_stage_default = {
13193
13281
  left: "50%",
13194
13282
  transform: "translateX(-50%)"
13195
13283
  },
13196
- '& [part="doc-header"][data-placement="in-column"]': {
13197
- $description: "Edit: the card is a block of the artefact's own column and scrolls with it, so it is part of the document rather than a strip above it.",
13198
- display: "block",
13199
- paddingBottom: "16px"
13200
- },
13201
- '& [part="doc-header"][data-placement="pinned"]': {
13202
- $description: "Design: the artboards are drawn under a pan/zoom transform, and a form drawn at 40% is a picture of a form — so the card sits above the surface at 1:1 and keeps its own scroll. This is the one placement that still needs a cap.",
13284
+ '& [part="doc-header"]': {
13285
+ $description: "Edit only: the card is docked above the page's scroller as a band across the pane at 1:1, so its width is the pane's rather than the breakpoint's, and the page scrolls under it rather than taking it along. It used to be a block of Edit's column, which made it exactly as wide as the column's breakpoint or dragged width and scrolled it off with the first screen of the page. `canvas-render.ts` stacks the stage cell when it draws one — and closes its gap, so the band's own bottom border is where the scroller clips; `flex-shrink: 0` keeps the band whole while the scroller below takes the squeeze. THE CAP IS TWO CAPS, because the band has two ways to eat the page: `40vh` is the tall-pane answer, and `50%` is the short-pane one — a percentage `max-height` resolves against this flex container, the stage cell, whose height is definite (row 2 of the pane's `auto minmax(0, 1fr)` grid), so the page always keeps half the pane however little of it the dock and the bars have left. `40vh` alone is a share of the WINDOW: with the bottom dock open in a 650px window it is most of a 280px stage, and the page was a sliver. No `z-index`: the one it had was for Design, where the band's sibling was a pan/zoom surface whose boards could paint over it. Edit's sibling is the scroller, which clips its own content, and Design draws no card at all. The shadow is the same one-edge idiom as `panel-outline.json`'s verb cluster — it reaches over the scroller's top padding, so the page reads as passing under a docked band rather than ending at a rule. No `display`, so `hidden` on the host still hides it.",
13203
13286
  flexShrink: "0",
13204
- maxHeight: "40vh",
13205
- overflowY: "auto"
13206
- },
13207
- '& [part="doc-header"][data-placement="pinned"] .doc-header': {
13208
- $description: "FOREIGN DOM: `.doc-header` is the frontmatter card's own root, mounted into this host by `panels/frontmatter-panel.ts`. Pinned, the card is a band across the stage rather than a card on it, so it loses its own rounding and keeps only the edge that separates it from the boards.",
13209
- borderWidth: "0 0 1px",
13210
- borderRadius: "0"
13287
+ maxHeight: "min(40vh, 50%)",
13288
+ overflowY: "auto",
13289
+ boxShadow: "0 8px 8px -8px rgb(0 0 0 / 0.35)"
13211
13290
  },
13212
13291
  '& [part="edit-canvas"]': {
13213
13292
  $description: "The side gutter is LOAD-BEARING, not breathing room: the resize handles hang outside the column's own box — flex siblings with a negative margin, so they take no width from it — and this container clips horizontally, so without it the handles would be unreachable at exactly the width where dragging inward matters most. It must stay at least as wide as `EDIT_CANVAS_GUTTER` in `canvas/edit-width.ts`, which is what the drag's own ceiling subtracts. This box is also the TRACK the two `jx-split`s measure: the first ancestor of a handle that generates a box, which is why the handles are drawn here beside the column rather than inside it — inside, the column would have been the track, and the track cannot be the thing being resized.",
@@ -13294,7 +13373,7 @@ var canvas_stage_default = {
13294
13373
  },
13295
13374
  children: [
13296
13375
  {
13297
- $description: "What stands over the frame, and only ever one of the three: the Document Header card above a pan/zoom surface, the Compare bar over a comparison, or the Project Styles bar over the catalogue. Each is an empty host — the surface that fills it is mounted by the module that owns it.",
13376
+ $description: "What stands over the frame, and only ever one of the three: the Document Header card docked above Edit's scrolling column, the Compare bar over a comparison, or the Project Styles bar over the catalogue. Each is an empty host — the surface that fills it is mounted by the module that owns it.",
13298
13377
  tagName: "div",
13299
13378
  attributes: {
13300
13379
  part: "lead-slot"
@@ -13307,10 +13386,10 @@ var canvas_stage_default = {
13307
13386
  },
13308
13387
  cases: {
13309
13388
  header: {
13389
+ $description: "The card has ONE place, so the host says nothing about where it is. It used to carry `data-placement`, one of two, and the card read it back as the host was announced — which is before the runtime writes a node's attributes, so the card read no attribute at all and drew `in-column` even where the host said `pinned`.",
13310
13390
  tagName: "div",
13311
13391
  attributes: {
13312
- part: "doc-header",
13313
- "data-placement": "pinned"
13392
+ part: "doc-header"
13314
13393
  }
13315
13394
  },
13316
13395
  toolbar: {
@@ -13391,28 +13470,6 @@ var canvas_stage_default = {
13391
13470
  }
13392
13471
  },
13393
13472
  children: [
13394
- {
13395
- $description: "Edit draws the Document Header card INSIDE the document's column, as its first block — the column's first child by contract. The width handles are not in the column at all (they stand beside it, see the frame), so nothing here can get in front of it.",
13396
- tagName: "div",
13397
- attributes: {
13398
- part: "column-header-slot"
13399
- },
13400
- style: {
13401
- display: "contents"
13402
- },
13403
- $switch: {
13404
- $ref: "#/state/columnHeader"
13405
- },
13406
- cases: {
13407
- shown: {
13408
- tagName: "div",
13409
- attributes: {
13410
- part: "doc-header",
13411
- "data-placement": "in-column"
13412
- }
13413
- }
13414
- }
13415
- },
13416
13473
  {
13417
13474
  $prototype: "Array",
13418
13475
  items: { $ref: "#/state/panels" },
@@ -13566,7 +13623,6 @@ function project10(scope, view) {
13566
13623
  scope.frameVars = view.frameVars;
13567
13624
  scope.hug = view.hug;
13568
13625
  scope.lead = view.lead;
13569
- scope.columnHeader = view.columnHeader;
13570
13626
  scope.handles = view.handles;
13571
13627
  scope.panels = view.panels;
13572
13628
  }
@@ -13574,7 +13630,6 @@ function mountCanvasStage(host, view, actions) {
13574
13630
  host.replaceChildren();
13575
13631
  const boards = new Map;
13576
13632
  const scope = reactive({
13577
- columnHeader: "hidden",
13578
13633
  frame: "boards",
13579
13634
  framePart: "panzoom",
13580
13635
  frameVars: "",
@@ -13596,7 +13651,7 @@ function mountCanvasStage(host, view, actions) {
13596
13651
  } else if (part && element instanceof HTMLElement) {
13597
13652
  const slot = attrOf(def, "data-slot");
13598
13653
  const named = slot === "frame" ? scope.framePart : slot === "inner" ? scope.innerPart : part;
13599
- actions.host(named, element, attrOf(def, "data-side") || attrOf(def, "data-placement"));
13654
+ actions.host(named, element, attrOf(def, "data-side"));
13600
13655
  }
13601
13656
  }
13602
13657
  }).then((surface) => {
@@ -13645,19 +13700,6 @@ function resolveValue(target, wanted, modifiers) {
13645
13700
  }
13646
13701
  return bound(target.snap(bounded, modifiers));
13647
13702
  }
13648
- function trackOf(handle, axis) {
13649
- for (let node = handle.parentElement;node; node = node.parentElement) {
13650
- if (globalThis.getComputedStyle(node).display === "contents") {
13651
- continue;
13652
- }
13653
- const box = rectOf(node);
13654
- const length = axis === "x" ? box.width : box.height;
13655
- if (length > 0) {
13656
- return length;
13657
- }
13658
- }
13659
- return 0;
13660
- }
13661
13703
  function shareOf(target, size, track) {
13662
13704
  const scale = target.scale();
13663
13705
  const lead = target.lead?.(track) ?? 0;
@@ -13672,7 +13714,7 @@ function sizeOf(target, share, track) {
13672
13714
  }
13673
13715
  function bindSplit(handle, target) {
13674
13716
  const sync = () => {
13675
- const track = trackOf(handle, target.axis);
13717
+ const track = trackOf(handle, target.axis === "x");
13676
13718
  if (track <= 0) {
13677
13719
  return;
13678
13720
  }
@@ -13684,7 +13726,7 @@ function bindSplit(handle, target) {
13684
13726
  handle.value = shareOf(target, target.read(), track);
13685
13727
  };
13686
13728
  const onInput = (event) => {
13687
- const track = trackOf(handle, target.axis);
13729
+ const track = trackOf(handle, target.axis === "x");
13688
13730
  if (track <= 0) {
13689
13731
  return;
13690
13732
  }
@@ -13953,7 +13995,6 @@ function drawStage(surface, view, entries) {
13953
13995
  }
13954
13996
  function editorStageView(frame) {
13955
13997
  return {
13956
- columnHeader: "hidden",
13957
13998
  frame,
13958
13999
  framePart: "panzoom",
13959
14000
  frameVars: "",
@@ -13980,7 +14021,7 @@ function disposeSourceCollab(surface) {
13980
14021
  surface.sourceCollabCleanup = null;
13981
14022
  }
13982
14023
  async function createSourceCollabBinding(model, editor, ctx) {
13983
- const { bindMonacoToYText } = await (__preload("sw6en4dw"), import("./chunks/monaco-binding-sw6en4dw.js"));
14024
+ const { bindMonacoToYText } = await (__preload("e8d9wzyk"), import("./chunks/monaco-binding-e8d9wzyk.js"));
13984
14025
  const unbind = bindMonacoToYText({
13985
14026
  awareness: ctx.awareness,
13986
14027
  editors: [editor],
@@ -14023,9 +14064,6 @@ function sideMarks(marks, modified) {
14023
14064
  path: mark.path
14024
14065
  })) ?? null;
14025
14066
  }
14026
- function diffShowsCode(paneId, filePath) {
14027
- return !canRenderComparison(filePath) || diffViewOf(paneId) === "code";
14028
- }
14029
14067
  async function mountDiffEditor(surface, container, state) {
14030
14068
  const { paneId } = surface;
14031
14069
  const key = state.filePath;
@@ -14033,7 +14071,7 @@ async function mountDiffEditor(surface, container, state) {
14033
14071
  disposeDiffEditor(surface);
14034
14072
  }
14035
14073
  const monaco = await loadMonaco();
14036
- if (!mountStillWanted(container, surface.monacoDiffEditor, () => canvasModeOfPane(paneId) === "git-diff" && diffShowsCode(paneId, key))) {
14074
+ if (!mountStillWanted(container, surface.monacoDiffEditor, () => canvasModeOfPane(paneId) === "git-diff" && diffShowsCode(paneId, canRenderComparison(key)))) {
14037
14075
  return;
14038
14076
  }
14039
14077
  const uris = diffModelUrisFor(paneId, key || "document.json");
@@ -14070,6 +14108,7 @@ function resetCanvasView(surface) {
14070
14108
  surface.centerObserver.disconnect();
14071
14109
  surface.centerObserver = null;
14072
14110
  }
14111
+ releaseStageScrollbar(surface);
14073
14112
  for (const p of surface.panels) {
14074
14113
  p.renderScope?.stop();
14075
14114
  p.renderScope = null;
@@ -14080,6 +14119,7 @@ function resetCanvasView(surface) {
14080
14119
  canvasWrap.style.padding = "";
14081
14120
  canvasWrap.style.alignItems = "";
14082
14121
  canvasWrap.style.flexDirection = "";
14122
+ canvasWrap.style.gap = "";
14083
14123
  canvasWrap.style.display = "";
14084
14124
  canvasWrap.style.overflow = "";
14085
14125
  dismissBlockActionBar();
@@ -14251,7 +14291,7 @@ function renderCanvasImpl(surface) {
14251
14291
  ui: tab.session.ui
14252
14292
  };
14253
14293
  const canvasMode = canvasModeOfPane(surface.paneId);
14254
- const wantsDocHeader = (canvasMode === "edit" || canvasMode === "design") && derivationOfPane(surface.paneId)?.kind !== "lens" && hasDocumentHeader(tab);
14294
+ const wantsDocHeader = canvasMode === "edit" && derivationOfPane(surface.paneId)?.kind !== "lens" && hasDocumentHeader(tab);
14255
14295
  if (!wantsDocHeader) {
14256
14296
  attachDocumentHeaderHost(surface.paneId, null);
14257
14297
  }
@@ -14324,6 +14364,7 @@ function renderCanvasImpl(surface) {
14324
14364
  surface.centerObserver.disconnect();
14325
14365
  surface.centerObserver = null;
14326
14366
  }
14367
+ releaseStageScrollbar(surface);
14327
14368
  detachGridPanel(surface.paneId);
14328
14369
  detachLibraryPane(surface.paneId);
14329
14370
  detachEntryPane(surface.paneId);
@@ -14338,9 +14379,9 @@ function renderCanvasImpl(surface) {
14338
14379
  canvasWrap.style.padding = "";
14339
14380
  canvasWrap.style.alignItems = "";
14340
14381
  canvasWrap.style.flexDirection = "";
14382
+ canvasWrap.style.gap = "";
14341
14383
  canvasWrap.style.display = "";
14342
14384
  canvasWrap.style.overflow = "";
14343
- canvasWrap.style.overflow = "";
14344
14385
  dismissBlockActionBar();
14345
14386
  dismissLinkPopover();
14346
14387
  dismissContextMenu();
@@ -14376,7 +14417,6 @@ function renderCanvasImpl(surface) {
14376
14417
  canvasPanels.push(entry.panel);
14377
14418
  const { featureToggles: previewToggles } = S.ui;
14378
14419
  drawStage(surface, {
14379
- columnHeader: "hidden",
14380
14420
  frame: "boards",
14381
14421
  framePart: "preview-stage",
14382
14422
  frameVars: `--preview-w:${previewWidth}px`,
@@ -14457,7 +14497,7 @@ function renderCanvasImpl(surface) {
14457
14497
  canvasWrap.style.overflow = "hidden";
14458
14498
  }
14459
14499
  const renderable = canRenderComparison(gitDiffState.filePath ?? "");
14460
- if (diffShowsCode(surface.paneId, gitDiffState.filePath ?? "")) {
14500
+ if (diffShowsCode(surface.paneId, renderable)) {
14461
14501
  if (!renderable) {
14462
14502
  setDiffChangeMap(surface.paneId, null);
14463
14503
  }
@@ -14479,7 +14519,6 @@ function renderCanvasImpl(surface) {
14479
14519
  const origEntry = canvasPanelEntry("git-diff-original", "Original", false, panelWidth);
14480
14520
  const currEntry = canvasPanelEntry("git-diff-current", "Current", false, panelWidth);
14481
14521
  const diffStage = drawStage(surface, {
14482
- columnHeader: "hidden",
14483
14522
  frame: "boards",
14484
14523
  framePart: "panzoom",
14485
14524
  frameVars: "",
@@ -14549,22 +14588,26 @@ function renderCanvasImpl(surface) {
14549
14588
  const rootTag = S.document.tagName;
14550
14589
  const isComponentDoc = typeof rootTag === "string" && rootTag.includes("-");
14551
14590
  const { featureToggles: editToggles } = S.ui;
14591
+ canvasWrap.style.flexDirection = wantsDocHeader ? "column" : "";
14592
+ canvasWrap.style.alignItems = wantsDocHeader ? "stretch" : "";
14593
+ canvasWrap.style.gap = wantsDocHeader ? "0px" : "";
14552
14594
  drawStage(surface, {
14553
- columnHeader: wantsDocHeader ? "shown" : "hidden",
14554
14595
  frame: "boards",
14555
14596
  framePart: "edit-canvas",
14556
14597
  frameVars: "",
14557
14598
  handles: "shown",
14558
14599
  hug: isComponentDoc,
14559
14600
  innerPart: "edit-column",
14560
- lead: "none",
14601
+ lead: wantsDocHeader ? "header" : "none",
14561
14602
  panels: [entry.item]
14562
14603
  }, [entry]).then((stage) => {
14563
14604
  if (!stage) {
14564
14605
  return;
14565
14606
  }
14566
14607
  const editColumn = stageHost(surface, "edit-column");
14567
- entry.panel.scrollContainer = stageHost(surface, "edit-canvas");
14608
+ const scroller = stageHost(surface, "edit-canvas");
14609
+ entry.panel.scrollContainer = scroller;
14610
+ trackStageScrollbar(surface, scroller);
14568
14611
  if (editColumn) {
14569
14612
  editColumn.style.maxWidth = `${Math.round(columnWidth)}px`;
14570
14613
  mountEditWidthHandle(surface, stageHost(surface, "edit-handle:start") ?? undefined, editColumn, -1);
@@ -14580,9 +14623,6 @@ function renderCanvasImpl(surface) {
14580
14623
  canvasWrap.style.padding = "0";
14581
14624
  canvasWrap.style.overflow = "hidden";
14582
14625
  }
14583
- canvasWrap.style.flexDirection = wantsDocHeader ? "column" : "";
14584
- canvasWrap.style.alignItems = wantsDocHeader ? "stretch" : "";
14585
- const designLead = wantsDocHeader ? "header" : "none";
14586
14626
  const {
14587
14627
  sizeBreakpoints,
14588
14628
  featureQueries: _featureQueries,
@@ -14596,14 +14636,13 @@ function renderCanvasImpl(surface) {
14596
14636
  const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
14597
14637
  const entry = canvasPanelEntry(hasBaseWidth ? "base" : null, label, !hasBaseWidth, hasBaseWidth ? baseWidth : undefined);
14598
14638
  drawStage(surface, {
14599
- columnHeader: "hidden",
14600
14639
  frame: "boards",
14601
14640
  framePart: "panzoom",
14602
14641
  frameVars: "",
14603
14642
  handles: "hidden",
14604
14643
  hug: false,
14605
14644
  innerPart: "boards",
14606
- lead: designLead,
14645
+ lead: "none",
14607
14646
  panels: [entry.item]
14608
14647
  }, [entry]).then((stage) => {
14609
14648
  if (!stage) {
@@ -14639,14 +14678,13 @@ function renderCanvasImpl(surface) {
14639
14678
  const panelDefs = chosen.length > 0 ? chosen : allPanelDefs;
14640
14679
  const panelEntries = panelDefs.map((def) => canvasPanelEntry(def.name, `${def.displayName} (${def.width}px)`, false, def.width));
14641
14680
  const stageDrawn = drawStage(surface, {
14642
- columnHeader: "hidden",
14643
14681
  frame: "boards",
14644
14682
  framePart: "panzoom",
14645
14683
  frameVars: "",
14646
14684
  handles: "hidden",
14647
14685
  hug: false,
14648
14686
  innerPart: "boards",
14649
- lead: designLead,
14687
+ lead: "none",
14650
14688
  panels: panelEntries.map((e) => e.item)
14651
14689
  }, panelEntries);
14652
14690
  const passGen = surface.renderGeneration;
@@ -14710,6 +14748,7 @@ setSurfaceTeardown((surface) => {
14710
14748
  disposeSourceEditor(surface);
14711
14749
  surface.centerObserver?.disconnect();
14712
14750
  surface.centerObserver = null;
14751
+ releaseStageScrollbar(surface);
14713
14752
  surface.panzoomWrap = null;
14714
14753
  detachCanvasStage(surface);
14715
14754
  });
@@ -17497,7 +17536,7 @@ async function onAddPackage() {
17497
17536
  sync2();
17498
17537
  try {
17499
17538
  await getPlatform2().addPackage(name);
17500
- const { loadComponentRegistry } = await (__preload("5y78e05d"), import("./chunks/components-5y78e05d.js"));
17539
+ const { loadComponentRegistry } = await (__preload("xm826zjw"), import("./chunks/components-xm826zjw.js"));
17501
17540
  await loadComponentRegistry();
17502
17541
  context()?.renderLeftPanel();
17503
17542
  } catch (error) {
@@ -17515,7 +17554,7 @@ async function onRemovePackage(pkg) {
17515
17554
  try {
17516
17555
  await getPlatform2().removePackage(pkg);
17517
17556
  const updated = removePackageElements(siteElements(), pkg);
17518
- const { loadComponentRegistry } = await (__preload("5y78e05d"), import("./chunks/components-5y78e05d.js"));
17557
+ const { loadComponentRegistry } = await (__preload("xm826zjw"), import("./chunks/components-xm826zjw.js"));
17519
17558
  await loadComponentRegistry();
17520
17559
  await updateSiteConfig({ $elements: updated });
17521
17560
  context()?.renderLeftPanel();
@@ -17755,7 +17794,7 @@ var panel_problems_default = {
17755
17794
  overflowWrap: "anywhere"
17756
17795
  },
17757
17796
  '& [part="file-path"]': {
17758
- $description: 'The path is a control, not a label: it opens the file the problem is about, which is the click-through §7.1 asks for. Underlined on hover only — a permanently underlined path in a dense list reads as a paragraph of links. A plain button rather than a `jx-button`, for the reason the status bar\'s items are: what it has to look like is a line of text, and every fill, border and control height the kit draws would have to be taken back off again. Named `file-path` and not `path` because `jx-icon` puts `part="path"` on the `<path>` of every glyph it draws, so `[part="path"]` inside a row selects this button and both SVG paths beside it — a styling rule that reaches into the kit\'s own drawing, and a test or a devtools query that cannot tell them apart.',
17797
+ $description: 'The path is a control, not a label: it opens the file the problem is about, which is the click-through §16.1 asks for. Underlined on hover only — a permanently underlined path in a dense list reads as a paragraph of links. A plain button rather than a `jx-button`, for the reason the status bar\'s items are: what it has to look like is a line of text, and every fill, border and control height the kit draws would have to be taken back off again. Named `file-path` and not `path` because `jx-icon` puts `part="path"` on the `<path>` of every glyph it draws, so `[part="path"]` inside a row selects this button and both SVG paths beside it — a styling rule that reaches into the kit\'s own drawing, and a test or a devtools query that cannot tell them apart.',
17759
17798
  alignSelf: "flex-start",
17760
17799
  maxWidth: "100%",
17761
17800
  overflow: "hidden",
@@ -17781,7 +17820,8 @@ var panel_problems_default = {
17781
17820
  }
17782
17821
  },
17783
17822
  '& [part="detail"]': {
17784
- $description: "The captured log the progress modal used to own. It scrolls inside the row rather than growing it, so one 400-line `bun install` failure does not become the whole panel.",
17823
+ $description: "The captured log the progress modal used to own. It scrolls inside the row rather than growing it, so one 400-line `bun install` failure does not become the whole panel. It is the one part of a problem row that is selectable against the root's `user-select: none`: the log is what a reader pastes into a bug report, and the row offers no copy action.",
17824
+ userSelect: "text",
17785
17825
  maxHeight: "180px",
17786
17826
  margin: "var(--jx-space-1) 0 0",
17787
17827
  padding: "6px",
@@ -17813,7 +17853,7 @@ var panel_problems_default = {
17813
17853
  flex: "none"
17814
17854
  },
17815
17855
  '& [part="empty"]': {
17816
- $description: "The teaching empty state, drawn here rather than borrowed from `panels/empty-state.ts`: that helper is a lit template, and a document cannot call one. What is kept is the pattern's rules (§11) — one sentence saying what the region is FOR, then where its content comes from. It offers no action, because the action that fills this region is a failure and nothing here should invite one.",
17856
+ $description: "The teaching empty state, drawn here rather than borrowed from `panels/empty-state.ts`: that helper is a lit template, and a document cannot call one. What is kept is the pattern's rules (studio-ui-guidelines.md §11) — one sentence saying what the region is FOR, then where its content comes from. It offers no action, because the action that fills this region is a failure and nothing here should invite one.",
17817
17857
  display: "flex",
17818
17858
  flexDirection: "column",
17819
17859
  alignItems: "center",
@@ -18200,7 +18240,7 @@ function groupProblems(records = problems2) {
18200
18240
  return groups;
18201
18241
  }
18202
18242
  function openProblemPath(path) {
18203
- (__preload("msaf95y6"), import("./chunks/files-msaf95y6.js")).then((m) => m.openFileInTab(path));
18243
+ (__preload("egsb0ry8"), import("./chunks/files-egsb0ry8.js")).then((m) => m.openFileInTab(path));
18204
18244
  }
18205
18245
  function projectRow2(record) {
18206
18246
  const id = record.action;
@@ -18304,7 +18344,7 @@ function registerProblemsPanel() {
18304
18344
  var bottom_dock_default = {
18305
18345
  $schema: "https://jxsuite.com/schema/v1",
18306
18346
  $id: "StudioBottomDock",
18307
- $description: "⑪ The Bottom dock's chrome as a Jx document over the kit: a real `tablist` of the dock's tabs, the button that shuts it, and ONE `tabpanel` for whichever tab is showing. Every tab names that one panel — the dock draws the selected tab's body and no other, so there is exactly one panel element and its `aria-labelledby` follows the selection, which is the same shape `surfaces/git-panel.json` uses for its two sub-tabs. It draws no tab BODY: `panels/bottom-dock.ts` keeps the panel registry, the `when` filter, the badge in each tab's label, the Logic reveal and every tab's `afterRender`, and renders the active record into the one empty `[part=\"dock-body\"]` island this document leaves for it (studio-ui-guidelines.md §9.4).",
18347
+ $description: "The Bottom dock's chrome (studio.md §16.3) as a Jx document over the kit: a real `tablist` of the dock's tabs, the button that shuts it, and ONE `tabpanel` for whichever tab is showing. Every tab names that one panel — the dock draws the selected tab's body and no other, so there is exactly one panel element and its `aria-labelledby` follows the selection, which is the same shape `surfaces/git-panel.json` uses for its two sub-tabs. It draws no tab BODY: `panels/bottom-dock.ts` keeps the panel registry, the `when` filter, the badge in each tab's label, the Logic reveal and every tab's `afterRender`, and renders the active record into the one empty `[part=\"dock-body\"]` island this document leaves for it (studio-ui-guidelines.md §9.4).",
18308
18348
  tagName: "div",
18309
18349
  attributes: {
18310
18350
  part: "bottom-dock"
@@ -19159,6 +19199,28 @@ var SELF_SCROLLING_MODES = new Set([
19159
19199
  "preview",
19160
19200
  "settings"
19161
19201
  ]);
19202
+ function wheelBelongsInside(e, stage, pageScroller) {
19203
+ const { target } = e;
19204
+ let el = target instanceof Element ? target : target?.parentElement ?? null;
19205
+ while (el && el !== stage) {
19206
+ if (el instanceof HTMLElement && el !== pageScroller && canScrollBy(el, e.deltaX, e.deltaY)) {
19207
+ return true;
19208
+ }
19209
+ el = el.parentElement;
19210
+ }
19211
+ return false;
19212
+ }
19213
+ function canScrollBy(el, dx, dy) {
19214
+ const downRoom = dy > 0 && el.scrollTop < el.scrollHeight - el.clientHeight - 1;
19215
+ const upRoom = dy < 0 && el.scrollTop > 0;
19216
+ const acrossRoom = dx > 0 && el.scrollLeft < el.scrollWidth - el.clientWidth - 1 || dx < 0 && el.scrollLeft > 0;
19217
+ if (!downRoom && !upRoom && !acrossRoom) {
19218
+ return false;
19219
+ }
19220
+ const style = getComputedStyle(el);
19221
+ const scrolls = (overflow) => overflow === "auto" || overflow === "scroll" || overflow === "overlay";
19222
+ return (downRoom || upRoom ? scrolls(style.overflowY) : false) || (acrossRoom ? scrolls(style.overflowX) : false);
19223
+ }
19162
19224
  var _stageContext = null;
19163
19225
  function installStageGestures(surface) {
19164
19226
  const canvasWrap = surface.wrap;
@@ -19176,7 +19238,7 @@ function installStageGestures(surface) {
19176
19238
  return;
19177
19239
  }
19178
19240
  const sc = canvasWrap.querySelector('[part="edit-canvas"]');
19179
- if (sc) {
19241
+ if (sc && !wheelBelongsInside(e, canvasWrap, sc)) {
19180
19242
  e.preventDefault();
19181
19243
  sc.scrollTop += e.deltaY;
19182
19244
  sc.scrollLeft += e.deltaX;
@@ -19262,7 +19324,7 @@ function stageless() {
19262
19324
  var pane_context_default = {
19263
19325
  $schema: "https://jxsuite.com/schema/v1",
19264
19326
  $id: "StudioPaneContext",
19265
- $description: "The pane's own chrome — region ⑦ (the context bar) and region ⑩ (the floating zoom pod), one mount per pane. It replaces `#tab-bar`, a 28px band styled identically to the tab strip above it that held five unrelated axes with no labels on any of them; the three axes here each say what they are, and the label is what stops the band re-accreting into an anonymous strip of chips. `panels/pane-context.ts` keeps every decision — which editor kinds a document declares, which canvas views it has, whether preview is a flag over them, what a lens may not write, which pane's stage a zoom verb lands on, and what a route param's candidate values are — and hands this document one projection of words, booleans and keyed rows. The bar and the banner share `[part=\"band\"]` so the adapter can MEASURE what the stage must clear: a banner's height is its wrapped text, and only layout knows how many lines that is.",
19327
+ $description: "The pane's own chrome — the pane context bar and the floating zoom pod, one mount per pane. It replaces `#tab-bar`, a 28px band styled identically to the tab strip above it that held five unrelated axes with no labels on any of them; the three axes here each say what they are, and the label is what stops the band re-accreting into an anonymous strip of chips. `panels/pane-context.ts` keeps every decision — which editor kinds a document declares, which canvas views it has, whether preview is a flag over them, what a lens may not write, which pane's stage a zoom verb lands on, and what a route param's candidate values are — and hands this document one projection of words, booleans and keyed rows. The bar and the banner share `[part=\"band\"]` so the adapter can MEASURE what the stage must clear: a banner's height is its wrapped text, and only layout knows how many lines that is.",
19266
19328
  tagName: "div",
19267
19329
  attributes: {
19268
19330
  part: "chrome"
@@ -19320,10 +19382,14 @@ var pane_context_default = {
19320
19382
  flexShrink: "0"
19321
19383
  },
19322
19384
  '& [part="preview-toggle"]': {
19323
- $description: "Not a third radio, and it must not read as one: it says the canvas is inert. The margin separates the two axes the eye is being asked to read as separate; the accent fill is the kit's `emphasized`, which is what deleted the four Spectrum custom properties this rule used to carry.",
19385
+ $description: "Not a third radio, and it must not read as one: it says the canvas is inert. The margin separates the two axes the eye is being asked to read as separate; the accent fill is the kit's `emphasized`, which is what deleted the four Spectrum custom properties this rule used to carry. The toggle is `quiet: false`, so it rests as a framed button of its own beside the framed View switch, as the Spectrum button did; quiet, it read as a fourth loose word once the switch beside it drew its frame.",
19324
19386
  flexShrink: "0",
19325
19387
  marginInlineStart: "6px"
19326
19388
  },
19389
+ '& jx-action-button[part="preview-toggle"] > [part="control"]': {
19390
+ $description: "The text padding the View switch's segments get from their group, which this toggle stands outside: the small size's 2px is for a glyph, and 6px is what the group gives a text segment. (0,3,1) outranks the kit's small padding at (0,3,0).",
19391
+ paddingInline: "6px"
19392
+ },
19327
19393
  '& [part="context-trigger"]': {
19328
19394
  fontVariantNumeric: "tabular-nums"
19329
19395
  },
@@ -19349,29 +19415,47 @@ var pane_context_default = {
19349
19415
  fontSize: "var(--jx-text-md)"
19350
19416
  },
19351
19417
  '& [part="pod"]': {
19352
- $description: "Bottom-right of the pane, over the stage — a viewport control, which is not a document axis and so never belonged in a band beside one.",
19418
+ $description: "Bottom-right of the pane, over the stage — a viewport control, which is not a document axis and so never belonged in a band beside one. It is a ROW of two controls and draws no box of its own: a 1px border and a 6px radius round 2px/4px of padding framed the zoom group's own frame and the Fit select's own frame, a frame inside a frame and busier than any other group in the chrome. Each control draws itself and takes `--pod-lift` to stand off the document under it, which is what the pod's fill and border used to do for both at once, and the gap between them says they are two controls rather than one strip with a seam. The right inset ADDS `--stage-scrollbar-w` because the chrome layer spans the whole cell, scrollbar gutter included: a fixed 12px put the pod 3px into Edit's 15px track, painting over it and swallowing its clicks. CSS cannot measure a scrollbar and this document cannot see the scroller, so `canvas/stage-scrollbar.ts` measures the stage's own and writes the width on the cell, where this reads it by cascade. It is 0 wherever the stage does not scroll or the scrollbar is an overlay. Physical right on purpose: Studio's chrome is never mirrored.",
19419
+ "--pod-lift": "0 2px 8px rgb(0 0 0 / 25%)",
19353
19420
  alignSelf: "flex-end",
19421
+ display: "flex",
19422
+ alignItems: "center",
19423
+ gap: "var(--jx-space-2)",
19354
19424
  pointerEvents: "auto",
19355
- margin: "0 var(--jx-space-4) var(--jx-space-4) 0",
19356
- padding: "var(--jx-space-1) var(--jx-space-2)",
19357
- border: "1px solid var(--border)",
19358
- borderRadius: "var(--jx-radius-md)",
19359
- background: "var(--bg)",
19360
- boxShadow: "0 2px 8px rgb(0 0 0 / 25%)"
19425
+ margin: "0 calc(var(--jx-space-4) + var(--stage-scrollbar-w, 0px)) var(--jx-space-4) 0"
19426
+ },
19427
+ '& [part="zoom-group"]': {
19428
+ $description: "The segmented frame floats on its own now that the pod draws no box around it. The radius is the one the first and last segments already draw, so the shadow follows the shape of the strip instead of a rectangle around it — the host's box IS that strip, since the group has no padding and each member's -1px pulls its own host in by a pixel.",
19429
+ borderRadius: "var(--jx-radius-sm)",
19430
+ boxShadow: "var(--pod-lift)"
19431
+ },
19432
+ '& [part="pod"] > [part="zoom-group"][data-compact] > jx-action-button > [part="control"]': {
19433
+ $description: "One resting ink across the pod. A compact group rests its unselected members on `--jx-fg-dim` and brightens the chosen one, which is how a segmented control says which segment is on — and nothing in this pod is ever chosen: the three are a viewport verb, a readout and a verb. So the dim ink said nothing here and made `100%` read as disabled beside the Fit select's value, which is the same kind of readout at `--jx-fg`. (0,5,1) over the group's own resting rule at (0,4,1); the group's hover is (0,6,1) and still wins, which now leaves it only the wash.",
19434
+ color: "var(--fg)"
19361
19435
  },
19362
19436
  '& [part="zoom-label"]': {
19363
- minWidth: "48px",
19364
- fontFamily: "var(--font-mono)"
19437
+ $description: "A floor under the percentage, so the pod does not shift as the value changes length. The width is the host's and the face is the control's, below: the control sets its own font and sits inside the host at its content width, so this rule used to carry a mono face nothing inherited and to open a 6px hole in the segmented frame before the `+` segment (measured: a 48px host round a 41.8px control, drawn in Inter).",
19438
+ minWidth: "48px"
19439
+ },
19440
+ '& [part="zoom-label"] > [part="control"]': {
19441
+ $description: "The control fills its 48px host, closing the frame, and draws the percentage in the mono face with tabular digits so a changing value does not jitter. (0,3,0) outranks the kit control's own face at (0,2,0).",
19442
+ flex: "1",
19443
+ fontFamily: "var(--font-mono)",
19444
+ fontVariantNumeric: "tabular-nums"
19365
19445
  },
19366
19446
  '& [part="fit"]': {
19367
19447
  maxWidth: "120px"
19368
19448
  },
19449
+ '& [part="fit"] > [part="control"]': {
19450
+ $description: "The pod's second control lifts off the document the way the first does, on the frame the kit's select already draws. On the control rather than the host, because the host is a column whose error region and help slot are the select's neighbours: the shadow has to follow the box that is painted.",
19451
+ boxShadow: "var(--pod-lift)"
19452
+ },
19369
19453
  '& [part="popover"]': {
19370
19454
  $description: "Both panels take the pointer that `.pane-chrome` withholds. A popover is in the top layer while it shows, which changes where it PAINTS and not where it is in the tree, so it inherits `pointer-events: none` from the chrome layer exactly as the band does.",
19371
19455
  pointerEvents: "auto"
19372
19456
  },
19373
19457
  '& [part="ctx"]': {
19374
- $description: "One column of labelled groups over a footer that leaves for the definition site. This control only SELECTS (§2 principle 5) — nothing in it defines a breakpoint, a scheme or a feature query.",
19458
+ $description: "One column of labelled groups over a footer that leaves for the definition site. This control only SELECTS; the definitions live in Project Settings › Contexts (§6.2), and nothing in it defines a breakpoint, a scheme or a feature query.",
19375
19459
  display: "flex",
19376
19460
  flexDirection: "column",
19377
19461
  gap: "10px",
@@ -19390,11 +19474,15 @@ var pane_context_default = {
19390
19474
  letterSpacing: "0.04em",
19391
19475
  color: "var(--fg-dim)"
19392
19476
  },
19393
- '& [part="sizes"]': {
19394
- flexWrap: "wrap"
19477
+ '& [part="sizes"], & [part="features"]': {
19478
+ $description: "The two groups that WRAP, and the reason neither is a segmented control. A breakpoint or feature list is as long as the project makes it and this popover is a 240–340px column, so both fold onto a second line in any project with more than a few; a compact group cannot draw that — its seam is written in tree order, no selector can find the ends of a flex line, and it therefore refuses to wrap and would overflow the column instead (ui.md §5.4). Chips instead: each member draws its own rounded frame (`quiet: false`, since the group no longer frames it) and the chosen one takes the accent edge below. The gap is --jx-space-2 rather than the group's own 2px, which puts every small chip's hit area at the 24px floor on both axes instead of the 22px two overlapping extensions leave (§4.3).",
19479
+ flexWrap: "wrap",
19480
+ gap: "var(--jx-space-2)"
19395
19481
  },
19396
- '& [part="features"]': {
19397
- flexWrap: "wrap"
19482
+ '& [part="sizes-item"][data-selected] > [part="control"], & [part="features-item"][data-selected] > [part="control"]': {
19483
+ $description: "A chosen chip reads the way a chosen segment does: the accent tint with an accent EDGE. A member's own selected state is a 15% tint over its neutral border, which among framed neighbours reads as a faint wash rather than as the one that is on — the measurement that gave the compact group its selected rule, restated here at the one place the group does not draw it. (0,4,0) over the member's own (0,3,0).",
19484
+ borderColor: "var(--jx-accent)",
19485
+ background: "var(--jx-accent-20)"
19398
19486
  },
19399
19487
  '& [part="fields"]': {
19400
19488
  $description: "The fields, stacked. They were a row on the bar capped at 140px; in the popover each takes the full column so a route value or a JSON test prop is readable while you type it.",
@@ -19722,6 +19810,7 @@ var pane_context_default = {
19722
19810
  },
19723
19811
  $props: {
19724
19812
  size: "sm",
19813
+ quiet: false,
19725
19814
  toggles: true,
19726
19815
  emphasized: true,
19727
19816
  selected: {
@@ -20060,55 +20149,55 @@ var pane_context_default = {
20060
20149
  }
20061
20150
  ]
20062
20151
  }
20063
- },
20064
- {
20065
- tagName: "div",
20152
+ }
20153
+ ]
20154
+ },
20155
+ {
20156
+ tagName: "div",
20157
+ attributes: {
20158
+ part: "fit-slot",
20159
+ role: "none"
20160
+ },
20161
+ style: {
20162
+ display: "contents"
20163
+ },
20164
+ $switch: {
20165
+ $ref: "#/state/fitState"
20166
+ },
20167
+ cases: {
20168
+ shown: {
20169
+ tagName: "jx-select",
20066
20170
  attributes: {
20067
- part: "fit-slot",
20068
- role: "none"
20069
- },
20070
- style: {
20071
- display: "contents"
20171
+ part: "fit"
20072
20172
  },
20073
- $switch: {
20074
- $ref: "#/state/fitState"
20173
+ $props: {
20174
+ label: "Fit",
20175
+ size: "sm",
20176
+ options: {
20177
+ $ref: "#/state/fitOptions"
20178
+ },
20179
+ value: {
20180
+ $ref: "#/state/fitValue"
20181
+ }
20075
20182
  },
20076
- cases: {
20077
- shown: {
20078
- tagName: "jx-select",
20079
- attributes: {
20080
- part: "fit"
20081
- },
20082
- $props: {
20083
- label: "Fit",
20084
- size: "sm",
20085
- options: {
20086
- $ref: "#/state/fitOptions"
20183
+ onchange: {
20184
+ $prototype: "Function",
20185
+ body: [
20186
+ {
20187
+ operator: "call",
20188
+ target: {
20189
+ $ref: "#/state/chooseFit"
20087
20190
  },
20088
- value: {
20089
- $ref: "#/state/fitValue"
20090
- }
20091
- },
20092
- onchange: {
20093
- $prototype: "Function",
20094
- body: [
20191
+ value: [
20095
20192
  {
20096
- operator: "call",
20097
- target: {
20098
- $ref: "#/state/chooseFit"
20099
- },
20100
- value: [
20101
- {
20102
- $ref: "event#/target/value"
20103
- }
20104
- ]
20193
+ $ref: "event#/target/value"
20105
20194
  }
20106
20195
  ]
20107
20196
  }
20108
- }
20197
+ ]
20109
20198
  }
20110
20199
  }
20111
- ]
20200
+ }
20112
20201
  }
20113
20202
  ]
20114
20203
  }
@@ -20159,7 +20248,7 @@ var pane_context_default = {
20159
20248
  },
20160
20249
  $props: {
20161
20250
  selects: "single",
20162
- compact: true,
20251
+ compact: false,
20163
20252
  label: "Size"
20164
20253
  },
20165
20254
  children: [
@@ -20179,6 +20268,7 @@ var pane_context_default = {
20179
20268
  },
20180
20269
  $props: {
20181
20270
  size: "sm",
20271
+ quiet: false,
20182
20272
  checked: {
20183
20273
  $ref: "$map/item/checked"
20184
20274
  },
@@ -20431,7 +20521,7 @@ var pane_context_default = {
20431
20521
  },
20432
20522
  $props: {
20433
20523
  selects: "multiple",
20434
- compact: true,
20524
+ compact: false,
20435
20525
  label: "Features"
20436
20526
  },
20437
20527
  children: [
@@ -20451,6 +20541,7 @@ var pane_context_default = {
20451
20541
  },
20452
20542
  $props: {
20453
20543
  size: "sm",
20544
+ quiet: false,
20454
20545
  toggles: true,
20455
20546
  selected: {
20456
20547
  $ref: "$map/item/selected"
@@ -21383,6 +21474,11 @@ function fitChoiceValue(fit) {
21383
21474
  return fit;
21384
21475
  }
21385
21476
  var STAGE_ZOOM_MODES = new Set(["design", "stylebook", "git-diff"]);
21477
+ function comparisonPathOf(paneId) {
21478
+ const derived = derivationOfPane(paneId);
21479
+ const state = derived?.kind === "lens" && derived.preset === "diff" ? derived.diff : shell.git.diffState;
21480
+ return state?.filePath ?? "";
21481
+ }
21386
21482
  function podFor(tab, paneId) {
21387
21483
  const mode = canvasModeOfPane(paneId);
21388
21484
  const surface = surfaceForPane(paneId);
@@ -21393,6 +21489,9 @@ function podFor(tab, paneId) {
21393
21489
  if (!STAGE_ZOOM_MODES.has(mode)) {
21394
21490
  return { podState: "hidden" };
21395
21491
  }
21492
+ if (mode === "git-diff" && diffShowsCode(paneId, canRenderComparison(comparisonPathOf(paneId)))) {
21493
+ return { podState: "hidden" };
21494
+ }
21396
21495
  const zoom = stageZoom(surface);
21397
21496
  return {
21398
21497
  fitOptions: FIT_CHOICES.map(({ label, value }) => ({ label, value })),
@@ -21989,130 +22088,15 @@ async function applyFileDrop(tab, files, hit, preview) {
21989
22088
  }
21990
22089
  }
21991
22090
  }
21992
-
21993
- // ../ai/src/tools.ts
21994
- function toolSuccess(data, summary) {
21995
- return { success: true, data, summary };
21996
- }
21997
- function toolError(error) {
21998
- return { success: false, error };
21999
- }
22000
- function createToolDefinition({
22001
- name,
22002
- description,
22003
- parameters,
22004
- strict = true,
22005
- llmStrict = false,
22006
- execute
22007
- }) {
22008
- return { name, description, parameters, strict, llmStrict, execute };
22009
- }
22010
- function createToolRegistry() {
22011
- const _tools = new Map;
22012
- return {
22013
- register(tool) {
22014
- if (_tools.has(tool.name)) {
22015
- console.warn(`Tool "${tool.name}" is being re-registered.`);
22016
- }
22017
- _tools.set(tool.name, tool);
22018
- },
22019
- list() {
22020
- return [..._tools.values()];
22021
- },
22022
- listForLLM() {
22023
- return [..._tools.values()].map((t) => ({
22024
- type: "function",
22025
- function: {
22026
- name: t.name,
22027
- description: t.description,
22028
- parameters: t.parameters,
22029
- ...t.llmStrict === true ? { strict: true } : {}
22030
- }
22031
- }));
22032
- },
22033
- getDefinition(toolName) {
22034
- return _tools.get(toolName);
22035
- },
22036
- validate(toolName, args) {
22037
- const tool = _tools.get(toolName);
22038
- if (!tool) {
22039
- return { valid: false, errors: [`Unknown tool: "${toolName}"`] };
22040
- }
22041
- if (!tool.strict) {
22042
- return { valid: true };
22043
- }
22044
- const schema = tool.parameters;
22045
- if (!schema || !schema.properties) {
22046
- return { valid: true };
22047
- }
22048
- const errors = [];
22049
- const props = schema.properties;
22050
- const required = schema.required || [];
22051
- const argRecord = args;
22052
- for (const key of required) {
22053
- if (!(key in args) || argRecord[key] === undefined || argRecord[key] === null) {
22054
- errors.push(`Missing required argument: "${key}"`);
22055
- }
22056
- }
22057
- for (const [key, value] of Object.entries(argRecord)) {
22058
- const propSchema = props[key];
22059
- if (!propSchema) {
22060
- continue;
22061
- }
22062
- const expectedType = propSchema.type;
22063
- if (!expectedType) {
22064
- continue;
22065
- }
22066
- const actualType = Array.isArray(value) ? "array" : typeof value;
22067
- if (expectedType === "number" || expectedType === "integer") {
22068
- if (actualType === "number") {
22069
- continue;
22070
- }
22071
- if (actualType === "string") {
22072
- if (!Number.isNaN(Number(value))) {
22073
- continue;
22074
- }
22075
- errors.push(`Argument "${key}" should be a number, got non-numeric string "${value}"`);
22076
- continue;
22077
- }
22078
- errors.push(`Argument "${key}" should be a number, got ${actualType}`);
22079
- continue;
22080
- }
22081
- if (expectedType === "string" && actualType !== "string") {
22082
- errors.push(`Argument "${key}" should be a string, got ${actualType}`);
22083
- } else if (expectedType === "boolean" && actualType !== "boolean") {
22084
- errors.push(`Argument "${key}" should be a boolean, got ${actualType}`);
22085
- } else if (expectedType === "array" && actualType !== "array") {
22086
- errors.push(`Argument "${key}" should be an array, got ${actualType}`);
22087
- } else if (expectedType === "object" && actualType !== "object") {
22088
- errors.push(`Argument "${key}" should be an object, got ${actualType}`);
22089
- }
22090
- }
22091
- return errors.length > 0 ? { valid: false, errors } : { valid: true };
22092
- },
22093
- async execute(toolName, args) {
22094
- const tool = _tools.get(toolName);
22095
- if (!tool) {
22096
- return toolError(`Unknown tool: "${toolName}"`);
22097
- }
22098
- const validation = this.validate(toolName, args);
22099
- if (!validation.valid) {
22100
- return toolError(`Validation failed: ${(validation.errors || []).join("; ")}`);
22101
- }
22102
- try {
22103
- return await tool.execute(args);
22104
- } catch (error) {
22105
- return toolError(`Tool "${toolName}" execution error: ${error.message}`);
22106
- }
22107
- }
22108
- };
22109
- }
22110
22091
  // ../ai/src/chat-state.ts
22111
22092
  var _idCounter = 0;
22112
22093
  function uid() {
22113
22094
  _idCounter += 1;
22114
22095
  return `msg_${Date.now()}_${_idCounter}`;
22115
22096
  }
22097
+ function isCount(value) {
22098
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
22099
+ }
22116
22100
  function createChatState(opts = {}) {
22117
22101
  const model = opts.model || "gpt-4o";
22118
22102
  const store = reactive({
@@ -22123,7 +22107,8 @@ function createChatState(opts = {}) {
22123
22107
  error: null,
22124
22108
  model,
22125
22109
  tokenCount: 0,
22126
- contextWarning: false
22110
+ contextWarning: false,
22111
+ usage: null
22127
22112
  });
22128
22113
  let _streamingMessage = null;
22129
22114
  function sendMessage(text) {
@@ -22204,15 +22189,14 @@ function createChatState(opts = {}) {
22204
22189
  }
22205
22190
  function appendToolCallEnd(_id) {}
22206
22191
  function appendToolResult(id, result) {
22207
- const tc = store.pendingToolCalls.find((t) => t.id === id);
22208
- if (tc) {
22209
- tc.result = result;
22192
+ let index = store.messages.length - 1;
22193
+ while (index >= 0 && store.messages[index].role === "tool") {
22194
+ index -= 1;
22210
22195
  }
22211
- if (_streamingMessage?.toolCalls) {
22212
- const mtc = _streamingMessage.toolCalls.find((t) => t.id === id);
22213
- if (mtc) {
22214
- mtc.result = result;
22215
- }
22196
+ const request = store.messages[index];
22197
+ const record = request?.role === "assistant" ? request.toolCalls?.find((t) => t.id === id) : null;
22198
+ if (record) {
22199
+ record.result = result;
22216
22200
  }
22217
22201
  }
22218
22202
  function finishStream(_stopReason) {
@@ -22225,6 +22209,7 @@ function createChatState(opts = {}) {
22225
22209
  store.status = "error";
22226
22210
  store.error = message;
22227
22211
  store.streamingContent = "";
22212
+ store.pendingToolCalls = [];
22228
22213
  if (_streamingMessage) {
22229
22214
  const idx = store.messages.lastIndexOf(_streamingMessage);
22230
22215
  if (idx !== -1) {
@@ -22253,6 +22238,7 @@ function createChatState(opts = {}) {
22253
22238
  store.pendingToolCalls = [];
22254
22239
  store.error = null;
22255
22240
  store.contextWarning = false;
22241
+ store.usage = null;
22256
22242
  _streamingMessage = null;
22257
22243
  }
22258
22244
  function retryLast() {
@@ -22271,6 +22257,27 @@ function createChatState(opts = {}) {
22271
22257
  function setTokenCount(count) {
22272
22258
  store.tokenCount = count;
22273
22259
  }
22260
+ function recordUsage(usage, extra = {}) {
22261
+ if (!isCount(usage.inputTokens) || !isCount(usage.outputTokens)) {
22262
+ return;
22263
+ }
22264
+ const { type: _type, ...counts } = usage;
22265
+ const counted = _streamingMessage ?? store.messages.at(-1);
22266
+ const replayed = Boolean(counted?.reasoningContent && (counted.content || (counted.toolCalls?.length ?? 0) > 0));
22267
+ const unreplayed = replayed || !isCount(usage.reasoningTokens) ? 0 : usage.reasoningTokens;
22268
+ const contextTokens = Math.max(0, usage.inputTokens + usage.outputTokens - unreplayed);
22269
+ store.usage = {
22270
+ ...counts,
22271
+ contextTokens,
22272
+ lastMessageId: store.messages.at(-1)?.id ?? "",
22273
+ messageCount: store.messages.length,
22274
+ ...extra.systemTokens === undefined ? {} : { systemTokens: extra.systemTokens }
22275
+ };
22276
+ store.tokenCount = contextTokens;
22277
+ }
22278
+ function clearUsage() {
22279
+ store.usage = null;
22280
+ }
22274
22281
  function setContextWarning(warning) {
22275
22282
  store.contextWarning = warning;
22276
22283
  }
@@ -22332,6 +22339,8 @@ function createChatState(opts = {}) {
22332
22339
  retryLast,
22333
22340
  setModel,
22334
22341
  setTokenCount,
22342
+ recordUsage,
22343
+ clearUsage,
22335
22344
  setContextWarning,
22336
22345
  toMessagesArray
22337
22346
  });
@@ -22368,6 +22377,7 @@ function createProxyStreamingClient({
22368
22377
  apiKey,
22369
22378
  baseUrl
22370
22379
  }) {
22380
+ let resolvedUrl = null;
22371
22381
  async function* streamChat(messages, tools, systemPrompt, signal) {
22372
22382
  const headers = { "Content-Type": "application/json" };
22373
22383
  if (apiKey) {
@@ -22376,9 +22386,24 @@ function createProxyStreamingClient({
22376
22386
  if (baseUrl) {
22377
22387
  headers["X-Api-Base-URL"] = baseUrl;
22378
22388
  }
22389
+ resolvedUrl ??= Promise.resolve(typeof chatUrl === "function" ? chatUrl() : chatUrl);
22390
+ let url;
22391
+ try {
22392
+ url = await resolvedUrl;
22393
+ } catch (error) {
22394
+ if (signal?.aborted) {
22395
+ yield { type: "done", stopReason: "cancelled" };
22396
+ return;
22397
+ }
22398
+ throw error;
22399
+ }
22400
+ if (signal?.aborted) {
22401
+ yield { type: "done", stopReason: "cancelled" };
22402
+ return;
22403
+ }
22379
22404
  let response;
22380
22405
  try {
22381
- response = await fetch(chatUrl, {
22406
+ response = await fetch(url, {
22382
22407
  method: "POST",
22383
22408
  headers,
22384
22409
  body: JSON.stringify({ messages, tools, systemPrompt, model }),
@@ -22464,54 +22489,28 @@ function createProxyStreamingClient({
22464
22489
  }
22465
22490
  return { streamChat };
22466
22491
  }
22467
- // src/services/ai-writes.ts
22468
- var turns = [];
22469
- var MAX_TURNS = 50;
22470
- var open = null;
22471
- function beginTurn(id) {
22472
- if (open?.id === id) {
22473
- return;
22492
+ // src/services/project-path.ts
22493
+ function normalizeRelPath(path) {
22494
+ if (typeof path !== "string" || !path.trim()) {
22495
+ return null;
22474
22496
  }
22475
- open = { id, writes: [] };
22476
- }
22477
- function recordWrite(write) {
22478
- open?.writes.push(write);
22479
- }
22480
- function endTurn(id) {
22481
- if (!open) {
22482
- return [];
22497
+ let p = path.trim().replaceAll("\\", "/");
22498
+ while (p.startsWith("./")) {
22499
+ p = p.slice(2);
22483
22500
  }
22484
- const turn = { id, writes: open.writes };
22485
- open = null;
22486
- if (turn.writes.length === 0) {
22487
- return [];
22501
+ if (p.startsWith("/") || p.startsWith("~") || /^[A-Za-z]:/.test(p)) {
22502
+ return null;
22488
22503
  }
22489
- turns.push(turn);
22490
- while (turns.length > MAX_TURNS) {
22491
- turns.shift();
22504
+ if (p.split("/").includes("..")) {
22505
+ return null;
22492
22506
  }
22493
- return turn.writes;
22494
- }
22495
- function writesForTurn(id) {
22496
- return turns.find((turn) => turn.id === id)?.writes ?? [];
22507
+ return p;
22497
22508
  }
22498
- function summarizeWrites(writes) {
22499
- const applied = writes.filter((w) => w.ok);
22500
- const paths = new Set(applied.map((w) => w.path));
22501
- const failed = writes.length - applied.length;
22502
- if (paths.size === 0) {
22503
- return failed > 0 ? `${failed} change${failed === 1 ? "" : "s"} failed` : "";
22504
- }
22505
- const files = `Changed ${paths.size} file${paths.size === 1 ? "" : "s"}`;
22506
- const diskPaths = new Set(applied.filter((w) => w.disk).map((w) => w.path));
22507
- const parts = [files];
22508
- if (diskPaths.size > 0) {
22509
- parts.push(`${diskPaths.size} written to disk — undo cannot reach ${diskPaths.size === 1 ? "it" : "them"}`);
22510
- }
22511
- if (failed > 0) {
22512
- parts.push(`${failed} failed`);
22513
- }
22514
- return parts.join(" · ");
22509
+ function invalidPathResult(path) {
22510
+ return {
22511
+ success: false,
22512
+ error: `Invalid path ${JSON.stringify(path)} — use a path relative to the project root (no leading "/", no "..").`
22513
+ };
22515
22514
  }
22516
22515
 
22517
22516
  // src/services/token-lint.ts
@@ -22681,7 +22680,7 @@ function noDocError() {
22681
22680
  error: "No document is open — use open_document(path) to open one on the canvas, or " + "read_file/write_file to work with project files directly."
22682
22681
  };
22683
22682
  }
22684
- async function applyAndValidate(tab, mutationFn, summary, validate, renderCheck, projectStyle) {
22683
+ async function applyAndValidate(ledger, tab, mutationFn, summary, validate, renderCheck, projectStyle) {
22685
22684
  const deps = { getProjectStyle: () => projectStyle, renderCheck, validate };
22686
22685
  const before = await snapshotBeforeWrite(tab, deps);
22687
22686
  if (!transactDoc(tab, mutationFn)) {
@@ -22690,7 +22689,7 @@ async function applyAndValidate(tab, mutationFn, summary, validate, renderCheck,
22690
22689
  error: "The document is frozen: a collaborator is editing its source, so structural edits are " + "paused. Nothing was changed. Try again once source editing ends."
22691
22690
  };
22692
22691
  }
22693
- recordWrite({ disk: false, ok: true, path: tab.documentPath ?? "(untitled)", tool: summary });
22692
+ ledger.record({ disk: false, ok: true, path: tab.documentPath ?? "(untitled)", tool: summary });
22694
22693
  return reportDocumentWrite(tab, before, summary, deps);
22695
22694
  }
22696
22695
  function registerAiTools(registry, {
@@ -22769,7 +22768,7 @@ function registerAiTools(registry, {
22769
22768
  },
22770
22769
  required: ["path", "key"]
22771
22770
  },
22772
- async execute(args) {
22771
+ async execute(args, ctx) {
22773
22772
  const tab = getTab();
22774
22773
  if (!tab) {
22775
22774
  return noDocError();
@@ -22779,7 +22778,7 @@ function registerAiTools(registry, {
22779
22778
  if (node === undefined) {
22780
22779
  return { success: false, error: `No node exists at path ${JSON.stringify(path)}.` };
22781
22780
  }
22782
- return applyAndValidate(tab, (t) => mutateUpdateProperty(t, path, key, value ?? undefined), `Set "${key}" at ${JSON.stringify(path)}.`, validate, renderCheck, styleOf());
22781
+ return applyAndValidate(ctx.ledger, tab, (t) => mutateUpdateProperty(t, path, key, value ?? undefined), `Set "${key}" at ${JSON.stringify(path)}.`, validate, renderCheck, styleOf());
22783
22782
  }
22784
22783
  }));
22785
22784
  registry.register(createToolDefinition({
@@ -22801,7 +22800,7 @@ function registerAiTools(registry, {
22801
22800
  },
22802
22801
  required: ["parentPath", "index", "node"]
22803
22802
  },
22804
- async execute(args) {
22803
+ async execute(args, ctx) {
22805
22804
  const tab = getTab();
22806
22805
  if (!tab) {
22807
22806
  return noDocError();
@@ -22827,7 +22826,7 @@ function registerAiTools(registry, {
22827
22826
  error: "Cannot insert into mapped-array children; edit the map template instead."
22828
22827
  };
22829
22828
  }
22830
- return applyAndValidate(tab, (t) => mutateInsertNode(t, parentPath, index, childNode), `Inserted node at ${JSON.stringify([...parentPath, "children", index])}.`, validate, renderCheck, styleOf());
22829
+ return applyAndValidate(ctx.ledger, tab, (t) => mutateInsertNode(t, parentPath, index, childNode), `Inserted node at ${JSON.stringify([...parentPath, "children", index])}.`, validate, renderCheck, styleOf());
22831
22830
  }
22832
22831
  }));
22833
22832
  registry.register(createToolDefinition({
@@ -22851,7 +22850,7 @@ function registerAiTools(registry, {
22851
22850
  },
22852
22851
  required: ["path", "property"]
22853
22852
  },
22854
- async execute(args) {
22853
+ async execute(args, ctx) {
22855
22854
  const tab = getTab();
22856
22855
  if (!tab) {
22857
22856
  return noDocError();
@@ -22862,7 +22861,7 @@ function registerAiTools(registry, {
22862
22861
  }
22863
22862
  const prop = property;
22864
22863
  const val = value == null ? undefined : String(value);
22865
- return applyAndValidate(tab, (t) => mutateUpdateStyle(t, path, prop, val), `Set style "${prop}" at ${JSON.stringify(path)}.`, validate, renderCheck, styleOf());
22864
+ return applyAndValidate(ctx.ledger, tab, (t) => mutateUpdateStyle(t, path, prop, val), `Set style "${prop}" at ${JSON.stringify(path)}.`, validate, renderCheck, styleOf());
22866
22865
  }
22867
22866
  }));
22868
22867
  registry.register(createToolDefinition({
@@ -22880,7 +22879,7 @@ function registerAiTools(registry, {
22880
22879
  },
22881
22880
  required: ["path", "value"]
22882
22881
  },
22883
- async execute(args) {
22882
+ async execute(args, ctx) {
22884
22883
  const tab = getTab();
22885
22884
  if (!tab) {
22886
22885
  return noDocError();
@@ -22889,10 +22888,9 @@ function registerAiTools(registry, {
22889
22888
  if (getNodeAtPath2(tab.doc.document, path) === undefined) {
22890
22889
  return { success: false, error: `No node exists at path ${JSON.stringify(path)}.` };
22891
22890
  }
22892
- return applyAndValidate(tab, (t) => {
22893
- const node = getNodeAtPath2(t.doc.document, path);
22894
- delete node.textContent;
22895
- node.children = [value];
22891
+ return applyAndValidate(ctx.ledger, tab, (t) => {
22892
+ mutateUpdateProperty(t, path, "textContent");
22893
+ mutateUpdateProperty(t, path, "children", [value]);
22896
22894
  }, `Set text at ${JSON.stringify(path)}.`, validate, renderCheck, styleOf());
22897
22895
  }
22898
22896
  }));
@@ -22909,7 +22907,7 @@ function registerAiTools(registry, {
22909
22907
  },
22910
22908
  required: ["key", "value"]
22911
22909
  },
22912
- async execute(args) {
22910
+ async execute(args, ctx) {
22913
22911
  const tab = getTab();
22914
22912
  if (!tab) {
22915
22913
  return noDocError();
@@ -22921,12 +22919,7 @@ function registerAiTools(registry, {
22921
22919
  error: `State key "${key}" already exists. Use update_state to change it, or remove it first.`
22922
22920
  };
22923
22921
  }
22924
- return applyAndValidate(tab, (t) => {
22925
- if (!t.doc.document.state) {
22926
- t.doc.document.state = {};
22927
- }
22928
- t.doc.document.state[key] = value;
22929
- }, `Added state "${key}".`, validate, renderCheck, styleOf());
22922
+ return applyAndValidate(ctx.ledger, tab, (t) => mutateAddDef(t, key, value), `Added state "${key}".`, validate, renderCheck, styleOf());
22930
22923
  }
22931
22924
  }));
22932
22925
  registry.register(createToolDefinition({
@@ -22942,7 +22935,7 @@ function registerAiTools(registry, {
22942
22935
  },
22943
22936
  required: ["key"]
22944
22937
  },
22945
- async execute(args) {
22938
+ async execute(args, ctx) {
22946
22939
  const tab = getTab();
22947
22940
  if (!tab) {
22948
22941
  return noDocError();
@@ -22954,15 +22947,11 @@ function registerAiTools(registry, {
22954
22947
  error: `State key "${key}" does not exist. Use add_state to create it, or check the name. Current state keys: ${Object.keys(tab.doc.document.state || {}).join(", ") || "(none)"}`
22955
22948
  };
22956
22949
  }
22957
- return applyAndValidate(tab, (t) => {
22958
- const { state } = t.doc.document;
22959
- if (!state) {
22960
- return;
22961
- }
22950
+ return applyAndValidate(ctx.ledger, tab, (t) => {
22962
22951
  if (value == null) {
22963
- delete state[key];
22952
+ mutateRemoveDef(t, key);
22964
22953
  } else {
22965
- state[key] = value;
22954
+ mutateAddDef(t, key, value);
22966
22955
  }
22967
22956
  }, value == null ? `Removed state "${key}".` : `Updated state "${key}".`, validate, renderCheck, styleOf());
22968
22957
  }
@@ -22990,7 +22979,7 @@ function registerAiTools(registry, {
22990
22979
  },
22991
22980
  required: ["fromPath", "toParentPath", "toIndex"]
22992
22981
  },
22993
- async execute(args) {
22982
+ async execute(args, ctx) {
22994
22983
  const tab = getTab();
22995
22984
  if (!tab) {
22996
22985
  return noDocError();
@@ -23011,7 +23000,7 @@ function registerAiTools(registry, {
23011
23000
  error: `No node exists at toParentPath ${JSON.stringify(toParentPath)}.`
23012
23001
  };
23013
23002
  }
23014
- return applyAndValidate(tab, (t) => mutateMoveNode(t, fromPath, toParentPath, toIndex), `Moved node from ${JSON.stringify(fromPath)} to ${JSON.stringify([...toParentPath, "children", toIndex])}.`, validate, renderCheck, styleOf());
23003
+ return applyAndValidate(ctx.ledger, tab, (t) => mutateMoveNode(t, fromPath, toParentPath, toIndex), `Moved node from ${JSON.stringify(fromPath)} to ${JSON.stringify([...toParentPath, "children", toIndex])}.`, validate, renderCheck, styleOf());
23015
23004
  }
23016
23005
  }));
23017
23006
  registry.register(createToolDefinition({
@@ -23031,14 +23020,18 @@ function registerAiTools(registry, {
23031
23020
  },
23032
23021
  required: ["path", "content"]
23033
23022
  },
23034
- async execute(args) {
23023
+ async execute(args, ctx) {
23035
23024
  if (!saveFile) {
23036
23025
  return {
23037
23026
  success: false,
23038
23027
  error: "File operations are not available in this environment."
23039
23028
  };
23040
23029
  }
23041
- const { path: relPath, content } = args;
23030
+ const { path, content } = args;
23031
+ const relPath = normalizeRelPath(path);
23032
+ if (relPath === null) {
23033
+ return invalidPathResult(path);
23034
+ }
23042
23035
  const dirtyError = dirtyTabError(relPath);
23043
23036
  if (dirtyError) {
23044
23037
  return dirtyError;
@@ -23065,14 +23058,14 @@ ${formatted}`
23065
23058
  }
23066
23059
  try {
23067
23060
  await saveFile(relPath, serializeJson(content, null));
23068
- recordWrite({ disk: true, ok: true, path: relPath, tool: "create_component" });
23061
+ ctx.ledger.record({ disk: true, ok: true, path: relPath, tool: "create_component" });
23069
23062
  return {
23070
23063
  success: true,
23071
23064
  summary: await reconcileAfterWrite(relPath, `Created component at "${relPath}".`)
23072
23065
  };
23073
23066
  } catch (error) {
23074
23067
  const message = error instanceof Error ? error.message : String(error);
23075
- recordWrite({
23068
+ ctx.ledger.record({
23076
23069
  disk: true,
23077
23070
  error: message,
23078
23071
  ok: false,
@@ -23103,14 +23096,18 @@ ${formatted}`
23103
23096
  },
23104
23097
  required: ["path", "content"]
23105
23098
  },
23106
- async execute(args) {
23099
+ async execute(args, ctx) {
23107
23100
  if (!saveFile) {
23108
23101
  return {
23109
23102
  success: false,
23110
23103
  error: "File operations are not available in this environment."
23111
23104
  };
23112
23105
  }
23113
- const { path: relPath, content } = args;
23106
+ const { path, content } = args;
23107
+ const relPath = normalizeRelPath(path);
23108
+ if (relPath === null) {
23109
+ return invalidPathResult(path);
23110
+ }
23114
23111
  const dirtyError = dirtyTabError(relPath);
23115
23112
  if (dirtyError) {
23116
23113
  return dirtyError;
@@ -23137,14 +23134,14 @@ ${formatted}`
23137
23134
  }
23138
23135
  try {
23139
23136
  await saveFile(relPath, serializeJson(content, null));
23140
- recordWrite({ disk: true, ok: true, path: relPath, tool: "create_page" });
23137
+ ctx.ledger.record({ disk: true, ok: true, path: relPath, tool: "create_page" });
23141
23138
  return {
23142
23139
  success: true,
23143
23140
  summary: await reconcileAfterWrite(relPath, `Created page at "${relPath}".`)
23144
23141
  };
23145
23142
  } catch (error) {
23146
23143
  const message = error instanceof Error ? error.message : String(error);
23147
- recordWrite({
23144
+ ctx.ledger.record({
23148
23145
  disk: true,
23149
23146
  error: message,
23150
23147
  ok: false,
@@ -23160,26 +23157,6 @@ ${formatted}`
23160
23157
  }));
23161
23158
  }
23162
23159
 
23163
- // src/services/ai-turn-signal.ts
23164
- var _signal;
23165
- function beginTurnSignal(signal) {
23166
- _signal = signal;
23167
- }
23168
- function turnSignal() {
23169
- return _signal;
23170
- }
23171
- function endTurnSignal() {
23172
- _signal = undefined;
23173
- _toolCallId = "";
23174
- }
23175
- var _toolCallId = "";
23176
- function beginToolCall(id) {
23177
- _toolCallId = id;
23178
- }
23179
- function currentToolCallId() {
23180
- return _toolCallId;
23181
- }
23182
-
23183
23160
  // src/services/ai-ask.ts
23184
23161
  var MAX_OPTIONS = 6;
23185
23162
  var state2 = reactive({ pending: null });
@@ -23197,11 +23174,10 @@ function pendingAsk() {
23197
23174
  function isAwaitingAnswer() {
23198
23175
  return state2.pending !== null;
23199
23176
  }
23200
- function askUser(ask) {
23177
+ function askUser(ask, signal) {
23201
23178
  return new Promise((resolve) => {
23202
23179
  state2.pending = ask;
23203
23180
  settle2 = resolve;
23204
- const signal = turnSignal();
23205
23181
  if (signal?.aborted) {
23206
23182
  clear();
23207
23183
  resolve({ answer: null, skipped: false });
@@ -23246,9 +23222,10 @@ function resetAsk() {
23246
23222
  cancelAsk();
23247
23223
  clear();
23248
23224
  }
23249
- function registerAskTool(registry) {
23225
+ function registerAskTool(registry, { onPending } = {}) {
23250
23226
  registry.register(createToolDefinition({
23251
23227
  name: "ask_user",
23228
+ interactive: true,
23252
23229
  description: "Pause and put a question to the user; the turn waits for their reply and resumes with " + "it. Use it ONLY for a judgement that is genuinely theirs — which pages matter, whether to " + "keep the original design, whether a low-fidelity page is acceptable. Never ask something " + "you can decide yourself or discover with another tool, and never ask about an option you " + "cannot act on. One question at a time; offer options when the sensible answers are a " + "short list, and the user may always reply in their own words instead.",
23253
23230
  parameters: {
23254
23231
  type: "object",
@@ -23269,7 +23246,7 @@ function registerAskTool(registry) {
23269
23246
  },
23270
23247
  required: ["question"]
23271
23248
  },
23272
- execute: async (args) => {
23249
+ execute: async (args, ctx) => {
23273
23250
  const { question, options, context } = args;
23274
23251
  if (typeof question !== "string" || !question.trim()) {
23275
23252
  return toolError('Pass "question" — the question to put to the user.');
@@ -23278,12 +23255,16 @@ function registerAskTool(registry) {
23278
23255
  return toolError("A question is already waiting for the user.");
23279
23256
  }
23280
23257
  const choices = Array.isArray(options) ? options.filter((o) => typeof o === "string" && o.trim().length > 0).slice(0, MAX_OPTIONS) : [];
23281
- const { answer, skipped } = await askUser({
23258
+ const answering = askUser({
23282
23259
  context: typeof context === "string" ? context.trim() : "",
23283
- id: currentToolCallId(),
23260
+ id: ctx.callId,
23284
23261
  options: choices,
23285
23262
  question: question.trim()
23286
- });
23263
+ }, ctx.signal);
23264
+ if (isAwaitingAnswer()) {
23265
+ onPending?.();
23266
+ }
23267
+ const { answer, skipped } = await answering;
23287
23268
  if (skipped) {
23288
23269
  return toolSuccess({ answer: null, skipped: true }, "The user declined to answer; proceed with your best judgment.");
23289
23270
  }
@@ -23350,7 +23331,7 @@ function definitionOf(registry, command, execute) {
23350
23331
  parameters,
23351
23332
  strict: false,
23352
23333
  llmStrict: false,
23353
- execute: (args) => execute(tool.name, args)
23334
+ execute: (args, ctx) => execute(tool.name, args, ctx)
23354
23335
  };
23355
23336
  }
23356
23337
  function advertisedCommandTools(registry, execute = async (name) => toolError(`Tool "${name}" was listed without an executor.`)) {
@@ -23393,20 +23374,20 @@ function defaultPaths(command, tab) {
23393
23374
  }
23394
23375
  return ["(unknown file)"];
23395
23376
  }
23396
- function fileLedger(command, tab, wrote) {
23377
+ function fileLedger(ledger, command, tab, wrote) {
23397
23378
  if (command.undo === undefined) {
23398
23379
  return;
23399
23380
  }
23400
23381
  const disk = command.undo === "none";
23401
23382
  for (const path of wrote ?? defaultPaths(command, tab)) {
23402
- recordWrite({ disk, ok: true, path, tool: command.title });
23383
+ ledger.record({ disk, ok: true, path, tool: command.title });
23403
23384
  }
23404
23385
  }
23405
23386
  function createCommandToolRegistry(deps) {
23406
23387
  function commandFor(name) {
23407
23388
  return activeRegistry()?.list().find((command) => command.aiTool?.name === name);
23408
23389
  }
23409
- async function execute(name, received) {
23390
+ async function execute(name, received, ctx = createToolContext()) {
23410
23391
  const registry = activeRegistry();
23411
23392
  const command = commandFor(name);
23412
23393
  if (!registry || !command) {
@@ -23448,7 +23429,7 @@ function createCommandToolRegistry(deps) {
23448
23429
  try {
23449
23430
  report = normaliseReport(tool.report({ after, args, before }));
23450
23431
  } catch (error) {
23451
- fileLedger(command, tab, projectUnchanged ? [] : undefined);
23432
+ fileLedger(ctx.ledger, command, tab, projectUnchanged ? [] : undefined);
23452
23433
  const message = error instanceof Error ? error.message : String(error);
23453
23434
  return { error: `${command.title} ran, but its report failed: ${message}`, success: false };
23454
23435
  }
@@ -23459,7 +23440,7 @@ function createCommandToolRegistry(deps) {
23459
23440
  summary = `${command.title} changed nothing: project.json is exactly as it was.`;
23460
23441
  }
23461
23442
  }
23462
- fileLedger(command, tab, wrote);
23443
+ fileLedger(ctx.ledger, command, tab, wrote);
23463
23444
  if (tab && snapshot) {
23464
23445
  const verdict = await reportDocumentWrite(tab, snapshot, summary, deps);
23465
23446
  if (!verdict.success) {
@@ -23526,9 +23507,9 @@ function composeToolRegistries(hand, commands) {
23526
23507
  const side = sideOf(name);
23527
23508
  return side ? side.validate(name, args) : { valid: false, errors: [`Unknown tool: "${name}"`] };
23528
23509
  },
23529
- async execute(name, args) {
23510
+ async execute(name, args, ctx) {
23530
23511
  const side = sideOf(name);
23531
- return side ? side.execute(name, args) : toolError(`Unknown tool: "${name}"`);
23512
+ return side ? side.execute(name, args, ctx) : toolError(`Unknown tool: "${name}"`);
23532
23513
  }
23533
23514
  };
23534
23515
  }
@@ -23546,6 +23527,14 @@ async function adoptProject(root) {
23546
23527
  }
23547
23528
  async function adoptCreatedProject(root, { getTab, adopt = adoptProject, initRepo = initProjectRepo2 }) {
23548
23529
  await initRepo(root);
23530
+ const platform = hasPlatform2() ? getPlatform2() : null;
23531
+ const shownRoot = workspace2.projectRoot;
23532
+ const openedElsewhere = Boolean(platform && shownRoot && shownRoot !== root && platform.openProjectInNewWindow);
23533
+ if (openedElsewhere && shownRoot && platform?.setWindowProject) {
23534
+ try {
23535
+ await platform.setWindowProject(shownRoot);
23536
+ } catch {}
23537
+ }
23549
23538
  const wasBatching = isBatching();
23550
23539
  if (wasBatching) {
23551
23540
  endBatch();
@@ -23559,7 +23548,7 @@ async function adoptCreatedProject(root, { getTab, adopt = adoptProject, initRep
23559
23548
  if (wasBatching) {
23560
23549
  beginBatch(getTab());
23561
23550
  }
23562
- return { adopted: workspace2.projectRoot === root, error: adoptionError };
23551
+ return { adopted: workspace2.projectRoot === root, error: adoptionError, openedElsewhere };
23563
23552
  }
23564
23553
 
23565
23554
  // src/services/ai-project-tools.ts
@@ -23569,28 +23558,6 @@ var SEARCH_CAP = 100;
23569
23558
  var READ_CAP_BYTES = 48 * 1024;
23570
23559
  var WRITE_CAP_BYTES = 256 * 1024;
23571
23560
  var NOT_UNDOABLE = "(saved to disk; not undoable with Cmd+Z)";
23572
- function normalizeRelPath(path) {
23573
- if (typeof path !== "string" || !path.trim()) {
23574
- return null;
23575
- }
23576
- let p = path.trim().replaceAll("\\", "/");
23577
- while (p.startsWith("./")) {
23578
- p = p.slice(2);
23579
- }
23580
- if (p.startsWith("/") || p.startsWith("~") || /^[A-Za-z]:/.test(p)) {
23581
- return null;
23582
- }
23583
- if (p.split("/").includes("..")) {
23584
- return null;
23585
- }
23586
- return p;
23587
- }
23588
- function pathError(path) {
23589
- return {
23590
- success: false,
23591
- error: `Invalid path ${JSON.stringify(path)} — use a path relative to the project root (no leading "/", no "..").`
23592
- };
23593
- }
23594
23561
  function isJxDocPath(path) {
23595
23562
  return /^(pages|layouts|components|elements)\//.test(path) && path.endsWith(".json");
23596
23563
  }
@@ -23626,7 +23593,7 @@ function registerProjectTools(registry, {
23626
23593
  const { dir } = args;
23627
23594
  const start = dir === undefined || dir === "" ? "." : normalizeRelPath(dir);
23628
23595
  if (start === null) {
23629
- return pathError(dir);
23596
+ return invalidPathResult(dir);
23630
23597
  }
23631
23598
  const platform = getPlatform2();
23632
23599
  const entries = [];
@@ -23683,7 +23650,7 @@ function registerProjectTools(registry, {
23683
23650
  async execute(args) {
23684
23651
  const relPath = normalizeRelPath(args.path);
23685
23652
  if (relPath === null) {
23686
- return pathError(args.path);
23653
+ return invalidPathResult(args.path);
23687
23654
  }
23688
23655
  try {
23689
23656
  const content = await getPlatform2().readFile(relPath);
@@ -23723,11 +23690,11 @@ function registerProjectTools(registry, {
23723
23690
  },
23724
23691
  required: ["path", "content"]
23725
23692
  },
23726
- async execute(args) {
23693
+ async execute(args, ctx) {
23727
23694
  const { path, content } = args;
23728
23695
  const relPath = normalizeRelPath(path);
23729
23696
  if (relPath === null) {
23730
- return pathError(path);
23697
+ return invalidPathResult(path);
23731
23698
  }
23732
23699
  if (content.length > WRITE_CAP_BYTES) {
23733
23700
  return {
@@ -23808,10 +23775,16 @@ ${formatted}`
23808
23775
  }
23809
23776
  try {
23810
23777
  await getPlatform2().writeFile(relPath, content);
23811
- recordWrite({ disk: true, ok: true, path: relPath, tool: "write_file" });
23778
+ ctx.ledger.record({ disk: true, ok: true, path: relPath, tool: "write_file" });
23812
23779
  } catch (error) {
23813
23780
  const message = error instanceof Error ? error.message : String(error);
23814
- recordWrite({ disk: true, error: message, ok: false, path: relPath, tool: "write_file" });
23781
+ ctx.ledger.record({
23782
+ disk: true,
23783
+ error: message,
23784
+ ok: false,
23785
+ path: relPath,
23786
+ tool: "write_file"
23787
+ });
23815
23788
  return {
23816
23789
  success: false,
23817
23790
  error: `Failed to write "${relPath}": ${message}`
@@ -23898,7 +23871,7 @@ ${tokenHints}` : summary };
23898
23871
  },
23899
23872
  required: ["name"]
23900
23873
  },
23901
- async execute(args) {
23874
+ async execute(args, ctx) {
23902
23875
  const {
23903
23876
  name,
23904
23877
  description,
@@ -23962,6 +23935,7 @@ ${tokenHints}` : summary };
23962
23935
  error: `Failed to create project: ${error instanceof Error ? error.message : String(error)}`
23963
23936
  };
23964
23937
  }
23938
+ ctx.ledger.record({ disk: true, ok: true, path: result.root, tool: "create_project" });
23965
23939
  const { adopted, error: adoptionError } = await adoptCreatedProject(result.root, {
23966
23940
  adopt: adoptProject,
23967
23941
  getTab
@@ -24124,10 +24098,7 @@ function destinationProblem(destination) {
24124
24098
  }
24125
24099
  return null;
24126
24100
  }
24127
- var imported = false;
24128
- function resetImportGuard() {
24129
- imported = false;
24130
- }
24101
+ var IMPORT_DONE = "import.done";
24131
24102
  function describeRun(id, root, summary) {
24132
24103
  const record = importRun(id);
24133
24104
  const warnings = [...record?.warnings ?? [], ...summary?.warnings ?? []];
@@ -24157,7 +24128,12 @@ function describeRun(id, root, summary) {
24157
24128
  lines.push("The file and document tools are available now. Read the emitted pages, layouts and " + "components before proposing changes — then ask the user about anything the import had to " + "guess at (pages it skipped, components it may have split wrongly, a layout it did not find, " + "a page that did not render faithfully).");
24158
24129
  return lines.join(" ");
24159
24130
  }
24160
- function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted }) {
24131
+ function registerImportTools(registry, {
24132
+ getTab,
24133
+ adoptProject,
24134
+ onProjectAdopted,
24135
+ brief: briefStore = { clear: clearPendingImportBrief, read: pendingImportBrief }
24136
+ }) {
24161
24137
  registry.register(createToolDefinition({
24162
24138
  name: "import_site",
24163
24139
  description: "Clone a live website into a new Jx project and open it in the studio: pages, styles, " + "assets, a shared layout and recurring components. Only available while no project is " + "open. Progress streams into the conversation as it runs. When the user reached you " + "through the New Project Import form, the URL, crawl options and destination are already " + "settled — call this with the url alone and the rest is filled in. After it succeeds the " + "file and document tools become available in the next round.",
@@ -24203,7 +24179,7 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24203
24179
  },
24204
24180
  required: ["url"]
24205
24181
  },
24206
- async execute(args) {
24182
+ async execute(args, ctx) {
24207
24183
  const {
24208
24184
  url,
24209
24185
  directory,
@@ -24219,7 +24195,7 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24219
24195
  if (workspace2.projectRoot) {
24220
24196
  return toolError("A project is already open in this window — import_site is only for bootstrapping.");
24221
24197
  }
24222
- if (imported) {
24198
+ if (ctx.session.get(IMPORT_DONE) === true) {
24223
24199
  return toolError("A site has already been imported in this conversation. Start a new chat to import " + "another, or use the file tools on the project that was created.");
24224
24200
  }
24225
24201
  const platform = getPlatform2();
@@ -24235,7 +24211,7 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24235
24211
  if (target.protocol !== "http:" && target.protocol !== "https:") {
24236
24212
  return toolError("The url must start with http:// or https://.");
24237
24213
  }
24238
- const brief = pendingImportBrief();
24214
+ const brief = briefStore.read();
24239
24215
  const requested = typeof directory === "string" ? directory.trim() : "";
24240
24216
  if (requested && brief && requested !== brief.directory) {
24241
24217
  return toolError(`The destination is already set to ${brief.directory} — call import_site without ` + '"directory", or ask the user before changing it.');
@@ -24248,12 +24224,14 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24248
24224
  if (problem) {
24249
24225
  return toolError(problem);
24250
24226
  }
24251
- const id = currentToolCallId();
24227
+ const id = ctx.callId;
24252
24228
  const signal = beginImportRun(id, { directory: destination, url: target.href });
24253
- turnSignal()?.addEventListener("abort", () => {
24229
+ const callSignal = ctx.signal;
24230
+ const stopRun = () => {
24254
24231
  abortImportRun();
24255
24232
  finishImportRun(id, { status: "stopped" });
24256
- });
24233
+ };
24234
+ callSignal.addEventListener("abort", stopRun, { once: true });
24257
24235
  const apiKey = getOpenAiKey();
24258
24236
  const baseUrl = getBaseUrl();
24259
24237
  const model = brief?.model || preferredModel();
@@ -24280,8 +24258,11 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24280
24258
  ...apiKey ? { apiKey } : {},
24281
24259
  ...baseUrl ? { baseUrl } : {},
24282
24260
  ...model ? { model } : {}
24283
- }, (evt) => recordImportProgress(id, evt), signal, onReady);
24261
+ }, (evt) => {
24262
+ ctx.progress(evt);
24263
+ }, signal, onReady);
24284
24264
  } catch (error) {
24265
+ callSignal.removeEventListener("abort", stopRun);
24285
24266
  const message = error instanceof Error ? error.message : String(error);
24286
24267
  if (signal.aborted) {
24287
24268
  finishImportRun(id, { error: message, status: "stopped" });
@@ -24295,9 +24276,11 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24295
24276
  Last steps:
24296
24277
  ${tail}` : ""}`);
24297
24278
  }
24279
+ callSignal.removeEventListener("abort", stopRun);
24298
24280
  finishImportRun(id, { status: "done" });
24299
- imported = true;
24300
- clearPendingImportBrief();
24281
+ ctx.ledger.record({ disk: true, ok: true, path: result.root, tool: "import_site" });
24282
+ ctx.session.set(IMPORT_DONE, true);
24283
+ briefStore.clear();
24301
24284
  const { adopted, error: adoptionError } = await (adoption ?? adoptCreatedProject(result.root, {
24302
24285
  getTab,
24303
24286
  ...adoptProject ? { adopt: adoptProject } : {}
@@ -24334,7 +24317,7 @@ function createGatedToolRegistry(inner, availability) {
24334
24317
  validate(toolName, args) {
24335
24318
  return inner.validate(toolName, args);
24336
24319
  },
24337
- async execute(toolName, args) {
24320
+ async execute(toolName, args, ctx) {
24338
24321
  if (inner.getDefinition(toolName) && !isAvailable(toolName)) {
24339
24322
  const gate = availability.get(toolName);
24340
24323
  return {
@@ -24342,28 +24325,207 @@ function createGatedToolRegistry(inner, availability) {
24342
24325
  error: `Tool "${toolName}" is not available right now — it requires ${gate?.requires ?? "a different studio state"}.`
24343
24326
  };
24344
24327
  }
24345
- return inner.execute(toolName, args);
24328
+ return inner.execute(toolName, args, ctx);
24346
24329
  }
24347
24330
  };
24348
24331
  }
24349
24332
 
24333
+ // src/services/context-manager.ts
24334
+ var CHARS_PER_TOKEN = 4;
24335
+ var MODEL_CONTEXT_WINDOWS = [
24336
+ ["gpt-4o", 128000],
24337
+ ["gpt-4.1", 1e6],
24338
+ ["gpt-4-turbo", 128000],
24339
+ ["gpt-4", 8192],
24340
+ ["gpt-3.5", 16385],
24341
+ ["o1", 200000],
24342
+ ["o3", 200000],
24343
+ ["claude", 200000]
24344
+ ];
24345
+ var DEFAULT_CONTEXT_WINDOW = 32000;
24346
+ var BUDGET_FRACTION = 0.8;
24347
+ var WARN_FRACTION = 0.5;
24348
+ function contextWindowFor(model) {
24349
+ if (!model) {
24350
+ return DEFAULT_CONTEXT_WINDOW;
24351
+ }
24352
+ const reported = modelContextWindow(model);
24353
+ if (reported !== undefined && reported > 0) {
24354
+ return reported;
24355
+ }
24356
+ const id = model.toLowerCase();
24357
+ let best = 0;
24358
+ let window2 = DEFAULT_CONTEXT_WINDOW;
24359
+ for (const [prefix, size] of MODEL_CONTEXT_WINDOWS) {
24360
+ if (id.startsWith(prefix) && prefix.length > best) {
24361
+ best = prefix.length;
24362
+ window2 = size;
24363
+ }
24364
+ }
24365
+ return window2;
24366
+ }
24367
+ var KEEP_RECENT = 20;
24368
+ var MIN_USER_TURNS = 3;
24369
+ function estimateTokens(input) {
24370
+ if (typeof input === "string") {
24371
+ return Math.ceil(input.length / CHARS_PER_TOKEN);
24372
+ }
24373
+ let total = 0;
24374
+ for (const msg of input) {
24375
+ total += estimateTokens(msg.content || "");
24376
+ if (msg.tool_calls) {
24377
+ for (const tc of msg.tool_calls) {
24378
+ total += estimateTokens(tc.function?.name || "") + estimateTokens(tc.function?.arguments || "") + 4;
24379
+ }
24380
+ }
24381
+ total += 4;
24382
+ }
24383
+ return total;
24384
+ }
24385
+ function wireEntry(msg) {
24386
+ const entry = { content: msg.content, role: msg.role };
24387
+ if (msg.toolCalls) {
24388
+ entry.tool_calls = msg.toolCalls.map((tc) => ({
24389
+ function: { arguments: tc.arguments, name: tc.name }
24390
+ }));
24391
+ }
24392
+ return entry;
24393
+ }
24394
+ function contextTokens(chatState, systemTokens) {
24395
+ const { usage, messages } = chatState;
24396
+ const boundary = usage ? messages[usage.messageCount - 1] : undefined;
24397
+ if (usage && boundary && boundary.id === usage.lastMessageId) {
24398
+ const since = messages.slice(usage.messageCount).filter((msg) => msg.role !== "assistant" || msg.content || (msg.toolCalls?.length ?? 0) > 0).map((msg) => wireEntry(msg));
24399
+ const promptDrift = usage.systemTokens === undefined ? 0 : systemTokens - usage.systemTokens;
24400
+ const reused = Math.max(0, usage.contextTokens + promptDrift) + estimateTokens(since);
24401
+ if (Number.isFinite(reused)) {
24402
+ return reused;
24403
+ }
24404
+ }
24405
+ return systemTokens + estimateTokens(chatState.toMessagesArray());
24406
+ }
24407
+ function estimatePromptTokens(systemPrompt) {
24408
+ return estimateTokens(systemPrompt);
24409
+ }
24410
+ function trimContext(chatState, systemPrompt) {
24411
+ const window2 = contextWindowFor(chatState.model);
24412
+ const maxTokens = Math.floor(window2 * BUDGET_FRACTION);
24413
+ const warnTokens = Math.floor(window2 * WARN_FRACTION);
24414
+ const systemTokens = estimateTokens(systemPrompt);
24415
+ const allMessages = chatState.messages;
24416
+ const total = contextTokens(chatState, systemTokens);
24417
+ if (total <= maxTokens) {
24418
+ chatState.setTokenCount(total);
24419
+ chatState.setContextWarning(total >= warnTokens);
24420
+ return { estimatedTokens: total, droppedCount: 0 };
24421
+ }
24422
+ let keepFrom = Math.max(0, allMessages.length - KEEP_RECENT);
24423
+ let preservedTurns = 0;
24424
+ for (let i = allMessages.length - 1;i >= keepFrom; i--) {
24425
+ const { role } = allMessages[i];
24426
+ if (role === "user" || role === "tool") {
24427
+ preservedTurns += 1;
24428
+ }
24429
+ }
24430
+ if (preservedTurns < MIN_USER_TURNS) {
24431
+ for (let i = keepFrom - 1;i >= 0 && preservedTurns < MIN_USER_TURNS; i--) {
24432
+ keepFrom = i;
24433
+ const { role } = allMessages[i];
24434
+ if (role === "user" || role === "tool") {
24435
+ preservedTurns += 1;
24436
+ }
24437
+ }
24438
+ }
24439
+ if (keepFrom <= 0) {
24440
+ chatState.setTokenCount(total);
24441
+ chatState.setContextWarning(true);
24442
+ return { estimatedTokens: total, droppedCount: 0 };
24443
+ }
24444
+ const droppedCount = keepFrom;
24445
+ allMessages.splice(0, keepFrom);
24446
+ chatState.clearUsage();
24447
+ allMessages.unshift({
24448
+ id: `ctx_summary_${Date.now()}`,
24449
+ role: "user",
24450
+ content: `[Earlier conversation truncated. ${droppedCount} messages dropped to stay within token budget. The most recent ${allMessages.length - 1} messages are preserved.]`,
24451
+ timestamp: Date.now()
24452
+ });
24453
+ const newTotal = systemTokens + estimateTokens(chatState.toMessagesArray());
24454
+ chatState.setTokenCount(newTotal);
24455
+ chatState.setContextWarning(true);
24456
+ return { estimatedTokens: newTotal, droppedCount };
24457
+ }
24458
+ var UNANSWERED_TOOL_RESULT = JSON.stringify({
24459
+ success: false,
24460
+ error: "This tool call was never completed — the session was reloaded or the history was trimmed."
24461
+ });
24462
+ function pruneOrphanToolMessages(chatState) {
24463
+ const { messages } = chatState;
24464
+ const requested = new Set;
24465
+ const answered = new Set;
24466
+ for (const msg of messages) {
24467
+ if (msg.role === "assistant" && msg.toolCalls) {
24468
+ for (const call of msg.toolCalls) {
24469
+ requested.add(call.id);
24470
+ }
24471
+ } else if (msg.role === "tool" && msg.toolCallId) {
24472
+ answered.add(msg.toolCallId);
24473
+ }
24474
+ }
24475
+ let dropped = 0;
24476
+ for (let i = messages.length - 1;i >= 0; i--) {
24477
+ const msg = messages[i];
24478
+ if (msg.role === "tool" && (!msg.toolCallId || !requested.has(msg.toolCallId))) {
24479
+ messages.splice(i, 1);
24480
+ dropped += 1;
24481
+ }
24482
+ }
24483
+ let sealed = 0;
24484
+ for (let i = messages.length - 1;i >= 0; i--) {
24485
+ const msg = messages[i];
24486
+ if (msg.role !== "assistant" || !msg.toolCalls) {
24487
+ continue;
24488
+ }
24489
+ const missing = msg.toolCalls.filter((call) => !answered.has(call.id));
24490
+ if (missing.length === 0) {
24491
+ continue;
24492
+ }
24493
+ messages.splice(i + 1, 0, ...missing.map((call, n) => ({
24494
+ content: UNANSWERED_TOOL_RESULT,
24495
+ id: `sealed_${call.id}_${n}`,
24496
+ role: "tool",
24497
+ timestamp: msg.timestamp,
24498
+ toolCallId: call.id
24499
+ })));
24500
+ sealed += missing.length;
24501
+ }
24502
+ return { dropped, sealed };
24503
+ }
24504
+
24350
24505
  // src/services/tool-executor.ts
24351
24506
  var MAX_ROUNDS = 5;
24507
+ var EMPTY_TURN_TEXT = "The model sent back an empty reply.";
24352
24508
  var MAX_TOTAL_ROUNDS = 25;
24353
- var INTERACTIVE_TOOLS = new Set(["ask_user"]);
24354
24509
  async function runAgentLoop({
24355
24510
  chatState,
24356
24511
  streamingClient,
24357
24512
  toolRegistry,
24358
24513
  systemPrompt,
24359
24514
  signal,
24360
- getTab
24515
+ getTab,
24516
+ session = createSessionFacts()
24361
24517
  }) {
24362
24518
  const allErrors = [];
24363
24519
  const appliedSummaries = [];
24364
- const turnId = () => chatState.messages.at(-1)?.id ?? "";
24365
- beginTurn(`turn:${chatState.messages.length}`);
24366
- beginTurnSignal(signal);
24520
+ const turnUserId = chatState.messages.findLast((m) => m.role === "user")?.id;
24521
+ const ledger = openTurnLedger(`turn:${chatState.messages.length}`);
24522
+ const actor = {
24523
+ id: `assistant:${session.sessionId ?? "local"}:${ledger.turnId}`,
24524
+ kind: "assistant",
24525
+ model: chatState.model,
24526
+ sessionId: session.sessionId,
24527
+ turnId: ledger.turnId
24528
+ };
24367
24529
  if (getTab) {
24368
24530
  beginBatch(getTab());
24369
24531
  }
@@ -24413,6 +24575,10 @@ async function runAgentLoop({
24413
24575
  chatState.appendToolCallEnd(event.id);
24414
24576
  break;
24415
24577
  }
24578
+ case "usage": {
24579
+ chatState.recordUsage(event, { systemTokens: estimatePromptTokens(systemPrompt) });
24580
+ break;
24581
+ }
24416
24582
  case "done": {
24417
24583
  ({ stopReason } = event);
24418
24584
  break;
@@ -24427,7 +24593,6 @@ async function runAgentLoop({
24427
24593
  }
24428
24594
  }
24429
24595
  }
24430
- chatState.finishStream(stopReason);
24431
24596
  if (streamError) {
24432
24597
  if (streamErrorCode === "cf_reconnect_required") {
24433
24598
  resetModelCache();
@@ -24436,30 +24601,55 @@ async function runAgentLoop({
24436
24601
  chatState.setError(streamError);
24437
24602
  return;
24438
24603
  }
24439
- if (stopReason !== "tool_calls" || toolCalls.size === 0) {
24604
+ const stopped = stopReason === "cancelled" || signal?.aborted === true;
24605
+ if (toolCalls.size === 0 && !stopped && turnAnchor(chatState.messages, turnUserId) === null) {
24606
+ chatState.setError(EMPTY_TURN_TEXT);
24607
+ return;
24608
+ }
24609
+ chatState.finishStream(stopReason);
24610
+ if (toolCalls.size === 0 || stopReason === "cancelled" || signal?.aborted) {
24440
24611
  return;
24441
24612
  }
24442
24613
  let didWork = false;
24443
24614
  for (const [id, call] of toolCalls) {
24615
+ if (signal?.aborted) {
24616
+ return;
24617
+ }
24444
24618
  let result;
24445
- beginToolCall(id);
24619
+ const writesBefore = ledger.writes.length;
24620
+ const link = signal ? linkCallSignal(signal) : null;
24621
+ const ctx = createToolContext({
24622
+ actor,
24623
+ callId: id,
24624
+ ledger,
24625
+ progress: (event) => {
24626
+ recordImportProgress(id, event);
24627
+ },
24628
+ session,
24629
+ ...link ? { signal: link.signal } : {}
24630
+ });
24446
24631
  try {
24447
- const args = call.arguments ? JSON.parse(call.arguments) : {};
24448
- result = await toolRegistry.execute(call.name, args);
24632
+ result = await toolRegistry.execute(call.name, parseToolArguments(call.arguments), ctx);
24449
24633
  } catch (error) {
24450
24634
  result = {
24451
24635
  success: false,
24452
24636
  error: `Failed to parse arguments: ${error.message}`
24453
24637
  };
24638
+ } finally {
24639
+ link?.release();
24454
24640
  }
24455
- if (!INTERACTIVE_TOOLS.has(call.name)) {
24641
+ if (turnUserId !== undefined && !chatState.messages.some((m) => m.id === turnUserId)) {
24642
+ return;
24643
+ }
24644
+ if (toolRegistry.getDefinition(call.name)?.interactive !== true) {
24456
24645
  didWork = true;
24457
24646
  }
24458
24647
  reanchorBatch();
24459
24648
  if (!result.success && result.error) {
24460
24649
  allErrors.push(result.error);
24461
24650
  }
24462
- if (result.success && result.summary) {
24651
+ const wrote = ledger.writes.slice(writesBefore).some((write) => write.ok);
24652
+ if (result.success && result.summary && wrote) {
24463
24653
  appliedSummaries.push(result.summary);
24464
24654
  }
24465
24655
  chatState.appendToolResult(id, result);
@@ -24468,6 +24658,9 @@ async function runAgentLoop({
24468
24658
  if (didWork) {
24469
24659
  workRounds += 1;
24470
24660
  }
24661
+ if (signal?.aborted) {
24662
+ return;
24663
+ }
24471
24664
  if (workRounds < MAX_ROUNDS && round < MAX_TOTAL_ROUNDS) {
24472
24665
  chatState.beginAssistantTurn();
24473
24666
  }
@@ -24494,10 +24687,20 @@ You can continue by sending another message, or try a more specific request.`;
24494
24687
  }
24495
24688
  chatState.setError(tail);
24496
24689
  } finally {
24497
- endTurnSignal();
24498
24690
  endBatch();
24499
- endTurn(turnId());
24691
+ fileTurn(turnAnchor(chatState.messages, turnUserId) ?? "", ledger.writes);
24692
+ }
24693
+ }
24694
+ function parseToolArguments(text) {
24695
+ if (!text) {
24696
+ return {};
24697
+ }
24698
+ const parsed = JSON.parse(text);
24699
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
24700
+ const type = parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed;
24701
+ throw new TypeError(`arguments must be a JSON object, got ${type}`);
24500
24702
  }
24703
+ return parsed;
24501
24704
  }
24502
24705
 
24503
24706
  // src/services/ai-system-prompt.ts
@@ -25209,153 +25412,6 @@ function buildProjectSummary({
25209
25412
  `);
25210
25413
  }
25211
25414
 
25212
- // src/services/context-manager.ts
25213
- var CHARS_PER_TOKEN = 4;
25214
- var MODEL_CONTEXT_WINDOWS = [
25215
- ["gpt-4o", 128000],
25216
- ["gpt-4.1", 1e6],
25217
- ["gpt-4-turbo", 128000],
25218
- ["gpt-4", 8192],
25219
- ["gpt-3.5", 16385],
25220
- ["o1", 200000],
25221
- ["o3", 200000],
25222
- ["claude", 200000]
25223
- ];
25224
- var DEFAULT_CONTEXT_WINDOW = 32000;
25225
- var BUDGET_FRACTION = 0.8;
25226
- var WARN_FRACTION = 0.5;
25227
- function contextWindowFor(model) {
25228
- if (!model) {
25229
- return DEFAULT_CONTEXT_WINDOW;
25230
- }
25231
- const reported = modelContextWindow(model);
25232
- if (reported !== undefined && reported > 0) {
25233
- return reported;
25234
- }
25235
- const id = model.toLowerCase();
25236
- let best = 0;
25237
- let window2 = DEFAULT_CONTEXT_WINDOW;
25238
- for (const [prefix, size] of MODEL_CONTEXT_WINDOWS) {
25239
- if (id.startsWith(prefix) && prefix.length > best) {
25240
- best = prefix.length;
25241
- window2 = size;
25242
- }
25243
- }
25244
- return window2;
25245
- }
25246
- var KEEP_RECENT = 20;
25247
- var MIN_USER_TURNS = 3;
25248
- function estimateTokens(input) {
25249
- if (typeof input === "string") {
25250
- return Math.ceil(input.length / CHARS_PER_TOKEN);
25251
- }
25252
- let total = 0;
25253
- for (const msg of input) {
25254
- total += estimateTokens(msg.content || "");
25255
- if (msg.tool_calls) {
25256
- for (const tc of msg.tool_calls) {
25257
- total += estimateTokens(tc.function?.name || "") + estimateTokens(tc.function?.arguments || "") + 4;
25258
- }
25259
- }
25260
- total += 4;
25261
- }
25262
- return total;
25263
- }
25264
- function trimContext(chatState, systemPrompt) {
25265
- const window2 = contextWindowFor(chatState.model);
25266
- const maxTokens = Math.floor(window2 * BUDGET_FRACTION);
25267
- const warnTokens = Math.floor(window2 * WARN_FRACTION);
25268
- const systemTokens = estimateTokens(systemPrompt);
25269
- const allMessages = chatState.messages;
25270
- const messageTokens = estimateTokens(chatState.toMessagesArray());
25271
- const total = systemTokens + messageTokens;
25272
- if (total <= maxTokens) {
25273
- chatState.setTokenCount(total);
25274
- chatState.setContextWarning(total >= warnTokens);
25275
- return { estimatedTokens: total, droppedCount: 0 };
25276
- }
25277
- let keepFrom = Math.max(0, allMessages.length - KEEP_RECENT);
25278
- let preservedTurns = 0;
25279
- for (let i = allMessages.length - 1;i >= keepFrom; i--) {
25280
- const { role } = allMessages[i];
25281
- if (role === "user" || role === "tool") {
25282
- preservedTurns += 1;
25283
- }
25284
- }
25285
- if (preservedTurns < MIN_USER_TURNS) {
25286
- for (let i = keepFrom - 1;i >= 0 && preservedTurns < MIN_USER_TURNS; i--) {
25287
- keepFrom = i;
25288
- const { role } = allMessages[i];
25289
- if (role === "user" || role === "tool") {
25290
- preservedTurns += 1;
25291
- }
25292
- }
25293
- }
25294
- if (keepFrom <= 0) {
25295
- chatState.setTokenCount(total);
25296
- chatState.setContextWarning(true);
25297
- return { estimatedTokens: total, droppedCount: 0 };
25298
- }
25299
- const droppedCount = keepFrom;
25300
- allMessages.splice(0, keepFrom);
25301
- allMessages.unshift({
25302
- id: `ctx_summary_${Date.now()}`,
25303
- role: "user",
25304
- content: `[Earlier conversation truncated. ${droppedCount} messages dropped to stay within token budget. The most recent ${allMessages.length - 1} messages are preserved.]`,
25305
- timestamp: Date.now()
25306
- });
25307
- const newTotal = systemTokens + estimateTokens(chatState.toMessagesArray());
25308
- chatState.setTokenCount(newTotal);
25309
- chatState.setContextWarning(true);
25310
- return { estimatedTokens: newTotal, droppedCount };
25311
- }
25312
- var UNANSWERED_TOOL_RESULT = JSON.stringify({
25313
- success: false,
25314
- error: "This tool call was never completed — the session was reloaded or the history was trimmed."
25315
- });
25316
- function pruneOrphanToolMessages(chatState) {
25317
- const { messages } = chatState;
25318
- const requested = new Set;
25319
- const answered = new Set;
25320
- for (const msg of messages) {
25321
- if (msg.role === "assistant" && msg.toolCalls) {
25322
- for (const call of msg.toolCalls) {
25323
- requested.add(call.id);
25324
- }
25325
- } else if (msg.role === "tool" && msg.toolCallId) {
25326
- answered.add(msg.toolCallId);
25327
- }
25328
- }
25329
- let dropped = 0;
25330
- for (let i = messages.length - 1;i >= 0; i--) {
25331
- const msg = messages[i];
25332
- if (msg.role === "tool" && (!msg.toolCallId || !requested.has(msg.toolCallId))) {
25333
- messages.splice(i, 1);
25334
- dropped += 1;
25335
- }
25336
- }
25337
- let sealed = 0;
25338
- for (let i = messages.length - 1;i >= 0; i--) {
25339
- const msg = messages[i];
25340
- if (msg.role !== "assistant" || !msg.toolCalls) {
25341
- continue;
25342
- }
25343
- const missing = msg.toolCalls.filter((call) => !answered.has(call.id));
25344
- if (missing.length === 0) {
25345
- continue;
25346
- }
25347
- messages.splice(i + 1, 0, ...missing.map((call, n) => ({
25348
- content: UNANSWERED_TOOL_RESULT,
25349
- id: `sealed_${call.id}_${n}`,
25350
- role: "tool",
25351
- timestamp: msg.timestamp,
25352
- toolCallId: call.id
25353
- })));
25354
- sealed += missing.length;
25355
- }
25356
- return { dropped, sealed };
25357
- }
25358
-
25359
25415
  // src/services/render-critic.ts
25360
25416
  function translateRenderError(err) {
25361
25417
  const msg = err.message || String(err);
@@ -25558,6 +25614,76 @@ function moveSession(fromRoot, toRoot, id) {
25558
25614
  writeIndex(toRoot, toIndex);
25559
25615
  }
25560
25616
 
25617
+ // src/services/tool-outcomes.ts
25618
+ var ASK_TOOL = "ask_user";
25619
+ var NEVER_COMPLETED = JSON.parse(UNANSWERED_TOOL_RESULT);
25620
+ function stripToolResults(messages) {
25621
+ return messages.map((message) => {
25622
+ if (!Array.isArray(message.toolCalls)) {
25623
+ return message;
25624
+ }
25625
+ return {
25626
+ ...message,
25627
+ toolCalls: message.toolCalls.map((call) => {
25628
+ if (!call || typeof call !== "object") {
25629
+ return call;
25630
+ }
25631
+ const { result: _result, ...rest } = call;
25632
+ return rest;
25633
+ })
25634
+ };
25635
+ });
25636
+ }
25637
+ function parseToolResult(content) {
25638
+ if (typeof content !== "string") {
25639
+ return null;
25640
+ }
25641
+ try {
25642
+ const parsed = JSON.parse(content);
25643
+ return parsed && typeof parsed === "object" && typeof parsed.success === "boolean" ? parsed : null;
25644
+ } catch {
25645
+ return null;
25646
+ }
25647
+ }
25648
+ function backfillToolResults(messages) {
25649
+ return messages.map((message, index) => {
25650
+ if (message.role !== "assistant" || !Array.isArray(message.toolCalls)) {
25651
+ return message;
25652
+ }
25653
+ const replies = new Map;
25654
+ const sealed = new Set;
25655
+ for (let next = index + 1;next < messages.length; next++) {
25656
+ const reply = messages[next];
25657
+ if (reply.role !== "tool") {
25658
+ break;
25659
+ }
25660
+ const result = parseToolResult(reply.content);
25661
+ if (typeof reply.toolCallId === "string" && result && !replies.has(reply.toolCallId)) {
25662
+ replies.set(reply.toolCallId, result);
25663
+ if (reply.content === UNANSWERED_TOOL_RESULT) {
25664
+ sealed.add(reply.toolCallId);
25665
+ }
25666
+ }
25667
+ }
25668
+ return {
25669
+ ...message,
25670
+ toolCalls: message.toolCalls.map((call) => {
25671
+ if (!call || typeof call !== "object") {
25672
+ return call;
25673
+ }
25674
+ const record = call;
25675
+ const id = typeof record.id === "string" ? record.id : undefined;
25676
+ const answered = id === undefined ? undefined : replies.get(id);
25677
+ if (record.name === ASK_TOOL) {
25678
+ const reply = answered && id !== undefined && !sealed.has(id) ? answered : undefined;
25679
+ return { ...record, result: reply ?? record.result ?? null };
25680
+ }
25681
+ return { ...record, result: answered ?? record.result ?? NEVER_COMPLETED };
25682
+ })
25683
+ };
25684
+ });
25685
+ }
25686
+
25561
25687
  // src/services/document-assistant.ts
25562
25688
  function projectRoot() {
25563
25689
  return workspace2.projectRoot || "";
@@ -25565,6 +25691,7 @@ function projectRoot() {
25565
25691
  function createDocumentAssistant() {
25566
25692
  const chatState = createChatState({ model: preferredModel() });
25567
25693
  let sessionId = null;
25694
+ let sessionFacts = createSessionFacts();
25568
25695
  const getProjectStyle = () => workspace2.projectConfig?.style;
25569
25696
  const findOpenTab = (path) => {
25570
25697
  for (const tab of workspace2.tabs.values()) {
@@ -25575,7 +25702,7 @@ function createDocumentAssistant() {
25575
25702
  return null;
25576
25703
  };
25577
25704
  const innerRegistry = createToolRegistry();
25578
- registerAskTool(innerRegistry);
25705
+ registerAskTool(innerRegistry, { onPending: () => persistChat() });
25579
25706
  registerAiTools(innerRegistry, {
25580
25707
  getTab: () => activeTab2.value,
25581
25708
  saveFile: async (relPath, content) => {
@@ -25644,6 +25771,8 @@ function createDocumentAssistant() {
25644
25771
  });
25645
25772
  const toolRegistry = composeToolRegistries(createGatedToolRegistry(innerRegistry, availability), commandTools);
25646
25773
  let controller = null;
25774
+ const turnActive = shallowRef(false);
25775
+ let turnEnded = Promise.resolve();
25647
25776
  function buildPrompt() {
25648
25777
  const tab = activeTab2.value;
25649
25778
  const inventory = projectState ? [...projectState.dirs.values()].flat().filter((e) => e.type === "file").map((e) => e.path) : undefined;
@@ -25670,7 +25799,7 @@ function createDocumentAssistant() {
25670
25799
  });
25671
25800
  }
25672
25801
  async function sendMessage(text) {
25673
- if (!text.trim() || chatState.status === "streaming") {
25802
+ if (!text.trim() || turnActive.value || chatState.status === "streaming") {
25674
25803
  return;
25675
25804
  }
25676
25805
  if (!sessionId) {
@@ -25683,32 +25812,49 @@ function createDocumentAssistant() {
25683
25812
  }
25684
25813
  pruneOrphanToolMessages(chatState);
25685
25814
  persistChat();
25815
+ controller = new AbortController;
25816
+ const { signal } = controller;
25817
+ turnActive.value = true;
25818
+ let settleTurnEnded;
25819
+ turnEnded = new Promise((settle) => {
25820
+ settleTurnEnded = settle;
25821
+ });
25686
25822
  try {
25687
25823
  const plat = getPlatform2();
25688
- const chatUrl = await Promise.resolve(plat.aiChatUrl());
25689
25824
  chatState.setModel(preferredModel());
25690
25825
  const streamingClient = createProxyStreamingClient({
25691
- chatUrl,
25826
+ chatUrl: () => plat.aiChatUrl(),
25692
25827
  model: chatState.model,
25693
25828
  apiKey: getOpenAiKey() || undefined,
25694
25829
  baseUrl: getBaseUrl() || undefined
25695
25830
  });
25696
- controller = new AbortController;
25831
+ if (sessionFacts.sessionId !== sessionId) {
25832
+ sessionFacts = createSessionFacts(sessionId, sessionFacts.toJSON());
25833
+ }
25697
25834
  await runAgentLoop({
25698
25835
  chatState,
25699
25836
  streamingClient,
25700
25837
  toolRegistry,
25701
25838
  systemPrompt: buildPrompt(),
25702
- signal: controller.signal,
25839
+ signal,
25840
+ session: sessionFacts,
25703
25841
  getTab: () => activeTab2.value
25704
25842
  });
25705
25843
  } catch (error) {
25706
25844
  chatState.setError(error instanceof Error ? error.message : String(error));
25707
25845
  } finally {
25708
25846
  controller = null;
25847
+ turnActive.value = false;
25709
25848
  persistChat();
25849
+ settleTurnEnded();
25710
25850
  }
25711
25851
  }
25852
+ function isTurnActive() {
25853
+ return turnActive.value;
25854
+ }
25855
+ function whenTurnEnds() {
25856
+ return turnEnded;
25857
+ }
25712
25858
  function stop() {
25713
25859
  controller?.abort();
25714
25860
  cancelAsk();
@@ -25719,7 +25865,8 @@ function createDocumentAssistant() {
25719
25865
  stop();
25720
25866
  resetAsk();
25721
25867
  resetImportRuns();
25722
- resetImportGuard();
25868
+ resetAiWrites();
25869
+ sessionFacts = createSessionFacts();
25723
25870
  chatState.clearChat();
25724
25871
  sessionId = null;
25725
25872
  setActiveSession(projectRoot(), null);
@@ -25756,10 +25903,10 @@ function createDocumentAssistant() {
25756
25903
  return;
25757
25904
  }
25758
25905
  const msgs = chatState.messages.filter((m) => m.role !== "assistant" || m.content || (m.toolCalls?.length ?? 0) > 0);
25759
- saveSession(projectRoot(), sessionId, msgs);
25906
+ saveSession(projectRoot(), sessionId, stripToolResults(msgs));
25760
25907
  }
25761
25908
  function pushRestoredMessages(msgs) {
25762
- for (const m of msgs) {
25909
+ for (const m of backfillToolResults(msgs)) {
25763
25910
  chatState.messages.push({
25764
25911
  ...m,
25765
25912
  id: m.id || `restored_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
@@ -25782,7 +25929,9 @@ function createDocumentAssistant() {
25782
25929
  restoreChat();
25783
25930
  return {
25784
25931
  chatState,
25932
+ isTurnActive,
25785
25933
  sendMessage,
25934
+ whenTurnEnds,
25786
25935
  stop,
25787
25936
  newChat,
25788
25937
  listSessions: listSessions2,
@@ -25795,7 +25944,7 @@ function createDocumentAssistant() {
25795
25944
  var ai_credentials_form_default = {
25796
25945
  $schema: "https://jxsuite.com/schema/v1",
25797
25946
  $id: "StudioAiCredentialsForm",
25798
- $description: "The AI provider credentials form every gate embeds below the keyless offer: a key, a model, an endpoint, and the two buttons that keep or abandon them. Everything here is a projection — `ui/ai-credentials-form.ts` keeps the drafts, decides what a fetch is sent with, what Save actually persists and whether Cancel is on offer at all. This document never reads a setting and never calls the proxy.",
25947
+ $description: "The AI provider credentials form every gate embeds below the keyless offer: a key, a model, an endpoint, and the two buttons that keep or abandon them. Everything here is a projection — `ui/ai-credentials-form.ts` keeps the drafts, decides what a fetch is sent with, what Save actually persists and whether there is anything to Save or Cancel at all. This document never reads a setting and never calls the proxy.",
25799
25948
  tagName: "div",
25800
25949
  attributes: {
25801
25950
  part: "ai-creds-form",
@@ -25811,8 +25960,7 @@ var ai_credentials_form_default = {
25811
25960
  maxWidth: "320px",
25812
25961
  textAlign: "left",
25813
25962
  '& [part="title"]': {
25814
- $description: "Centred over a left-aligned column: the form is a titled block, not a list of labelled rows.",
25815
- alignSelf: "center",
25963
+ $description: "Weight alone, and no `align-self`. It was centred over the column on the grounds that the form is a titled block rather than a list of labelled rows, and in Preferences it is neither: the sheet's own heading, the blurb, this note, the `Model` label and all three fields start at the column's inline edge, so a centred line 102px wide in a 320px column read as a stray, not as a title. A heading belongs over the first character of the thing it names.",
25816
25964
  fontWeight: "600"
25817
25965
  },
25818
25966
  '& [part="note"]': {
@@ -25832,17 +25980,22 @@ var ai_credentials_form_default = {
25832
25980
  width: "100%"
25833
25981
  },
25834
25982
  '& [part="models"]': {
25983
+ $description: "A wrapping row that holds the button and, when there is one, the reason the listing did not finish — and the reason always takes a line of its own, which is what `flex-basis: 100%` on it (below) is for. Beside the button it began 96px into a 320px column, so a six-line upstream body read as a paragraph indented past the control it answers for, while the button floated at that block's vertical middle with about 40px of dead space around it. Under the button, at the column's own inline edge, the reason starts where every other line in the form starts. `flex-start` rather than `center` so that a line which does hold two boxes lines up their first lines and not their middles.",
25835
25984
  display: "flex",
25836
- alignItems: "center",
25985
+ flexWrap: "wrap",
25986
+ alignItems: "flex-start",
25837
25987
  gap: "var(--jx-space-3)"
25838
25988
  },
25839
25989
  '& [part="models-error"]': {
25840
- $description: 'Why the listing did not finish, beside the button that would try it again. Named `models-error` rather than `error` because the kit\'s own fields render a permanent `[part="error"]` region of their own, and a surface that reused the name would be addressing three empty paragraphs before it reached its own.',
25990
+ $description: 'Why the listing did not finish, said under the button that would try it again. Named `models-error` rather than `error` because the kit\'s own fields render a permanent `[part="error"]` region of their own, and a surface that reused the name would be addressing three empty paragraphs before it reached its own. `flexBasis: 100%` is what puts it under the button rather than beside it: the row wraps, and a basis of the whole line cannot share one, so however short the reason is it always reads as a line of the form (`[part="models"]`). `minWidth: 0` and `overflowWrap: anywhere` because the reason is often an upstream body passed through whole — a 401\'s JSON is one unbroken word — and a flex item\'s automatic minimum is its min-content width: one measured 853px inside the 320px column, and scrolled the whole Preferences section sideways over the buttons below it.',
25841
25991
  color: "var(--danger)",
25842
- fontSize: "var(--jx-text-sm)"
25992
+ fontSize: "var(--jx-text-sm)",
25993
+ flexBasis: "100%",
25994
+ minWidth: "0",
25995
+ overflowWrap: "anywhere"
25843
25996
  },
25844
25997
  '& [part="actions"]': {
25845
- $description: "Buttons at the end of the column, in the order a dismissal precedes a commitment.",
25998
+ $description: 'Buttons at the end of the column, in the order a dismissal precedes a commitment. Still a flex item of the column although it is drawn inside `[part="actions-slot"]`, because that slot is `display: contents` — which is what keeps `alignSelf` meaning the column\'s end.',
25846
25999
  display: "flex",
25847
26000
  alignSelf: "flex-end",
25848
26001
  gap: "var(--jx-space-3)"
@@ -26044,26 +26197,26 @@ var ai_credentials_form_default = {
26044
26197
  }
26045
26198
  },
26046
26199
  {
26200
+ $description: "The commit pair exists only while there is something to commit: `dirty` is the flow's answer to whether Save would change what is stored, compared after the store's own normalisation, so a pasted key's whitespace is not an edit. At rest neither button is drawn. The rule this replaced offered Cancel whenever a key was stored and Save always, so Preferences › Assistant showed a Save that would re-store what was there and a Cancel that abandoned nothing, beside the dialog's own Close. Absent rather than disabled, against the house rule for a control that cannot act, because the pair IS the pending edit's affordance: a disabled pair at rest would still be the two buttons the reader reported seeing with nothing to save, each needing a reason to give for being there. Answering either takes away the button that was pressed, so `surfaces/ai-credentials-form.ts` lands focus on the key field rather than letting it fall out of the sheet.",
26047
26201
  tagName: "div",
26048
26202
  attributes: {
26049
- part: "actions"
26203
+ part: "actions-slot",
26204
+ role: "none"
26050
26205
  },
26051
- children: [
26052
- {
26206
+ style: {
26207
+ display: "contents"
26208
+ },
26209
+ $switch: {
26210
+ $ref: "#/state/dirty"
26211
+ },
26212
+ cases: {
26213
+ true: {
26053
26214
  tagName: "div",
26054
26215
  attributes: {
26055
- part: "cancel-slot",
26056
- role: "none"
26057
- },
26058
- style: {
26059
- display: "contents"
26060
- },
26061
- $switch: {
26062
- $ref: "#/state/haveKey"
26216
+ part: "actions"
26063
26217
  },
26064
- cases: {
26065
- true: {
26066
- $description: "Only offered once a key exists: with nothing stored there is nothing to go back to, and a Cancel that abandons an empty form to an empty form is a button that does nothing.",
26218
+ children: [
26219
+ {
26067
26220
  tagName: "jx-button",
26068
26221
  attributes: {
26069
26222
  part: "cancel"
@@ -26089,38 +26242,38 @@ var ai_credentials_form_default = {
26089
26242
  textContent: "Cancel"
26090
26243
  }
26091
26244
  ]
26092
- }
26093
- }
26094
- },
26095
- {
26096
- tagName: "jx-button",
26097
- attributes: {
26098
- part: "save"
26099
- },
26100
- $props: {
26101
- size: "sm",
26102
- variant: "accent"
26103
- },
26104
- onclick: {
26105
- $prototype: "Function",
26106
- body: [
26107
- {
26108
- operator: "call",
26109
- target: {
26110
- $ref: "#/state/save"
26111
- },
26112
- value: []
26113
- }
26114
- ]
26115
- },
26116
- children: [
26245
+ },
26117
26246
  {
26118
- tagName: "span",
26119
- textContent: "Save"
26247
+ tagName: "jx-button",
26248
+ attributes: {
26249
+ part: "save"
26250
+ },
26251
+ $props: {
26252
+ size: "sm",
26253
+ variant: "accent"
26254
+ },
26255
+ onclick: {
26256
+ $prototype: "Function",
26257
+ body: [
26258
+ {
26259
+ operator: "call",
26260
+ target: {
26261
+ $ref: "#/state/save"
26262
+ },
26263
+ value: []
26264
+ }
26265
+ ]
26266
+ },
26267
+ children: [
26268
+ {
26269
+ tagName: "span",
26270
+ textContent: "Save"
26271
+ }
26272
+ ]
26120
26273
  }
26121
26274
  ]
26122
26275
  }
26123
- ]
26276
+ }
26124
26277
  }
26125
26278
  ]
26126
26279
  };
@@ -26151,7 +26304,11 @@ function createAiCredentialsSurface(view, actions) {
26151
26304
  return {
26152
26305
  host,
26153
26306
  update(next) {
26307
+ const answering = !next.dirty && host.querySelector('[part="actions"]')?.contains(document.activeElement);
26154
26308
  Object.assign(scope, derive3(next));
26309
+ if (answering && !host.contains(document.activeElement)) {
26310
+ host.querySelector('[part="key"] [part="input"]')?.focus();
26311
+ }
26155
26312
  if (mounting || handle !== null && host.contains(handle.root)) {
26156
26313
  return;
26157
26314
  }
@@ -26172,11 +26329,23 @@ function createAiCredentialsForm(opts) {
26172
26329
  let availableModels = [];
26173
26330
  let modelsLoading = false;
26174
26331
  let modelsError = "";
26332
+ let basis = { apiKey: "", baseUrl: "", model: "" };
26175
26333
  let surface = null;
26334
+ function stored() {
26335
+ return { apiKey: getOpenAiKey(), baseUrl: getBaseUrl(), model: storedModel() };
26336
+ }
26176
26337
  function loadDrafts() {
26177
- keyDraft = getOpenAiKey();
26178
- baseUrlDraft = getBaseUrl();
26179
- modelDraft = storedModel();
26338
+ basis = stored();
26339
+ keyDraft = basis.apiKey;
26340
+ baseUrlDraft = basis.baseUrl;
26341
+ modelDraft = basis.model;
26342
+ }
26343
+ function followStore() {
26344
+ const now = stored();
26345
+ keyDraft = storedEquals(SETTINGS.aiOpenAiKey, keyDraft, basis.apiKey) ? now.apiKey : keyDraft;
26346
+ baseUrlDraft = storedEquals(SETTINGS.aiBaseUrl, baseUrlDraft, basis.baseUrl) ? now.baseUrl : baseUrlDraft;
26347
+ modelDraft = storedEquals(SETTINGS.aiModel, modelDraft, basis.model) ? now.model : modelDraft;
26348
+ basis = now;
26180
26349
  }
26181
26350
  function startEdit() {
26182
26351
  loadDrafts();
@@ -26227,8 +26396,8 @@ function createAiCredentialsForm(opts) {
26227
26396
  function view() {
26228
26397
  return {
26229
26398
  baseUrlDraft,
26399
+ dirty: aiProviderDiffers({ apiKey: keyDraft, baseUrl: baseUrlDraft, model: modelDraft }),
26230
26400
  fetchLabel: modelsLoading ? "Fetching…" : availableModels.length > 0 ? "Refresh models" : "Fetch models",
26231
- haveKey: hasOpenAiKey(),
26232
26401
  intro: opts.intro ?? DEFAULT_INTRO,
26233
26402
  keyDraft,
26234
26403
  modelDraft,
@@ -26250,6 +26419,7 @@ function createAiCredentialsForm(opts) {
26250
26419
  surface?.update(view());
26251
26420
  }
26252
26421
  function render() {
26422
+ followStore();
26253
26423
  if (surface) {
26254
26424
  surface.update(view());
26255
26425
  } else {
@@ -26293,8 +26463,8 @@ var ai_managed_connect_default = {
26293
26463
  maxWidth: "280px",
26294
26464
  textAlign: "center"
26295
26465
  },
26296
- '& [part="error"]': {
26297
- $description: "Why the last attempt did not finish, said under the button that would try it again. Narrower than the lede: a refusal is a sentence to read once, not the thing the surface is about.",
26466
+ '& [part="connect-failure"]': {
26467
+ $description: 'Why the last attempt did not finish, said under the button that would try it again. Narrower than the lede: a refusal is a sentence to read once, not the thing the surface is about. The name is qualified, and BOTH halves of it are load-bearing. Not `error`, because every kit field draws a permanent, empty `[part="error"]` of its own and the kit is light DOM, so a rule on that name would reach any field this surface ever holds. Not the bare `failure` every panel banner uses either, because this document is MOUNTED inside another one: `new-project.json` puts it in `[part="managed-island"]` and styles its own backend refusal with a descendant `[part="failure"]`, which would draw this sentence with that dialog\'s footer border and padding. A guest surface picks a name its host does not write, and `tests/surface-part-names.test.ts` holds the two island hosts to it.',
26298
26468
  margin: "0",
26299
26469
  color: "var(--danger)",
26300
26470
  fontSize: "var(--jx-text-sm)",
@@ -26349,7 +26519,7 @@ var ai_managed_connect_default = {
26349
26519
  {
26350
26520
  tagName: "div",
26351
26521
  attributes: {
26352
- part: "error-slot",
26522
+ part: "connect-failure-slot",
26353
26523
  role: "none"
26354
26524
  },
26355
26525
  style: {
@@ -26362,7 +26532,7 @@ var ai_managed_connect_default = {
26362
26532
  true: {
26363
26533
  tagName: "p",
26364
26534
  attributes: {
26365
- part: "error"
26535
+ part: "connect-failure"
26366
26536
  },
26367
26537
  textContent: "${state.error}"
26368
26538
  }
@@ -26415,6 +26585,7 @@ function createManagedConnectSurface(view, actions) {
26415
26585
  // src/ui/ai-managed-connect.ts
26416
26586
  var LEDE_FRESH = "Recommended — run the assistant on Workers AI in your own Cloudflare account. No API key to create, copy or rotate.";
26417
26587
  var LEDE_LAPSED = "Your Cloudflare connection has expired. Reconnect to keep using the assistant.";
26588
+ var LEDE_ACCOUNT = "Cloudflare is connected, but no account is chosen yet. Pick the account the assistant should run on.";
26418
26589
  function createManagedConnect(opts) {
26419
26590
  let busy = false;
26420
26591
  let connectError = "";
@@ -26425,6 +26596,26 @@ function createManagedConnect(opts) {
26425
26596
  }
26426
26597
  return !isProxyConfigured() || proxyStateCode() === "cf_reconnect_required";
26427
26598
  }
26599
+ const needsAccount = () => proxyStateCode() === "cf_account_required";
26600
+ async function verifyConnection() {
26601
+ resetModelCache();
26602
+ await fetchAvailableModels({ force: true });
26603
+ if (proxyStateCode() === "cf_account_required") {
26604
+ connectError = "Cloudflare still reports no account chosen for the assistant. Pick one again, or use your own API key below.";
26605
+ } else if (!isProxyConfigured() || proxyStateCode() === "cf_reconnect_required") {
26606
+ connectError = "Cloudflare connected, but the assistant backend still reports the connection as unusable. " + "Try reconnecting, or use your own API key below.";
26607
+ }
26608
+ }
26609
+ async function chooseAccount() {
26610
+ const { openCfAccountPicker } = await (__preload("3f08rry6"), import("./chunks/cf-account-picker-3f08rry6.js"));
26611
+ if (!await openCfAccountPicker()) {
26612
+ resetModelCache();
26613
+ await fetchAvailableModels({ force: true });
26614
+ connectError = "Cloudflare is connected, but no account is chosen yet — pick one to finish setting up the assistant.";
26615
+ return;
26616
+ }
26617
+ await verifyConnection();
26618
+ }
26428
26619
  function ensureProbe() {
26429
26620
  ensureProxyProbe(opts.requestRender);
26430
26621
  }
@@ -26437,17 +26628,10 @@ function createManagedConnect(opts) {
26437
26628
  return;
26438
26629
  }
26439
26630
  if (!outcome.connection.accountId) {
26440
- const { openCfAccountPicker } = await (__preload("h4dab9bq"), import("./chunks/cf-account-picker-h4dab9bq.js"));
26441
- if (!await openCfAccountPicker()) {
26442
- connectError = "Cloudflare is connected, but no account is chosen yet — pick one to finish setting up the assistant.";
26443
- return;
26444
- }
26445
- }
26446
- resetModelCache();
26447
- await fetchAvailableModels({ force: true });
26448
- if (!isProxyConfigured() || proxyStateCode() === "cf_reconnect_required") {
26449
- connectError = "Cloudflare connected, but the assistant backend still reports the connection as unusable. Try reconnecting, or use your own API key below.";
26631
+ await chooseAccount();
26632
+ return;
26450
26633
  }
26634
+ await verifyConnection();
26451
26635
  }
26452
26636
  async function connect() {
26453
26637
  if (busy) {
@@ -26457,7 +26641,7 @@ function createManagedConnect(opts) {
26457
26641
  connectError = "";
26458
26642
  opts.requestRender();
26459
26643
  try {
26460
- await settle(await getPlatform2().cfConnect?.() ?? null);
26644
+ await (needsAccount() ? chooseAccount() : settle(await getPlatform2().cfConnect?.() ?? null));
26461
26645
  } catch (error) {
26462
26646
  connectError = error instanceof Error ? error.message : String(error);
26463
26647
  }
@@ -26465,6 +26649,14 @@ function createManagedConnect(opts) {
26465
26649
  opts.requestRender();
26466
26650
  }
26467
26651
  function view() {
26652
+ if (needsAccount()) {
26653
+ return {
26654
+ buttonLabel: busy ? "Choosing…" : "Choose Cloudflare account",
26655
+ busy,
26656
+ error: connectError,
26657
+ intro: LEDE_ACCOUNT
26658
+ };
26659
+ }
26468
26660
  const lapsed = proxyStateCode() === "cf_reconnect_required";
26469
26661
  const idleLabel = lapsed ? "Reconnect Cloudflare" : "Connect Cloudflare";
26470
26662
  return {
@@ -26758,7 +26950,8 @@ var preferences_default = {
26758
26950
  maxHeight: "60vh",
26759
26951
  overflowY: "auto"
26760
26952
  },
26761
- '& [part="title"]': {
26953
+ '& [part="section"] > [part="title"]': {
26954
+ $description: 'The sheet\'s own heading, and a CHILD selector because the Assistant section mounts other documents into this one\'s light DOM: the credentials form draws a `[part="title"]` of its own ("AI provider key"), and as a bare descendant this rule set the section heading\'s size on it. A host styles the boxes it draws, never a name its guest also uses — `tests/surface-part-names.test.ts` holds the island hosts to that.',
26762
26955
  margin: "0",
26763
26956
  fontSize: "var(--jx-text-lg)",
26764
26957
  color: "var(--fg)"
@@ -26794,8 +26987,10 @@ var preferences_default = {
26794
26987
  gap: "var(--jx-space-2)"
26795
26988
  },
26796
26989
  '& [part="account"]': {
26990
+ $description: "A wrapping row (studio-ui-guidelines.md §4.6): the sentence and the actions share one line while the sentence keeps its basis, and the actions drop under it when they would not. A brokered Cloudflare row carries two actions (Reconnect or Choose account, beside Disconnect), and on one line they squeezed the sentence into a column about 114px wide at the default width. Every single-action row measures exactly what it did before the wrap existed. `align-items: flex-start`, not `center`, because the sentence's height is not the row's: the AI row's endpoint wraps to five lines, and centred that put Disconnect level with the fourth line of a URL while the heading it acts on sat at the top, so the card read as two unrelated things. Pinned to the start, the button is always beside its label.",
26797
26991
  display: "flex",
26798
- alignItems: "center",
26992
+ flexWrap: "wrap",
26993
+ alignItems: "flex-start",
26799
26994
  justifyContent: "space-between",
26800
26995
  gap: "var(--jx-space-4)",
26801
26996
  padding: "var(--jx-space-3)",
@@ -26803,8 +26998,9 @@ var preferences_default = {
26803
26998
  borderRadius: "var(--radius)"
26804
26999
  },
26805
27000
  '& [part="account-text"]': {
26806
- $description: "`min-width: 0` so a long detail sentence ellipses inside the row rather than pushing the row's own buttons off the end of it.",
27001
+ $description: "The wrap threshold is this basis, not a width: 120px of sentence is the least a row keeps before its actions take a line of their own. Keep it well under 200px, or a single-action row at the default sheet width wraps at rest. `min-width: 0` lets the column shrink below its content, and what then fits the detail into it is the detail's own `overflow-wrap`. Nothing here ellipsizes: the detail is the only place the row names its endpoint or account, so a truncated one would hide exactly the part that tells two of them apart.",
26807
27002
  display: "flex",
27003
+ flex: "1 1 120px",
26808
27004
  flexDirection: "column",
26809
27005
  gap: "var(--jx-space-1)",
26810
27006
  minWidth: "0"
@@ -26813,19 +27009,25 @@ var preferences_default = {
26813
27009
  color: "var(--fg)"
26814
27010
  },
26815
27011
  '& [part="account-detail"]': {
27012
+ $description: "`overflow-wrap: anywhere` because the detail carries author-supplied strings with no break opportunity in them: the AI row's endpoint URL (a Cloudflare AI Gateway URL is a 32-hex account id between slashes) and the local Cloudflare row's account id. Without it one of them measured 419px in a 223px column, ran under Disconnect, past the card's border, and scrolled the whole section sideways. `anywhere` rather than `break-word`, because only `anywhere` lowers the min-content the row is sized from; not `word-break: break-all`, which would split every ordinary word in the sentences around it. It sits here rather than on the column because the label is a fixed service name and only the detail is unbounded.",
26816
27013
  fontSize: "var(--jx-text-sm)",
26817
- color: "var(--fg-dim)"
27014
+ color: "var(--fg-dim)",
27015
+ overflowWrap: "anywhere"
26818
27016
  },
26819
27017
  '& [part="account-actions"]': {
27018
+ $description: "`margin-inline-start: auto` keeps the actions at the row's end when the wrap puts them on a line of their own; on a shared line `space-between` already puts them there, so the margin changes nothing.",
26820
27019
  display: "flex",
26821
27020
  alignItems: "center",
26822
27021
  gap: "var(--jx-space-3)",
26823
- flexShrink: "0"
27022
+ flexShrink: "0",
27023
+ marginInlineStart: "auto"
26824
27024
  },
26825
27025
  '& [part="keys"]': {
27026
+ $description: "The inline-end padding is for the scrollbar, not for rhythm. A row whose title wraps pushes its `Change` button to the very end of the line, and the sheet always scrolls (64 bindings), so that button's right edge landed exactly on the scroller's content edge and read as touching the scrollbar track.",
26826
27027
  display: "flex",
26827
27028
  flexDirection: "column",
26828
- gap: "var(--jx-space-1)"
27029
+ gap: "var(--jx-space-1)",
27030
+ paddingInlineEnd: "var(--jx-space-2)"
26829
27031
  },
26830
27032
  '& [part="refusal"]': {
26831
27033
  display: "flex",
@@ -26850,9 +27052,10 @@ var preferences_default = {
26850
27052
  textTransform: "uppercase",
26851
27053
  color: "var(--fg-dim)"
26852
27054
  },
26853
- '& [part="key"]': {
27055
+ '& [part="key"][data-command]': {
27056
+ $description: "Qualified by `data-command`, the attribute a binding row carries, because `key` is also the credentials form's API-key FIELD and that form is mounted into this sheet's own light DOM: as a bare descendant this rule laid a 16px flex row over a `jx-textfield`, which drew it 24px tall beside the 20px endpoint field under it. `center`, not `baseline`: the `Change` and `Reset` buttons are small kit action buttons, and a small kit control is layout-contained so its outset hit area adds nothing to this sheet's scrollable overflow, which leaves it with no baseline to export. A baseline row synthesises one from the button's bottom edge instead, and every row grew from 24px to 29px with the chord and title dropped onto it. Centred, every single-line row measures exactly what the baseline layout did (24px, with the chord, the title and the button label on one baseline); a row whose title wraps centres its chord and buttons between the title's lines. surface-baseline-rows.test.ts holds the rule for every surface.",
26854
27057
  display: "flex",
26855
- alignItems: "baseline",
27058
+ alignItems: "center",
26856
27059
  gap: "var(--jx-space-4)",
26857
27060
  padding: "var(--jx-space-1) 0"
26858
27061
  },
@@ -26864,6 +27067,8 @@ var preferences_default = {
26864
27067
  color: "var(--fg)"
26865
27068
  },
26866
27069
  '& [part="key-title"]': {
27070
+ $description: "`flex: 1` for the same reason the chord beside it is a fixed 96px column: down 64 rows, an action that starts where the last title ended is not scannable. The title took its content's width, so `Change` landed at fourteen different offsets in the first twenty rows, and at the line's very end on a row whose title wrapped. Growing the title instead pins the actions to that end, where a reader finds them by scanning one edge. `minWidth: 0` is what still lets a long title wrap rather than widen the sheet.",
27071
+ flex: "1",
26867
27072
  color: "var(--fg-dim)",
26868
27073
  minWidth: "0"
26869
27074
  }
@@ -27608,6 +27813,7 @@ function openPreferencesSurface(options) {
27608
27813
  const box = element.querySelector('dialog[part="dialog"]') ?? element;
27609
27814
  box.setAttribute(REGION_ATTR, overlayRegion("dialog", "preferences"));
27610
27815
  showModal(element);
27816
+ element.querySelector('[part="nav-item"][aria-current="true"]')?.focus();
27611
27817
  return element;
27612
27818
  });
27613
27819
  return {
@@ -38895,6 +39101,9 @@ function formatErrorAdvice(error) {
38895
39101
  if (lower.includes("500") || lower.includes("internal")) {
38896
39102
  return "The upstream API returned a server error. Try again in a moment.";
38897
39103
  }
39104
+ if (lower.includes("empty reply")) {
39105
+ return "Try again, or pick another model: some send back nothing for a request they cannot handle.";
39106
+ }
38898
39107
  return "";
38899
39108
  }
38900
39109
  function projectCommand(id, opts = {}, registry = activeRegistry()) {
@@ -38924,7 +39133,7 @@ function tokenLabel2(tokens) {
38924
39133
  function tokenHint(tokens, overBudget) {
38925
39134
  return overBudget ? `About ${tokens.toLocaleString()} tokens — past half the model's context. ` + "The oldest turns are dropped as this grows; start a new chat to keep them." : `About ${tokens.toLocaleString()} tokens of the model's context in use`;
38926
39135
  }
38927
- var ASK_TOOL = "ask_user";
39136
+ var ASK_TOOL2 = "ask_user";
38928
39137
  var IMPORT_TOOL = "import_site";
38929
39138
  function parseAsk(tc) {
38930
39139
  let parsed;
@@ -38944,7 +39153,7 @@ function parseAsk(tc) {
38944
39153
  }
38945
39154
  function toolOutcome(tc, live = true) {
38946
39155
  if (!tc.result) {
38947
- return tc.name === ASK_TOOL && !live ? "unanswered" : "pending";
39156
+ return tc.name === ASK_TOOL2 && !live ? "unanswered" : "pending";
38948
39157
  }
38949
39158
  return tc.result.success ? "ok" : "failed";
38950
39159
  }
@@ -38953,7 +39162,12 @@ function toolOutcomeText(tc) {
38953
39162
  if (!result) {
38954
39163
  return "";
38955
39164
  }
38956
- return (result.success ? result.summary : result.error) ?? "";
39165
+ const text = (result.success ? result.summary : result.error) ?? "";
39166
+ if (tc.name === IMPORT_TOOL && result.success) {
39167
+ const end = /\.(?:\s|$)/.exec(text);
39168
+ return end ? text.slice(0, end.index + 1) : text;
39169
+ }
39170
+ return text;
38957
39171
  }
38958
39172
  function noImport() {
38959
39173
  return {
@@ -38992,7 +39206,7 @@ function projectChip(tc, handlers = {}) {
38992
39206
  const outcome = toolOutcome(tc, tc.id === handlers.pendingId || Boolean(tc.result));
38993
39207
  const text = toolOutcomeText(tc);
38994
39208
  const run = tc.name === IMPORT_TOOL ? handlers.importRun?.(tc.id) ?? null : null;
38995
- const ask = tc.name === ASK_TOOL ? parseAsk(tc) : null;
39209
+ const ask = tc.name === ASK_TOOL2 ? parseAsk(tc) : null;
38996
39210
  const answered = tc.result?.success ? tc.result.data ?? null : null;
38997
39211
  const base = {
38998
39212
  answer: answered ? answered.skipped ? "You decide" : answered.answer ?? "" : "",
@@ -39441,7 +39655,7 @@ function createComposer(opts) {
39441
39655
  }
39442
39656
  }
39443
39657
  function send() {
39444
- if (!draft.trim() || opts.isStreaming()) {
39658
+ if (!draft.trim() || opts.isStreaming() && !(opts.isAwaiting?.() ?? false)) {
39445
39659
  return;
39446
39660
  }
39447
39661
  const text = buildMessageWithContext(draft, chips);
@@ -39474,7 +39688,7 @@ function createComposer(opts) {
39474
39688
  placeholder: awaiting ? "Answer the assistant… (Enter to reply)" : "Ask the assistant… (Enter to send)",
39475
39689
  sendDisabled: draft.trim().length === 0,
39476
39690
  sendLabel: awaiting ? "Answer" : "Send",
39477
- sendState: streaming ? "stop" : "send"
39691
+ sendState: !awaiting && streaming ? "stop" : "send"
39478
39692
  };
39479
39693
  }
39480
39694
  return {
@@ -39635,12 +39849,15 @@ var ai_chat_default = {
39635
39849
  color: "var(--fg-dim)"
39636
39850
  },
39637
39851
  '& [part="user-body"]': {
39852
+ $description: "The transcript is content, not interface, and the panel offers no copy action, so each message body opts back in to selection against the root's `user-select: none`: this bubble, the model's `md`, the `streaming` text and the `import-log`. The header, the chips and the composer's buttons around them stay unselectable.",
39638
39853
  whiteSpace: "pre-wrap",
39639
- overflowWrap: "anywhere"
39854
+ overflowWrap: "anywhere",
39855
+ userSelect: "text"
39640
39856
  },
39641
39857
  '& [part="md"]': {
39642
- $description: "The island `panels/ai-chat/chat-markdown.ts` fills — the app's ONE injection sink, and the markup reaching it has been through `markdownToHtml` and the Trusted Types policy that asserts it did. The document renders the host node and nothing inside it (specs/studio-ui-guidelines.md §9.4).",
39643
- overflowWrap: "anywhere"
39858
+ $description: "The island `panels/ai-chat/chat-markdown.ts` fills — the app's ONE injection sink, and the markup reaching it has been through `markdownToHtml` and the Trusted Types policy that asserts it did. The document renders the host node and nothing inside it (specs/studio-ui-guidelines.md §9.4). A reply is the text a reader most often copies out of this panel, code blocks included, so it is selectable.",
39859
+ overflowWrap: "anywhere",
39860
+ userSelect: "text"
39644
39861
  },
39645
39862
  '& [part="md"] > *:first-child': {
39646
39863
  marginTop: "0"
@@ -39675,8 +39892,10 @@ var ai_chat_default = {
39675
39892
  padding: "var(--jx-space-1) 6px"
39676
39893
  },
39677
39894
  '& [part="streaming"]': {
39895
+ $description: 'The reply while it arrives, selectable for the reason `[part="md"]` is. The cursor is generated content, which no selection ever includes.',
39678
39896
  whiteSpace: "pre-wrap",
39679
39897
  overflowWrap: "anywhere",
39898
+ userSelect: "text",
39680
39899
  "::after": {
39681
39900
  content: '"▋"',
39682
39901
  color: "var(--accent)",
@@ -39852,6 +40071,8 @@ var ai_chat_default = {
39852
40071
  marginRight: "var(--jx-space-2)"
39853
40072
  },
39854
40073
  '& [part="import-log"]': {
40074
+ $description: "A captured log, kept selectable against the root's `user-select: none`: its lines are what a reader pastes into a bug report, and nothing here offers to copy them.",
40075
+ userSelect: "text",
39855
40076
  marginTop: "var(--jx-space-2)",
39856
40077
  maxHeight: "40vh",
39857
40078
  overflowY: "auto",
@@ -39907,7 +40128,8 @@ var ai_chat_default = {
39907
40128
  color: "var(--warning)",
39908
40129
  fontStyle: "normal"
39909
40130
  },
39910
- '& [part="error"]': {
40131
+ '& [part="error-card"]': {
40132
+ $description: "The failed turn's card. It is `error-card` rather than `error` because the model picker mounted into the composer is a `jx-select`, and every kit field draws a permanent, empty `[part=\"error\"]` of its own in this same light-DOM tree: under that name this rule, padding and wash drew an 18×12 red bar over the picker's top-left corner that took the click. Its children keep the `error-` prefix, which the kit does not use.",
39911
40133
  borderLeft: "2px solid var(--danger)",
39912
40134
  borderRadius: "var(--jx-radius-sm)",
39913
40135
  background: "var(--hover-bg)",
@@ -40090,7 +40312,7 @@ var ai_chat_default = {
40090
40312
  '& [part="tool-chip"][data-outcome="failed"]': {
40091
40313
  borderLeftColor: "LinkText"
40092
40314
  },
40093
- '& [part="error"]': {
40315
+ '& [part="error-card"]': {
40094
40316
  borderLeftColor: "LinkText"
40095
40317
  }
40096
40318
  },
@@ -41049,7 +41271,7 @@ var ai_chat_default = {
41049
41271
  shown: {
41050
41272
  tagName: "div",
41051
41273
  attributes: {
41052
- part: "error",
41274
+ part: "error-card",
41053
41275
  role: "alert"
41054
41276
  },
41055
41277
  children: [
@@ -41995,8 +42217,13 @@ function watchAssistant() {
41995
42217
  const last = cs.messages.at(-1);
41996
42218
  last?.content;
41997
42219
  last?.toolCalls?.length;
42220
+ for (const tc of latestRequest(cs.messages)?.toolCalls ?? []) {
42221
+ tc.result;
42222
+ }
41998
42223
  cs.status;
41999
42224
  cs.error;
42225
+ assistant.isTurnActive();
42226
+ cs.tokenCount;
42000
42227
  activeRegistry();
42001
42228
  pendingAsk();
42002
42229
  const run = activeImportRun();
@@ -42007,6 +42234,14 @@ function watchAssistant() {
42007
42234
  });
42008
42235
  });
42009
42236
  }
42237
+ function latestRequest(messages) {
42238
+ for (let index = messages.length - 1;index >= 0; index--) {
42239
+ if (messages[index].role !== "tool") {
42240
+ return messages[index];
42241
+ }
42242
+ }
42243
+ return;
42244
+ }
42010
42245
  function mountAiPanel() {
42011
42246
  if (mounted) {
42012
42247
  return;
@@ -42015,7 +42250,7 @@ function mountAiPanel() {
42015
42250
  onCredentialsChanged(renderAiPanel);
42016
42251
  }
42017
42252
  async function handleAssistantSend(text) {
42018
- if (!text.trim() || assistant.chatState.status === "streaming") {
42253
+ if (!text.trim()) {
42019
42254
  return;
42020
42255
  }
42021
42256
  if (answerAsk(text.trim())) {
@@ -42023,12 +42258,17 @@ async function handleAssistantSend(text) {
42023
42258
  renderAiPanel();
42024
42259
  return;
42025
42260
  }
42261
+ if (isAssistantStreaming()) {
42262
+ return;
42263
+ }
42026
42264
  view5 = "chat";
42027
42265
  surface?.pin();
42028
42266
  renderAiPanel();
42029
42267
  try {
42030
42268
  await assistant.sendMessage(text);
42031
- } catch {}
42269
+ } catch {} finally {
42270
+ renderAiPanel();
42271
+ }
42032
42272
  }
42033
42273
  async function handleRetry() {
42034
42274
  const cs = assistant.chatState;
@@ -42062,6 +42302,9 @@ function handleRestore(messageId) {
42062
42302
  renderAiPanel();
42063
42303
  }
42064
42304
  async function seedAssistantPrompt(text) {
42305
+ if (assistant.isTurnActive()) {
42306
+ await assistant.whenTurnEnds();
42307
+ }
42065
42308
  await handleAssistantSend(text);
42066
42309
  }
42067
42310
  async function revealImportHandoff(brief) {
@@ -42120,7 +42363,7 @@ function isAssistantWaiting() {
42120
42363
  return isAwaitingAnswer();
42121
42364
  }
42122
42365
  function isAssistantStreaming() {
42123
- return assistant.chatState.status === "streaming";
42366
+ return assistant.isTurnActive();
42124
42367
  }
42125
42368
  function stop() {
42126
42369
  assistant.stop();
@@ -42293,6 +42536,9 @@ function assistantCommands() {
42293
42536
  ];
42294
42537
  }
42295
42538
 
42539
+ // src/hosting/document.ts
42540
+ var BOOT_META = { name: "jx-boot", content: "launcher" };
42541
+
42296
42542
  // src/services/import-client.ts
42297
42543
  var IMPORT_WARNING_PHASE = "warning";
42298
42544
  async function streamImport(endpoint, opts, onProgress, signal, onReady) {
@@ -42466,7 +42712,7 @@ var STUDIO_ROUTES = {
42466
42712
  resolveSite: route("GET", "/__studio/resolve-site", "Resolve the owning site of a file path {sitePath, projectConfig?, fileRelPath?}"),
42467
42713
  sites: route("GET", "/__studio/sites", "Enumerate site projects [{config, path}] (backs listProjects)", "The welcome screen's Projects catalogue stays hidden."),
42468
42714
  findProject: route("GET", "/__studio/find-project", "Locate a project directory by name outside the root", "openProject falls back to config-matching only."),
42469
- locateDirectory: route("GET", "/__studio/locate-directory", `Resolve the absolute path of a showDirectoryPicker() folder by the id in its ${LOCATION_ID_FILE}`, "The New Project Location field loses its Browse… button and is typed by hand."),
42715
+ locateDirectory: route("GET", "/__studio/locate-directory", `Resolve the absolute path of a showDirectoryPicker() folder by the id in its ${LOCATION_ID_FILE}`, "Browse… reports that the folder could not be placed; the Location is typed by hand."),
42470
42716
  createProject: route("POST", "/__studio/create-project", "Scaffold a project at a caller-chosen destination → {root, config}"),
42471
42717
  starters: route("GET", "/__studio/starters", "Starter templates (StarterInfo[])", "The New Project picker offers only blank/templates."),
42472
42718
  importSite: route("POST", "/__studio/import-site", "Clone a live website into a project; streams NDJSON progress", "The New Project Import tab is unavailable."),
@@ -42475,6 +42721,7 @@ var STUDIO_ROUTES = {
42475
42721
  fileWrite: route("PUT", "/__studio/file", "Write a file's text content"),
42476
42722
  fileDelete: route("DELETE", "/__studio/file", "Delete a file"),
42477
42723
  fileUpload: route("POST", "/__studio/file/upload", "Upload binary content to a path → UploadResult {path, size?}. `path` is the answer, not an " + "echo: a store that de-duplicates, suffixes, or normalizes names reports what it really wrote"),
42724
+ fileBytes: route("GET", "/__studio/file/bytes", "A project file's raw bytes, undecoded — the read side of `fileUpload`, for a caller that must " + "not go through UTF-8", "Studio can only read a project file as text, so any surface that must decode one stays hidden."),
42478
42725
  documentRaw: route("GET", "/__studio/raw/", "A project file as its own bytes, at its own URL — the mount a canvas renders against when " + "the editor origin does not serve the site's URL space. The project-relative path is " + "appended to this prefix", "The canvas cannot address project files by URL, so a host whose origin does not serve the " + "site URL space renders no component $refs and no images."),
42479
42726
  fileRename: route("POST", "/__studio/file/rename", "Rename/move (+ refactor report). `from`/`to` are server-root-relative; every path in the " + "report is relative to the active project (specs/server.md §4.1)"),
42480
42727
  references: route("GET", "/__studio/references", "Where a file or a component tag is used (?path=&tag=, at least one) → ReferencesResult " + "{files, filesReferencing, refsTotal} — the read side of the rename refactor's own walker, " + "cached until the backend's watcher sees the tree move (backs findReferences). `?path=` is " + "server-root-relative and every path in the answer is relative to the active project; a " + "target outside that project is a 400, never a zero-result 200 (specs/server.md §4.1)", 'Usage counts are hidden: no "Used on N pages" in the inspector, no Selection: Find Usages, ' + "and delete/rename confirmations state no reference count."),
@@ -42775,6 +43022,13 @@ function createCloudPlatform(project) {
42775
43022
  const data = await okJson(await api(`/file?path=${encodeURIComponent(path)}`), `Failed to read file: ${path}`);
42776
43023
  return data.content;
42777
43024
  },
43025
+ async readFileBytes(path) {
43026
+ const res = await api(`/file/bytes?path=${encodeURIComponent(path)}`);
43027
+ if (!res.ok) {
43028
+ throw new Error(await errorMessage(res, `Failed to read file bytes: ${path}`));
43029
+ }
43030
+ return res.arrayBuffer();
43031
+ },
42778
43032
  async writeFile(path, content) {
42779
43033
  const res = await api("/file", {
42780
43034
  method: "PUT",
@@ -42825,7 +43079,7 @@ function createCloudPlatform(project) {
42825
43079
  }
42826
43080
  const { offer } = negotiated;
42827
43081
  collabConnection ??= (async () => {
42828
- const { createWsCollabConnection } = await (__preload("ny4zc51r"), import("./chunks/ws-client-ny4zc51r.js"));
43082
+ const { createWsCollabConnection } = await (__preload("erze4czt"), import("./chunks/ws-client-erze4czt.js"));
42829
43083
  const scheme = location.protocol === "https:" ? "wss" : "ws";
42830
43084
  return createWsCollabConnection({
42831
43085
  hydratePath: async (path) => {
@@ -43325,32 +43579,50 @@ function newLocationId() {
43325
43579
  globalThis.crypto.getRandomValues(bytes);
43326
43580
  return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
43327
43581
  }
43582
+ function nameOf(error) {
43583
+ return typeof error === "object" && error !== null && "name" in error && typeof error.name === "string" ? error.name : "";
43584
+ }
43328
43585
  async function pickDirectoryPath(locate) {
43329
43586
  const show = globalThis.showDirectoryPicker;
43330
43587
  if (typeof show !== "function") {
43331
- return null;
43588
+ throw new TypeError("This browser has no folder chooser. Type the folder's path into Location instead.");
43332
43589
  }
43333
43590
  let handle;
43334
43591
  try {
43335
43592
  handle = await show({ id: "jx-new-project-location", mode: "readwrite", startIn: "documents" });
43336
- } catch {
43337
- return null;
43593
+ } catch (error) {
43594
+ if (nameOf(error) === "AbortError") {
43595
+ return null;
43596
+ }
43597
+ throw new Error(`The folder chooser could not open (${errorMessage2(error)}). Type the folder's path into Location instead.`, { cause: error });
43338
43598
  }
43599
+ const { name } = handle;
43339
43600
  const id = newLocationId();
43601
+ let tagged = false;
43340
43602
  try {
43341
- const file = await handle.getFileHandle(LOCATION_ID_FILE, { create: true });
43342
- const writable = await file.createWritable();
43343
- await writable.write(id);
43344
- await writable.close();
43345
- } catch {
43346
- return null;
43347
- }
43348
- try {
43349
- return await locate({ id, name: handle.name });
43350
- } catch {
43351
- return null;
43603
+ try {
43604
+ const file = await handle.getFileHandle(LOCATION_ID_FILE, { create: true });
43605
+ tagged = true;
43606
+ const writable = await file.createWritable();
43607
+ await writable.write(id);
43608
+ await writable.close();
43609
+ } catch (error) {
43610
+ throw new Error(`Jx Studio can't write to "${name}" (${errorMessage2(error)}), so it can't create a project there. Choose a folder you can write to.`, { cause: error });
43611
+ }
43612
+ let path;
43613
+ try {
43614
+ path = await locate({ id, name });
43615
+ } catch (error) {
43616
+ throw new Error(`Jx Studio could not look up where "${name}" is: ${errorMessage2(error)}. Type the folder's path into Location instead.`, { cause: error });
43617
+ }
43618
+ if (!path) {
43619
+ throw new Error(`Jx Studio could not find where "${name}" is. From a browser it can only find folders inside your home folder. Type its full path into Location instead.`);
43620
+ }
43621
+ return path;
43352
43622
  } finally {
43353
- await handle.removeEntry(LOCATION_ID_FILE).catch(() => {});
43623
+ if (tagged) {
43624
+ await handle.removeEntry(LOCATION_ID_FILE).catch(() => {});
43625
+ }
43354
43626
  }
43355
43627
  }
43356
43628
 
@@ -43486,7 +43758,8 @@ function createDevServerPlatform() {
43486
43758
  return pickDirectoryPath(async ({ id, name }) => {
43487
43759
  const res = await fetch(`/__studio/locate-directory?name=${encodeURIComponent(name)}&id=${encodeURIComponent(id)}`);
43488
43760
  if (!res.ok) {
43489
- return null;
43761
+ const body = await res.json().catch(() => null);
43762
+ throw new Error(problemMessage(body, res.status));
43490
43763
  }
43491
43764
  const found = await readJson2(res);
43492
43765
  return found.path ?? null;
@@ -43522,6 +43795,13 @@ function createDevServerPlatform() {
43522
43795
  const data = await readJson2(res);
43523
43796
  return data.content;
43524
43797
  },
43798
+ async readFileBytes(path) {
43799
+ const res = await fetch(`/__studio/file/bytes?path=${encodeURIComponent(serverPath(path))}`);
43800
+ if (!res.ok) {
43801
+ throw new Error(`Failed to read file bytes: ${path}`);
43802
+ }
43803
+ return res.arrayBuffer();
43804
+ },
43525
43805
  async writeFile(path, content) {
43526
43806
  const res = await fetch(`/__studio/file?path=${encodeURIComponent(serverPath(path))}`, {
43527
43807
  body: content,
@@ -43621,7 +43901,7 @@ function createDevServerPlatform() {
43621
43901
  }
43622
43902
  const { offer } = negotiation;
43623
43903
  _collabConnection ??= (async () => {
43624
- const { createWsCollabConnection } = await (__preload("ny4zc51r"), import("./chunks/ws-client-ny4zc51r.js"));
43904
+ const { createWsCollabConnection } = await (__preload("erze4czt"), import("./chunks/ws-client-erze4czt.js"));
43625
43905
  const scheme = location.protocol === "https:" ? "wss" : "ws";
43626
43906
  return createWsCollabConnection({
43627
43907
  protocols: offer,
@@ -44143,7 +44423,7 @@ function createDevServerPlatform() {
44143
44423
  }
44144
44424
  },
44145
44425
  async cfApi(apiPath, init) {
44146
- const { getCfToken } = await (__preload("adg74qsk"), import("./chunks/cf-settings-adg74qsk.js"));
44426
+ const { getCfToken } = await (__preload("m79r7p5y"), import("./chunks/cf-settings-m79r7p5y.js"));
44147
44427
  const token = getCfToken();
44148
44428
  if (!token) {
44149
44429
  throw new Error("No Cloudflare API token configured");
@@ -44161,7 +44441,7 @@ function createDevServerPlatform() {
44161
44441
  return envelope.result ?? envelope;
44162
44442
  },
44163
44443
  async cfConnection() {
44164
- const { getCfAccountId, getCfToken, setCfAccountId } = await (__preload("adg74qsk"), import("./chunks/cf-settings-adg74qsk.js"));
44444
+ const { getCfAccountId, getCfToken, setCfAccountId } = await (__preload("m79r7p5y"), import("./chunks/cf-settings-m79r7p5y.js"));
44165
44445
  if (!getCfToken()) {
44166
44446
  return null;
44167
44447
  }
@@ -44199,7 +44479,59 @@ function createDevServerPlatform() {
44199
44479
  }
44200
44480
 
44201
44481
  // src/platforms/default-platform.ts
44482
+ var BACKENDLESS_SCHEMES = new Set(["views:", "file:"]);
44483
+ function bootEnvironment() {
44484
+ const doc = globalThis.document;
44485
+ const meta = `meta[name="${BOOT_META.name}"][content="${BOOT_META.content}"]`;
44486
+ return {
44487
+ cloud: globalThis.__jxCloud != null,
44488
+ declared: doc?.querySelector(meta) != null,
44489
+ launcher: launcherSignal2(),
44490
+ protocol: globalThis.location?.protocol ?? ""
44491
+ };
44492
+ }
44493
+ function bootRefusal(env = bootEnvironment()) {
44494
+ if (env.launcher) {
44495
+ return { kind: "launcher", launcher: env.launcher.launcher };
44496
+ }
44497
+ if (env.cloud) {
44498
+ return null;
44499
+ }
44500
+ if (env.declared) {
44501
+ return { kind: "declared" };
44502
+ }
44503
+ if (BACKENDLESS_SCHEMES.has(env.protocol)) {
44504
+ return { kind: "scheme", protocol: env.protocol };
44505
+ }
44506
+ return null;
44507
+ }
44508
+ function refusalMessage(refusal) {
44509
+ switch (refusal.kind) {
44510
+ case "launcher": {
44511
+ return `Jx Studio's launcher (${refusal.launcher ?? "unnamed"}) announced itself but registered no platform adapter.`;
44512
+ }
44513
+ case "declared": {
44514
+ return "Jx Studio's page declares a boot module, but nothing registered a platform adapter.";
44515
+ }
44516
+ default: {
44517
+ return `Jx Studio was opened from a ${refusal.protocol} URL, which has no backend for the dev-server adapter.`;
44518
+ }
44519
+ }
44520
+ }
44521
+
44522
+ class PlatformUnavailableError extends Error {
44523
+ refusal;
44524
+ constructor(refusal) {
44525
+ super(refusalMessage(refusal));
44526
+ this.name = "PlatformUnavailableError";
44527
+ this.refusal = refusal;
44528
+ }
44529
+ }
44202
44530
  function resolveDefaultPlatform() {
44531
+ const refusal = bootRefusal();
44532
+ if (refusal) {
44533
+ throw new PlatformUnavailableError(refusal);
44534
+ }
44203
44535
  const cloud = globalThis.__jxCloud;
44204
44536
  return cloud ? createCloudPlatform(cloud.project) : createDevServerPlatform();
44205
44537
  }
@@ -45911,6 +46243,7 @@ function capabilities(platform) {
45911
46243
  gitClone: has("gitClone"),
45912
46244
  importSite: has("importSite"),
45913
46245
  openProjectInNewWindow: has("openProjectInNewWindow"),
46246
+ readFileBytes: has("readFileBytes"),
45914
46247
  windowControls: has("newWindow")
45915
46248
  };
45916
46249
  }
@@ -46080,7 +46413,7 @@ function openSettingsMenu(anchor, opts = {}) {
46080
46413
  var rail_default = {
46081
46414
  $schema: "https://jxsuite.com/schema/v1",
46082
46415
  $id: "StudioRail",
46083
- $description: "The Navigator rail: a rendering of the panel registry. The adapter hands in `groups`, every rail-able panel grouped by level with its selected state and badge, and `settingsVisible`/`settingsOpen` for the foot's gear. Nothing here is a list of panels.",
46416
+ $description: "The Navigator rail: a rendering of the panel registry. The adapter hands in `groups`, every rail-able panel grouped by level with its selected state and badge, and `settingsVisible`/`settingsOpen` for the foot's gear. Nothing here is a list of panels. Each button prints its record's short `railLabel` under the glyph, falling back to `title`, and carries the full `title` as its accessible name and tooltip: the visible words lead the name a reader hears (WCAG 2.5.3), and the tooltip restores what a short label leaves out.",
46084
46417
  tagName: "div",
46085
46418
  attributes: {
46086
46419
  part: "rail"
@@ -46224,7 +46557,7 @@ var rail_default = {
46224
46557
  children: [
46225
46558
  {
46226
46559
  tagName: "span",
46227
- textContent: "${$map.item.title}"
46560
+ textContent: "${$map.item.railLabel}"
46228
46561
  }
46229
46562
  ]
46230
46563
  }
@@ -46355,6 +46688,7 @@ function project17() {
46355
46688
  badge: String(panel.badge?.(ctx) ?? ""),
46356
46689
  icon: panel.icon,
46357
46690
  id: panel.id,
46691
+ railLabel: panel.railLabel ?? panel.title,
46358
46692
  selected: isRailPanelShowing(panel),
46359
46693
  title: panel.title
46360
46694
  }))
@@ -48014,6 +48348,14 @@ var ASSISTANT_REGION = "inspector.assistant";
48014
48348
  var _host5 = null;
48015
48349
  var _container = null;
48016
48350
  var _scope9 = null;
48351
+ var _surfaceBound = false;
48352
+ function ensureAssistantSurface() {
48353
+ if (_surfaceBound || !_container) {
48354
+ return;
48355
+ }
48356
+ _surfaceBound = true;
48357
+ bindAiPanelHost(_container);
48358
+ }
48017
48359
  function mount9(host) {
48018
48360
  if (!host || _host5 === host && _container) {
48019
48361
  return;
@@ -48025,10 +48367,15 @@ function mount9(host) {
48025
48367
  host.textContent = "";
48026
48368
  host.append(_container);
48027
48369
  mountAiPanel();
48028
- bindAiPanelHost(_container);
48370
+ _surfaceBound = false;
48029
48371
  _scope9?.stop();
48030
48372
  _scope9 = effectScope();
48031
48373
  _scope9.run(() => {
48374
+ effect(() => {
48375
+ if (inspectorTab2() === "assistant") {
48376
+ ensureAssistantSurface();
48377
+ }
48378
+ });
48032
48379
  effect(() => {
48033
48380
  const root = workspace2.projectRoot;
48034
48381
  if (!root || !hasPendingAgentPrompt(root)) {
@@ -48563,7 +48910,7 @@ function installUrlOf(error) {
48563
48910
  var new_project_default = {
48564
48911
  $schema: "https://jxsuite.com/schema/v1",
48565
48912
  $id: "StudioNewProject",
48566
- $description: "The New Project wizard: two steps in one dialog. Step 1 asks where the site comes from — a starter gallery that ends with one Start from scratch card, an Import of a site that already exists, or an Agent brief — and step 2 asks what it is called and where it is written. A `jx-dialog`, so the platform owns modality, Escape, focus restoration and the backdrop, which is what retired the fixed `.new-project-modal` card, the `<sp-underlay>` beside it, the `z-index: 1000` that card needed to climb back out from under its own scrim, and the hand-drawn header with its own close button. The footer is the dialog's too: Cancel, Back and the primary are `cancelLabel`, `secondaryLabel` and `confirmLabel`, so the wizard says which answers exist by naming them rather than by drawing three buttons per step. Nothing here knows what a starter is, which platform writes to a repository rather than a folder, what makes an AI gate open, or how a breakpoint policy is spelled — `new-project/new-project-modal.ts` keeps the flow, `new-project/import-tab.ts` the brief and `new-project/location-fields.ts` the destination, and all three hand over strings, flags and one discriminant per branch. The three AI controllers are islands: their own mounted documents, placed by the adapter into boxes this document renders empty (§9.4).",
48913
+ $description: "The New Project wizard: two steps in one dialog. Step 1 asks where the site comes from — a starter gallery that ends with one Start from scratch card, an Import of a site that already exists, or an Agent brief — and step 2 asks what it is called and where it is written. A `jx-dialog`, so the platform owns modality, Escape, focus restoration and the backdrop, which is what retired the fixed `.new-project-modal` card, the `<sp-underlay>` beside it, the `z-index: 1000` that card needed to climb back out from under its own scrim, and the hand-drawn header with its own close button. The footer is the dialog's too: Cancel, Back and the primary are `cancelLabel`, `secondaryLabel` and `confirmLabel`, so the wizard says which answers exist by naming them rather than by drawing three buttons per step. Nothing here knows what a starter is, which platform writes to a repository rather than a folder, what makes an AI gate open, or how a breakpoint policy is spelled — `new-project/new-project-modal.ts` keeps the flow, `new-project/import-tab.ts` the brief and `new-project/location-fields.ts` the destination, and all three hand over strings, flags and one discriminant per branch. The three AI controllers are islands: their own mounted documents, placed by the adapter into boxes this document renders empty (studio-ui-guidelines.md §9.4).",
48567
48914
  tagName: "jx-dialog",
48568
48915
  $props: {
48569
48916
  headline: {
@@ -50468,6 +50815,8 @@ async function browseLocation(rerender) {
50468
50815
  _parent = picked;
50469
50816
  _error2 = "";
50470
50817
  }
50818
+ } catch (error) {
50819
+ _error2 = errorMessage2(error) || "The folder could not be chosen. Type its path into Location instead.";
50471
50820
  } finally {
50472
50821
  _browsing = false;
50473
50822
  rerender();
@@ -50848,7 +51197,15 @@ function finish(result) {
50848
51197
  settle3(result);
50849
51198
  }
50850
51199
  async function finishCreated(result) {
50851
- await initProjectRepo2(result.root);
51200
+ const { adopted, error, openedElsewhere } = await adoptCreatedProject(result.root, {
51201
+ getTab: () => null
51202
+ });
51203
+ if (!adopted && !openedElsewhere) {
51204
+ notify2.warn("Created the project, but could not open it here.", {
51205
+ ...error === null ? {} : { detail: error },
51206
+ source: "New Project"
51207
+ });
51208
+ }
50852
51209
  finish(result);
50853
51210
  }
50854
51211
  function deriveSlug(name) {
@@ -51318,7 +51675,7 @@ var seo_default = {
51318
51675
  }
51319
51676
  },
51320
51677
  '& [part="chip"]': {
51321
- $description: "The provenance vocabulary of `panels/provenance.ts` §6.2, drawn by this document rather than borrowed as a class: `set` keeps the accent dot it has always been, `inherited` is an amber pill naming its donor, and a field nothing supplies gets no chip at all — because a ghost dot on every unset row says less than silence. The two chips that can go somewhere are `<button>`s; the ones with nowhere to go are not, which is §6.2's rule that a control which looks pressable and does nothing is a defect.",
51678
+ $description: "The provenance vocabulary of `panels/provenance.ts` (§6.7), drawn by this document rather than borrowed as a class: `set` keeps the accent dot it has always been, `inherited` is an amber pill naming its donor, and a field nothing supplies gets no chip at all — because a ghost dot on every unset row says less than silence. The two chips that can go somewhere are `<button>`s; the ones with nowhere to go are not, which is §6.7's rule that a control which looks pressable and does nothing is a defect.",
51322
51679
  flexShrink: "0"
51323
51680
  },
51324
51681
  '& jx-dot[part="chip"][data-state="set"]': {
@@ -54251,8 +54608,8 @@ var settings_css_vars_default = {
54251
54608
  fontWeight: "600",
54252
54609
  color: "var(--fg)"
54253
54610
  },
54254
- '& [part="error"]': {
54255
- $description: "A write that failed, said under the section title rather than pinned to a control. This section saves on change and the predecessor dropped every rejection on the floor, so a read-only file made each edit look as though it had been taken and then quietly forgotten.",
54611
+ '& [part="section-error"]': {
54612
+ $description: 'A write that failed, said under the section title rather than pinned to a control. This section saves on change and the predecessor dropped every rejection on the floor, so a read-only file made each edit look as though it had been taken and then quietly forgotten. It is `section-error` rather than `error` because every text field and select in the section draws a permanent `[part="error"]` of its own in this same light-DOM tree, and a rule on that name would give their refusal sentences this margin and size too.',
54256
54613
  margin: "0 0 12px",
54257
54614
  fontSize: "var(--jx-text-md)",
54258
54615
  color: "var(--danger)"
@@ -54408,7 +54765,7 @@ var settings_css_vars_default = {
54408
54765
  {
54409
54766
  tagName: "div",
54410
54767
  attributes: {
54411
- part: "error-slot",
54768
+ part: "section-error-slot",
54412
54769
  role: "none"
54413
54770
  },
54414
54771
  $switch: {
@@ -54418,7 +54775,7 @@ var settings_css_vars_default = {
54418
54775
  true: {
54419
54776
  tagName: "p",
54420
54777
  attributes: {
54421
- part: "error",
54778
+ part: "section-error",
54422
54779
  role: "alert"
54423
54780
  },
54424
54781
  textContent: "${state.error}"
@@ -57898,8 +58255,8 @@ var settings_extensions_default = {
57898
58255
  fontSize: "var(--jx-text-md)",
57899
58256
  color: "var(--fg-dim)"
57900
58257
  },
57901
- '& [part="error"]': {
57902
- $description: 'The last operation that failed, said under the section title. Not a second announcement: `updateSiteConfig` already files a Problem for a rejected write and `activity.fail` files one for a rejected install. §13.1 makes INLINE the tier for "the value is on screen, and nothing else is the right place" — the Problem is about the file, and this line is about the switch the reader is looking at.',
58258
+ '& [part="section-error"]': {
58259
+ $description: 'The last operation that failed, said under the section title. Not a second announcement: `updateSiteConfig` already files a Problem for a rejected write and `activity.fail` files one for a rejected install. studio-ui-guidelines.md §13.1 makes INLINE the tier for "the value is on screen, and nothing else is the right place" — the Problem is about the file, and this line is about the switch the reader is looking at. It is `section-error` rather than `error`, the name every settings section uses, because every kit field draws a permanent `[part="error"]` of its own and the kit is light DOM: a rule on that name here would reach any field the section ever holds.',
57903
58260
  margin: "6px 0 0",
57904
58261
  fontSize: "var(--jx-text-md)",
57905
58262
  color: "var(--danger)"
@@ -57916,7 +58273,7 @@ var settings_extensions_default = {
57916
58273
  color: "var(--fg)"
57917
58274
  },
57918
58275
  '& [part="row"]': {
57919
- $description: "A wrapping row, never an overflowing one (guidelines §4.6): the package name is author-supplied and unbounded, and Project Settings renders at two very different widths.",
58276
+ $description: "A wrapping row, never an overflowing one (studio-ui-guidelines.md §4.6): the package name is author-supplied and unbounded, and Project Settings renders at two very different widths.",
57920
58277
  display: "flex",
57921
58278
  flexWrap: "wrap",
57922
58279
  alignItems: "flex-start",
@@ -57992,7 +58349,7 @@ var settings_extensions_default = {
57992
58349
  flexShrink: "0"
57993
58350
  },
57994
58351
  '& [part="empty"]': {
57995
- $description: 'The teaching empty state, drawn here rather than borrowed from `panels/empty-state.ts`: that helper is a lit template, and a document cannot call one. What is kept is the pattern\'s rules (§11) — one sentence saying what the region is FOR, never "no extensions", then where its content comes from — in the compact shape, which reads as a note inside an otherwise populated pane rather than as a whole empty region. It offers no action because there is nothing here to act on: a backend that lists no catalogue is not a project waiting to be filled in.',
58352
+ $description: 'The teaching empty state, drawn here rather than borrowed from `panels/empty-state.ts`: that helper is a lit template, and a document cannot call one. What is kept is the pattern\'s rules (studio-ui-guidelines.md §11) — one sentence saying what the region is FOR, never "no extensions", then where its content comes from — in the compact shape, which reads as a note inside an otherwise populated pane rather than as a whole empty region. It offers no action because there is nothing here to act on: a backend that lists no catalogue is not a project waiting to be filled in.',
57996
58353
  display: "flex",
57997
58354
  flexDirection: "column",
57998
58355
  alignItems: "flex-start",
@@ -58029,7 +58386,7 @@ var settings_extensions_default = {
58029
58386
  {
58030
58387
  tagName: "div",
58031
58388
  attributes: {
58032
- part: "error-slot",
58389
+ part: "section-error-slot",
58033
58390
  role: "none"
58034
58391
  },
58035
58392
  style: {
@@ -58042,7 +58399,7 @@ var settings_extensions_default = {
58042
58399
  true: {
58043
58400
  tagName: "p",
58044
58401
  attributes: {
58045
- part: "error",
58402
+ part: "section-error",
58046
58403
  role: "alert"
58047
58404
  },
58048
58405
  textContent: "${state.error}"
@@ -58584,8 +58941,8 @@ var settings_deploy_default = {
58584
58941
  fontWeight: "600",
58585
58942
  color: "var(--fg)"
58586
58943
  },
58587
- '& [part="error"]': {
58588
- $description: "A write that failed, said under the section title rather than pinned to the picker: settings save on change, so a silent rejection would read as \"my edit just vanished\", and what failed was the whole file rather than the value. Its size read `--spectrum-font-size-75` with a 12px fallback, which resolved only inside `<sp-theme>` — a document is mounted in the settings pane, where that name is not in scope and the fallback is all it would ever have taken — so the size is the kit's scale and the colour is the studio layer's.",
58944
+ '& [part="section-error"]': {
58945
+ $description: 'A write that failed, said under the section title rather than pinned to the picker: settings save on change, so a silent rejection would read as "my edit just vanished", and what failed was the whole file rather than the value. Its size read `--spectrum-font-size-75` with a 12px fallback, which resolved only inside `<sp-theme>` — a document is mounted in the settings pane, where that name is not in scope and the fallback is all it would ever have taken — so the size is the kit\'s scale and the colour is the studio layer\'s. It is `section-error` rather than `error` because the adapter picker is a `jx-select`, which draws a permanent `[part="error"]` of its own in this same light-DOM tree: a rule on that name would give the picker\'s own refusal sentence this 16px margin too.',
58589
58946
  margin: "0 0 16px",
58590
58947
  fontSize: "var(--jx-text-md)",
58591
58948
  color: "var(--danger)"
@@ -58606,7 +58963,7 @@ var settings_deploy_default = {
58606
58963
  {
58607
58964
  tagName: "div",
58608
58965
  attributes: {
58609
- part: "error-slot",
58966
+ part: "section-error-slot",
58610
58967
  role: "none"
58611
58968
  },
58612
58969
  style: {
@@ -58619,7 +58976,7 @@ var settings_deploy_default = {
58619
58976
  true: {
58620
58977
  tagName: "p",
58621
58978
  attributes: {
58622
- part: "error",
58979
+ part: "section-error",
58623
58980
  role: "alert"
58624
58981
  },
58625
58982
  textContent: "${state.error}"
@@ -60580,8 +60937,8 @@ var settings_locales_default = {
60580
60937
  fontSize: "var(--jx-text-md)",
60581
60938
  color: "var(--fg-dim)"
60582
60939
  },
60583
- '& [part="error"]': {
60584
- $description: 'A write that failed, said under the section title rather than pinned to a control: settings save on change, so a silent rejection would read as "my edit just vanished", and the whole file failed rather than one field.',
60940
+ '& [part="section-error"]': {
60941
+ $description: "A write that failed, said under the section title rather than pinned to a control: settings save on change, so a silent rejection would read as \"my edit just vanished\", and the whole file failed rather than one field. It is `section-error` rather than `error` because the tag field's refusal IS the kit's own `[part=\"error\"]` sentence, in this same light-DOM tree: a rule on that name gave the tag field's refusal this margin and size, and reached the two pickers' empty regions as well.",
60585
60942
  margin: "6px 0 0",
60586
60943
  fontSize: "var(--jx-text-md)",
60587
60944
  color: "var(--danger)"
@@ -60655,7 +61012,7 @@ var settings_locales_default = {
60655
61012
  {
60656
61013
  tagName: "div",
60657
61014
  attributes: {
60658
- part: "error-slot",
61015
+ part: "section-error-slot",
60659
61016
  role: "none"
60660
61017
  },
60661
61018
  $switch: {
@@ -60665,7 +61022,7 @@ var settings_locales_default = {
60665
61022
  true: {
60666
61023
  tagName: "p",
60667
61024
  attributes: {
60668
- part: "error",
61025
+ part: "section-error",
60669
61026
  role: "alert"
60670
61027
  },
60671
61028
  textContent: "${state.error}"
@@ -61261,7 +61618,7 @@ async function openProjectSettings(section) {
61261
61618
  showSettingsDocument();
61262
61619
  notifySettingsDocument2();
61263
61620
  try {
61264
- const { syncExtensionSettingsSections } = await (__preload("c6zwzbre"), import("./chunks/extension-sections-c6zwzbre.js"));
61621
+ const { syncExtensionSettingsSections } = await (__preload("r73an5s6"), import("./chunks/extension-sections-r73an5s6.js"));
61265
61622
  await syncExtensionSettingsSections();
61266
61623
  } catch (error) {
61267
61624
  notify2.warn(`Could not load extension settings sections — ${errorMessage2(error)}`, {
@@ -61398,9 +61755,11 @@ var about_default = {
61398
61755
  color: "var(--fg-dim)"
61399
61756
  },
61400
61757
  '& [part="meta-value"]': {
61758
+ $description: "One of the two places in the chrome that opt back in to selection (`STYLING.md` §Text selection). The root sets `user-select: none` and `styles/tokens.json` restates it on every modal `dialog`, but this dialog's body IS its message: the version, the build date, the commit and the channel exist to be pasted into a bug report, and Studio offers no copy action for them.",
61401
61759
  margin: "0",
61402
61760
  color: "var(--fg)",
61403
- wordBreak: "break-word"
61761
+ wordBreak: "break-word",
61762
+ userSelect: "text"
61404
61763
  },
61405
61764
  '& [part="links"]': {
61406
61765
  display: "flex",
@@ -61438,10 +61797,12 @@ var about_default = {
61438
61797
  gap: "4px"
61439
61798
  },
61440
61799
  '& [part="package-row"]': {
61800
+ $description: "Selectable for the same reason `meta-value` is, and on the ROW rather than on the version alone: what a reader pastes is the pair, `@jxsuite/ai 0.37.0`, so a drag has to be able to cross from the name to the number and on down the list.",
61441
61801
  display: "flex",
61442
61802
  justifyContent: "space-between",
61443
61803
  gap: "12px",
61444
- fontSize: "var(--jx-text-md)"
61804
+ fontSize: "var(--jx-text-md)",
61805
+ userSelect: "text"
61445
61806
  },
61446
61807
  '& [part="package-name"]': {
61447
61808
  color: "var(--fg-dim)"
@@ -61671,9 +62032,9 @@ function openAboutSurface(options) {
61671
62032
 
61672
62033
  // src/version.ts
61673
62034
  var APP_NAME = "Jx Studio";
61674
- var VERSION = "5.2.0";
61675
- var BUILD_DATE = "2026-09-18T22:01:00.346Z";
61676
- var GIT_COMMIT = "09806b6";
62035
+ var VERSION = "5.4.0";
62036
+ var BUILD_DATE = "2026-09-29T19:41:26.105Z";
62037
+ var GIT_COMMIT = "fd18dbe";
61677
62038
  var LINKS = {
61678
62039
  github: "https://github.com/jxsuite/jx",
61679
62040
  docs: "https://github.com/jxsuite/jx#readme",
@@ -62081,9 +62442,9 @@ function withExtension(path, ext) {
62081
62442
  return current === "" ? `${path}${ext}` : `${path.slice(0, path.length - current.length)}${ext}`;
62082
62443
  }
62083
62444
  async function readDocument(path, source) {
62084
- const { formatForPath } = await (__preload("f64fnesp"), import("./chunks/format-host-f64fnesp.js"));
62445
+ const { formatForPath } = await (__preload("mym8qnd0"), import("./chunks/format-host-mym8qnd0.js"));
62085
62446
  if (formatForPath(path)) {
62086
- const { parseSourceForPath } = await (__preload("vrpq1jjk"), import("./chunks/file-ops-vrpq1jjk.js"));
62447
+ const { parseSourceForPath } = await (__preload("3xan8mnj"), import("./chunks/file-ops-3xan8mnj.js"));
62087
62448
  const parsed = await parseSourceForPath(path, source);
62088
62449
  return { document: parsed.document, frontmatter: parsed.frontmatter };
62089
62450
  }
@@ -62094,7 +62455,7 @@ async function writeDocument(targetExt, document2, frontmatter) {
62094
62455
  if (targetExt === ".json") {
62095
62456
  return serializeJson(merged, null);
62096
62457
  }
62097
- const { formatByExtension, formatSerialize } = await (__preload("f64fnesp"), import("./chunks/format-host-f64fnesp.js"));
62458
+ const { formatByExtension, formatSerialize } = await (__preload("mym8qnd0"), import("./chunks/format-host-mym8qnd0.js"));
62098
62459
  const format = formatByExtension(targetExt, "serialize");
62099
62460
  if (!format) {
62100
62461
  throw new Error(`No installed format writes ${targetExt} files.`);
@@ -62131,7 +62492,7 @@ async function buildPlan(path, source, targetExt) {
62131
62492
  };
62132
62493
  }
62133
62494
  if (targetExt === ".json") {
62134
- const { validateDocOrNull } = await (__preload("j0wxk72w"), import("./chunks/jx-validate-j0wxk72w.js"));
62495
+ const { validateDocOrNull } = await (__preload("epp2z3s3"), import("./chunks/jx-validate-epp2z3s3.js"));
62135
62496
  const errors = await validateDocOrNull(JSON.parse(text));
62136
62497
  if (errors === null) {
62137
62498
  lines.push("The result could not be checked against the document schema — that is not the same as " + "it being valid.");
@@ -62149,7 +62510,7 @@ async function buildPlan(path, source, targetExt) {
62149
62510
  if (!await readsBackIdentically(targetPath, text)) {
62150
62511
  lines.push("This file will not read back identically after conversion.");
62151
62512
  }
62152
- const { loadUsages, usageWarning } = await (__preload("h69xp9zz"), import("./chunks/references-h69xp9zz.js"));
62513
+ const { loadUsages, usageWarning } = await (__preload("f7bdv3z5"), import("./chunks/references-f7bdv3z5.js"));
62153
62514
  const sentence = usageWarning(await loadUsages({ path }), "convert");
62154
62515
  if (sentence !== null) {
62155
62516
  lines.push(sentence);
@@ -62164,14 +62525,14 @@ async function convertFile(path, targetExt) {
62164
62525
  notify.error(message, { detail, path, source: "Convert" });
62165
62526
  return null;
62166
62527
  };
62167
- const { loadFormats } = await (__preload("f64fnesp"), import("./chunks/format-host-f64fnesp.js"));
62528
+ const { loadFormats } = await (__preload("mym8qnd0"), import("./chunks/format-host-mym8qnd0.js"));
62168
62529
  await loadFormats();
62169
62530
  const targets = convertTargets(path);
62170
62531
  if (targets.length === 0) {
62171
62532
  return fail(`${path} cannot be converted to another format.`, CANNOT_CONVERT);
62172
62533
  }
62173
- const { workspace } = await (__preload("bnw54cha"), import("./chunks/workspace-bnw54cha.js"));
62174
- const { commitTabBuffers } = await (__preload("pykwbm92"), import("./chunks/monaco-buffer-pykwbm92.js"));
62534
+ const { workspace } = await (__preload("b9k2frks"), import("./chunks/workspace-b9k2frks.js"));
62535
+ const { commitTabBuffers } = await (__preload("jaq1t9zj"), import("./chunks/monaco-buffer-jaq1t9zj.js"));
62175
62536
  const openTabEntry = [...workspace.tabs.values()].find((tab) => tab.documentPath === path);
62176
62537
  if (openTabEntry) {
62177
62538
  await commitTabBuffers(openTabEntry);
@@ -62186,7 +62547,7 @@ async function convertFile(path, targetExt) {
62186
62547
  if (!targets.some((target) => target.ext === chosen)) {
62187
62548
  return fail(`${path} cannot be converted to ${chosen}.`, CANNOT_CONVERT);
62188
62549
  }
62189
- const { getPlatform } = await (__preload("ts9cg1a4"), import("./chunks/platform-ts9cg1a4.js"));
62550
+ const { getPlatform } = await (__preload("fnchmhy7"), import("./chunks/platform-fnchmhy7.js"));
62190
62551
  const platform = getPlatform();
62191
62552
  const targetPath = withExtension(path, chosen);
62192
62553
  try {
@@ -62208,7 +62569,7 @@ async function convertFile(path, targetExt) {
62208
62569
  if (plan.blocker !== null) {
62209
62570
  return fail(`${path} cannot be converted to ${chosen}.`, plan.blocker);
62210
62571
  }
62211
- const { showConfirmDialog } = await (__preload("1vcfdbet"), import("./chunks/layers-1vcfdbet.js"));
62572
+ const { showConfirmDialog } = await (__preload("s6mc01ry"), import("./chunks/layers-s6mc01ry.js"));
62212
62573
  const { html } = await (__preload("m244w9nh"), import("./chunks/lit-html-m244w9nh.js"));
62213
62574
  const confirmed = await showConfirmDialog("Convert Format", html`<span
62214
62575
  >Convert <strong>${path.split("/").pop()}</strong> to
@@ -62217,8 +62578,8 @@ async function convertFile(path, targetExt) {
62217
62578
  if (!confirmed) {
62218
62579
  return null;
62219
62580
  }
62220
- const { markLocalMutation } = await (__preload("3y6y0nrm"), import("./chunks/fs-events-3y6y0nrm.js"));
62221
- const { notifyMoveOutcome, settleRename } = await (__preload("msaf95y6"), import("./chunks/files-msaf95y6.js"));
62581
+ const { markLocalMutation } = await (__preload("z2gg8qjt"), import("./chunks/fs-events-z2gg8qjt.js"));
62582
+ const { notifyMoveOutcome, settleRename } = await (__preload("egsb0ry8"), import("./chunks/files-egsb0ry8.js"));
62222
62583
  markLocalMutation(path, plan.targetPath);
62223
62584
  try {
62224
62585
  await platform.writeFile(path, plan.text);
@@ -62243,8 +62604,8 @@ async function convertFile(path, targetExt) {
62243
62604
  return plan.targetPath;
62244
62605
  }
62245
62606
  async function rebuildTab(from, to, text) {
62246
- const { openTab, renameTab } = await (__preload("bnw54cha"), import("./chunks/workspace-bnw54cha.js"));
62247
- const { formatForPath } = await (__preload("f64fnesp"), import("./chunks/format-host-f64fnesp.js"));
62607
+ const { openTab, renameTab } = await (__preload("b9k2frks"), import("./chunks/workspace-b9k2frks.js"));
62608
+ const { formatForPath } = await (__preload("mym8qnd0"), import("./chunks/format-host-mym8qnd0.js"));
62248
62609
  renameTab(from, to, to);
62249
62610
  const format = formatForPath(to);
62250
62611
  let document2;
@@ -62252,7 +62613,7 @@ async function rebuildTab(from, to, text) {
62252
62613
  let layout = null;
62253
62614
  try {
62254
62615
  if (format) {
62255
- const { parseSourceForPath } = await (__preload("vrpq1jjk"), import("./chunks/file-ops-vrpq1jjk.js"));
62616
+ const { parseSourceForPath } = await (__preload("3xan8mnj"), import("./chunks/file-ops-3xan8mnj.js"));
62256
62617
  ({ document: document2, frontmatter } = await parseSourceForPath(to, text));
62257
62618
  } else {
62258
62619
  ({ document: document2, layout } = parseJsonDocument(text));
@@ -62281,7 +62642,7 @@ async function pickTarget(path, targets) {
62281
62642
  if (targets.length === 1) {
62282
62643
  return targets[0].ext;
62283
62644
  }
62284
- const { showPromptDialog } = await (__preload("1vcfdbet"), import("./chunks/layers-1vcfdbet.js"));
62645
+ const { showPromptDialog } = await (__preload("s6mc01ry"), import("./chunks/layers-s6mc01ry.js"));
62285
62646
  let picked = targets[0].ext;
62286
62647
  const answer = await showPromptDialog("Convert Format", {
62287
62648
  choice: {
@@ -62406,7 +62767,7 @@ function publishCommands() {
62406
62767
  when: (ctx) => ctx.project.open,
62407
62768
  enablement: () => platformSupportsPublish(),
62408
62769
  run: async () => {
62409
- const { openPublishPanel } = await (__preload("vz23wwmg"), import("./chunks/publish-panel-vz23wwmg.js"));
62770
+ const { openPublishPanel } = await (__preload("ak6hxfma"), import("./chunks/publish-panel-ak6hxfma.js"));
62410
62771
  openPublishPanel();
62411
62772
  },
62412
62773
  title: "Set Up Publishing"
@@ -63107,7 +63468,7 @@ function i18nCommands() {
63107
63468
  });
63108
63469
  return;
63109
63470
  }
63110
- const { openFileInTab } = await (__preload("msaf95y6"), import("./chunks/files-msaf95y6.js"));
63471
+ const { openFileInTab } = await (__preload("egsb0ry8"), import("./chunks/files-egsb0ry8.js"));
63111
63472
  await openFileInTab(path);
63112
63473
  },
63113
63474
  title: "Open Translation"
@@ -63152,7 +63513,7 @@ function i18nCommands() {
63152
63513
  return;
63153
63514
  }
63154
63515
  const cut = path.lastIndexOf("/");
63155
- const { createFileIn, openFileInTab } = await (__preload("msaf95y6"), import("./chunks/files-msaf95y6.js"));
63516
+ const { createFileIn, openFileInTab } = await (__preload("egsb0ry8"), import("./chunks/files-egsb0ry8.js"));
63156
63517
  const created = await createFileIn({
63157
63518
  content,
63158
63519
  dir: cut === -1 ? "." : path.slice(0, cut),
@@ -63323,6 +63684,293 @@ registerSurface("shell", shell_default);
63323
63684
  function mountShellSurface(host) {
63324
63685
  return mountSurface("shell", {}, host);
63325
63686
  }
63687
+ // src/surfaces/boot-failure.json
63688
+ var boot_failure_default = {
63689
+ $schema: "https://jxsuite.com/schema/v1",
63690
+ $id: "StudioBootFailure",
63691
+ $description: 'What Studio draws INSTEAD of the application frame when a launcher was supposed to register a platform adapter and did not (`platforms/default-platform.ts`\'s `bootRefusal`). It replaces a failure that drew everything: desktop 5.0.0 to 5.1.3 fell back to the dev-server adapter inside a `views://` window, so every panel rendered and the first click said "Failed to fetch". This says so up front instead: what failed, the error the launcher recorded, and the facts a support thread needs, with a Reload for a transient failure and a Copy for everything else. The adapter decides every sentence; the document only draws them. It is the whole page, so it carries a window drag strip of its own — the desktop window hides its title bar, and with no Command Bar mounted there is otherwise nothing to move the window by.',
63692
+ tagName: "div",
63693
+ attributes: {
63694
+ part: "screen",
63695
+ role: "alert"
63696
+ },
63697
+ style: {
63698
+ $description: "Tokens throughout: the screen is mounted before anything but the kit's theme has run, so it can lean on nothing the frame's stylesheets declare. The top padding clears the macOS traffic lights, which sit over the page because the window's title bar is hidden.",
63699
+ position: "fixed",
63700
+ inset: "0",
63701
+ display: "flex",
63702
+ alignItems: "flex-start",
63703
+ justifyContent: "center",
63704
+ paddingTop: "max(10vh, calc(var(--jx-control-h) * 2))",
63705
+ overflowY: "auto",
63706
+ boxSizing: "border-box",
63707
+ background: "var(--jx-bg)",
63708
+ fontFamily: "var(--jx-font-sans)",
63709
+ color: "var(--jx-fg)",
63710
+ '& > [part="drag-strip"]': {
63711
+ $description: "The window's handle. Electrobun's preload recognises the class and supplies the drag behaviour itself, so the rule here only sizes the strip: as tall as the hidden title bar would have been.",
63712
+ position: "absolute",
63713
+ top: "0",
63714
+ left: "0",
63715
+ right: "0",
63716
+ height: "calc(var(--jx-control-h) + var(--jx-space-3))"
63717
+ },
63718
+ '& > [part="content"]': {
63719
+ position: "relative",
63720
+ maxWidth: "40rem",
63721
+ width: "100%",
63722
+ padding: "0 var(--jx-space-6) var(--jx-space-7)",
63723
+ boxSizing: "border-box",
63724
+ display: "flex",
63725
+ flexDirection: "column",
63726
+ gap: "var(--jx-space-4)"
63727
+ },
63728
+ '& > [part="content"] > [part="icon"]': {
63729
+ $description: 'The child, not any descendant: `jx-button` renders its own `<span part="icon">` into light DOM unconditionally, so a bare `[part="icon"]` reached the Reload and Copy buttons too. `--jx-icon-size` is a custom property and `color` inherits, so both passed through that span\'s `display: contents` and drew the Copy button\'s clipboard glyph at 32px in warning amber, stretching the actions row with it. This is the collision `[part="error"]` taught: a part name is shared with the kit unless a selector says otherwise.',
63730
+ "--jx-icon-size": "2rem",
63731
+ color: "var(--jx-warning)"
63732
+ },
63733
+ '& [part="title"]': {
63734
+ margin: "0",
63735
+ fontSize: "1.5rem",
63736
+ fontWeight: "400",
63737
+ color: "var(--jx-fg)"
63738
+ },
63739
+ '& [part="lead"]': {
63740
+ margin: "0",
63741
+ fontSize: "var(--jx-text-lg)",
63742
+ lineHeight: "1.5",
63743
+ color: "var(--jx-fg)"
63744
+ },
63745
+ '& [part="detail"]': {
63746
+ $description: "The launcher's own error, verbatim. Selectable, because the point of printing it is that someone pastes it into an issue, and wrapped, because a stack line is wider than any window it is likely to be read in.",
63747
+ margin: "0",
63748
+ padding: "var(--jx-space-3) var(--jx-space-4)",
63749
+ maxHeight: "40vh",
63750
+ overflow: "auto",
63751
+ background: "var(--jx-bg-panel)",
63752
+ border: "1px solid var(--jx-border)",
63753
+ borderRadius: "var(--jx-radius-sm)",
63754
+ fontFamily: "var(--jx-font-mono)",
63755
+ fontSize: "var(--jx-text-sm)",
63756
+ whiteSpace: "pre-wrap",
63757
+ wordBreak: "break-word",
63758
+ userSelect: "text"
63759
+ },
63760
+ '& [part="facts"]': {
63761
+ margin: "0",
63762
+ fontSize: "var(--jx-text-sm)",
63763
+ color: "var(--jx-fg-dim)",
63764
+ wordBreak: "break-all",
63765
+ userSelect: "text"
63766
+ },
63767
+ '& [part="actions"]': {
63768
+ display: "flex",
63769
+ flexWrap: "wrap",
63770
+ gap: "var(--jx-space-3)"
63771
+ }
63772
+ },
63773
+ children: [
63774
+ {
63775
+ tagName: "div",
63776
+ attributes: {
63777
+ part: "drag-strip",
63778
+ class: "electrobun-webkit-app-region-drag",
63779
+ "aria-hidden": "true"
63780
+ }
63781
+ },
63782
+ {
63783
+ tagName: "div",
63784
+ attributes: {
63785
+ part: "content"
63786
+ },
63787
+ children: [
63788
+ {
63789
+ tagName: "jx-icon",
63790
+ attributes: {
63791
+ part: "icon"
63792
+ },
63793
+ $props: {
63794
+ name: "warning",
63795
+ weight: "fill"
63796
+ }
63797
+ },
63798
+ {
63799
+ tagName: "h1",
63800
+ attributes: {
63801
+ part: "title"
63802
+ },
63803
+ textContent: "${state.title}"
63804
+ },
63805
+ {
63806
+ tagName: "p",
63807
+ attributes: {
63808
+ part: "lead"
63809
+ },
63810
+ textContent: "${state.lead}"
63811
+ },
63812
+ {
63813
+ tagName: "div",
63814
+ attributes: {
63815
+ part: "detail-slot",
63816
+ role: "none"
63817
+ },
63818
+ style: {
63819
+ display: "contents"
63820
+ },
63821
+ $switch: {
63822
+ $ref: "#/state/hasDetail"
63823
+ },
63824
+ cases: {
63825
+ true: {
63826
+ tagName: "pre",
63827
+ attributes: {
63828
+ part: "detail"
63829
+ },
63830
+ textContent: "${state.detail}"
63831
+ }
63832
+ }
63833
+ },
63834
+ {
63835
+ tagName: "p",
63836
+ attributes: {
63837
+ part: "facts"
63838
+ },
63839
+ textContent: "${state.facts}"
63840
+ },
63841
+ {
63842
+ tagName: "div",
63843
+ attributes: {
63844
+ part: "actions"
63845
+ },
63846
+ children: [
63847
+ {
63848
+ tagName: "jx-button",
63849
+ attributes: {
63850
+ part: "reload"
63851
+ },
63852
+ $props: {
63853
+ variant: "accent"
63854
+ },
63855
+ onclick: {
63856
+ $prototype: "Function",
63857
+ body: [
63858
+ {
63859
+ operator: "call",
63860
+ target: {
63861
+ $ref: "#/state/reload"
63862
+ },
63863
+ value: []
63864
+ }
63865
+ ]
63866
+ },
63867
+ children: [
63868
+ {
63869
+ tagName: "span",
63870
+ textContent: "Reload"
63871
+ }
63872
+ ]
63873
+ },
63874
+ {
63875
+ tagName: "jx-button",
63876
+ attributes: {
63877
+ part: "copy"
63878
+ },
63879
+ $props: {
63880
+ quiet: true
63881
+ },
63882
+ onclick: {
63883
+ $prototype: "Function",
63884
+ body: [
63885
+ {
63886
+ operator: "call",
63887
+ target: {
63888
+ $ref: "#/state/copy"
63889
+ },
63890
+ value: []
63891
+ }
63892
+ ]
63893
+ },
63894
+ children: [
63895
+ {
63896
+ tagName: "jx-icon",
63897
+ attributes: {
63898
+ slot: "icon"
63899
+ },
63900
+ $props: {
63901
+ name: "copy"
63902
+ }
63903
+ },
63904
+ {
63905
+ tagName: "span",
63906
+ textContent: "${state.copied ? 'Copied' : 'Copy details'}"
63907
+ }
63908
+ ]
63909
+ }
63910
+ ]
63911
+ }
63912
+ ]
63913
+ }
63914
+ ]
63915
+ };
63916
+
63917
+ // src/surfaces/boot-failure.ts
63918
+ registerSurface("boot-failure", boot_failure_default);
63919
+ function leadFor(refusal) {
63920
+ if (refusal.kind === "launcher") {
63921
+ return "The desktop app's bridge to its backend failed to start, so nothing can be opened, saved or created; reinstalling the latest release from the download page fixes this.";
63922
+ }
63923
+ if (refusal.kind === "declared") {
63924
+ return "This page's startup script never ran, so Studio has no backend to open, save or create projects with.";
63925
+ }
63926
+ return `This page was opened from a ${refusal.protocol} URL, which has no Studio backend behind it; open Studio through the desktop app or a Jx dev server instead.`;
63927
+ }
63928
+ function detailFor() {
63929
+ const error = launcherSignal2()?.error;
63930
+ if (!error) {
63931
+ return "";
63932
+ }
63933
+ const { message, stack } = error;
63934
+ if (!stack) {
63935
+ return message;
63936
+ }
63937
+ return stack.includes(message) ? stack : `${message}
63938
+ ${stack}`;
63939
+ }
63940
+ function bootFailureView(refusal, page = location) {
63941
+ const detail = detailFor();
63942
+ const launcher = (refusal.kind === "launcher" ? refusal.launcher : undefined) ?? launcherSignal2()?.launcher ?? "none";
63943
+ const [where] = page.href.split(/[?#]/u);
63944
+ return {
63945
+ detail,
63946
+ facts: `${APP_NAME} bundle ${VERSION} (${GIT_COMMIT}) · launcher ${launcher} · ${where}`,
63947
+ hasDetail: detail !== "",
63948
+ lead: leadFor(refusal),
63949
+ title: "Jx Studio couldn't connect to its backend"
63950
+ };
63951
+ }
63952
+ function bootFailureReport(view) {
63953
+ return [view.title, view.lead, view.detail, view.facts].filter(Boolean).join(`
63954
+
63955
+ `);
63956
+ }
63957
+ function mountBootFailureSurface(host, refusal, page = location) {
63958
+ const view = bootFailureView(refusal, page);
63959
+ const scope = reactive({
63960
+ ...view,
63961
+ copied: false,
63962
+ async copy() {
63963
+ try {
63964
+ await navigator.clipboard.writeText(bootFailureReport(view));
63965
+ scope.copied = true;
63966
+ } catch {}
63967
+ },
63968
+ reload() {
63969
+ location.reload();
63970
+ }
63971
+ });
63972
+ return mountSurface("boot-failure", scope, host);
63973
+ }
63326
63974
 
63327
63975
  // src/shell/tree.ts
63328
63976
  function overlayLayers() {
@@ -63357,6 +64005,12 @@ async function mountShellTree(host = document.body) {
63357
64005
  await mountShellSurface(root);
63358
64006
  render4(overlayLayers(), root);
63359
64007
  }
64008
+ async function mountBootFailureTree(refusal, host = document.body) {
64009
+ const root = shellRoot(host);
64010
+ root.textContent = "";
64011
+ delete root["_$litPart$"];
64012
+ await mountBootFailureSurface(root, refusal);
64013
+ }
63360
64014
  function shellRoot(host) {
63361
64015
  const existing = host.querySelector?.("#shell-root");
63362
64016
  if (existing) {
@@ -63369,6 +64023,7 @@ function shellRoot(host) {
63369
64023
  }
63370
64024
 
63371
64025
  // src/studio.ts
64026
+ installModulePreloadBatcher();
63372
64027
  setBundleBase(import.meta.url);
63373
64028
  var derivationDeps = {
63374
64029
  fileExists: async (path) => {
@@ -63407,7 +64062,14 @@ async function navigateToComponent(componentPath) {
63407
64062
  }
63408
64063
  initCssData(webdata_default);
63409
64064
  if (!hasPlatform2()) {
63410
- registerPlatform2(resolveDefaultPlatform());
64065
+ try {
64066
+ registerPlatform2(resolveDefaultPlatform());
64067
+ } catch (error) {
64068
+ if (error instanceof PlatformUnavailableError) {
64069
+ await mountBootFailureTree(error.refusal);
64070
+ }
64071
+ throw error;
64072
+ }
63411
64073
  }
63412
64074
  mountResizeEdges();
63413
64075
  registerKit();
@@ -63434,6 +64096,7 @@ mount5(primaryCell?.chrome ?? document.createElement("div"), {
63434
64096
  parseMediaEntries,
63435
64097
  setCanvasMode
63436
64098
  });
64099
+ setDiffChromeRepaint(render7);
63437
64100
  mount4({
63438
64101
  isEditing,
63439
64102
  renderBlockActionBar
@@ -63515,11 +64178,8 @@ initWelcome({
63515
64178
  }
63516
64179
  },
63517
64180
  cloneRepository: () => cloneRepository2({ openRecentProject }),
63518
- openNewProject: async (options) => {
63519
- const result = await openNewProjectModal(options);
63520
- if (result) {
63521
- openRecentProject(result.root);
63522
- }
64181
+ openNewProject: (options) => {
64182
+ openNewProjectModal(options);
63523
64183
  },
63524
64184
  openProject: () => openProject(),
63525
64185
  openRecentProject: (root) => openRecentProject(root)
@@ -64047,5 +64707,5 @@ export {
64047
64707
  hasUnsavedTabs
64048
64708
  };
64049
64709
 
64050
- //# debugId=D568C37BC343E53964756E2164756E21
64710
+ //# debugId=17BBB037A446E70F64756E2164756E21
64051
64711
  //# sourceMappingURL=studio.js.map