@jxsuite/studio 5.3.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 (389) hide show
  1. package/data/css-meta.json +28 -27
  2. package/dist/chunks/{cf-account-picker-nwnwdm6p.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-4rzvmp0m.js → content-source-jt7rbcbc.js} +22 -22
  6. package/dist/chunks/{convert-to-repeater-jxsv62v7.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-2mrptyek.js → extension-sections-r73an5s6.js} +30 -29
  10. package/dist/chunks/{file-ops-knfqshrm.js → file-ops-3xan8mnj.js} +21 -21
  11. package/dist/chunks/{files-y6fr4he9.js → files-egsb0ry8.js} +28 -28
  12. package/dist/chunks/{format-host-1k4qrcqd.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-2rkrsjva.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-a7b87n2c.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-scpw01h8.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/layers-panel-83a16ytx.js +41 -0
  25. package/dist/chunks/{layers-jnvdbqva.js → layers-s6mc01ry.js} +3 -3
  26. package/dist/chunks/{media-picker-f66nav16.js → media-picker-nhn6rv4w.js} +11 -11
  27. package/dist/chunks/{menu-1dfhry4s.js → menu-8j4qxjrp.js} +4 -4
  28. package/dist/chunks/{monaco-binding-sw6en4dw.js → monaco-binding-e8d9wzyk.js} +3 -3
  29. package/dist/chunks/{monaco-buffer-pykwbm92.js → monaco-buffer-jaq1t9zj.js} +2 -2
  30. package/dist/chunks/{monaco-setup-8vbyqnhr.css → monaco-setup-ey9cjcm3.css} +954 -137
  31. package/dist/chunks/{monaco-setup-km6abc6q.js → monaco-setup-gsjdysze.js} +6 -6
  32. package/dist/chunks/{platform-ts9cg1a4.js → platform-fnchmhy7.js} +5 -3
  33. package/dist/chunks/publish-panel-ak6hxfma.js +27 -0
  34. package/dist/chunks/quick-search-c63mkt3b.js +47 -0
  35. package/dist/chunks/{references-h69xp9zz.js → references-f7bdv3z5.js} +3 -3
  36. package/dist/chunks/right-panel-t5n8wttg.js +45 -0
  37. package/dist/chunks/{studio-ze46gzjh.js → studio-01269reb.js} +7 -7
  38. package/dist/chunks/{studio-rnyf3f8e.js → studio-0a4gncx0.js} +3 -3
  39. package/dist/chunks/{studio-e1f24eff.js → studio-0d4p4wxj.js} +6778 -2993
  40. package/dist/chunks/{studio-4wbb9ct0.js → studio-1tejxvba.js} +3006 -1855
  41. package/dist/chunks/{studio-d1n2bykn.js → studio-3nprwbd9.js} +117 -25
  42. package/dist/chunks/{studio-4c041r8y.js → studio-40z39dme.js} +3 -3
  43. package/dist/chunks/{studio-151w0xnh.js → studio-482eeazk.js} +20 -20
  44. package/dist/chunks/{studio-cgprcdxn.js → studio-4998spet.js} +10 -9
  45. package/dist/chunks/{studio-nq5yf5mp.js → studio-557bmn0x.js} +2 -2
  46. package/dist/chunks/{studio-rtskkm43.js → studio-5w26wza5.js} +3 -3
  47. package/dist/chunks/{studio-6nzaktvy.js → studio-6pwn9j8c.js} +6 -3
  48. package/dist/chunks/{studio-wnsyffg1.js → studio-6vg0x5fz.js} +3 -2
  49. package/dist/chunks/{studio-vn5hske5.js → studio-7d3c1m59.js} +12 -5
  50. package/dist/chunks/{studio-6gryj0sg.js → studio-9vsvz7w7.js} +6 -6
  51. package/dist/chunks/{studio-50wfwy44.js → studio-9wm0xzpf.js} +19 -19
  52. package/dist/chunks/{studio-0j7r7s9g.js → studio-ag3z6shb.js} +10 -4
  53. package/dist/chunks/studio-aj9drheq.js +177 -0
  54. package/dist/chunks/{studio-xajk70w0.js → studio-apv9x70c.js} +189 -130
  55. package/dist/chunks/{studio-c04nzbby.js → studio-b9q3znz8.js} +2 -2
  56. package/dist/chunks/{studio-nfx354t1.js → studio-bh47w0cy.js} +9 -10
  57. package/dist/chunks/{studio-gk2twyh4.js → studio-dgdm6vvt.js} +264 -52
  58. package/dist/chunks/{studio-gtfq1dkz.js → studio-dwdcr2e8.js} +4 -4
  59. package/dist/chunks/{studio-nqgawfks.js → studio-fbcyz234.js} +146 -139
  60. package/dist/chunks/{studio-27q131j3.js → studio-fqpkpq1t.js} +7 -4
  61. package/dist/chunks/{studio-c0qgc3tn.js → studio-fy320dfc.js} +62 -68
  62. package/dist/chunks/{studio-zjjde71a.js → studio-gjj4bbfx.js} +2 -2
  63. package/dist/chunks/{studio-g33v4fe9.js → studio-jfhxy8h3.js} +7 -7
  64. package/dist/chunks/{studio-ab87phj1.js → studio-kwewmhhq.js} +4 -4
  65. package/dist/chunks/{studio-7rrb1esn.js → studio-m58xn8at.js} +25 -24
  66. package/dist/chunks/{studio-fzjx96t2.js → studio-mfkh1q8d.js} +17 -17
  67. package/dist/chunks/{studio-drg8qj9k.js → studio-mrh0a92f.js} +17 -17
  68. package/dist/chunks/{studio-jt27ta6x.js → studio-mt79tn56.js} +3 -3
  69. package/dist/chunks/{studio-b66rsh78.js → studio-ne593z1j.js} +38 -31
  70. package/dist/chunks/{studio-h4nvnjvz.js → studio-ptfcxkz5.js} +418 -232
  71. package/dist/chunks/{studio-dd1qqkry.js → studio-qxye31h7.js} +7 -7
  72. package/dist/chunks/{studio-ye9drbk8.js → studio-rav5bpmp.js} +73 -52
  73. package/dist/chunks/{studio-6kvgrkrp.js → studio-rwmmbrth.js} +6 -6
  74. package/dist/chunks/{studio-b7xacrwe.js → studio-sceg8ws0.js} +8 -8
  75. package/dist/chunks/{studio-msc1ee1m.js → studio-sjas5b3a.js} +19 -13
  76. package/dist/chunks/{studio-a65w20ex.js → studio-vd67b66d.js} +3 -3
  77. package/dist/chunks/{studio-7144epdr.js → studio-w475cbf9.js} +10 -10
  78. package/dist/chunks/{studio-4f3psrk1.js → studio-w7gpcvfj.js} +9 -9
  79. package/dist/chunks/{studio-nsgw7dbk.js → studio-xrbfdxgk.js} +7 -7
  80. package/dist/chunks/{studio-ke09tn9k.js → studio-y49b6mjv.js} +35 -35
  81. package/dist/chunks/{studio-j0w60gft.js → studio-ygvr9e4q.js} +10 -10
  82. package/dist/chunks/{studio-z2smx5ce.js → studio-yxmmjy2a.js} +9 -6
  83. package/dist/chunks/{tsMode-jy4m86rv.js → tsMode-ay4c78ne.js} +4 -4
  84. package/dist/chunks/{tsMode-hnxxb73h.css → tsMode-c72wyz27.css} +954 -137
  85. package/dist/chunks/{workspace-np10mayv.js → workspace-b9k2frks.js} +5 -5
  86. package/dist/chunks/{ws-client-ny4zc51r.js → ws-client-erze4czt.js} +356 -20
  87. package/dist/codicon.ttf +0 -0
  88. package/dist/iframe-entry.js +18 -16
  89. package/dist/studio.css +956 -139
  90. package/dist/studio.js +1453 -830
  91. package/dist/workers/editor.worker.js +196 -107
  92. package/dist/workers/json.worker.js +196 -107
  93. package/dist/workers/ts.worker.js +196 -107
  94. package/package.json +15 -14
  95. package/src/about/about-modal.ts +4 -3
  96. package/src/browse/library-commands.ts +15 -13
  97. package/src/browse/library-pane.ts +2 -2
  98. package/src/canvas/canvas-render.ts +97 -83
  99. package/src/canvas/canvas-surface.ts +2 -2
  100. package/src/canvas/canvas-utils.ts +42 -32
  101. package/src/canvas/diff-toolbar.ts +21 -2
  102. package/src/canvas/diff-view.ts +30 -6
  103. package/src/canvas/iframe-entry.ts +2 -2
  104. package/src/canvas/iframe-host.ts +5 -5
  105. package/src/canvas/iframe-interaction.ts +7 -6
  106. package/src/canvas/iframe-overlay.ts +1 -1
  107. package/src/canvas/iframe-protocol.ts +6 -5
  108. package/src/canvas/iframe-render.ts +3 -3
  109. package/src/canvas/popover-state.ts +1 -1
  110. package/src/canvas/stage-scrollbar.ts +112 -0
  111. package/src/canvas/surface-registry.ts +9 -2
  112. package/src/collab/collab-commands.ts +8 -6
  113. package/src/collab/collab-session.ts +4 -4
  114. package/src/collab/collab-state.ts +4 -4
  115. package/src/collab/presence-chips.ts +3 -3
  116. package/src/commands/app-commands.ts +7 -7
  117. package/src/commands/budget.ts +17 -16
  118. package/src/commands/command-args.ts +10 -9
  119. package/src/commands/context.ts +20 -10
  120. package/src/commands/defaults.ts +30 -29
  121. package/src/commands/levels.ts +8 -6
  122. package/src/commands/live-context.ts +12 -10
  123. package/src/commands/reference.ts +5 -5
  124. package/src/commands/registry.ts +2 -2
  125. package/src/content/draft-state.ts +9 -8
  126. package/src/content/entry-commands.ts +16 -14
  127. package/src/content/entry-editor.ts +2 -2
  128. package/src/content/entry-fields.ts +1 -1
  129. package/src/editor/context-menu.ts +4 -4
  130. package/src/editor/convert-to-component.ts +6 -5
  131. package/src/editor/inline-edit.ts +3 -3
  132. package/src/editor/shortcuts.ts +88 -20
  133. package/src/editor/slash-menu.ts +11 -10
  134. package/src/files/elements.ts +4 -5
  135. package/src/files/file-ops.ts +1 -1
  136. package/src/files/files.ts +21 -17
  137. package/src/files/serialize-document.ts +1 -1
  138. package/src/format/convert-file.ts +5 -5
  139. package/src/github/github-auth-status.ts +107 -0
  140. package/src/github/github-auth.ts +18 -82
  141. package/src/github/github-publish.ts +7 -6
  142. package/src/grid/cell-editors.ts +8 -7
  143. package/src/grid/grid-layout.ts +1 -1
  144. package/src/grid/grid-open.ts +6 -5
  145. package/src/grid/grid-panel.ts +13 -12
  146. package/src/grid/redirects-grid.ts +14 -11
  147. package/src/grid/redirects.ts +10 -5
  148. package/src/grid/sources/content-source.ts +10 -9
  149. package/src/hosting/document.ts +28 -1
  150. package/src/hosting/stage.ts +15 -4
  151. package/src/i18n/i18n-commands.ts +9 -7
  152. package/src/new-project/location-fields.ts +16 -3
  153. package/src/new-project/new-project-modal.ts +23 -7
  154. package/src/page-params.ts +6 -6
  155. package/src/panels/activity-panel.ts +11 -10
  156. package/src/panels/ai-chat/chat-view.ts +27 -14
  157. package/src/panels/ai-chat/composer.ts +10 -3
  158. package/src/panels/ai-panel.ts +87 -29
  159. package/src/panels/block-action-bar.ts +59 -33
  160. package/src/panels/bottom-dock.ts +22 -22
  161. package/src/panels/data-explorer.ts +9 -9
  162. package/src/panels/data-grid.ts +1 -1
  163. package/src/panels/editors.ts +8 -8
  164. package/src/panels/elements-panel.ts +7 -8
  165. package/src/panels/empty-state.ts +1 -1
  166. package/src/panels/events-panel.ts +13 -12
  167. package/src/panels/formula-workspace.ts +8 -5
  168. package/src/panels/frontmatter-fields.ts +4 -1
  169. package/src/panels/frontmatter-panel.ts +18 -18
  170. package/src/panels/git-panel.ts +19 -13
  171. package/src/panels/head-panel.ts +9 -10
  172. package/src/panels/i18n-panel.ts +6 -4
  173. package/src/panels/imports-panel.ts +1 -1
  174. package/src/panels/jump-bar.ts +20 -18
  175. package/src/panels/layers-panel.ts +19 -17
  176. package/src/panels/left-panel.ts +3 -3
  177. package/src/panels/navigator-panels.ts +8 -8
  178. package/src/panels/overlays.ts +1 -1
  179. package/src/panels/pane-context.ts +56 -28
  180. package/src/panels/pane-grid.ts +9 -8
  181. package/src/panels/panel-registry.ts +47 -19
  182. package/src/panels/problems-panel.ts +11 -12
  183. package/src/panels/properties-panel.ts +21 -20
  184. package/src/panels/provenance.ts +3 -3
  185. package/src/panels/quick-search.ts +15 -11
  186. package/src/panels/right-panel.ts +4 -3
  187. package/src/panels/seo-modal.ts +5 -4
  188. package/src/panels/settings-menu.ts +9 -9
  189. package/src/panels/settings-pane.ts +3 -3
  190. package/src/panels/signals-panel.ts +16 -9
  191. package/src/panels/statement-editor.ts +12 -9
  192. package/src/panels/style-panel.ts +45 -41
  193. package/src/panels/stylebook-panel.ts +0 -1
  194. package/src/panels/tab-strip.ts +26 -23
  195. package/src/platform-errors.ts +2 -2
  196. package/src/platform.ts +44 -4
  197. package/src/platforms/cloud.ts +11 -1
  198. package/src/platforms/default-platform.ts +141 -4
  199. package/src/platforms/devserver.ts +23 -3
  200. package/src/publish/deploy-checklist.ts +11 -10
  201. package/src/publish/publish-commands.ts +9 -8
  202. package/src/services/a11y-report.ts +1 -1
  203. package/src/services/ai-ask.ts +39 -16
  204. package/src/services/ai-command-tools.ts +28 -14
  205. package/src/services/ai-import-tools.ts +43 -29
  206. package/src/services/ai-project-tools.ts +20 -38
  207. package/src/services/ai-settings.ts +55 -2
  208. package/src/services/ai-tools.ts +69 -58
  209. package/src/services/ai-write-report.ts +4 -4
  210. package/src/services/ai-writes.ts +74 -55
  211. package/src/services/automation.ts +12 -12
  212. package/src/services/clock.ts +2 -1
  213. package/src/services/connection.ts +2 -2
  214. package/src/services/context-manager.ts +69 -6
  215. package/src/services/directory-picker.ts +93 -27
  216. package/src/services/document-assistant.ts +86 -17
  217. package/src/services/gated-registry.ts +4 -3
  218. package/src/services/idle.ts +5 -4
  219. package/src/services/live-surfaces.ts +2 -1
  220. package/src/services/notify.ts +9 -7
  221. package/src/services/popover-report.ts +3 -3
  222. package/src/services/preview-eval.ts +1 -1
  223. package/src/services/project-adoption.ts +34 -1
  224. package/src/services/project-path.ts +53 -0
  225. package/src/services/settings/kernel.ts +13 -2
  226. package/src/services/tool-executor.ts +135 -30
  227. package/src/services/tool-outcomes.ts +143 -0
  228. package/src/settings/contexts-section.ts +8 -8
  229. package/src/settings/contributed-section.ts +6 -6
  230. package/src/settings/css-vars-editor.ts +6 -5
  231. package/src/settings/extension-commands.ts +19 -18
  232. package/src/settings/extension-sections.ts +7 -7
  233. package/src/settings/extensions-section.ts +3 -3
  234. package/src/settings/general-settings.ts +21 -18
  235. package/src/settings/preferences-accounts.ts +3 -3
  236. package/src/settings/preferences-dialog.ts +9 -8
  237. package/src/settings/preferences-sections.ts +1 -1
  238. package/src/settings/project-sections.ts +2 -2
  239. package/src/settings/section-registry.ts +7 -6
  240. package/src/settings/settings-document.ts +41 -43
  241. package/src/shell/tree.ts +29 -0
  242. package/src/shell.ts +63 -55
  243. package/src/site-context.ts +1 -1
  244. package/src/store.ts +2 -2
  245. package/src/studio.ts +46 -28
  246. package/src/style/live-preview.ts +8 -8
  247. package/src/style/project-styles.ts +8 -9
  248. package/src/style/token-ref.ts +2 -2
  249. package/src/surfaces/about.json +6 -2
  250. package/src/surfaces/add-repo.json +2 -2
  251. package/src/surfaces/add-repo.ts +2 -2
  252. package/src/surfaces/ai-chat.json +14 -6
  253. package/src/surfaces/ai-chat.ts +4 -2
  254. package/src/surfaces/ai-credentials-form.json +53 -49
  255. package/src/surfaces/ai-credentials-form.ts +18 -4
  256. package/src/surfaces/ai-managed-connect.json +4 -4
  257. package/src/surfaces/block-action-bar.json +4 -4
  258. package/src/surfaces/block-action-bar.ts +3 -3
  259. package/src/surfaces/boot-failure.json +228 -0
  260. package/src/surfaces/boot-failure.ts +156 -0
  261. package/src/surfaces/bottom-dock.json +1 -1
  262. package/src/surfaces/canvas-stage.json +9 -40
  263. package/src/surfaces/canvas-stage.ts +9 -9
  264. package/src/surfaces/cf-account-picker.json +11 -8
  265. package/src/surfaces/commandbar.ts +11 -10
  266. package/src/surfaces/convert-repeater.ts +3 -2
  267. package/src/surfaces/dialog.json +7 -1
  268. package/src/surfaces/dialog.ts +2 -2
  269. package/src/surfaces/doc-header.json +6 -13
  270. package/src/surfaces/doc-header.ts +4 -5
  271. package/src/surfaces/entry-editor.json +1 -1
  272. package/src/surfaces/entry-editor.ts +2 -1
  273. package/src/surfaces/expression-editor.json +5 -5
  274. package/src/surfaces/files-panel.json +4 -3
  275. package/src/surfaces/files-panel.ts +1 -1
  276. package/src/surfaces/formula-palette.json +2 -2
  277. package/src/surfaces/formula-palette.ts +1 -1
  278. package/src/surfaces/git-panel.json +167 -109
  279. package/src/surfaces/git-panel.ts +2 -2
  280. package/src/surfaces/github-auth.json +2 -1
  281. package/src/surfaces/github-publish.ts +3 -3
  282. package/src/surfaces/grid-open.json +1 -1
  283. package/src/surfaces/grid-panel.json +2 -2
  284. package/src/surfaces/grid-views.json +1 -1
  285. package/src/surfaces/inspector-dock.json +2 -2
  286. package/src/surfaces/jump-bar.json +1 -1
  287. package/src/surfaces/jump-bar.ts +1 -1
  288. package/src/surfaces/library-pane.ts +2 -1
  289. package/src/surfaces/logic-panel.json +7 -7
  290. package/src/surfaces/logic-panel.ts +6 -4
  291. package/src/surfaces/logic-workspace.json +2 -2
  292. package/src/surfaces/logic-workspace.ts +2 -2
  293. package/src/surfaces/media-browser.ts +3 -3
  294. package/src/surfaces/media-pane.ts +2 -1
  295. package/src/surfaces/menu.ts +5 -5
  296. package/src/surfaces/new-project.json +1 -1
  297. package/src/surfaces/pane-context.json +86 -57
  298. package/src/surfaces/pane-context.ts +2 -1
  299. package/src/surfaces/pane-grid.json +3 -3
  300. package/src/surfaces/panel-activity.json +2 -1
  301. package/src/surfaces/panel-deploy-checklist.json +1 -1
  302. package/src/surfaces/panel-elements.ts +2 -1
  303. package/src/surfaces/panel-i18n.ts +2 -1
  304. package/src/surfaces/panel-outline.json +1 -1
  305. package/src/surfaces/panel-outline.ts +4 -3
  306. package/src/surfaces/panel-page.json +2 -2
  307. package/src/surfaces/panel-page.ts +4 -1
  308. package/src/surfaces/panel-problems.json +4 -3
  309. package/src/surfaces/panel-problems.ts +2 -2
  310. package/src/surfaces/panel-signals.json +23 -8
  311. package/src/surfaces/panel-signals.ts +5 -1
  312. package/src/surfaces/panel-stylebook-layers.json +1 -1
  313. package/src/surfaces/panel-stylebook-layers.ts +2 -2
  314. package/src/surfaces/preferences.json +21 -8
  315. package/src/surfaces/preferences.ts +9 -1
  316. package/src/surfaces/properties-panel.json +4 -4
  317. package/src/surfaces/properties-panel.ts +2 -2
  318. package/src/surfaces/publish.json +1 -1
  319. package/src/surfaces/publish.ts +2 -1
  320. package/src/surfaces/push-plan.json +4 -3
  321. package/src/surfaces/rail.json +2 -2
  322. package/src/surfaces/rail.ts +10 -4
  323. package/src/surfaces/reference-field.json +1 -1
  324. package/src/surfaces/schema-form.json +2 -2
  325. package/src/surfaces/schema-form.ts +1 -1
  326. package/src/surfaces/seo.json +1 -1
  327. package/src/surfaces/seo.ts +2 -2
  328. package/src/surfaces/settings-contributed.ts +2 -1
  329. package/src/surfaces/settings-css-vars.json +4 -4
  330. package/src/surfaces/settings-deploy.json +4 -4
  331. package/src/surfaces/settings-deploy.ts +2 -1
  332. package/src/surfaces/settings-extensions.json +6 -6
  333. package/src/surfaces/settings-extensions.ts +6 -2
  334. package/src/surfaces/settings-locales.json +4 -4
  335. package/src/surfaces/settings-overview.ts +2 -1
  336. package/src/surfaces/settings-pane.ts +3 -2
  337. package/src/surfaces/settings-rawjson.ts +1 -1
  338. package/src/surfaces/slash-menu.ts +4 -4
  339. package/src/surfaces/statements.json +3 -3
  340. package/src/surfaces/statusbar.ts +23 -23
  341. package/src/surfaces/style-panel.json +9 -5
  342. package/src/surfaces/style-panel.ts +3 -3
  343. package/src/surfaces/tab-strip.json +2 -2
  344. package/src/surfaces/target-line.json +1 -1
  345. package/src/surfaces/target-line.ts +12 -10
  346. package/src/surfaces/welcome.json +7 -2
  347. package/src/tabs/doc-op-apply.ts +5 -5
  348. package/src/tabs/patch-ops.ts +3 -3
  349. package/src/tabs/project-config.ts +10 -10
  350. package/src/tabs/selection.ts +1 -1
  351. package/src/tabs/tab.ts +13 -14
  352. package/src/tabs/transact.ts +18 -24
  353. package/src/types.ts +27 -3
  354. package/src/ui/ai-credentials-form.ts +74 -16
  355. package/src/ui/ai-managed-connect.ts +68 -19
  356. package/src/ui/color-selector.ts +1 -1
  357. package/src/ui/expression-editor.ts +3 -3
  358. package/src/ui/form-controls.ts +6 -5
  359. package/src/ui/formula-catalog.ts +4 -4
  360. package/src/ui/formula-chips.ts +2 -2
  361. package/src/ui/layers.ts +20 -18
  362. package/src/ui/panel-resize.ts +5 -5
  363. package/src/ui/popover-surface.ts +2 -2
  364. package/src/ui/progress-modal.ts +3 -3
  365. package/src/ui/regions.ts +14 -9
  366. package/src/ui/schema-form.ts +10 -9
  367. package/src/ui/timing.ts +9 -7
  368. package/src/ui/value-source.ts +4 -4
  369. package/src/ui/virtual-window.ts +1 -1
  370. package/src/utils/base64.ts +20 -1
  371. package/src/utils/canvas-media.ts +1 -1
  372. package/src/utils/studio-utils.ts +0 -31
  373. package/src/view.ts +4 -3
  374. package/src/workspace/pane-derive.ts +14 -13
  375. package/src/workspace/session.ts +7 -6
  376. package/src/workspace/workspace.ts +49 -39
  377. package/styles/canvas.css +1 -27
  378. package/styles/inspector.css +4 -3
  379. package/styles/overlays.css +1 -1
  380. package/styles/tokens.css +17 -1
  381. package/styles/tokens.json +6 -1
  382. package/dist/chunks/entry-commands-1wnsqn5d.js +0 -42
  383. package/dist/chunks/layers-panel-73gfkcga.js +0 -41
  384. package/dist/chunks/publish-panel-z1zd2da1.js +0 -27
  385. package/dist/chunks/quick-search-zdv7h8ht.js +0 -47
  386. package/dist/chunks/right-panel-889vbnm5.js +0 -45
  387. package/dist/chunks/studio-727xh0e3.js +0 -76
  388. package/src/files/json-layout.ts +0 -474
  389. 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-gk2twyh4.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-27q131j3.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-6gryj0sg.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-fzjx96t2.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-drg8qj9k.js";
372
+ } from "./chunks/studio-mrh0a92f.js";
356
373
  import {
357
374
  PUBLIC_DIR,
358
375
  setSkipServerFunctions,
@@ -373,7 +390,7 @@ import {
373
390
  KIT_BASE,
374
391
  setActiveRegistry,
375
392
  activeRegistry
376
- } from "./chunks/studio-d1n2bykn.js";
393
+ } from "./chunks/studio-3nprwbd9.js";
377
394
  import {
378
395
  DERIVE_PRESETS,
379
396
  PRESET_LABELS,
@@ -403,7 +420,7 @@ import {
403
420
  defBadgeLabel,
404
421
  mountSignalsPanel,
405
422
  registerSignalsCommands
406
- } from "./chunks/studio-ye9drbk8.js";
423
+ } from "./chunks/studio-rav5bpmp.js";
407
424
  import {
408
425
  mountsUsing,
409
426
  mountsOf,
@@ -429,7 +446,7 @@ import {
429
446
  showConfirmDialog2,
430
447
  showSaveDiscardDialog2,
431
448
  showPromptDialog2
432
- } from "./chunks/studio-0j7r7s9g.js";
449
+ } from "./chunks/studio-ag3z6shb.js";
433
450
  import {
434
451
  html3,
435
452
  nothing2,
@@ -445,11 +462,11 @@ import {
445
462
  uploadAssets,
446
463
  mediaSiteUrl,
447
464
  previewFileSrc
448
- } from "./chunks/studio-b7xacrwe.js";
449
- import"./chunks/studio-gtfq1dkz.js";
465
+ } from "./chunks/studio-sceg8ws0.js";
466
+ import"./chunks/studio-dwdcr2e8.js";
450
467
  import {
451
468
  invalidateUsages2
452
- } from "./chunks/studio-nq5yf5mp.js";
469
+ } from "./chunks/studio-557bmn0x.js";
453
470
  import {
454
471
  hydrateProjectList,
455
472
  getProjectList,
@@ -471,11 +488,7 @@ import {
471
488
  registerSourceControlCommands2,
472
489
  cleanupGitPanel2,
473
490
  registerGitPanel2
474
- } from "./chunks/studio-xajk70w0.js";
475
- import"./chunks/studio-a65w20ex.js";
476
- import {
477
- openMenu2
478
- } from "./chunks/studio-jt27ta6x.js";
491
+ } from "./chunks/studio-apv9x70c.js";
479
492
  import {
480
493
  parseGridTabId,
481
494
  gridTabLabel,
@@ -501,22 +514,26 @@ import {
501
514
  exportFile2,
502
515
  confirmFileDelete2,
503
516
  renamePromptMessage2
504
- } from "./chunks/studio-ke09tn9k.js";
517
+ } from "./chunks/studio-y49b6mjv.js";
518
+ import"./chunks/studio-vd67b66d.js";
519
+ import {
520
+ openMenu2
521
+ } from "./chunks/studio-mt79tn56.js";
505
522
  import {
506
523
  platformSupportsPublish,
507
524
  latestDeployment,
508
525
  currentDeploy,
509
526
  noteDeployment
510
- } from "./chunks/studio-nsgw7dbk.js";
527
+ } from "./chunks/studio-xrbfdxgk.js";
511
528
  import {
512
529
  resetIgnoreCache,
513
530
  startFsSync2
514
- } from "./chunks/studio-4c041r8y.js";
531
+ } from "./chunks/studio-40z39dme.js";
515
532
  import {
516
533
  beginActivity,
517
534
  registerActivityPanel,
518
535
  showProgressModal
519
- } from "./chunks/studio-cgprcdxn.js";
536
+ } from "./chunks/studio-4998spet.js";
520
537
  import {
521
538
  _defineProperty,
522
539
  combine,
@@ -545,12 +562,12 @@ import {
545
562
  createLibrarySource,
546
563
  openLibraryTab,
547
564
  registerGridCommands
548
- } from "./chunks/studio-50wfwy44.js";
565
+ } from "./chunks/studio-9wm0xzpf.js";
549
566
  import {
550
567
  DRAFT_FIELD,
551
568
  isDraftEntry
552
- } from "./chunks/studio-j0w60gft.js";
553
- import"./chunks/studio-msc1ee1m.js";
569
+ } from "./chunks/studio-ygvr9e4q.js";
570
+ import"./chunks/studio-sjas5b3a.js";
554
571
  import {
555
572
  computeWindow
556
573
  } from "./chunks/studio-a5h5j33a.js";
@@ -577,11 +594,11 @@ import {
577
594
  reloadCleanTab2,
578
595
  reloadFileInTab2,
579
596
  registerFilesPanel2
580
- } from "./chunks/studio-7rrb1esn.js";
597
+ } from "./chunks/studio-m58xn8at.js";
581
598
  import {
582
599
  REDIRECT_STATUSES,
583
600
  referenceTarget
584
- } from "./chunks/studio-4f3psrk1.js";
601
+ } from "./chunks/studio-w7gpcvfj.js";
585
602
  import"./chunks/studio-tjjk7jvk.js";
586
603
  import {
587
604
  FIELD_TYPES,
@@ -597,7 +614,7 @@ import {
597
614
  renderEntryMode,
598
615
  createEntry2,
599
616
  registerContentCommands2
600
- } from "./chunks/studio-151w0xnh.js";
617
+ } from "./chunks/studio-482eeazk.js";
601
618
  import {
602
619
  notifySettingsDocument2,
603
620
  onSettingsDocumentChanged2,
@@ -627,11 +644,11 @@ import {
627
644
  renderStylebookMode,
628
645
  selectStylebookTag,
629
646
  defaultCommands
630
- } from "./chunks/studio-nfx354t1.js";
647
+ } from "./chunks/studio-bh47w0cy.js";
631
648
  import {
632
649
  openPopoverSurface,
633
650
  invalidateMediaCache2
634
- } from "./chunks/studio-6kvgrkrp.js";
651
+ } from "./chunks/studio-rwmmbrth.js";
635
652
  import {
636
653
  findEnclosingRepeater,
637
654
  convertToComponent,
@@ -674,11 +691,11 @@ import {
674
691
  liveElementCommands,
675
692
  dismissContextMenu,
676
693
  showContextMenu
677
- } from "./chunks/studio-b66rsh78.js";
694
+ } from "./chunks/studio-ne593z1j.js";
678
695
  import {
679
696
  initQuickSearch2,
680
697
  openQuickSearch2
681
- } from "./chunks/studio-g33v4fe9.js";
698
+ } from "./chunks/studio-jfhxy8h3.js";
682
699
  import {
683
700
  initCssData,
684
701
  registerStyleCommands,
@@ -686,7 +703,7 @@ import {
686
703
  render2,
687
704
  inspectorTab2,
688
705
  setInspectorTab2
689
- } from "./chunks/studio-c0qgc3tn.js";
706
+ } from "./chunks/studio-fy320dfc.js";
690
707
  import {
691
708
  validateDoc2,
692
709
  validateProjectConfig2
@@ -694,11 +711,12 @@ import {
694
711
  import {
695
712
  getOpenAiKey,
696
713
  setOpenAiKey,
697
- hasOpenAiKey,
698
714
  getBaseUrl,
699
715
  storedModel,
700
716
  setModel,
701
717
  saveAiProvider,
718
+ aiProviderDiffers,
719
+ storedEquals,
702
720
  aiConnection,
703
721
  resetModelCache,
704
722
  cachedModels,
@@ -712,8 +730,8 @@ import {
712
730
  proxyModelsErrorMessage,
713
731
  preferredModel,
714
732
  fetchAvailableModels
715
- } from "./chunks/studio-vn5hske5.js";
716
- import"./chunks/studio-c04nzbby.js";
733
+ } from "./chunks/studio-7d3c1m59.js";
734
+ import"./chunks/studio-b9q3znz8.js";
717
735
  import {
718
736
  platformBrokersCf,
719
737
  refreshCfConnection,
@@ -723,13 +741,13 @@ import {
723
741
  revokeAccount,
724
742
  onCredentialsChanged,
725
743
  notifyCredentialsChanged
726
- } from "./chunks/studio-dd1qqkry.js";
744
+ } from "./chunks/studio-qxye31h7.js";
727
745
  import {
728
746
  seedPublishConnected2
729
- } from "./chunks/studio-7144epdr.js";
747
+ } from "./chunks/studio-w475cbf9.js";
730
748
  import {
731
749
  selectContributedEntry
732
- } from "./chunks/studio-ze46gzjh.js";
750
+ } from "./chunks/studio-01269reb.js";
733
751
  import {
734
752
  deepEqual2,
735
753
  matchChildren
@@ -751,7 +769,7 @@ import {
751
769
  import {
752
770
  entryCollections,
753
771
  collectionOfPath
754
- } from "./chunks/studio-rtskkm43.js";
772
+ } from "./chunks/studio-5w26wza5.js";
755
773
  import"./chunks/studio-q0a3yk4n.js";
756
774
  import {
757
775
  resolveAssetRef,
@@ -759,7 +777,7 @@ import {
759
777
  assetContextFor,
760
778
  previewAssetSrc,
761
779
  hostAssetDeclarations
762
- } from "./chunks/studio-ab87phj1.js";
780
+ } from "./chunks/studio-kwewmhhq.js";
763
781
  import {
764
782
  rectOf,
765
783
  elementAtPoint
@@ -774,7 +792,7 @@ import {
774
792
  __preload,
775
793
  __chunks
776
794
  } from "./chunks/studio-5te5tqng.js";
777
- __chunks(import.meta.url,["gvb32txh","3y6y0nrm","z1zd2da1","reg9eevt","h69xp9zz","42gk8ha1","np10mayv","pykwbm92","ts9cg1a4","jnvdbqva","m244w9nh","sw6en4dw","5y78e05d","nwnwdm6p","ny4zc51r","adg74qsk","5pq32t0q","knfqshrm","scpw01h8","a7b87n2c","f66nav16","epp2z3s3","1k4qrcqd","y6fr4he9","2mrptyek","3ntvq6cg","yxa81f45","gk2twyh4","27q131j3","m1p9b6c3","z2smx5ce","6nzaktvy","d8z5bajd","wnsyffg1","6gryj0sg","fzjx96t2","zjjde71a","drg8qj9k","d1n2bykn","ye9drbk8","0j7r7s9g","1k325pwf","b7xacrwe","gtfq1dkz","nq5yf5mp","xajk70w0","a65w20ex","jt27ta6x","ke09tn9k","nsgw7dbk","4c041r8y","cgprcdxn","esrxgwh6","t171hxhe","50wfwy44","j0w60gft","msc1ee1m","a5h5j33a","7rrb1esn","4f3psrk1","tjjk7jvk","151w0xnh","0hqbpbd0","vhv5tgxm","nfx354t1","6kvgrkrp","b66rsh78","g33v4fe9","c0qgc3tn","fgmfksz8","vn5hske5","c04nzbby","dd1qqkry","7144epdr","ze46gzjh","wqpngpsk","98b3z07n","1tbjfqv4","rtskkm43","q0a3yk4n","ab87phj1","rs76k67r","r6xmmtfh","5te5tqng","rnyf3f8e","h4nvnjvz","n4j7qxyr","727xh0e3","3qhbg51x","km6abc6q","zdv7h8ht","nj7tnjfn","889vbnm5","2rkrsjva","1dfhry4s","1wnsqn5d","4rzvmp0m","jxsv62v7","73gfkcga","g79n01v9","mdmyx6s2","ks2zzth2","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-z1zd2da1.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-h69xp9zz.js",44,31,26,25],["./chunks/notify-42gk8ha1.js",32],["./chunks/workspace-np10mayv.js",27,33,29,34,31,25,32],["./chunks/monaco-buffer-pykwbm92.js",33,32],["./chunks/platform-ts9cg1a4.js",31],["./chunks/layers-jnvdbqva.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-nwnwdm6p.js",31,25,26,32,41,40,38,56,30,71,70,72],["./chunks/ws-client-ny4zc51r.js",85],["./chunks/cf-settings-adg74qsk.js",71,30,31,32],["./chunks/section-registry-5pq32t0q.js",62],["./chunks/file-ops-knfqshrm.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-scpw01h8.js",37,35,36,30,80,27,33,29,52,34,31,38,26,25,32],["./chunks/grid-view-a7b87n2c.js",31,34,25,29,27,41,40,38,32,26,65,33,42,43,55,50,44,49,35,30,51,28,48,46,47,37,36,52,53,77,79,80,81],["./chunks/media-picker-f66nav16.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-1k4qrcqd.js",34,31],["./chunks/files-y6fr4he9.js",58,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-2mrptyek.js",31,34,62,25,26,32,33,29,27,38,40,59,41,47,60,69,74,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-gk2twyh4.js",32,33,31,34,25,29,87,83],["./chunks/studio-27q131j3.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-6gryj0sg.js",31,83],["./chunks/studio-fzjx96t2.js",32,31,34,29,27,30,26,83],["./chunks/studio-zjjde71a.js",31,29],["./chunks/studio-drg8qj9k.js",31,25,29,32,33,34,27,35,36,38,52,80,81,82],["./chunks/studio-d1n2bykn.js",25,32,83],["./chunks/studio-ye9drbk8.js",31,25,29,36,37,32,33,34,27,38,35,41,40,28,46,59,52,47,65,44,87,81,83],["./chunks/studio-0j7r7s9g.js",41,38,32,26],["./chunks/studio-1k325pwf.js"],["./chunks/studio-b7xacrwe.js",26,31,32,29,27,38,80],["./chunks/studio-gtfq1dkz.js",31,38,42],["./chunks/studio-nq5yf5mp.js",31,26],["./chunks/studio-xajk70w0.js",41,26,31,34,29,27,32,42,40,38,43,35,28,56,51,37,53,81,83],["./chunks/studio-a65w20ex.js",31,25,29,34],["./chunks/studio-jt27ta6x.js",38,40,32,81],["./chunks/studio-ke09tn9k.js",31,25,26,44,32,34,29,27,42,38,40,47,35,49,37,50,51,79],["./chunks/studio-nsgw7dbk.js",31,29,32,35,38,40],["./chunks/studio-4c041r8y.js",31,44,29],["./chunks/studio-cgprcdxn.js",41,38,49,32,35,40,28,44],["./chunks/studio-esrxgwh6.js",83],["./chunks/studio-t171hxhe.js"],["./chunks/studio-50wfwy44.js",32,31,34,29,27,40,26,50,48,55,42],["./chunks/studio-j0w60gft.js",31,29,32,34,27,50,48,83],["./chunks/studio-msc1ee1m.js",26,32,40,38],["./chunks/studio-a5h5j33a.js",81],["./chunks/studio-7rrb1esn.js",41,26,27,40,38,32,29,31,34,50,44,35,30,51,28,48,36,52,45,42,54,47,57,78,83],["./chunks/studio-4f3psrk1.js",38,27,25,32,40,47,87],["./chunks/studio-tjjk7jvk.js"],["./chunks/studio-151w0xnh.js",34,32,29,27,26,38,40,59,55,35,48,78,87,83],["./chunks/studio-0hqbpbd0.js"],["./chunks/studio-vhv5tgxm.js"],["./chunks/studio-nfx354t1.js",29,38,40,32,27,35,36,37,83],["./chunks/studio-6kvgrkrp.js",31,29,40,32,42,43,80,81],["./chunks/studio-b66rsh78.js",41,25,29,32,33,31,27,35,28,36,38,40,64,26,37,52,39,47,44,57,79,81,83],["./chunks/studio-g33v4fe9.js",31,29,34,27,40,38,32,58],["./chunks/studio-c0qgc3tn.js",25,29,32,27,38,39,40,59,37,47,35,44,64,87],["./chunks/studio-fgmfksz8.js",101,83],["./chunks/studio-vn5hske5.js",31,30],["./chunks/studio-c04nzbby.js",30],["./chunks/studio-dd1qqkry.js",56,31,71,70,83],["./chunks/studio-7144epdr.js",38,49,31,29,32,40,71,70,83],["./chunks/studio-ze46gzjh.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-rtskkm43.js",29,27,55],["./chunks/studio-q0a3yk4n.js"],["./chunks/studio-ab87phj1.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,75,87],["./chunks/studio-727xh0e3.js"],["./chunks/editor-3qhbg51x.js",102],["./chunks/monaco-setup-km6abc6q.js",102,105,60,63,101,82,83],["./chunks/quick-search-zdv7h8ht.js",67,58,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-889vbnm5.js",68,64,39,65,43,59,46,28,44,37,35,36,30,42,80,27,33,29,52,34,31,47,40,41,38,26,25,32,60],["./chunks/git-panel-2rkrsjva.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-1dfhry4s.js",47,40,41,38,26,25,32],["./chunks/entry-commands-1wnsqn5d.js",61,78,59,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,60],["./chunks/content-source-4rzvmp0m.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-jxsv62v7.js",25,29,32,33,31,34,27,41,40,38,26,37,39,59,60,28,30,35,36,52,46,47,65,42,43,44,80],["./chunks/layers-panel-73gfkcga.js",66,64,39,65,57,43,59,79,46,28,44,37,35,36,30,42,80,27,33,29,52,34,31,47,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-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);
778
796
 
779
797
  // ../../node_modules/ms/index.js
780
798
  var require_ms = __commonJS(function(exports, module) {
@@ -1385,6 +1403,9 @@ function stepDiff(paneId, delta) {
1385
1403
  function diffViewOf(paneId) {
1386
1404
  return _records.get(paneId)?.view ?? "visual";
1387
1405
  }
1406
+ function diffShowsCode(paneId, renderable) {
1407
+ return !renderable || diffViewOf(paneId) === "code";
1408
+ }
1388
1409
  function setDiffView(paneId, view) {
1389
1410
  recordFor(paneId).view = view;
1390
1411
  }
@@ -1729,9 +1750,14 @@ var _repaint = () => {};
1729
1750
  function setDiffRepaint(repaint) {
1730
1751
  _repaint = repaint;
1731
1752
  }
1753
+ var _chromeRepaint = null;
1754
+ function setDiffChromeRepaint(repaint) {
1755
+ _chromeRepaint = repaint;
1756
+ }
1732
1757
  function repaintDiffStage(paneId) {
1733
1758
  surfaceForPane(paneId).prevCanvasMode = null;
1734
1759
  _repaint(paneId);
1760
+ _chromeRepaint?.();
1735
1761
  }
1736
1762
  function renderDiffToolbar(paneId) {
1737
1763
  const bar = _bars.get(paneId);
@@ -1779,7 +1805,7 @@ async function stepDiffAndReveal(paneId, delta) {
1779
1805
  if (!step) {
1780
1806
  return;
1781
1807
  }
1782
- const { hostForCanvas, measureInCanvas, revealCanvasPathIn } = await (__preload("scpw01h8"), import("./chunks/iframe-host-scpw01h8.js"));
1808
+ const { hostForCanvas, measureInCanvas, revealCanvasPathIn } = await (__preload("psetcv20"), import("./chunks/iframe-host-psetcv20.js"));
1783
1809
  const measured = await Promise.all([
1784
1810
  step.originalPath && original ? measureInCanvas(original, step.originalPath) : null,
1785
1811
  step.currentPath && current ? measureInCanvas(current, step.currentPath) : null
@@ -1997,6 +2023,37 @@ function buildChangeMap(a, b, opts = {}) {
1997
2023
  };
1998
2024
  }
1999
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
+
2000
2057
  // src/collab/monaco-cursors.ts
2001
2058
  function cssString(value) {
2002
2059
  return `"${value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`)}"`;
@@ -2047,7 +2104,7 @@ function loadGridEngine() {
2047
2104
  if (_engine) {
2048
2105
  return Promise.resolve(_engine);
2049
2106
  }
2050
- _loading ??= (__preload("a7b87n2c"), import("./chunks/grid-view-a7b87n2c.js")).then((engine) => {
2107
+ _loading ??= (__preload("qaqer9kr"), import("./chunks/grid-view-qaqer9kr.js")).then((engine) => {
2051
2108
  _engine = engine;
2052
2109
  return engine;
2053
2110
  });
@@ -2066,7 +2123,7 @@ var grid_panel_default = {
2066
2123
  part: "grid"
2067
2124
  },
2068
2125
  style: {
2069
- $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.",
2070
2127
  display: "flex",
2071
2128
  flexDirection: "column",
2072
2129
  height: "100%",
@@ -2518,7 +2575,7 @@ var grid_panel_default = {
2518
2575
  textContent: "${state.viewLabel}"
2519
2576
  }
2520
2577
  ],
2521
- $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."
2522
2579
  },
2523
2580
  {
2524
2581
  tagName: "jx-textfield",
@@ -2854,7 +2911,7 @@ function mountGridPanelSurface(host, view, actions) {
2854
2911
  var grid_views_default = {
2855
2912
  $schema: "https://jxsuite.com/schema/v1",
2856
2913
  $id: "StudioGridViews",
2857
- $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.",
2858
2915
  tagName: "jx-popover",
2859
2916
  attributes: {
2860
2917
  part: "views-panel"
@@ -7021,10 +7078,10 @@ var add_repo_default = {
7021
7078
  color: "var(--danger)"
7022
7079
  },
7023
7080
  '& [part="access"]': {
7024
- $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.",
7025
7082
  display: "flex",
7026
7083
  flexWrap: "wrap",
7027
- alignItems: "baseline",
7084
+ alignItems: "center",
7028
7085
  gap: "var(--jx-space-2)",
7029
7086
  paddingTop: "var(--jx-space-3)",
7030
7087
  borderTop: "1px solid var(--border)",
@@ -7678,10 +7735,15 @@ var welcome_default = {
7678
7735
  letterSpacing: "0.3px"
7679
7736
  },
7680
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.",
7681
7739
  display: "flex",
7682
- alignItems: "baseline",
7740
+ alignItems: "center",
7683
7741
  justifyContent: "space-between",
7684
- 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"
7685
7747
  },
7686
7748
  '& [part="actions"]': {
7687
7749
  display: "flex",
@@ -8860,7 +8922,7 @@ var tab_strip_default = {
8860
8922
  lineHeight: "1"
8861
8923
  },
8862
8924
  '& [part="pill"]': {
8863
- $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.',
8864
8926
  padding: "0 5px",
8865
8927
  borderRadius: "var(--radius)",
8866
8928
  background: "var(--bg)",
@@ -9093,7 +9155,7 @@ var tab_strip_default = {
9093
9155
  textContent: "${$map.item.pill}"
9094
9156
  },
9095
9157
  {
9096
- $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.",
9097
9159
  tagName: "button",
9098
9160
  attributes: {
9099
9161
  slot: "actions",
@@ -10253,7 +10315,7 @@ var panel_page_default = {
10253
10315
  padding: "0 var(--jx-space-2)"
10254
10316
  },
10255
10317
  '& [part="row"]': {
10256
- $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.",
10257
10319
  display: "flex",
10258
10320
  flexDirection: "column",
10259
10321
  alignItems: "stretch",
@@ -10269,7 +10331,7 @@ var panel_page_default = {
10269
10331
  color: "var(--fg-dim)"
10270
10332
  },
10271
10333
  '& [part="chip"]': {
10272
- $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.",
10273
10335
  width: "6px",
10274
10336
  height: "6px",
10275
10337
  padding: "0",
@@ -11937,7 +11999,7 @@ function removeEntry(key) {
11937
11999
  }
11938
12000
  var _mediaPicker = null;
11939
12001
  function mediaPicker() {
11940
- _mediaPicker ??= (__preload("f66nav16"), import("./chunks/media-picker-f66nav16.js"));
12002
+ _mediaPicker ??= (__preload("nhn6rv4w"), import("./chunks/media-picker-nhn6rv4w.js"));
11941
12003
  return _mediaPicker;
11942
12004
  }
11943
12005
  var ACTIONS = {
@@ -12040,28 +12102,21 @@ function registerPagePanel() {
12040
12102
  var doc_header_default = {
12041
12103
  $schema: "https://jxsuite.com/schema/v1",
12042
12104
  $id: "StudioDocHeader",
12043
- $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.",
12044
12106
  tagName: "section",
12045
12107
  attributes: {
12046
12108
  part: "card",
12047
12109
  "aria-label": "Document header",
12048
- "data-jx-region": "${state.region}",
12049
- "data-placement": "${state.placement}"
12110
+ "data-jx-region": "${state.region}"
12050
12111
  },
12051
12112
  style: {
12052
- $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.",
12053
12114
  display: "flex",
12054
12115
  flexDirection: "column",
12055
12116
  padding: "8px 12px 10px",
12056
12117
  background: "var(--bg-panel)",
12057
- border: "1px solid var(--border)",
12058
- borderRadius: "var(--radius)",
12118
+ borderBottom: "1px solid var(--border)",
12059
12119
  fontFamily: "var(--jx-font-sans)",
12060
- '&[data-placement="pinned"]': {
12061
- $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.",
12062
- borderWidth: "0 0 1px",
12063
- borderRadius: "0"
12064
- },
12065
12120
  '& [part="bar"]': {
12066
12121
  display: "flex",
12067
12122
  alignItems: "baseline",
@@ -12087,7 +12142,7 @@ var doc_header_default = {
12087
12142
  gap: "2px"
12088
12143
  },
12089
12144
  '& [part="row"]': {
12090
- $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.",
12091
12146
  display: "flex",
12092
12147
  flexDirection: "column",
12093
12148
  alignItems: "stretch",
@@ -12103,7 +12158,7 @@ var doc_header_default = {
12103
12158
  color: "var(--fg-dim)"
12104
12159
  },
12105
12160
  '& [part="chip"]': {
12106
- $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.",
12107
12162
  width: "6px",
12108
12163
  height: "6px",
12109
12164
  padding: "0",
@@ -12781,7 +12836,6 @@ var doc_header_default = {
12781
12836
  registerSurface("doc-header", doc_header_default);
12782
12837
  function project9(scope, view) {
12783
12838
  scope.region = view.region;
12784
- scope.placement = view.placement;
12785
12839
  scope.collection = view.collection;
12786
12840
  scope.route = view.route;
12787
12841
  scope.hasRoute = view.hasRoute;
@@ -12795,7 +12849,6 @@ function mountDocHeaderSurface(host, view, actions) {
12795
12849
  ...actions,
12796
12850
  collection: "Document",
12797
12851
  hasRoute: false,
12798
- placement: "in-column",
12799
12852
  rawEntries: [],
12800
12853
  rawOpen: false,
12801
12854
  rawState: "empty",
@@ -12950,7 +13003,6 @@ function viewFor(tab, paneId, card) {
12950
13003
  return {
12951
13004
  collection: collection ? collection.name : "Document",
12952
13005
  hasRoute: route !== null,
12953
- placement: card.el.dataset["placement"] === "pinned" ? "pinned" : "in-column",
12954
13006
  rawEntries: raw,
12955
13007
  rawOpen: _rawOpen.has(tab.id),
12956
13008
  rawState: raw.length === 0 ? "empty" : "list",
@@ -13126,7 +13178,7 @@ function makeActions(card) {
13126
13178
  }
13127
13179
  var _mediaPicker2 = null;
13128
13180
  function mediaPicker2() {
13129
- _mediaPicker2 ??= (__preload("f66nav16"), import("./chunks/media-picker-f66nav16.js"));
13181
+ _mediaPicker2 ??= (__preload("nhn6rv4w"), import("./chunks/media-picker-nhn6rv4w.js"));
13130
13182
  return _mediaPicker2;
13131
13183
  }
13132
13184
  // src/surfaces/canvas-stage.json
@@ -13139,7 +13191,7 @@ var canvas_stage_default = {
13139
13191
  part: "stage"
13140
13192
  },
13141
13193
  style: {
13142
- $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.",
13143
13195
  display: "contents",
13144
13196
  '& [part="panzoom"]': {
13145
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.",
@@ -13229,21 +13281,12 @@ var canvas_stage_default = {
13229
13281
  left: "50%",
13230
13282
  transform: "translateX(-50%)"
13231
13283
  },
13232
- '& [part="doc-header"][data-placement="in-column"]': {
13233
- $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.",
13234
- display: "block",
13235
- paddingBottom: "16px"
13236
- },
13237
- '& [part="doc-header"][data-placement="pinned"]': {
13238
- $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.",
13239
13286
  flexShrink: "0",
13240
- maxHeight: "40vh",
13241
- overflowY: "auto"
13242
- },
13243
- '& [part="doc-header"][data-placement="pinned"] .doc-header': {
13244
- $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.",
13245
- borderWidth: "0 0 1px",
13246
- borderRadius: "0"
13287
+ maxHeight: "min(40vh, 50%)",
13288
+ overflowY: "auto",
13289
+ boxShadow: "0 8px 8px -8px rgb(0 0 0 / 0.35)"
13247
13290
  },
13248
13291
  '& [part="edit-canvas"]': {
13249
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.",
@@ -13330,7 +13373,7 @@ var canvas_stage_default = {
13330
13373
  },
13331
13374
  children: [
13332
13375
  {
13333
- $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.",
13334
13377
  tagName: "div",
13335
13378
  attributes: {
13336
13379
  part: "lead-slot"
@@ -13343,10 +13386,10 @@ var canvas_stage_default = {
13343
13386
  },
13344
13387
  cases: {
13345
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`.",
13346
13390
  tagName: "div",
13347
13391
  attributes: {
13348
- part: "doc-header",
13349
- "data-placement": "pinned"
13392
+ part: "doc-header"
13350
13393
  }
13351
13394
  },
13352
13395
  toolbar: {
@@ -13427,28 +13470,6 @@ var canvas_stage_default = {
13427
13470
  }
13428
13471
  },
13429
13472
  children: [
13430
- {
13431
- $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.",
13432
- tagName: "div",
13433
- attributes: {
13434
- part: "column-header-slot"
13435
- },
13436
- style: {
13437
- display: "contents"
13438
- },
13439
- $switch: {
13440
- $ref: "#/state/columnHeader"
13441
- },
13442
- cases: {
13443
- shown: {
13444
- tagName: "div",
13445
- attributes: {
13446
- part: "doc-header",
13447
- "data-placement": "in-column"
13448
- }
13449
- }
13450
- }
13451
- },
13452
13473
  {
13453
13474
  $prototype: "Array",
13454
13475
  items: { $ref: "#/state/panels" },
@@ -13602,7 +13623,6 @@ function project10(scope, view) {
13602
13623
  scope.frameVars = view.frameVars;
13603
13624
  scope.hug = view.hug;
13604
13625
  scope.lead = view.lead;
13605
- scope.columnHeader = view.columnHeader;
13606
13626
  scope.handles = view.handles;
13607
13627
  scope.panels = view.panels;
13608
13628
  }
@@ -13610,7 +13630,6 @@ function mountCanvasStage(host, view, actions) {
13610
13630
  host.replaceChildren();
13611
13631
  const boards = new Map;
13612
13632
  const scope = reactive({
13613
- columnHeader: "hidden",
13614
13633
  frame: "boards",
13615
13634
  framePart: "panzoom",
13616
13635
  frameVars: "",
@@ -13632,7 +13651,7 @@ function mountCanvasStage(host, view, actions) {
13632
13651
  } else if (part && element instanceof HTMLElement) {
13633
13652
  const slot = attrOf(def, "data-slot");
13634
13653
  const named = slot === "frame" ? scope.framePart : slot === "inner" ? scope.innerPart : part;
13635
- actions.host(named, element, attrOf(def, "data-side") || attrOf(def, "data-placement"));
13654
+ actions.host(named, element, attrOf(def, "data-side"));
13636
13655
  }
13637
13656
  }
13638
13657
  }).then((surface) => {
@@ -13976,7 +13995,6 @@ function drawStage(surface, view, entries) {
13976
13995
  }
13977
13996
  function editorStageView(frame) {
13978
13997
  return {
13979
- columnHeader: "hidden",
13980
13998
  frame,
13981
13999
  framePart: "panzoom",
13982
14000
  frameVars: "",
@@ -14003,7 +14021,7 @@ function disposeSourceCollab(surface) {
14003
14021
  surface.sourceCollabCleanup = null;
14004
14022
  }
14005
14023
  async function createSourceCollabBinding(model, editor, ctx) {
14006
- const { bindMonacoToYText } = await (__preload("sw6en4dw"), import("./chunks/monaco-binding-sw6en4dw.js"));
14024
+ const { bindMonacoToYText } = await (__preload("e8d9wzyk"), import("./chunks/monaco-binding-e8d9wzyk.js"));
14007
14025
  const unbind = bindMonacoToYText({
14008
14026
  awareness: ctx.awareness,
14009
14027
  editors: [editor],
@@ -14046,9 +14064,6 @@ function sideMarks(marks, modified) {
14046
14064
  path: mark.path
14047
14065
  })) ?? null;
14048
14066
  }
14049
- function diffShowsCode(paneId, filePath) {
14050
- return !canRenderComparison(filePath) || diffViewOf(paneId) === "code";
14051
- }
14052
14067
  async function mountDiffEditor(surface, container, state) {
14053
14068
  const { paneId } = surface;
14054
14069
  const key = state.filePath;
@@ -14056,7 +14071,7 @@ async function mountDiffEditor(surface, container, state) {
14056
14071
  disposeDiffEditor(surface);
14057
14072
  }
14058
14073
  const monaco = await loadMonaco();
14059
- 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)))) {
14060
14075
  return;
14061
14076
  }
14062
14077
  const uris = diffModelUrisFor(paneId, key || "document.json");
@@ -14093,6 +14108,7 @@ function resetCanvasView(surface) {
14093
14108
  surface.centerObserver.disconnect();
14094
14109
  surface.centerObserver = null;
14095
14110
  }
14111
+ releaseStageScrollbar(surface);
14096
14112
  for (const p of surface.panels) {
14097
14113
  p.renderScope?.stop();
14098
14114
  p.renderScope = null;
@@ -14103,6 +14119,7 @@ function resetCanvasView(surface) {
14103
14119
  canvasWrap.style.padding = "";
14104
14120
  canvasWrap.style.alignItems = "";
14105
14121
  canvasWrap.style.flexDirection = "";
14122
+ canvasWrap.style.gap = "";
14106
14123
  canvasWrap.style.display = "";
14107
14124
  canvasWrap.style.overflow = "";
14108
14125
  dismissBlockActionBar();
@@ -14274,7 +14291,7 @@ function renderCanvasImpl(surface) {
14274
14291
  ui: tab.session.ui
14275
14292
  };
14276
14293
  const canvasMode = canvasModeOfPane(surface.paneId);
14277
- 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);
14278
14295
  if (!wantsDocHeader) {
14279
14296
  attachDocumentHeaderHost(surface.paneId, null);
14280
14297
  }
@@ -14347,6 +14364,7 @@ function renderCanvasImpl(surface) {
14347
14364
  surface.centerObserver.disconnect();
14348
14365
  surface.centerObserver = null;
14349
14366
  }
14367
+ releaseStageScrollbar(surface);
14350
14368
  detachGridPanel(surface.paneId);
14351
14369
  detachLibraryPane(surface.paneId);
14352
14370
  detachEntryPane(surface.paneId);
@@ -14361,9 +14379,9 @@ function renderCanvasImpl(surface) {
14361
14379
  canvasWrap.style.padding = "";
14362
14380
  canvasWrap.style.alignItems = "";
14363
14381
  canvasWrap.style.flexDirection = "";
14382
+ canvasWrap.style.gap = "";
14364
14383
  canvasWrap.style.display = "";
14365
14384
  canvasWrap.style.overflow = "";
14366
- canvasWrap.style.overflow = "";
14367
14385
  dismissBlockActionBar();
14368
14386
  dismissLinkPopover();
14369
14387
  dismissContextMenu();
@@ -14399,7 +14417,6 @@ function renderCanvasImpl(surface) {
14399
14417
  canvasPanels.push(entry.panel);
14400
14418
  const { featureToggles: previewToggles } = S.ui;
14401
14419
  drawStage(surface, {
14402
- columnHeader: "hidden",
14403
14420
  frame: "boards",
14404
14421
  framePart: "preview-stage",
14405
14422
  frameVars: `--preview-w:${previewWidth}px`,
@@ -14480,7 +14497,7 @@ function renderCanvasImpl(surface) {
14480
14497
  canvasWrap.style.overflow = "hidden";
14481
14498
  }
14482
14499
  const renderable = canRenderComparison(gitDiffState.filePath ?? "");
14483
- if (diffShowsCode(surface.paneId, gitDiffState.filePath ?? "")) {
14500
+ if (diffShowsCode(surface.paneId, renderable)) {
14484
14501
  if (!renderable) {
14485
14502
  setDiffChangeMap(surface.paneId, null);
14486
14503
  }
@@ -14502,7 +14519,6 @@ function renderCanvasImpl(surface) {
14502
14519
  const origEntry = canvasPanelEntry("git-diff-original", "Original", false, panelWidth);
14503
14520
  const currEntry = canvasPanelEntry("git-diff-current", "Current", false, panelWidth);
14504
14521
  const diffStage = drawStage(surface, {
14505
- columnHeader: "hidden",
14506
14522
  frame: "boards",
14507
14523
  framePart: "panzoom",
14508
14524
  frameVars: "",
@@ -14572,22 +14588,26 @@ function renderCanvasImpl(surface) {
14572
14588
  const rootTag = S.document.tagName;
14573
14589
  const isComponentDoc = typeof rootTag === "string" && rootTag.includes("-");
14574
14590
  const { featureToggles: editToggles } = S.ui;
14591
+ canvasWrap.style.flexDirection = wantsDocHeader ? "column" : "";
14592
+ canvasWrap.style.alignItems = wantsDocHeader ? "stretch" : "";
14593
+ canvasWrap.style.gap = wantsDocHeader ? "0px" : "";
14575
14594
  drawStage(surface, {
14576
- columnHeader: wantsDocHeader ? "shown" : "hidden",
14577
14595
  frame: "boards",
14578
14596
  framePart: "edit-canvas",
14579
14597
  frameVars: "",
14580
14598
  handles: "shown",
14581
14599
  hug: isComponentDoc,
14582
14600
  innerPart: "edit-column",
14583
- lead: "none",
14601
+ lead: wantsDocHeader ? "header" : "none",
14584
14602
  panels: [entry.item]
14585
14603
  }, [entry]).then((stage) => {
14586
14604
  if (!stage) {
14587
14605
  return;
14588
14606
  }
14589
14607
  const editColumn = stageHost(surface, "edit-column");
14590
- entry.panel.scrollContainer = stageHost(surface, "edit-canvas");
14608
+ const scroller = stageHost(surface, "edit-canvas");
14609
+ entry.panel.scrollContainer = scroller;
14610
+ trackStageScrollbar(surface, scroller);
14591
14611
  if (editColumn) {
14592
14612
  editColumn.style.maxWidth = `${Math.round(columnWidth)}px`;
14593
14613
  mountEditWidthHandle(surface, stageHost(surface, "edit-handle:start") ?? undefined, editColumn, -1);
@@ -14603,9 +14623,6 @@ function renderCanvasImpl(surface) {
14603
14623
  canvasWrap.style.padding = "0";
14604
14624
  canvasWrap.style.overflow = "hidden";
14605
14625
  }
14606
- canvasWrap.style.flexDirection = wantsDocHeader ? "column" : "";
14607
- canvasWrap.style.alignItems = wantsDocHeader ? "stretch" : "";
14608
- const designLead = wantsDocHeader ? "header" : "none";
14609
14626
  const {
14610
14627
  sizeBreakpoints,
14611
14628
  featureQueries: _featureQueries,
@@ -14619,14 +14636,13 @@ function renderCanvasImpl(surface) {
14619
14636
  const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
14620
14637
  const entry = canvasPanelEntry(hasBaseWidth ? "base" : null, label, !hasBaseWidth, hasBaseWidth ? baseWidth : undefined);
14621
14638
  drawStage(surface, {
14622
- columnHeader: "hidden",
14623
14639
  frame: "boards",
14624
14640
  framePart: "panzoom",
14625
14641
  frameVars: "",
14626
14642
  handles: "hidden",
14627
14643
  hug: false,
14628
14644
  innerPart: "boards",
14629
- lead: designLead,
14645
+ lead: "none",
14630
14646
  panels: [entry.item]
14631
14647
  }, [entry]).then((stage) => {
14632
14648
  if (!stage) {
@@ -14662,14 +14678,13 @@ function renderCanvasImpl(surface) {
14662
14678
  const panelDefs = chosen.length > 0 ? chosen : allPanelDefs;
14663
14679
  const panelEntries = panelDefs.map((def) => canvasPanelEntry(def.name, `${def.displayName} (${def.width}px)`, false, def.width));
14664
14680
  const stageDrawn = drawStage(surface, {
14665
- columnHeader: "hidden",
14666
14681
  frame: "boards",
14667
14682
  framePart: "panzoom",
14668
14683
  frameVars: "",
14669
14684
  handles: "hidden",
14670
14685
  hug: false,
14671
14686
  innerPart: "boards",
14672
- lead: designLead,
14687
+ lead: "none",
14673
14688
  panels: panelEntries.map((e) => e.item)
14674
14689
  }, panelEntries);
14675
14690
  const passGen = surface.renderGeneration;
@@ -14733,6 +14748,7 @@ setSurfaceTeardown((surface) => {
14733
14748
  disposeSourceEditor(surface);
14734
14749
  surface.centerObserver?.disconnect();
14735
14750
  surface.centerObserver = null;
14751
+ releaseStageScrollbar(surface);
14736
14752
  surface.panzoomWrap = null;
14737
14753
  detachCanvasStage(surface);
14738
14754
  });
@@ -17520,7 +17536,7 @@ async function onAddPackage() {
17520
17536
  sync2();
17521
17537
  try {
17522
17538
  await getPlatform2().addPackage(name);
17523
- const { loadComponentRegistry } = await (__preload("5y78e05d"), import("./chunks/components-5y78e05d.js"));
17539
+ const { loadComponentRegistry } = await (__preload("xm826zjw"), import("./chunks/components-xm826zjw.js"));
17524
17540
  await loadComponentRegistry();
17525
17541
  context()?.renderLeftPanel();
17526
17542
  } catch (error) {
@@ -17538,7 +17554,7 @@ async function onRemovePackage(pkg) {
17538
17554
  try {
17539
17555
  await getPlatform2().removePackage(pkg);
17540
17556
  const updated = removePackageElements(siteElements(), pkg);
17541
- const { loadComponentRegistry } = await (__preload("5y78e05d"), import("./chunks/components-5y78e05d.js"));
17557
+ const { loadComponentRegistry } = await (__preload("xm826zjw"), import("./chunks/components-xm826zjw.js"));
17542
17558
  await loadComponentRegistry();
17543
17559
  await updateSiteConfig({ $elements: updated });
17544
17560
  context()?.renderLeftPanel();
@@ -17778,7 +17794,7 @@ var panel_problems_default = {
17778
17794
  overflowWrap: "anywhere"
17779
17795
  },
17780
17796
  '& [part="file-path"]': {
17781
- $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.',
17782
17798
  alignSelf: "flex-start",
17783
17799
  maxWidth: "100%",
17784
17800
  overflow: "hidden",
@@ -17804,7 +17820,8 @@ var panel_problems_default = {
17804
17820
  }
17805
17821
  },
17806
17822
  '& [part="detail"]': {
17807
- $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",
17808
17825
  maxHeight: "180px",
17809
17826
  margin: "var(--jx-space-1) 0 0",
17810
17827
  padding: "6px",
@@ -17836,7 +17853,7 @@ var panel_problems_default = {
17836
17853
  flex: "none"
17837
17854
  },
17838
17855
  '& [part="empty"]': {
17839
- $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.",
17840
17857
  display: "flex",
17841
17858
  flexDirection: "column",
17842
17859
  alignItems: "center",
@@ -18223,7 +18240,7 @@ function groupProblems(records = problems2) {
18223
18240
  return groups;
18224
18241
  }
18225
18242
  function openProblemPath(path) {
18226
- (__preload("y6fr4he9"), import("./chunks/files-y6fr4he9.js")).then((m) => m.openFileInTab(path));
18243
+ (__preload("egsb0ry8"), import("./chunks/files-egsb0ry8.js")).then((m) => m.openFileInTab(path));
18227
18244
  }
18228
18245
  function projectRow2(record) {
18229
18246
  const id = record.action;
@@ -18327,7 +18344,7 @@ function registerProblemsPanel() {
18327
18344
  var bottom_dock_default = {
18328
18345
  $schema: "https://jxsuite.com/schema/v1",
18329
18346
  $id: "StudioBottomDock",
18330
- $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).",
18331
18348
  tagName: "div",
18332
18349
  attributes: {
18333
18350
  part: "bottom-dock"
@@ -19182,6 +19199,28 @@ var SELF_SCROLLING_MODES = new Set([
19182
19199
  "preview",
19183
19200
  "settings"
19184
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
+ }
19185
19224
  var _stageContext = null;
19186
19225
  function installStageGestures(surface) {
19187
19226
  const canvasWrap = surface.wrap;
@@ -19199,7 +19238,7 @@ function installStageGestures(surface) {
19199
19238
  return;
19200
19239
  }
19201
19240
  const sc = canvasWrap.querySelector('[part="edit-canvas"]');
19202
- if (sc) {
19241
+ if (sc && !wheelBelongsInside(e, canvasWrap, sc)) {
19203
19242
  e.preventDefault();
19204
19243
  sc.scrollTop += e.deltaY;
19205
19244
  sc.scrollLeft += e.deltaX;
@@ -19285,7 +19324,7 @@ function stageless() {
19285
19324
  var pane_context_default = {
19286
19325
  $schema: "https://jxsuite.com/schema/v1",
19287
19326
  $id: "StudioPaneContext",
19288
- $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.",
19289
19328
  tagName: "div",
19290
19329
  attributes: {
19291
19330
  part: "chrome"
@@ -19343,10 +19382,14 @@ var pane_context_default = {
19343
19382
  flexShrink: "0"
19344
19383
  },
19345
19384
  '& [part="preview-toggle"]': {
19346
- $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.",
19347
19386
  flexShrink: "0",
19348
19387
  marginInlineStart: "6px"
19349
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
+ },
19350
19393
  '& [part="context-trigger"]': {
19351
19394
  fontVariantNumeric: "tabular-nums"
19352
19395
  },
@@ -19372,29 +19415,47 @@ var pane_context_default = {
19372
19415
  fontSize: "var(--jx-text-md)"
19373
19416
  },
19374
19417
  '& [part="pod"]': {
19375
- $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%)",
19376
19420
  alignSelf: "flex-end",
19421
+ display: "flex",
19422
+ alignItems: "center",
19423
+ gap: "var(--jx-space-2)",
19377
19424
  pointerEvents: "auto",
19378
- margin: "0 var(--jx-space-4) var(--jx-space-4) 0",
19379
- padding: "var(--jx-space-1) var(--jx-space-2)",
19380
- border: "1px solid var(--border)",
19381
- borderRadius: "var(--jx-radius-md)",
19382
- background: "var(--bg)",
19383
- 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)"
19384
19435
  },
19385
19436
  '& [part="zoom-label"]': {
19386
- minWidth: "48px",
19387
- 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"
19388
19445
  },
19389
19446
  '& [part="fit"]': {
19390
19447
  maxWidth: "120px"
19391
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
+ },
19392
19453
  '& [part="popover"]': {
19393
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.",
19394
19455
  pointerEvents: "auto"
19395
19456
  },
19396
19457
  '& [part="ctx"]': {
19397
- $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.",
19398
19459
  display: "flex",
19399
19460
  flexDirection: "column",
19400
19461
  gap: "10px",
@@ -19413,11 +19474,15 @@ var pane_context_default = {
19413
19474
  letterSpacing: "0.04em",
19414
19475
  color: "var(--fg-dim)"
19415
19476
  },
19416
- '& [part="sizes"]': {
19417
- 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)"
19418
19481
  },
19419
- '& [part="features"]': {
19420
- 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)"
19421
19486
  },
19422
19487
  '& [part="fields"]': {
19423
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.",
@@ -19745,6 +19810,7 @@ var pane_context_default = {
19745
19810
  },
19746
19811
  $props: {
19747
19812
  size: "sm",
19813
+ quiet: false,
19748
19814
  toggles: true,
19749
19815
  emphasized: true,
19750
19816
  selected: {
@@ -20083,55 +20149,55 @@ var pane_context_default = {
20083
20149
  }
20084
20150
  ]
20085
20151
  }
20086
- },
20087
- {
20088
- 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",
20089
20170
  attributes: {
20090
- part: "fit-slot",
20091
- role: "none"
20092
- },
20093
- style: {
20094
- display: "contents"
20171
+ part: "fit"
20095
20172
  },
20096
- $switch: {
20097
- $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
+ }
20098
20182
  },
20099
- cases: {
20100
- shown: {
20101
- tagName: "jx-select",
20102
- attributes: {
20103
- part: "fit"
20104
- },
20105
- $props: {
20106
- label: "Fit",
20107
- size: "sm",
20108
- options: {
20109
- $ref: "#/state/fitOptions"
20183
+ onchange: {
20184
+ $prototype: "Function",
20185
+ body: [
20186
+ {
20187
+ operator: "call",
20188
+ target: {
20189
+ $ref: "#/state/chooseFit"
20110
20190
  },
20111
- value: {
20112
- $ref: "#/state/fitValue"
20113
- }
20114
- },
20115
- onchange: {
20116
- $prototype: "Function",
20117
- body: [
20191
+ value: [
20118
20192
  {
20119
- operator: "call",
20120
- target: {
20121
- $ref: "#/state/chooseFit"
20122
- },
20123
- value: [
20124
- {
20125
- $ref: "event#/target/value"
20126
- }
20127
- ]
20193
+ $ref: "event#/target/value"
20128
20194
  }
20129
20195
  ]
20130
20196
  }
20131
- }
20197
+ ]
20132
20198
  }
20133
20199
  }
20134
- ]
20200
+ }
20135
20201
  }
20136
20202
  ]
20137
20203
  }
@@ -20182,7 +20248,7 @@ var pane_context_default = {
20182
20248
  },
20183
20249
  $props: {
20184
20250
  selects: "single",
20185
- compact: true,
20251
+ compact: false,
20186
20252
  label: "Size"
20187
20253
  },
20188
20254
  children: [
@@ -20202,6 +20268,7 @@ var pane_context_default = {
20202
20268
  },
20203
20269
  $props: {
20204
20270
  size: "sm",
20271
+ quiet: false,
20205
20272
  checked: {
20206
20273
  $ref: "$map/item/checked"
20207
20274
  },
@@ -20454,7 +20521,7 @@ var pane_context_default = {
20454
20521
  },
20455
20522
  $props: {
20456
20523
  selects: "multiple",
20457
- compact: true,
20524
+ compact: false,
20458
20525
  label: "Features"
20459
20526
  },
20460
20527
  children: [
@@ -20474,6 +20541,7 @@ var pane_context_default = {
20474
20541
  },
20475
20542
  $props: {
20476
20543
  size: "sm",
20544
+ quiet: false,
20477
20545
  toggles: true,
20478
20546
  selected: {
20479
20547
  $ref: "$map/item/selected"
@@ -21406,6 +21474,11 @@ function fitChoiceValue(fit) {
21406
21474
  return fit;
21407
21475
  }
21408
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
+ }
21409
21482
  function podFor(tab, paneId) {
21410
21483
  const mode = canvasModeOfPane(paneId);
21411
21484
  const surface = surfaceForPane(paneId);
@@ -21416,6 +21489,9 @@ function podFor(tab, paneId) {
21416
21489
  if (!STAGE_ZOOM_MODES.has(mode)) {
21417
21490
  return { podState: "hidden" };
21418
21491
  }
21492
+ if (mode === "git-diff" && diffShowsCode(paneId, canRenderComparison(comparisonPathOf(paneId)))) {
21493
+ return { podState: "hidden" };
21494
+ }
21419
21495
  const zoom = stageZoom(surface);
21420
21496
  return {
21421
21497
  fitOptions: FIT_CHOICES.map(({ label, value }) => ({ label, value })),
@@ -22012,130 +22088,15 @@ async function applyFileDrop(tab, files, hit, preview) {
22012
22088
  }
22013
22089
  }
22014
22090
  }
22015
-
22016
- // ../ai/src/tools.ts
22017
- function toolSuccess(data, summary) {
22018
- return { success: true, data, summary };
22019
- }
22020
- function toolError(error) {
22021
- return { success: false, error };
22022
- }
22023
- function createToolDefinition({
22024
- name,
22025
- description,
22026
- parameters,
22027
- strict = true,
22028
- llmStrict = false,
22029
- execute
22030
- }) {
22031
- return { name, description, parameters, strict, llmStrict, execute };
22032
- }
22033
- function createToolRegistry() {
22034
- const _tools = new Map;
22035
- return {
22036
- register(tool) {
22037
- if (_tools.has(tool.name)) {
22038
- console.warn(`Tool "${tool.name}" is being re-registered.`);
22039
- }
22040
- _tools.set(tool.name, tool);
22041
- },
22042
- list() {
22043
- return [..._tools.values()];
22044
- },
22045
- listForLLM() {
22046
- return [..._tools.values()].map((t) => ({
22047
- type: "function",
22048
- function: {
22049
- name: t.name,
22050
- description: t.description,
22051
- parameters: t.parameters,
22052
- ...t.llmStrict === true ? { strict: true } : {}
22053
- }
22054
- }));
22055
- },
22056
- getDefinition(toolName) {
22057
- return _tools.get(toolName);
22058
- },
22059
- validate(toolName, args) {
22060
- const tool = _tools.get(toolName);
22061
- if (!tool) {
22062
- return { valid: false, errors: [`Unknown tool: "${toolName}"`] };
22063
- }
22064
- if (!tool.strict) {
22065
- return { valid: true };
22066
- }
22067
- const schema = tool.parameters;
22068
- if (!schema || !schema.properties) {
22069
- return { valid: true };
22070
- }
22071
- const errors = [];
22072
- const props = schema.properties;
22073
- const required = schema.required || [];
22074
- const argRecord = args;
22075
- for (const key of required) {
22076
- if (!(key in args) || argRecord[key] === undefined || argRecord[key] === null) {
22077
- errors.push(`Missing required argument: "${key}"`);
22078
- }
22079
- }
22080
- for (const [key, value] of Object.entries(argRecord)) {
22081
- const propSchema = props[key];
22082
- if (!propSchema) {
22083
- continue;
22084
- }
22085
- const expectedType = propSchema.type;
22086
- if (!expectedType) {
22087
- continue;
22088
- }
22089
- const actualType = Array.isArray(value) ? "array" : typeof value;
22090
- if (expectedType === "number" || expectedType === "integer") {
22091
- if (actualType === "number") {
22092
- continue;
22093
- }
22094
- if (actualType === "string") {
22095
- if (!Number.isNaN(Number(value))) {
22096
- continue;
22097
- }
22098
- errors.push(`Argument "${key}" should be a number, got non-numeric string "${value}"`);
22099
- continue;
22100
- }
22101
- errors.push(`Argument "${key}" should be a number, got ${actualType}`);
22102
- continue;
22103
- }
22104
- if (expectedType === "string" && actualType !== "string") {
22105
- errors.push(`Argument "${key}" should be a string, got ${actualType}`);
22106
- } else if (expectedType === "boolean" && actualType !== "boolean") {
22107
- errors.push(`Argument "${key}" should be a boolean, got ${actualType}`);
22108
- } else if (expectedType === "array" && actualType !== "array") {
22109
- errors.push(`Argument "${key}" should be an array, got ${actualType}`);
22110
- } else if (expectedType === "object" && actualType !== "object") {
22111
- errors.push(`Argument "${key}" should be an object, got ${actualType}`);
22112
- }
22113
- }
22114
- return errors.length > 0 ? { valid: false, errors } : { valid: true };
22115
- },
22116
- async execute(toolName, args) {
22117
- const tool = _tools.get(toolName);
22118
- if (!tool) {
22119
- return toolError(`Unknown tool: "${toolName}"`);
22120
- }
22121
- const validation = this.validate(toolName, args);
22122
- if (!validation.valid) {
22123
- return toolError(`Validation failed: ${(validation.errors || []).join("; ")}`);
22124
- }
22125
- try {
22126
- return await tool.execute(args);
22127
- } catch (error) {
22128
- return toolError(`Tool "${toolName}" execution error: ${error.message}`);
22129
- }
22130
- }
22131
- };
22132
- }
22133
22091
  // ../ai/src/chat-state.ts
22134
22092
  var _idCounter = 0;
22135
22093
  function uid() {
22136
22094
  _idCounter += 1;
22137
22095
  return `msg_${Date.now()}_${_idCounter}`;
22138
22096
  }
22097
+ function isCount(value) {
22098
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
22099
+ }
22139
22100
  function createChatState(opts = {}) {
22140
22101
  const model = opts.model || "gpt-4o";
22141
22102
  const store = reactive({
@@ -22146,7 +22107,8 @@ function createChatState(opts = {}) {
22146
22107
  error: null,
22147
22108
  model,
22148
22109
  tokenCount: 0,
22149
- contextWarning: false
22110
+ contextWarning: false,
22111
+ usage: null
22150
22112
  });
22151
22113
  let _streamingMessage = null;
22152
22114
  function sendMessage(text) {
@@ -22227,15 +22189,14 @@ function createChatState(opts = {}) {
22227
22189
  }
22228
22190
  function appendToolCallEnd(_id) {}
22229
22191
  function appendToolResult(id, result) {
22230
- const tc = store.pendingToolCalls.find((t) => t.id === id);
22231
- if (tc) {
22232
- tc.result = result;
22192
+ let index = store.messages.length - 1;
22193
+ while (index >= 0 && store.messages[index].role === "tool") {
22194
+ index -= 1;
22233
22195
  }
22234
- if (_streamingMessage?.toolCalls) {
22235
- const mtc = _streamingMessage.toolCalls.find((t) => t.id === id);
22236
- if (mtc) {
22237
- mtc.result = result;
22238
- }
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;
22239
22200
  }
22240
22201
  }
22241
22202
  function finishStream(_stopReason) {
@@ -22248,6 +22209,7 @@ function createChatState(opts = {}) {
22248
22209
  store.status = "error";
22249
22210
  store.error = message;
22250
22211
  store.streamingContent = "";
22212
+ store.pendingToolCalls = [];
22251
22213
  if (_streamingMessage) {
22252
22214
  const idx = store.messages.lastIndexOf(_streamingMessage);
22253
22215
  if (idx !== -1) {
@@ -22276,6 +22238,7 @@ function createChatState(opts = {}) {
22276
22238
  store.pendingToolCalls = [];
22277
22239
  store.error = null;
22278
22240
  store.contextWarning = false;
22241
+ store.usage = null;
22279
22242
  _streamingMessage = null;
22280
22243
  }
22281
22244
  function retryLast() {
@@ -22294,6 +22257,27 @@ function createChatState(opts = {}) {
22294
22257
  function setTokenCount(count) {
22295
22258
  store.tokenCount = count;
22296
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
+ }
22297
22281
  function setContextWarning(warning) {
22298
22282
  store.contextWarning = warning;
22299
22283
  }
@@ -22355,6 +22339,8 @@ function createChatState(opts = {}) {
22355
22339
  retryLast,
22356
22340
  setModel,
22357
22341
  setTokenCount,
22342
+ recordUsage,
22343
+ clearUsage,
22358
22344
  setContextWarning,
22359
22345
  toMessagesArray
22360
22346
  });
@@ -22391,6 +22377,7 @@ function createProxyStreamingClient({
22391
22377
  apiKey,
22392
22378
  baseUrl
22393
22379
  }) {
22380
+ let resolvedUrl = null;
22394
22381
  async function* streamChat(messages, tools, systemPrompt, signal) {
22395
22382
  const headers = { "Content-Type": "application/json" };
22396
22383
  if (apiKey) {
@@ -22399,9 +22386,24 @@ function createProxyStreamingClient({
22399
22386
  if (baseUrl) {
22400
22387
  headers["X-Api-Base-URL"] = baseUrl;
22401
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
+ }
22402
22404
  let response;
22403
22405
  try {
22404
- response = await fetch(chatUrl, {
22406
+ response = await fetch(url, {
22405
22407
  method: "POST",
22406
22408
  headers,
22407
22409
  body: JSON.stringify({ messages, tools, systemPrompt, model }),
@@ -22487,54 +22489,28 @@ function createProxyStreamingClient({
22487
22489
  }
22488
22490
  return { streamChat };
22489
22491
  }
22490
- // src/services/ai-writes.ts
22491
- var turns = [];
22492
- var MAX_TURNS = 50;
22493
- var open = null;
22494
- function beginTurn(id) {
22495
- if (open?.id === id) {
22496
- return;
22492
+ // src/services/project-path.ts
22493
+ function normalizeRelPath(path) {
22494
+ if (typeof path !== "string" || !path.trim()) {
22495
+ return null;
22497
22496
  }
22498
- open = { id, writes: [] };
22499
- }
22500
- function recordWrite(write) {
22501
- open?.writes.push(write);
22502
- }
22503
- function endTurn(id) {
22504
- if (!open) {
22505
- return [];
22497
+ let p = path.trim().replaceAll("\\", "/");
22498
+ while (p.startsWith("./")) {
22499
+ p = p.slice(2);
22506
22500
  }
22507
- const turn = { id, writes: open.writes };
22508
- open = null;
22509
- if (turn.writes.length === 0) {
22510
- return [];
22501
+ if (p.startsWith("/") || p.startsWith("~") || /^[A-Za-z]:/.test(p)) {
22502
+ return null;
22511
22503
  }
22512
- turns.push(turn);
22513
- while (turns.length > MAX_TURNS) {
22514
- turns.shift();
22504
+ if (p.split("/").includes("..")) {
22505
+ return null;
22515
22506
  }
22516
- return turn.writes;
22517
- }
22518
- function writesForTurn(id) {
22519
- return turns.find((turn) => turn.id === id)?.writes ?? [];
22507
+ return p;
22520
22508
  }
22521
- function summarizeWrites(writes) {
22522
- const applied = writes.filter((w) => w.ok);
22523
- const paths = new Set(applied.map((w) => w.path));
22524
- const failed = writes.length - applied.length;
22525
- if (paths.size === 0) {
22526
- return failed > 0 ? `${failed} change${failed === 1 ? "" : "s"} failed` : "";
22527
- }
22528
- const files = `Changed ${paths.size} file${paths.size === 1 ? "" : "s"}`;
22529
- const diskPaths = new Set(applied.filter((w) => w.disk).map((w) => w.path));
22530
- const parts = [files];
22531
- if (diskPaths.size > 0) {
22532
- parts.push(`${diskPaths.size} written to disk — undo cannot reach ${diskPaths.size === 1 ? "it" : "them"}`);
22533
- }
22534
- if (failed > 0) {
22535
- parts.push(`${failed} failed`);
22536
- }
22537
- 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
+ };
22538
22514
  }
22539
22515
 
22540
22516
  // src/services/token-lint.ts
@@ -22704,7 +22680,7 @@ function noDocError() {
22704
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."
22705
22681
  };
22706
22682
  }
22707
- async function applyAndValidate(tab, mutationFn, summary, validate, renderCheck, projectStyle) {
22683
+ async function applyAndValidate(ledger, tab, mutationFn, summary, validate, renderCheck, projectStyle) {
22708
22684
  const deps = { getProjectStyle: () => projectStyle, renderCheck, validate };
22709
22685
  const before = await snapshotBeforeWrite(tab, deps);
22710
22686
  if (!transactDoc(tab, mutationFn)) {
@@ -22713,7 +22689,7 @@ async function applyAndValidate(tab, mutationFn, summary, validate, renderCheck,
22713
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."
22714
22690
  };
22715
22691
  }
22716
- recordWrite({ disk: false, ok: true, path: tab.documentPath ?? "(untitled)", tool: summary });
22692
+ ledger.record({ disk: false, ok: true, path: tab.documentPath ?? "(untitled)", tool: summary });
22717
22693
  return reportDocumentWrite(tab, before, summary, deps);
22718
22694
  }
22719
22695
  function registerAiTools(registry, {
@@ -22792,7 +22768,7 @@ function registerAiTools(registry, {
22792
22768
  },
22793
22769
  required: ["path", "key"]
22794
22770
  },
22795
- async execute(args) {
22771
+ async execute(args, ctx) {
22796
22772
  const tab = getTab();
22797
22773
  if (!tab) {
22798
22774
  return noDocError();
@@ -22802,7 +22778,7 @@ function registerAiTools(registry, {
22802
22778
  if (node === undefined) {
22803
22779
  return { success: false, error: `No node exists at path ${JSON.stringify(path)}.` };
22804
22780
  }
22805
- 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());
22806
22782
  }
22807
22783
  }));
22808
22784
  registry.register(createToolDefinition({
@@ -22824,7 +22800,7 @@ function registerAiTools(registry, {
22824
22800
  },
22825
22801
  required: ["parentPath", "index", "node"]
22826
22802
  },
22827
- async execute(args) {
22803
+ async execute(args, ctx) {
22828
22804
  const tab = getTab();
22829
22805
  if (!tab) {
22830
22806
  return noDocError();
@@ -22850,7 +22826,7 @@ function registerAiTools(registry, {
22850
22826
  error: "Cannot insert into mapped-array children; edit the map template instead."
22851
22827
  };
22852
22828
  }
22853
- 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());
22854
22830
  }
22855
22831
  }));
22856
22832
  registry.register(createToolDefinition({
@@ -22874,7 +22850,7 @@ function registerAiTools(registry, {
22874
22850
  },
22875
22851
  required: ["path", "property"]
22876
22852
  },
22877
- async execute(args) {
22853
+ async execute(args, ctx) {
22878
22854
  const tab = getTab();
22879
22855
  if (!tab) {
22880
22856
  return noDocError();
@@ -22885,7 +22861,7 @@ function registerAiTools(registry, {
22885
22861
  }
22886
22862
  const prop = property;
22887
22863
  const val = value == null ? undefined : String(value);
22888
- 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());
22889
22865
  }
22890
22866
  }));
22891
22867
  registry.register(createToolDefinition({
@@ -22903,7 +22879,7 @@ function registerAiTools(registry, {
22903
22879
  },
22904
22880
  required: ["path", "value"]
22905
22881
  },
22906
- async execute(args) {
22882
+ async execute(args, ctx) {
22907
22883
  const tab = getTab();
22908
22884
  if (!tab) {
22909
22885
  return noDocError();
@@ -22912,10 +22888,9 @@ function registerAiTools(registry, {
22912
22888
  if (getNodeAtPath2(tab.doc.document, path) === undefined) {
22913
22889
  return { success: false, error: `No node exists at path ${JSON.stringify(path)}.` };
22914
22890
  }
22915
- return applyAndValidate(tab, (t) => {
22916
- const node = getNodeAtPath2(t.doc.document, path);
22917
- delete node.textContent;
22918
- node.children = [value];
22891
+ return applyAndValidate(ctx.ledger, tab, (t) => {
22892
+ mutateUpdateProperty(t, path, "textContent");
22893
+ mutateUpdateProperty(t, path, "children", [value]);
22919
22894
  }, `Set text at ${JSON.stringify(path)}.`, validate, renderCheck, styleOf());
22920
22895
  }
22921
22896
  }));
@@ -22932,7 +22907,7 @@ function registerAiTools(registry, {
22932
22907
  },
22933
22908
  required: ["key", "value"]
22934
22909
  },
22935
- async execute(args) {
22910
+ async execute(args, ctx) {
22936
22911
  const tab = getTab();
22937
22912
  if (!tab) {
22938
22913
  return noDocError();
@@ -22944,12 +22919,7 @@ function registerAiTools(registry, {
22944
22919
  error: `State key "${key}" already exists. Use update_state to change it, or remove it first.`
22945
22920
  };
22946
22921
  }
22947
- return applyAndValidate(tab, (t) => {
22948
- if (!t.doc.document.state) {
22949
- t.doc.document.state = {};
22950
- }
22951
- t.doc.document.state[key] = value;
22952
- }, `Added state "${key}".`, validate, renderCheck, styleOf());
22922
+ return applyAndValidate(ctx.ledger, tab, (t) => mutateAddDef(t, key, value), `Added state "${key}".`, validate, renderCheck, styleOf());
22953
22923
  }
22954
22924
  }));
22955
22925
  registry.register(createToolDefinition({
@@ -22965,7 +22935,7 @@ function registerAiTools(registry, {
22965
22935
  },
22966
22936
  required: ["key"]
22967
22937
  },
22968
- async execute(args) {
22938
+ async execute(args, ctx) {
22969
22939
  const tab = getTab();
22970
22940
  if (!tab) {
22971
22941
  return noDocError();
@@ -22977,15 +22947,11 @@ function registerAiTools(registry, {
22977
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)"}`
22978
22948
  };
22979
22949
  }
22980
- return applyAndValidate(tab, (t) => {
22981
- const { state } = t.doc.document;
22982
- if (!state) {
22983
- return;
22984
- }
22950
+ return applyAndValidate(ctx.ledger, tab, (t) => {
22985
22951
  if (value == null) {
22986
- delete state[key];
22952
+ mutateRemoveDef(t, key);
22987
22953
  } else {
22988
- state[key] = value;
22954
+ mutateAddDef(t, key, value);
22989
22955
  }
22990
22956
  }, value == null ? `Removed state "${key}".` : `Updated state "${key}".`, validate, renderCheck, styleOf());
22991
22957
  }
@@ -23013,7 +22979,7 @@ function registerAiTools(registry, {
23013
22979
  },
23014
22980
  required: ["fromPath", "toParentPath", "toIndex"]
23015
22981
  },
23016
- async execute(args) {
22982
+ async execute(args, ctx) {
23017
22983
  const tab = getTab();
23018
22984
  if (!tab) {
23019
22985
  return noDocError();
@@ -23034,7 +23000,7 @@ function registerAiTools(registry, {
23034
23000
  error: `No node exists at toParentPath ${JSON.stringify(toParentPath)}.`
23035
23001
  };
23036
23002
  }
23037
- 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());
23038
23004
  }
23039
23005
  }));
23040
23006
  registry.register(createToolDefinition({
@@ -23054,14 +23020,18 @@ function registerAiTools(registry, {
23054
23020
  },
23055
23021
  required: ["path", "content"]
23056
23022
  },
23057
- async execute(args) {
23023
+ async execute(args, ctx) {
23058
23024
  if (!saveFile) {
23059
23025
  return {
23060
23026
  success: false,
23061
23027
  error: "File operations are not available in this environment."
23062
23028
  };
23063
23029
  }
23064
- const { path: relPath, content } = args;
23030
+ const { path, content } = args;
23031
+ const relPath = normalizeRelPath(path);
23032
+ if (relPath === null) {
23033
+ return invalidPathResult(path);
23034
+ }
23065
23035
  const dirtyError = dirtyTabError(relPath);
23066
23036
  if (dirtyError) {
23067
23037
  return dirtyError;
@@ -23088,14 +23058,14 @@ ${formatted}`
23088
23058
  }
23089
23059
  try {
23090
23060
  await saveFile(relPath, serializeJson(content, null));
23091
- recordWrite({ disk: true, ok: true, path: relPath, tool: "create_component" });
23061
+ ctx.ledger.record({ disk: true, ok: true, path: relPath, tool: "create_component" });
23092
23062
  return {
23093
23063
  success: true,
23094
23064
  summary: await reconcileAfterWrite(relPath, `Created component at "${relPath}".`)
23095
23065
  };
23096
23066
  } catch (error) {
23097
23067
  const message = error instanceof Error ? error.message : String(error);
23098
- recordWrite({
23068
+ ctx.ledger.record({
23099
23069
  disk: true,
23100
23070
  error: message,
23101
23071
  ok: false,
@@ -23126,14 +23096,18 @@ ${formatted}`
23126
23096
  },
23127
23097
  required: ["path", "content"]
23128
23098
  },
23129
- async execute(args) {
23099
+ async execute(args, ctx) {
23130
23100
  if (!saveFile) {
23131
23101
  return {
23132
23102
  success: false,
23133
23103
  error: "File operations are not available in this environment."
23134
23104
  };
23135
23105
  }
23136
- const { path: relPath, content } = args;
23106
+ const { path, content } = args;
23107
+ const relPath = normalizeRelPath(path);
23108
+ if (relPath === null) {
23109
+ return invalidPathResult(path);
23110
+ }
23137
23111
  const dirtyError = dirtyTabError(relPath);
23138
23112
  if (dirtyError) {
23139
23113
  return dirtyError;
@@ -23160,14 +23134,14 @@ ${formatted}`
23160
23134
  }
23161
23135
  try {
23162
23136
  await saveFile(relPath, serializeJson(content, null));
23163
- recordWrite({ disk: true, ok: true, path: relPath, tool: "create_page" });
23137
+ ctx.ledger.record({ disk: true, ok: true, path: relPath, tool: "create_page" });
23164
23138
  return {
23165
23139
  success: true,
23166
23140
  summary: await reconcileAfterWrite(relPath, `Created page at "${relPath}".`)
23167
23141
  };
23168
23142
  } catch (error) {
23169
23143
  const message = error instanceof Error ? error.message : String(error);
23170
- recordWrite({
23144
+ ctx.ledger.record({
23171
23145
  disk: true,
23172
23146
  error: message,
23173
23147
  ok: false,
@@ -23183,26 +23157,6 @@ ${formatted}`
23183
23157
  }));
23184
23158
  }
23185
23159
 
23186
- // src/services/ai-turn-signal.ts
23187
- var _signal;
23188
- function beginTurnSignal(signal) {
23189
- _signal = signal;
23190
- }
23191
- function turnSignal() {
23192
- return _signal;
23193
- }
23194
- function endTurnSignal() {
23195
- _signal = undefined;
23196
- _toolCallId = "";
23197
- }
23198
- var _toolCallId = "";
23199
- function beginToolCall(id) {
23200
- _toolCallId = id;
23201
- }
23202
- function currentToolCallId() {
23203
- return _toolCallId;
23204
- }
23205
-
23206
23160
  // src/services/ai-ask.ts
23207
23161
  var MAX_OPTIONS = 6;
23208
23162
  var state2 = reactive({ pending: null });
@@ -23220,11 +23174,10 @@ function pendingAsk() {
23220
23174
  function isAwaitingAnswer() {
23221
23175
  return state2.pending !== null;
23222
23176
  }
23223
- function askUser(ask) {
23177
+ function askUser(ask, signal) {
23224
23178
  return new Promise((resolve) => {
23225
23179
  state2.pending = ask;
23226
23180
  settle2 = resolve;
23227
- const signal = turnSignal();
23228
23181
  if (signal?.aborted) {
23229
23182
  clear();
23230
23183
  resolve({ answer: null, skipped: false });
@@ -23269,9 +23222,10 @@ function resetAsk() {
23269
23222
  cancelAsk();
23270
23223
  clear();
23271
23224
  }
23272
- function registerAskTool(registry) {
23225
+ function registerAskTool(registry, { onPending } = {}) {
23273
23226
  registry.register(createToolDefinition({
23274
23227
  name: "ask_user",
23228
+ interactive: true,
23275
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.",
23276
23230
  parameters: {
23277
23231
  type: "object",
@@ -23292,7 +23246,7 @@ function registerAskTool(registry) {
23292
23246
  },
23293
23247
  required: ["question"]
23294
23248
  },
23295
- execute: async (args) => {
23249
+ execute: async (args, ctx) => {
23296
23250
  const { question, options, context } = args;
23297
23251
  if (typeof question !== "string" || !question.trim()) {
23298
23252
  return toolError('Pass "question" — the question to put to the user.');
@@ -23301,12 +23255,16 @@ function registerAskTool(registry) {
23301
23255
  return toolError("A question is already waiting for the user.");
23302
23256
  }
23303
23257
  const choices = Array.isArray(options) ? options.filter((o) => typeof o === "string" && o.trim().length > 0).slice(0, MAX_OPTIONS) : [];
23304
- const { answer, skipped } = await askUser({
23258
+ const answering = askUser({
23305
23259
  context: typeof context === "string" ? context.trim() : "",
23306
- id: currentToolCallId(),
23260
+ id: ctx.callId,
23307
23261
  options: choices,
23308
23262
  question: question.trim()
23309
- });
23263
+ }, ctx.signal);
23264
+ if (isAwaitingAnswer()) {
23265
+ onPending?.();
23266
+ }
23267
+ const { answer, skipped } = await answering;
23310
23268
  if (skipped) {
23311
23269
  return toolSuccess({ answer: null, skipped: true }, "The user declined to answer; proceed with your best judgment.");
23312
23270
  }
@@ -23373,7 +23331,7 @@ function definitionOf(registry, command, execute) {
23373
23331
  parameters,
23374
23332
  strict: false,
23375
23333
  llmStrict: false,
23376
- execute: (args) => execute(tool.name, args)
23334
+ execute: (args, ctx) => execute(tool.name, args, ctx)
23377
23335
  };
23378
23336
  }
23379
23337
  function advertisedCommandTools(registry, execute = async (name) => toolError(`Tool "${name}" was listed without an executor.`)) {
@@ -23416,20 +23374,20 @@ function defaultPaths(command, tab) {
23416
23374
  }
23417
23375
  return ["(unknown file)"];
23418
23376
  }
23419
- function fileLedger(command, tab, wrote) {
23377
+ function fileLedger(ledger, command, tab, wrote) {
23420
23378
  if (command.undo === undefined) {
23421
23379
  return;
23422
23380
  }
23423
23381
  const disk = command.undo === "none";
23424
23382
  for (const path of wrote ?? defaultPaths(command, tab)) {
23425
- recordWrite({ disk, ok: true, path, tool: command.title });
23383
+ ledger.record({ disk, ok: true, path, tool: command.title });
23426
23384
  }
23427
23385
  }
23428
23386
  function createCommandToolRegistry(deps) {
23429
23387
  function commandFor(name) {
23430
23388
  return activeRegistry()?.list().find((command) => command.aiTool?.name === name);
23431
23389
  }
23432
- async function execute(name, received) {
23390
+ async function execute(name, received, ctx = createToolContext()) {
23433
23391
  const registry = activeRegistry();
23434
23392
  const command = commandFor(name);
23435
23393
  if (!registry || !command) {
@@ -23471,7 +23429,7 @@ function createCommandToolRegistry(deps) {
23471
23429
  try {
23472
23430
  report = normaliseReport(tool.report({ after, args, before }));
23473
23431
  } catch (error) {
23474
- fileLedger(command, tab, projectUnchanged ? [] : undefined);
23432
+ fileLedger(ctx.ledger, command, tab, projectUnchanged ? [] : undefined);
23475
23433
  const message = error instanceof Error ? error.message : String(error);
23476
23434
  return { error: `${command.title} ran, but its report failed: ${message}`, success: false };
23477
23435
  }
@@ -23482,7 +23440,7 @@ function createCommandToolRegistry(deps) {
23482
23440
  summary = `${command.title} changed nothing: project.json is exactly as it was.`;
23483
23441
  }
23484
23442
  }
23485
- fileLedger(command, tab, wrote);
23443
+ fileLedger(ctx.ledger, command, tab, wrote);
23486
23444
  if (tab && snapshot) {
23487
23445
  const verdict = await reportDocumentWrite(tab, snapshot, summary, deps);
23488
23446
  if (!verdict.success) {
@@ -23549,9 +23507,9 @@ function composeToolRegistries(hand, commands) {
23549
23507
  const side = sideOf(name);
23550
23508
  return side ? side.validate(name, args) : { valid: false, errors: [`Unknown tool: "${name}"`] };
23551
23509
  },
23552
- async execute(name, args) {
23510
+ async execute(name, args, ctx) {
23553
23511
  const side = sideOf(name);
23554
- return side ? side.execute(name, args) : toolError(`Unknown tool: "${name}"`);
23512
+ return side ? side.execute(name, args, ctx) : toolError(`Unknown tool: "${name}"`);
23555
23513
  }
23556
23514
  };
23557
23515
  }
@@ -23569,6 +23527,14 @@ async function adoptProject(root) {
23569
23527
  }
23570
23528
  async function adoptCreatedProject(root, { getTab, adopt = adoptProject, initRepo = initProjectRepo2 }) {
23571
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
+ }
23572
23538
  const wasBatching = isBatching();
23573
23539
  if (wasBatching) {
23574
23540
  endBatch();
@@ -23582,7 +23548,7 @@ async function adoptCreatedProject(root, { getTab, adopt = adoptProject, initRep
23582
23548
  if (wasBatching) {
23583
23549
  beginBatch(getTab());
23584
23550
  }
23585
- return { adopted: workspace2.projectRoot === root, error: adoptionError };
23551
+ return { adopted: workspace2.projectRoot === root, error: adoptionError, openedElsewhere };
23586
23552
  }
23587
23553
 
23588
23554
  // src/services/ai-project-tools.ts
@@ -23592,28 +23558,6 @@ var SEARCH_CAP = 100;
23592
23558
  var READ_CAP_BYTES = 48 * 1024;
23593
23559
  var WRITE_CAP_BYTES = 256 * 1024;
23594
23560
  var NOT_UNDOABLE = "(saved to disk; not undoable with Cmd+Z)";
23595
- function normalizeRelPath(path) {
23596
- if (typeof path !== "string" || !path.trim()) {
23597
- return null;
23598
- }
23599
- let p = path.trim().replaceAll("\\", "/");
23600
- while (p.startsWith("./")) {
23601
- p = p.slice(2);
23602
- }
23603
- if (p.startsWith("/") || p.startsWith("~") || /^[A-Za-z]:/.test(p)) {
23604
- return null;
23605
- }
23606
- if (p.split("/").includes("..")) {
23607
- return null;
23608
- }
23609
- return p;
23610
- }
23611
- function pathError(path) {
23612
- return {
23613
- success: false,
23614
- error: `Invalid path ${JSON.stringify(path)} — use a path relative to the project root (no leading "/", no "..").`
23615
- };
23616
- }
23617
23561
  function isJxDocPath(path) {
23618
23562
  return /^(pages|layouts|components|elements)\//.test(path) && path.endsWith(".json");
23619
23563
  }
@@ -23649,7 +23593,7 @@ function registerProjectTools(registry, {
23649
23593
  const { dir } = args;
23650
23594
  const start = dir === undefined || dir === "" ? "." : normalizeRelPath(dir);
23651
23595
  if (start === null) {
23652
- return pathError(dir);
23596
+ return invalidPathResult(dir);
23653
23597
  }
23654
23598
  const platform = getPlatform2();
23655
23599
  const entries = [];
@@ -23706,7 +23650,7 @@ function registerProjectTools(registry, {
23706
23650
  async execute(args) {
23707
23651
  const relPath = normalizeRelPath(args.path);
23708
23652
  if (relPath === null) {
23709
- return pathError(args.path);
23653
+ return invalidPathResult(args.path);
23710
23654
  }
23711
23655
  try {
23712
23656
  const content = await getPlatform2().readFile(relPath);
@@ -23746,11 +23690,11 @@ function registerProjectTools(registry, {
23746
23690
  },
23747
23691
  required: ["path", "content"]
23748
23692
  },
23749
- async execute(args) {
23693
+ async execute(args, ctx) {
23750
23694
  const { path, content } = args;
23751
23695
  const relPath = normalizeRelPath(path);
23752
23696
  if (relPath === null) {
23753
- return pathError(path);
23697
+ return invalidPathResult(path);
23754
23698
  }
23755
23699
  if (content.length > WRITE_CAP_BYTES) {
23756
23700
  return {
@@ -23831,10 +23775,16 @@ ${formatted}`
23831
23775
  }
23832
23776
  try {
23833
23777
  await getPlatform2().writeFile(relPath, content);
23834
- recordWrite({ disk: true, ok: true, path: relPath, tool: "write_file" });
23778
+ ctx.ledger.record({ disk: true, ok: true, path: relPath, tool: "write_file" });
23835
23779
  } catch (error) {
23836
23780
  const message = error instanceof Error ? error.message : String(error);
23837
- 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
+ });
23838
23788
  return {
23839
23789
  success: false,
23840
23790
  error: `Failed to write "${relPath}": ${message}`
@@ -23921,7 +23871,7 @@ ${tokenHints}` : summary };
23921
23871
  },
23922
23872
  required: ["name"]
23923
23873
  },
23924
- async execute(args) {
23874
+ async execute(args, ctx) {
23925
23875
  const {
23926
23876
  name,
23927
23877
  description,
@@ -23985,6 +23935,7 @@ ${tokenHints}` : summary };
23985
23935
  error: `Failed to create project: ${error instanceof Error ? error.message : String(error)}`
23986
23936
  };
23987
23937
  }
23938
+ ctx.ledger.record({ disk: true, ok: true, path: result.root, tool: "create_project" });
23988
23939
  const { adopted, error: adoptionError } = await adoptCreatedProject(result.root, {
23989
23940
  adopt: adoptProject,
23990
23941
  getTab
@@ -24147,10 +24098,7 @@ function destinationProblem(destination) {
24147
24098
  }
24148
24099
  return null;
24149
24100
  }
24150
- var imported = false;
24151
- function resetImportGuard() {
24152
- imported = false;
24153
- }
24101
+ var IMPORT_DONE = "import.done";
24154
24102
  function describeRun(id, root, summary) {
24155
24103
  const record = importRun(id);
24156
24104
  const warnings = [...record?.warnings ?? [], ...summary?.warnings ?? []];
@@ -24180,7 +24128,12 @@ function describeRun(id, root, summary) {
24180
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).");
24181
24129
  return lines.join(" ");
24182
24130
  }
24183
- function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted }) {
24131
+ function registerImportTools(registry, {
24132
+ getTab,
24133
+ adoptProject,
24134
+ onProjectAdopted,
24135
+ brief: briefStore = { clear: clearPendingImportBrief, read: pendingImportBrief }
24136
+ }) {
24184
24137
  registry.register(createToolDefinition({
24185
24138
  name: "import_site",
24186
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.",
@@ -24226,7 +24179,7 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24226
24179
  },
24227
24180
  required: ["url"]
24228
24181
  },
24229
- async execute(args) {
24182
+ async execute(args, ctx) {
24230
24183
  const {
24231
24184
  url,
24232
24185
  directory,
@@ -24242,7 +24195,7 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24242
24195
  if (workspace2.projectRoot) {
24243
24196
  return toolError("A project is already open in this window — import_site is only for bootstrapping.");
24244
24197
  }
24245
- if (imported) {
24198
+ if (ctx.session.get(IMPORT_DONE) === true) {
24246
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.");
24247
24200
  }
24248
24201
  const platform = getPlatform2();
@@ -24258,7 +24211,7 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24258
24211
  if (target.protocol !== "http:" && target.protocol !== "https:") {
24259
24212
  return toolError("The url must start with http:// or https://.");
24260
24213
  }
24261
- const brief = pendingImportBrief();
24214
+ const brief = briefStore.read();
24262
24215
  const requested = typeof directory === "string" ? directory.trim() : "";
24263
24216
  if (requested && brief && requested !== brief.directory) {
24264
24217
  return toolError(`The destination is already set to ${brief.directory} — call import_site without ` + '"directory", or ask the user before changing it.');
@@ -24271,12 +24224,14 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24271
24224
  if (problem) {
24272
24225
  return toolError(problem);
24273
24226
  }
24274
- const id = currentToolCallId();
24227
+ const id = ctx.callId;
24275
24228
  const signal = beginImportRun(id, { directory: destination, url: target.href });
24276
- turnSignal()?.addEventListener("abort", () => {
24229
+ const callSignal = ctx.signal;
24230
+ const stopRun = () => {
24277
24231
  abortImportRun();
24278
24232
  finishImportRun(id, { status: "stopped" });
24279
- });
24233
+ };
24234
+ callSignal.addEventListener("abort", stopRun, { once: true });
24280
24235
  const apiKey = getOpenAiKey();
24281
24236
  const baseUrl = getBaseUrl();
24282
24237
  const model = brief?.model || preferredModel();
@@ -24303,8 +24258,11 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24303
24258
  ...apiKey ? { apiKey } : {},
24304
24259
  ...baseUrl ? { baseUrl } : {},
24305
24260
  ...model ? { model } : {}
24306
- }, (evt) => recordImportProgress(id, evt), signal, onReady);
24261
+ }, (evt) => {
24262
+ ctx.progress(evt);
24263
+ }, signal, onReady);
24307
24264
  } catch (error) {
24265
+ callSignal.removeEventListener("abort", stopRun);
24308
24266
  const message = error instanceof Error ? error.message : String(error);
24309
24267
  if (signal.aborted) {
24310
24268
  finishImportRun(id, { error: message, status: "stopped" });
@@ -24318,9 +24276,11 @@ function registerImportTools(registry, { getTab, adoptProject, onProjectAdopted
24318
24276
  Last steps:
24319
24277
  ${tail}` : ""}`);
24320
24278
  }
24279
+ callSignal.removeEventListener("abort", stopRun);
24321
24280
  finishImportRun(id, { status: "done" });
24322
- imported = true;
24323
- 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();
24324
24284
  const { adopted, error: adoptionError } = await (adoption ?? adoptCreatedProject(result.root, {
24325
24285
  getTab,
24326
24286
  ...adoptProject ? { adopt: adoptProject } : {}
@@ -24357,7 +24317,7 @@ function createGatedToolRegistry(inner, availability) {
24357
24317
  validate(toolName, args) {
24358
24318
  return inner.validate(toolName, args);
24359
24319
  },
24360
- async execute(toolName, args) {
24320
+ async execute(toolName, args, ctx) {
24361
24321
  if (inner.getDefinition(toolName) && !isAvailable(toolName)) {
24362
24322
  const gate = availability.get(toolName);
24363
24323
  return {
@@ -24365,28 +24325,207 @@ function createGatedToolRegistry(inner, availability) {
24365
24325
  error: `Tool "${toolName}" is not available right now — it requires ${gate?.requires ?? "a different studio state"}.`
24366
24326
  };
24367
24327
  }
24368
- return inner.execute(toolName, args);
24328
+ return inner.execute(toolName, args, ctx);
24369
24329
  }
24370
24330
  };
24371
24331
  }
24372
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
+
24373
24505
  // src/services/tool-executor.ts
24374
24506
  var MAX_ROUNDS = 5;
24507
+ var EMPTY_TURN_TEXT = "The model sent back an empty reply.";
24375
24508
  var MAX_TOTAL_ROUNDS = 25;
24376
- var INTERACTIVE_TOOLS = new Set(["ask_user"]);
24377
24509
  async function runAgentLoop({
24378
24510
  chatState,
24379
24511
  streamingClient,
24380
24512
  toolRegistry,
24381
24513
  systemPrompt,
24382
24514
  signal,
24383
- getTab
24515
+ getTab,
24516
+ session = createSessionFacts()
24384
24517
  }) {
24385
24518
  const allErrors = [];
24386
24519
  const appliedSummaries = [];
24387
- const turnId = () => chatState.messages.at(-1)?.id ?? "";
24388
- beginTurn(`turn:${chatState.messages.length}`);
24389
- 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
+ };
24390
24529
  if (getTab) {
24391
24530
  beginBatch(getTab());
24392
24531
  }
@@ -24436,6 +24575,10 @@ async function runAgentLoop({
24436
24575
  chatState.appendToolCallEnd(event.id);
24437
24576
  break;
24438
24577
  }
24578
+ case "usage": {
24579
+ chatState.recordUsage(event, { systemTokens: estimatePromptTokens(systemPrompt) });
24580
+ break;
24581
+ }
24439
24582
  case "done": {
24440
24583
  ({ stopReason } = event);
24441
24584
  break;
@@ -24450,7 +24593,6 @@ async function runAgentLoop({
24450
24593
  }
24451
24594
  }
24452
24595
  }
24453
- chatState.finishStream(stopReason);
24454
24596
  if (streamError) {
24455
24597
  if (streamErrorCode === "cf_reconnect_required") {
24456
24598
  resetModelCache();
@@ -24459,30 +24601,55 @@ async function runAgentLoop({
24459
24601
  chatState.setError(streamError);
24460
24602
  return;
24461
24603
  }
24462
- 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) {
24463
24611
  return;
24464
24612
  }
24465
24613
  let didWork = false;
24466
24614
  for (const [id, call] of toolCalls) {
24615
+ if (signal?.aborted) {
24616
+ return;
24617
+ }
24467
24618
  let result;
24468
- 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
+ });
24469
24631
  try {
24470
- const args = call.arguments ? JSON.parse(call.arguments) : {};
24471
- result = await toolRegistry.execute(call.name, args);
24632
+ result = await toolRegistry.execute(call.name, parseToolArguments(call.arguments), ctx);
24472
24633
  } catch (error) {
24473
24634
  result = {
24474
24635
  success: false,
24475
24636
  error: `Failed to parse arguments: ${error.message}`
24476
24637
  };
24638
+ } finally {
24639
+ link?.release();
24477
24640
  }
24478
- 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) {
24479
24645
  didWork = true;
24480
24646
  }
24481
24647
  reanchorBatch();
24482
24648
  if (!result.success && result.error) {
24483
24649
  allErrors.push(result.error);
24484
24650
  }
24485
- if (result.success && result.summary) {
24651
+ const wrote = ledger.writes.slice(writesBefore).some((write) => write.ok);
24652
+ if (result.success && result.summary && wrote) {
24486
24653
  appliedSummaries.push(result.summary);
24487
24654
  }
24488
24655
  chatState.appendToolResult(id, result);
@@ -24491,6 +24658,9 @@ async function runAgentLoop({
24491
24658
  if (didWork) {
24492
24659
  workRounds += 1;
24493
24660
  }
24661
+ if (signal?.aborted) {
24662
+ return;
24663
+ }
24494
24664
  if (workRounds < MAX_ROUNDS && round < MAX_TOTAL_ROUNDS) {
24495
24665
  chatState.beginAssistantTurn();
24496
24666
  }
@@ -24517,10 +24687,20 @@ You can continue by sending another message, or try a more specific request.`;
24517
24687
  }
24518
24688
  chatState.setError(tail);
24519
24689
  } finally {
24520
- endTurnSignal();
24521
24690
  endBatch();
24522
- 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}`);
24523
24702
  }
24703
+ return parsed;
24524
24704
  }
24525
24705
 
24526
24706
  // src/services/ai-system-prompt.ts
@@ -25232,153 +25412,6 @@ function buildProjectSummary({
25232
25412
  `);
25233
25413
  }
25234
25414
 
25235
- // src/services/context-manager.ts
25236
- var CHARS_PER_TOKEN = 4;
25237
- var MODEL_CONTEXT_WINDOWS = [
25238
- ["gpt-4o", 128000],
25239
- ["gpt-4.1", 1e6],
25240
- ["gpt-4-turbo", 128000],
25241
- ["gpt-4", 8192],
25242
- ["gpt-3.5", 16385],
25243
- ["o1", 200000],
25244
- ["o3", 200000],
25245
- ["claude", 200000]
25246
- ];
25247
- var DEFAULT_CONTEXT_WINDOW = 32000;
25248
- var BUDGET_FRACTION = 0.8;
25249
- var WARN_FRACTION = 0.5;
25250
- function contextWindowFor(model) {
25251
- if (!model) {
25252
- return DEFAULT_CONTEXT_WINDOW;
25253
- }
25254
- const reported = modelContextWindow(model);
25255
- if (reported !== undefined && reported > 0) {
25256
- return reported;
25257
- }
25258
- const id = model.toLowerCase();
25259
- let best = 0;
25260
- let window2 = DEFAULT_CONTEXT_WINDOW;
25261
- for (const [prefix, size] of MODEL_CONTEXT_WINDOWS) {
25262
- if (id.startsWith(prefix) && prefix.length > best) {
25263
- best = prefix.length;
25264
- window2 = size;
25265
- }
25266
- }
25267
- return window2;
25268
- }
25269
- var KEEP_RECENT = 20;
25270
- var MIN_USER_TURNS = 3;
25271
- function estimateTokens(input) {
25272
- if (typeof input === "string") {
25273
- return Math.ceil(input.length / CHARS_PER_TOKEN);
25274
- }
25275
- let total = 0;
25276
- for (const msg of input) {
25277
- total += estimateTokens(msg.content || "");
25278
- if (msg.tool_calls) {
25279
- for (const tc of msg.tool_calls) {
25280
- total += estimateTokens(tc.function?.name || "") + estimateTokens(tc.function?.arguments || "") + 4;
25281
- }
25282
- }
25283
- total += 4;
25284
- }
25285
- return total;
25286
- }
25287
- function trimContext(chatState, systemPrompt) {
25288
- const window2 = contextWindowFor(chatState.model);
25289
- const maxTokens = Math.floor(window2 * BUDGET_FRACTION);
25290
- const warnTokens = Math.floor(window2 * WARN_FRACTION);
25291
- const systemTokens = estimateTokens(systemPrompt);
25292
- const allMessages = chatState.messages;
25293
- const messageTokens = estimateTokens(chatState.toMessagesArray());
25294
- const total = systemTokens + messageTokens;
25295
- if (total <= maxTokens) {
25296
- chatState.setTokenCount(total);
25297
- chatState.setContextWarning(total >= warnTokens);
25298
- return { estimatedTokens: total, droppedCount: 0 };
25299
- }
25300
- let keepFrom = Math.max(0, allMessages.length - KEEP_RECENT);
25301
- let preservedTurns = 0;
25302
- for (let i = allMessages.length - 1;i >= keepFrom; i--) {
25303
- const { role } = allMessages[i];
25304
- if (role === "user" || role === "tool") {
25305
- preservedTurns += 1;
25306
- }
25307
- }
25308
- if (preservedTurns < MIN_USER_TURNS) {
25309
- for (let i = keepFrom - 1;i >= 0 && preservedTurns < MIN_USER_TURNS; i--) {
25310
- keepFrom = i;
25311
- const { role } = allMessages[i];
25312
- if (role === "user" || role === "tool") {
25313
- preservedTurns += 1;
25314
- }
25315
- }
25316
- }
25317
- if (keepFrom <= 0) {
25318
- chatState.setTokenCount(total);
25319
- chatState.setContextWarning(true);
25320
- return { estimatedTokens: total, droppedCount: 0 };
25321
- }
25322
- const droppedCount = keepFrom;
25323
- allMessages.splice(0, keepFrom);
25324
- allMessages.unshift({
25325
- id: `ctx_summary_${Date.now()}`,
25326
- role: "user",
25327
- content: `[Earlier conversation truncated. ${droppedCount} messages dropped to stay within token budget. The most recent ${allMessages.length - 1} messages are preserved.]`,
25328
- timestamp: Date.now()
25329
- });
25330
- const newTotal = systemTokens + estimateTokens(chatState.toMessagesArray());
25331
- chatState.setTokenCount(newTotal);
25332
- chatState.setContextWarning(true);
25333
- return { estimatedTokens: newTotal, droppedCount };
25334
- }
25335
- var UNANSWERED_TOOL_RESULT = JSON.stringify({
25336
- success: false,
25337
- error: "This tool call was never completed — the session was reloaded or the history was trimmed."
25338
- });
25339
- function pruneOrphanToolMessages(chatState) {
25340
- const { messages } = chatState;
25341
- const requested = new Set;
25342
- const answered = new Set;
25343
- for (const msg of messages) {
25344
- if (msg.role === "assistant" && msg.toolCalls) {
25345
- for (const call of msg.toolCalls) {
25346
- requested.add(call.id);
25347
- }
25348
- } else if (msg.role === "tool" && msg.toolCallId) {
25349
- answered.add(msg.toolCallId);
25350
- }
25351
- }
25352
- let dropped = 0;
25353
- for (let i = messages.length - 1;i >= 0; i--) {
25354
- const msg = messages[i];
25355
- if (msg.role === "tool" && (!msg.toolCallId || !requested.has(msg.toolCallId))) {
25356
- messages.splice(i, 1);
25357
- dropped += 1;
25358
- }
25359
- }
25360
- let sealed = 0;
25361
- for (let i = messages.length - 1;i >= 0; i--) {
25362
- const msg = messages[i];
25363
- if (msg.role !== "assistant" || !msg.toolCalls) {
25364
- continue;
25365
- }
25366
- const missing = msg.toolCalls.filter((call) => !answered.has(call.id));
25367
- if (missing.length === 0) {
25368
- continue;
25369
- }
25370
- messages.splice(i + 1, 0, ...missing.map((call, n) => ({
25371
- content: UNANSWERED_TOOL_RESULT,
25372
- id: `sealed_${call.id}_${n}`,
25373
- role: "tool",
25374
- timestamp: msg.timestamp,
25375
- toolCallId: call.id
25376
- })));
25377
- sealed += missing.length;
25378
- }
25379
- return { dropped, sealed };
25380
- }
25381
-
25382
25415
  // src/services/render-critic.ts
25383
25416
  function translateRenderError(err) {
25384
25417
  const msg = err.message || String(err);
@@ -25581,6 +25614,76 @@ function moveSession(fromRoot, toRoot, id) {
25581
25614
  writeIndex(toRoot, toIndex);
25582
25615
  }
25583
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
+
25584
25687
  // src/services/document-assistant.ts
25585
25688
  function projectRoot() {
25586
25689
  return workspace2.projectRoot || "";
@@ -25588,6 +25691,7 @@ function projectRoot() {
25588
25691
  function createDocumentAssistant() {
25589
25692
  const chatState = createChatState({ model: preferredModel() });
25590
25693
  let sessionId = null;
25694
+ let sessionFacts = createSessionFacts();
25591
25695
  const getProjectStyle = () => workspace2.projectConfig?.style;
25592
25696
  const findOpenTab = (path) => {
25593
25697
  for (const tab of workspace2.tabs.values()) {
@@ -25598,7 +25702,7 @@ function createDocumentAssistant() {
25598
25702
  return null;
25599
25703
  };
25600
25704
  const innerRegistry = createToolRegistry();
25601
- registerAskTool(innerRegistry);
25705
+ registerAskTool(innerRegistry, { onPending: () => persistChat() });
25602
25706
  registerAiTools(innerRegistry, {
25603
25707
  getTab: () => activeTab2.value,
25604
25708
  saveFile: async (relPath, content) => {
@@ -25667,6 +25771,8 @@ function createDocumentAssistant() {
25667
25771
  });
25668
25772
  const toolRegistry = composeToolRegistries(createGatedToolRegistry(innerRegistry, availability), commandTools);
25669
25773
  let controller = null;
25774
+ const turnActive = shallowRef(false);
25775
+ let turnEnded = Promise.resolve();
25670
25776
  function buildPrompt() {
25671
25777
  const tab = activeTab2.value;
25672
25778
  const inventory = projectState ? [...projectState.dirs.values()].flat().filter((e) => e.type === "file").map((e) => e.path) : undefined;
@@ -25693,7 +25799,7 @@ function createDocumentAssistant() {
25693
25799
  });
25694
25800
  }
25695
25801
  async function sendMessage(text) {
25696
- if (!text.trim() || chatState.status === "streaming") {
25802
+ if (!text.trim() || turnActive.value || chatState.status === "streaming") {
25697
25803
  return;
25698
25804
  }
25699
25805
  if (!sessionId) {
@@ -25706,32 +25812,49 @@ function createDocumentAssistant() {
25706
25812
  }
25707
25813
  pruneOrphanToolMessages(chatState);
25708
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
+ });
25709
25822
  try {
25710
25823
  const plat = getPlatform2();
25711
- const chatUrl = await Promise.resolve(plat.aiChatUrl());
25712
25824
  chatState.setModel(preferredModel());
25713
25825
  const streamingClient = createProxyStreamingClient({
25714
- chatUrl,
25826
+ chatUrl: () => plat.aiChatUrl(),
25715
25827
  model: chatState.model,
25716
25828
  apiKey: getOpenAiKey() || undefined,
25717
25829
  baseUrl: getBaseUrl() || undefined
25718
25830
  });
25719
- controller = new AbortController;
25831
+ if (sessionFacts.sessionId !== sessionId) {
25832
+ sessionFacts = createSessionFacts(sessionId, sessionFacts.toJSON());
25833
+ }
25720
25834
  await runAgentLoop({
25721
25835
  chatState,
25722
25836
  streamingClient,
25723
25837
  toolRegistry,
25724
25838
  systemPrompt: buildPrompt(),
25725
- signal: controller.signal,
25839
+ signal,
25840
+ session: sessionFacts,
25726
25841
  getTab: () => activeTab2.value
25727
25842
  });
25728
25843
  } catch (error) {
25729
25844
  chatState.setError(error instanceof Error ? error.message : String(error));
25730
25845
  } finally {
25731
25846
  controller = null;
25847
+ turnActive.value = false;
25732
25848
  persistChat();
25849
+ settleTurnEnded();
25733
25850
  }
25734
25851
  }
25852
+ function isTurnActive() {
25853
+ return turnActive.value;
25854
+ }
25855
+ function whenTurnEnds() {
25856
+ return turnEnded;
25857
+ }
25735
25858
  function stop() {
25736
25859
  controller?.abort();
25737
25860
  cancelAsk();
@@ -25742,7 +25865,8 @@ function createDocumentAssistant() {
25742
25865
  stop();
25743
25866
  resetAsk();
25744
25867
  resetImportRuns();
25745
- resetImportGuard();
25868
+ resetAiWrites();
25869
+ sessionFacts = createSessionFacts();
25746
25870
  chatState.clearChat();
25747
25871
  sessionId = null;
25748
25872
  setActiveSession(projectRoot(), null);
@@ -25779,10 +25903,10 @@ function createDocumentAssistant() {
25779
25903
  return;
25780
25904
  }
25781
25905
  const msgs = chatState.messages.filter((m) => m.role !== "assistant" || m.content || (m.toolCalls?.length ?? 0) > 0);
25782
- saveSession(projectRoot(), sessionId, msgs);
25906
+ saveSession(projectRoot(), sessionId, stripToolResults(msgs));
25783
25907
  }
25784
25908
  function pushRestoredMessages(msgs) {
25785
- for (const m of msgs) {
25909
+ for (const m of backfillToolResults(msgs)) {
25786
25910
  chatState.messages.push({
25787
25911
  ...m,
25788
25912
  id: m.id || `restored_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
@@ -25805,7 +25929,9 @@ function createDocumentAssistant() {
25805
25929
  restoreChat();
25806
25930
  return {
25807
25931
  chatState,
25932
+ isTurnActive,
25808
25933
  sendMessage,
25934
+ whenTurnEnds,
25809
25935
  stop,
25810
25936
  newChat,
25811
25937
  listSessions: listSessions2,
@@ -25818,7 +25944,7 @@ function createDocumentAssistant() {
25818
25944
  var ai_credentials_form_default = {
25819
25945
  $schema: "https://jxsuite.com/schema/v1",
25820
25946
  $id: "StudioAiCredentialsForm",
25821
- $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.",
25822
25948
  tagName: "div",
25823
25949
  attributes: {
25824
25950
  part: "ai-creds-form",
@@ -25834,8 +25960,7 @@ var ai_credentials_form_default = {
25834
25960
  maxWidth: "320px",
25835
25961
  textAlign: "left",
25836
25962
  '& [part="title"]': {
25837
- $description: "Centred over a left-aligned column: the form is a titled block, not a list of labelled rows.",
25838
- 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.",
25839
25964
  fontWeight: "600"
25840
25965
  },
25841
25966
  '& [part="note"]': {
@@ -25855,17 +25980,22 @@ var ai_credentials_form_default = {
25855
25980
  width: "100%"
25856
25981
  },
25857
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.",
25858
25984
  display: "flex",
25859
- alignItems: "center",
25985
+ flexWrap: "wrap",
25986
+ alignItems: "flex-start",
25860
25987
  gap: "var(--jx-space-3)"
25861
25988
  },
25862
25989
  '& [part="models-error"]': {
25863
- $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.',
25864
25991
  color: "var(--danger)",
25865
- fontSize: "var(--jx-text-sm)"
25992
+ fontSize: "var(--jx-text-sm)",
25993
+ flexBasis: "100%",
25994
+ minWidth: "0",
25995
+ overflowWrap: "anywhere"
25866
25996
  },
25867
25997
  '& [part="actions"]': {
25868
- $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.',
25869
25999
  display: "flex",
25870
26000
  alignSelf: "flex-end",
25871
26001
  gap: "var(--jx-space-3)"
@@ -26067,26 +26197,26 @@ var ai_credentials_form_default = {
26067
26197
  }
26068
26198
  },
26069
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.",
26070
26201
  tagName: "div",
26071
26202
  attributes: {
26072
- part: "actions"
26203
+ part: "actions-slot",
26204
+ role: "none"
26073
26205
  },
26074
- children: [
26075
- {
26206
+ style: {
26207
+ display: "contents"
26208
+ },
26209
+ $switch: {
26210
+ $ref: "#/state/dirty"
26211
+ },
26212
+ cases: {
26213
+ true: {
26076
26214
  tagName: "div",
26077
26215
  attributes: {
26078
- part: "cancel-slot",
26079
- role: "none"
26080
- },
26081
- style: {
26082
- display: "contents"
26083
- },
26084
- $switch: {
26085
- $ref: "#/state/haveKey"
26216
+ part: "actions"
26086
26217
  },
26087
- cases: {
26088
- true: {
26089
- $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
+ {
26090
26220
  tagName: "jx-button",
26091
26221
  attributes: {
26092
26222
  part: "cancel"
@@ -26112,38 +26242,38 @@ var ai_credentials_form_default = {
26112
26242
  textContent: "Cancel"
26113
26243
  }
26114
26244
  ]
26115
- }
26116
- }
26117
- },
26118
- {
26119
- tagName: "jx-button",
26120
- attributes: {
26121
- part: "save"
26122
- },
26123
- $props: {
26124
- size: "sm",
26125
- variant: "accent"
26126
- },
26127
- onclick: {
26128
- $prototype: "Function",
26129
- body: [
26130
- {
26131
- operator: "call",
26132
- target: {
26133
- $ref: "#/state/save"
26134
- },
26135
- value: []
26136
- }
26137
- ]
26138
- },
26139
- children: [
26245
+ },
26140
26246
  {
26141
- tagName: "span",
26142
- 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
+ ]
26143
26273
  }
26144
26274
  ]
26145
26275
  }
26146
- ]
26276
+ }
26147
26277
  }
26148
26278
  ]
26149
26279
  };
@@ -26174,7 +26304,11 @@ function createAiCredentialsSurface(view, actions) {
26174
26304
  return {
26175
26305
  host,
26176
26306
  update(next) {
26307
+ const answering = !next.dirty && host.querySelector('[part="actions"]')?.contains(document.activeElement);
26177
26308
  Object.assign(scope, derive3(next));
26309
+ if (answering && !host.contains(document.activeElement)) {
26310
+ host.querySelector('[part="key"] [part="input"]')?.focus();
26311
+ }
26178
26312
  if (mounting || handle !== null && host.contains(handle.root)) {
26179
26313
  return;
26180
26314
  }
@@ -26195,11 +26329,23 @@ function createAiCredentialsForm(opts) {
26195
26329
  let availableModels = [];
26196
26330
  let modelsLoading = false;
26197
26331
  let modelsError = "";
26332
+ let basis = { apiKey: "", baseUrl: "", model: "" };
26198
26333
  let surface = null;
26334
+ function stored() {
26335
+ return { apiKey: getOpenAiKey(), baseUrl: getBaseUrl(), model: storedModel() };
26336
+ }
26199
26337
  function loadDrafts() {
26200
- keyDraft = getOpenAiKey();
26201
- baseUrlDraft = getBaseUrl();
26202
- 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;
26203
26349
  }
26204
26350
  function startEdit() {
26205
26351
  loadDrafts();
@@ -26250,8 +26396,8 @@ function createAiCredentialsForm(opts) {
26250
26396
  function view() {
26251
26397
  return {
26252
26398
  baseUrlDraft,
26399
+ dirty: aiProviderDiffers({ apiKey: keyDraft, baseUrl: baseUrlDraft, model: modelDraft }),
26253
26400
  fetchLabel: modelsLoading ? "Fetching…" : availableModels.length > 0 ? "Refresh models" : "Fetch models",
26254
- haveKey: hasOpenAiKey(),
26255
26401
  intro: opts.intro ?? DEFAULT_INTRO,
26256
26402
  keyDraft,
26257
26403
  modelDraft,
@@ -26273,6 +26419,7 @@ function createAiCredentialsForm(opts) {
26273
26419
  surface?.update(view());
26274
26420
  }
26275
26421
  function render() {
26422
+ followStore();
26276
26423
  if (surface) {
26277
26424
  surface.update(view());
26278
26425
  } else {
@@ -26316,8 +26463,8 @@ var ai_managed_connect_default = {
26316
26463
  maxWidth: "280px",
26317
26464
  textAlign: "center"
26318
26465
  },
26319
- '& [part="error"]': {
26320
- $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.',
26321
26468
  margin: "0",
26322
26469
  color: "var(--danger)",
26323
26470
  fontSize: "var(--jx-text-sm)",
@@ -26372,7 +26519,7 @@ var ai_managed_connect_default = {
26372
26519
  {
26373
26520
  tagName: "div",
26374
26521
  attributes: {
26375
- part: "error-slot",
26522
+ part: "connect-failure-slot",
26376
26523
  role: "none"
26377
26524
  },
26378
26525
  style: {
@@ -26385,7 +26532,7 @@ var ai_managed_connect_default = {
26385
26532
  true: {
26386
26533
  tagName: "p",
26387
26534
  attributes: {
26388
- part: "error"
26535
+ part: "connect-failure"
26389
26536
  },
26390
26537
  textContent: "${state.error}"
26391
26538
  }
@@ -26438,6 +26585,7 @@ function createManagedConnectSurface(view, actions) {
26438
26585
  // src/ui/ai-managed-connect.ts
26439
26586
  var LEDE_FRESH = "Recommended — run the assistant on Workers AI in your own Cloudflare account. No API key to create, copy or rotate.";
26440
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.";
26441
26589
  function createManagedConnect(opts) {
26442
26590
  let busy = false;
26443
26591
  let connectError = "";
@@ -26448,6 +26596,26 @@ function createManagedConnect(opts) {
26448
26596
  }
26449
26597
  return !isProxyConfigured() || proxyStateCode() === "cf_reconnect_required";
26450
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
+ }
26451
26619
  function ensureProbe() {
26452
26620
  ensureProxyProbe(opts.requestRender);
26453
26621
  }
@@ -26460,17 +26628,10 @@ function createManagedConnect(opts) {
26460
26628
  return;
26461
26629
  }
26462
26630
  if (!outcome.connection.accountId) {
26463
- const { openCfAccountPicker } = await (__preload("nwnwdm6p"), import("./chunks/cf-account-picker-nwnwdm6p.js"));
26464
- if (!await openCfAccountPicker()) {
26465
- connectError = "Cloudflare is connected, but no account is chosen yet — pick one to finish setting up the assistant.";
26466
- return;
26467
- }
26468
- }
26469
- resetModelCache();
26470
- await fetchAvailableModels({ force: true });
26471
- if (!isProxyConfigured() || proxyStateCode() === "cf_reconnect_required") {
26472
- 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;
26473
26633
  }
26634
+ await verifyConnection();
26474
26635
  }
26475
26636
  async function connect() {
26476
26637
  if (busy) {
@@ -26480,7 +26641,7 @@ function createManagedConnect(opts) {
26480
26641
  connectError = "";
26481
26642
  opts.requestRender();
26482
26643
  try {
26483
- await settle(await getPlatform2().cfConnect?.() ?? null);
26644
+ await (needsAccount() ? chooseAccount() : settle(await getPlatform2().cfConnect?.() ?? null));
26484
26645
  } catch (error) {
26485
26646
  connectError = error instanceof Error ? error.message : String(error);
26486
26647
  }
@@ -26488,6 +26649,14 @@ function createManagedConnect(opts) {
26488
26649
  opts.requestRender();
26489
26650
  }
26490
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
+ }
26491
26660
  const lapsed = proxyStateCode() === "cf_reconnect_required";
26492
26661
  const idleLabel = lapsed ? "Reconnect Cloudflare" : "Connect Cloudflare";
26493
26662
  return {
@@ -26781,7 +26950,8 @@ var preferences_default = {
26781
26950
  maxHeight: "60vh",
26782
26951
  overflowY: "auto"
26783
26952
  },
26784
- '& [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.',
26785
26955
  margin: "0",
26786
26956
  fontSize: "var(--jx-text-lg)",
26787
26957
  color: "var(--fg)"
@@ -26817,8 +26987,10 @@ var preferences_default = {
26817
26987
  gap: "var(--jx-space-2)"
26818
26988
  },
26819
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.",
26820
26991
  display: "flex",
26821
- alignItems: "center",
26992
+ flexWrap: "wrap",
26993
+ alignItems: "flex-start",
26822
26994
  justifyContent: "space-between",
26823
26995
  gap: "var(--jx-space-4)",
26824
26996
  padding: "var(--jx-space-3)",
@@ -26826,8 +26998,9 @@ var preferences_default = {
26826
26998
  borderRadius: "var(--radius)"
26827
26999
  },
26828
27000
  '& [part="account-text"]': {
26829
- $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.",
26830
27002
  display: "flex",
27003
+ flex: "1 1 120px",
26831
27004
  flexDirection: "column",
26832
27005
  gap: "var(--jx-space-1)",
26833
27006
  minWidth: "0"
@@ -26836,19 +27009,25 @@ var preferences_default = {
26836
27009
  color: "var(--fg)"
26837
27010
  },
26838
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.",
26839
27013
  fontSize: "var(--jx-text-sm)",
26840
- color: "var(--fg-dim)"
27014
+ color: "var(--fg-dim)",
27015
+ overflowWrap: "anywhere"
26841
27016
  },
26842
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.",
26843
27019
  display: "flex",
26844
27020
  alignItems: "center",
26845
27021
  gap: "var(--jx-space-3)",
26846
- flexShrink: "0"
27022
+ flexShrink: "0",
27023
+ marginInlineStart: "auto"
26847
27024
  },
26848
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.",
26849
27027
  display: "flex",
26850
27028
  flexDirection: "column",
26851
- gap: "var(--jx-space-1)"
27029
+ gap: "var(--jx-space-1)",
27030
+ paddingInlineEnd: "var(--jx-space-2)"
26852
27031
  },
26853
27032
  '& [part="refusal"]': {
26854
27033
  display: "flex",
@@ -26873,9 +27052,10 @@ var preferences_default = {
26873
27052
  textTransform: "uppercase",
26874
27053
  color: "var(--fg-dim)"
26875
27054
  },
26876
- '& [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.",
26877
27057
  display: "flex",
26878
- alignItems: "baseline",
27058
+ alignItems: "center",
26879
27059
  gap: "var(--jx-space-4)",
26880
27060
  padding: "var(--jx-space-1) 0"
26881
27061
  },
@@ -26887,6 +27067,8 @@ var preferences_default = {
26887
27067
  color: "var(--fg)"
26888
27068
  },
26889
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",
26890
27072
  color: "var(--fg-dim)",
26891
27073
  minWidth: "0"
26892
27074
  }
@@ -27631,6 +27813,7 @@ function openPreferencesSurface(options) {
27631
27813
  const box = element.querySelector('dialog[part="dialog"]') ?? element;
27632
27814
  box.setAttribute(REGION_ATTR, overlayRegion("dialog", "preferences"));
27633
27815
  showModal(element);
27816
+ element.querySelector('[part="nav-item"][aria-current="true"]')?.focus();
27634
27817
  return element;
27635
27818
  });
27636
27819
  return {
@@ -38918,6 +39101,9 @@ function formatErrorAdvice(error) {
38918
39101
  if (lower.includes("500") || lower.includes("internal")) {
38919
39102
  return "The upstream API returned a server error. Try again in a moment.";
38920
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
+ }
38921
39107
  return "";
38922
39108
  }
38923
39109
  function projectCommand(id, opts = {}, registry = activeRegistry()) {
@@ -38947,7 +39133,7 @@ function tokenLabel2(tokens) {
38947
39133
  function tokenHint(tokens, overBudget) {
38948
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`;
38949
39135
  }
38950
- var ASK_TOOL = "ask_user";
39136
+ var ASK_TOOL2 = "ask_user";
38951
39137
  var IMPORT_TOOL = "import_site";
38952
39138
  function parseAsk(tc) {
38953
39139
  let parsed;
@@ -38967,7 +39153,7 @@ function parseAsk(tc) {
38967
39153
  }
38968
39154
  function toolOutcome(tc, live = true) {
38969
39155
  if (!tc.result) {
38970
- return tc.name === ASK_TOOL && !live ? "unanswered" : "pending";
39156
+ return tc.name === ASK_TOOL2 && !live ? "unanswered" : "pending";
38971
39157
  }
38972
39158
  return tc.result.success ? "ok" : "failed";
38973
39159
  }
@@ -38976,7 +39162,12 @@ function toolOutcomeText(tc) {
38976
39162
  if (!result) {
38977
39163
  return "";
38978
39164
  }
38979
- 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;
38980
39171
  }
38981
39172
  function noImport() {
38982
39173
  return {
@@ -39015,7 +39206,7 @@ function projectChip(tc, handlers = {}) {
39015
39206
  const outcome = toolOutcome(tc, tc.id === handlers.pendingId || Boolean(tc.result));
39016
39207
  const text = toolOutcomeText(tc);
39017
39208
  const run = tc.name === IMPORT_TOOL ? handlers.importRun?.(tc.id) ?? null : null;
39018
- const ask = tc.name === ASK_TOOL ? parseAsk(tc) : null;
39209
+ const ask = tc.name === ASK_TOOL2 ? parseAsk(tc) : null;
39019
39210
  const answered = tc.result?.success ? tc.result.data ?? null : null;
39020
39211
  const base = {
39021
39212
  answer: answered ? answered.skipped ? "You decide" : answered.answer ?? "" : "",
@@ -39464,7 +39655,7 @@ function createComposer(opts) {
39464
39655
  }
39465
39656
  }
39466
39657
  function send() {
39467
- if (!draft.trim() || opts.isStreaming()) {
39658
+ if (!draft.trim() || opts.isStreaming() && !(opts.isAwaiting?.() ?? false)) {
39468
39659
  return;
39469
39660
  }
39470
39661
  const text = buildMessageWithContext(draft, chips);
@@ -39497,7 +39688,7 @@ function createComposer(opts) {
39497
39688
  placeholder: awaiting ? "Answer the assistant… (Enter to reply)" : "Ask the assistant… (Enter to send)",
39498
39689
  sendDisabled: draft.trim().length === 0,
39499
39690
  sendLabel: awaiting ? "Answer" : "Send",
39500
- sendState: streaming ? "stop" : "send"
39691
+ sendState: !awaiting && streaming ? "stop" : "send"
39501
39692
  };
39502
39693
  }
39503
39694
  return {
@@ -39658,12 +39849,15 @@ var ai_chat_default = {
39658
39849
  color: "var(--fg-dim)"
39659
39850
  },
39660
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.",
39661
39853
  whiteSpace: "pre-wrap",
39662
- overflowWrap: "anywhere"
39854
+ overflowWrap: "anywhere",
39855
+ userSelect: "text"
39663
39856
  },
39664
39857
  '& [part="md"]': {
39665
- $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).",
39666
- 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"
39667
39861
  },
39668
39862
  '& [part="md"] > *:first-child': {
39669
39863
  marginTop: "0"
@@ -39698,8 +39892,10 @@ var ai_chat_default = {
39698
39892
  padding: "var(--jx-space-1) 6px"
39699
39893
  },
39700
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.',
39701
39896
  whiteSpace: "pre-wrap",
39702
39897
  overflowWrap: "anywhere",
39898
+ userSelect: "text",
39703
39899
  "::after": {
39704
39900
  content: '"▋"',
39705
39901
  color: "var(--accent)",
@@ -39875,6 +40071,8 @@ var ai_chat_default = {
39875
40071
  marginRight: "var(--jx-space-2)"
39876
40072
  },
39877
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",
39878
40076
  marginTop: "var(--jx-space-2)",
39879
40077
  maxHeight: "40vh",
39880
40078
  overflowY: "auto",
@@ -39930,7 +40128,8 @@ var ai_chat_default = {
39930
40128
  color: "var(--warning)",
39931
40129
  fontStyle: "normal"
39932
40130
  },
39933
- '& [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.",
39934
40133
  borderLeft: "2px solid var(--danger)",
39935
40134
  borderRadius: "var(--jx-radius-sm)",
39936
40135
  background: "var(--hover-bg)",
@@ -40113,7 +40312,7 @@ var ai_chat_default = {
40113
40312
  '& [part="tool-chip"][data-outcome="failed"]': {
40114
40313
  borderLeftColor: "LinkText"
40115
40314
  },
40116
- '& [part="error"]': {
40315
+ '& [part="error-card"]': {
40117
40316
  borderLeftColor: "LinkText"
40118
40317
  }
40119
40318
  },
@@ -41072,7 +41271,7 @@ var ai_chat_default = {
41072
41271
  shown: {
41073
41272
  tagName: "div",
41074
41273
  attributes: {
41075
- part: "error",
41274
+ part: "error-card",
41076
41275
  role: "alert"
41077
41276
  },
41078
41277
  children: [
@@ -42018,8 +42217,13 @@ function watchAssistant() {
42018
42217
  const last = cs.messages.at(-1);
42019
42218
  last?.content;
42020
42219
  last?.toolCalls?.length;
42220
+ for (const tc of latestRequest(cs.messages)?.toolCalls ?? []) {
42221
+ tc.result;
42222
+ }
42021
42223
  cs.status;
42022
42224
  cs.error;
42225
+ assistant.isTurnActive();
42226
+ cs.tokenCount;
42023
42227
  activeRegistry();
42024
42228
  pendingAsk();
42025
42229
  const run = activeImportRun();
@@ -42030,6 +42234,14 @@ function watchAssistant() {
42030
42234
  });
42031
42235
  });
42032
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
+ }
42033
42245
  function mountAiPanel() {
42034
42246
  if (mounted) {
42035
42247
  return;
@@ -42038,7 +42250,7 @@ function mountAiPanel() {
42038
42250
  onCredentialsChanged(renderAiPanel);
42039
42251
  }
42040
42252
  async function handleAssistantSend(text) {
42041
- if (!text.trim() || assistant.chatState.status === "streaming") {
42253
+ if (!text.trim()) {
42042
42254
  return;
42043
42255
  }
42044
42256
  if (answerAsk(text.trim())) {
@@ -42046,12 +42258,17 @@ async function handleAssistantSend(text) {
42046
42258
  renderAiPanel();
42047
42259
  return;
42048
42260
  }
42261
+ if (isAssistantStreaming()) {
42262
+ return;
42263
+ }
42049
42264
  view5 = "chat";
42050
42265
  surface?.pin();
42051
42266
  renderAiPanel();
42052
42267
  try {
42053
42268
  await assistant.sendMessage(text);
42054
- } catch {}
42269
+ } catch {} finally {
42270
+ renderAiPanel();
42271
+ }
42055
42272
  }
42056
42273
  async function handleRetry() {
42057
42274
  const cs = assistant.chatState;
@@ -42085,6 +42302,9 @@ function handleRestore(messageId) {
42085
42302
  renderAiPanel();
42086
42303
  }
42087
42304
  async function seedAssistantPrompt(text) {
42305
+ if (assistant.isTurnActive()) {
42306
+ await assistant.whenTurnEnds();
42307
+ }
42088
42308
  await handleAssistantSend(text);
42089
42309
  }
42090
42310
  async function revealImportHandoff(brief) {
@@ -42143,7 +42363,7 @@ function isAssistantWaiting() {
42143
42363
  return isAwaitingAnswer();
42144
42364
  }
42145
42365
  function isAssistantStreaming() {
42146
- return assistant.chatState.status === "streaming";
42366
+ return assistant.isTurnActive();
42147
42367
  }
42148
42368
  function stop() {
42149
42369
  assistant.stop();
@@ -42316,6 +42536,9 @@ function assistantCommands() {
42316
42536
  ];
42317
42537
  }
42318
42538
 
42539
+ // src/hosting/document.ts
42540
+ var BOOT_META = { name: "jx-boot", content: "launcher" };
42541
+
42319
42542
  // src/services/import-client.ts
42320
42543
  var IMPORT_WARNING_PHASE = "warning";
42321
42544
  async function streamImport(endpoint, opts, onProgress, signal, onReady) {
@@ -42489,7 +42712,7 @@ var STUDIO_ROUTES = {
42489
42712
  resolveSite: route("GET", "/__studio/resolve-site", "Resolve the owning site of a file path {sitePath, projectConfig?, fileRelPath?}"),
42490
42713
  sites: route("GET", "/__studio/sites", "Enumerate site projects [{config, path}] (backs listProjects)", "The welcome screen's Projects catalogue stays hidden."),
42491
42714
  findProject: route("GET", "/__studio/find-project", "Locate a project directory by name outside the root", "openProject falls back to config-matching only."),
42492
- 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."),
42493
42716
  createProject: route("POST", "/__studio/create-project", "Scaffold a project at a caller-chosen destination → {root, config}"),
42494
42717
  starters: route("GET", "/__studio/starters", "Starter templates (StarterInfo[])", "The New Project picker offers only blank/templates."),
42495
42718
  importSite: route("POST", "/__studio/import-site", "Clone a live website into a project; streams NDJSON progress", "The New Project Import tab is unavailable."),
@@ -42498,6 +42721,7 @@ var STUDIO_ROUTES = {
42498
42721
  fileWrite: route("PUT", "/__studio/file", "Write a file's text content"),
42499
42722
  fileDelete: route("DELETE", "/__studio/file", "Delete a file"),
42500
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."),
42501
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."),
42502
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)"),
42503
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."),
@@ -42798,6 +43022,13 @@ function createCloudPlatform(project) {
42798
43022
  const data = await okJson(await api(`/file?path=${encodeURIComponent(path)}`), `Failed to read file: ${path}`);
42799
43023
  return data.content;
42800
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
+ },
42801
43032
  async writeFile(path, content) {
42802
43033
  const res = await api("/file", {
42803
43034
  method: "PUT",
@@ -42848,7 +43079,7 @@ function createCloudPlatform(project) {
42848
43079
  }
42849
43080
  const { offer } = negotiated;
42850
43081
  collabConnection ??= (async () => {
42851
- const { createWsCollabConnection } = await (__preload("ny4zc51r"), import("./chunks/ws-client-ny4zc51r.js"));
43082
+ const { createWsCollabConnection } = await (__preload("erze4czt"), import("./chunks/ws-client-erze4czt.js"));
42852
43083
  const scheme = location.protocol === "https:" ? "wss" : "ws";
42853
43084
  return createWsCollabConnection({
42854
43085
  hydratePath: async (path) => {
@@ -43348,32 +43579,50 @@ function newLocationId() {
43348
43579
  globalThis.crypto.getRandomValues(bytes);
43349
43580
  return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
43350
43581
  }
43582
+ function nameOf(error) {
43583
+ return typeof error === "object" && error !== null && "name" in error && typeof error.name === "string" ? error.name : "";
43584
+ }
43351
43585
  async function pickDirectoryPath(locate) {
43352
43586
  const show = globalThis.showDirectoryPicker;
43353
43587
  if (typeof show !== "function") {
43354
- return null;
43588
+ throw new TypeError("This browser has no folder chooser. Type the folder's path into Location instead.");
43355
43589
  }
43356
43590
  let handle;
43357
43591
  try {
43358
43592
  handle = await show({ id: "jx-new-project-location", mode: "readwrite", startIn: "documents" });
43359
- } catch {
43360
- 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 });
43361
43598
  }
43599
+ const { name } = handle;
43362
43600
  const id = newLocationId();
43601
+ let tagged = false;
43363
43602
  try {
43364
- const file = await handle.getFileHandle(LOCATION_ID_FILE, { create: true });
43365
- const writable = await file.createWritable();
43366
- await writable.write(id);
43367
- await writable.close();
43368
- } catch {
43369
- return null;
43370
- }
43371
- try {
43372
- return await locate({ id, name: handle.name });
43373
- } catch {
43374
- 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;
43375
43622
  } finally {
43376
- await handle.removeEntry(LOCATION_ID_FILE).catch(() => {});
43623
+ if (tagged) {
43624
+ await handle.removeEntry(LOCATION_ID_FILE).catch(() => {});
43625
+ }
43377
43626
  }
43378
43627
  }
43379
43628
 
@@ -43509,7 +43758,8 @@ function createDevServerPlatform() {
43509
43758
  return pickDirectoryPath(async ({ id, name }) => {
43510
43759
  const res = await fetch(`/__studio/locate-directory?name=${encodeURIComponent(name)}&id=${encodeURIComponent(id)}`);
43511
43760
  if (!res.ok) {
43512
- return null;
43761
+ const body = await res.json().catch(() => null);
43762
+ throw new Error(problemMessage(body, res.status));
43513
43763
  }
43514
43764
  const found = await readJson2(res);
43515
43765
  return found.path ?? null;
@@ -43545,6 +43795,13 @@ function createDevServerPlatform() {
43545
43795
  const data = await readJson2(res);
43546
43796
  return data.content;
43547
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
+ },
43548
43805
  async writeFile(path, content) {
43549
43806
  const res = await fetch(`/__studio/file?path=${encodeURIComponent(serverPath(path))}`, {
43550
43807
  body: content,
@@ -43644,7 +43901,7 @@ function createDevServerPlatform() {
43644
43901
  }
43645
43902
  const { offer } = negotiation;
43646
43903
  _collabConnection ??= (async () => {
43647
- const { createWsCollabConnection } = await (__preload("ny4zc51r"), import("./chunks/ws-client-ny4zc51r.js"));
43904
+ const { createWsCollabConnection } = await (__preload("erze4czt"), import("./chunks/ws-client-erze4czt.js"));
43648
43905
  const scheme = location.protocol === "https:" ? "wss" : "ws";
43649
43906
  return createWsCollabConnection({
43650
43907
  protocols: offer,
@@ -44166,7 +44423,7 @@ function createDevServerPlatform() {
44166
44423
  }
44167
44424
  },
44168
44425
  async cfApi(apiPath, init) {
44169
- const { getCfToken } = await (__preload("adg74qsk"), import("./chunks/cf-settings-adg74qsk.js"));
44426
+ const { getCfToken } = await (__preload("m79r7p5y"), import("./chunks/cf-settings-m79r7p5y.js"));
44170
44427
  const token = getCfToken();
44171
44428
  if (!token) {
44172
44429
  throw new Error("No Cloudflare API token configured");
@@ -44184,7 +44441,7 @@ function createDevServerPlatform() {
44184
44441
  return envelope.result ?? envelope;
44185
44442
  },
44186
44443
  async cfConnection() {
44187
- 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"));
44188
44445
  if (!getCfToken()) {
44189
44446
  return null;
44190
44447
  }
@@ -44222,7 +44479,59 @@ function createDevServerPlatform() {
44222
44479
  }
44223
44480
 
44224
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
+ }
44225
44530
  function resolveDefaultPlatform() {
44531
+ const refusal = bootRefusal();
44532
+ if (refusal) {
44533
+ throw new PlatformUnavailableError(refusal);
44534
+ }
44226
44535
  const cloud = globalThis.__jxCloud;
44227
44536
  return cloud ? createCloudPlatform(cloud.project) : createDevServerPlatform();
44228
44537
  }
@@ -45934,6 +46243,7 @@ function capabilities(platform) {
45934
46243
  gitClone: has("gitClone"),
45935
46244
  importSite: has("importSite"),
45936
46245
  openProjectInNewWindow: has("openProjectInNewWindow"),
46246
+ readFileBytes: has("readFileBytes"),
45937
46247
  windowControls: has("newWindow")
45938
46248
  };
45939
46249
  }
@@ -46103,7 +46413,7 @@ function openSettingsMenu(anchor, opts = {}) {
46103
46413
  var rail_default = {
46104
46414
  $schema: "https://jxsuite.com/schema/v1",
46105
46415
  $id: "StudioRail",
46106
- $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.",
46107
46417
  tagName: "div",
46108
46418
  attributes: {
46109
46419
  part: "rail"
@@ -46247,7 +46557,7 @@ var rail_default = {
46247
46557
  children: [
46248
46558
  {
46249
46559
  tagName: "span",
46250
- textContent: "${$map.item.title}"
46560
+ textContent: "${$map.item.railLabel}"
46251
46561
  }
46252
46562
  ]
46253
46563
  }
@@ -46378,6 +46688,7 @@ function project17() {
46378
46688
  badge: String(panel.badge?.(ctx) ?? ""),
46379
46689
  icon: panel.icon,
46380
46690
  id: panel.id,
46691
+ railLabel: panel.railLabel ?? panel.title,
46381
46692
  selected: isRailPanelShowing(panel),
46382
46693
  title: panel.title
46383
46694
  }))
@@ -48599,7 +48910,7 @@ function installUrlOf(error) {
48599
48910
  var new_project_default = {
48600
48911
  $schema: "https://jxsuite.com/schema/v1",
48601
48912
  $id: "StudioNewProject",
48602
- $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).",
48603
48914
  tagName: "jx-dialog",
48604
48915
  $props: {
48605
48916
  headline: {
@@ -50504,6 +50815,8 @@ async function browseLocation(rerender) {
50504
50815
  _parent = picked;
50505
50816
  _error2 = "";
50506
50817
  }
50818
+ } catch (error) {
50819
+ _error2 = errorMessage2(error) || "The folder could not be chosen. Type its path into Location instead.";
50507
50820
  } finally {
50508
50821
  _browsing = false;
50509
50822
  rerender();
@@ -50884,7 +51197,15 @@ function finish(result) {
50884
51197
  settle3(result);
50885
51198
  }
50886
51199
  async function finishCreated(result) {
50887
- 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
+ }
50888
51209
  finish(result);
50889
51210
  }
50890
51211
  function deriveSlug(name) {
@@ -51354,7 +51675,7 @@ var seo_default = {
51354
51675
  }
51355
51676
  },
51356
51677
  '& [part="chip"]': {
51357
- $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.",
51358
51679
  flexShrink: "0"
51359
51680
  },
51360
51681
  '& jx-dot[part="chip"][data-state="set"]': {
@@ -54287,8 +54608,8 @@ var settings_css_vars_default = {
54287
54608
  fontWeight: "600",
54288
54609
  color: "var(--fg)"
54289
54610
  },
54290
- '& [part="error"]': {
54291
- $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.',
54292
54613
  margin: "0 0 12px",
54293
54614
  fontSize: "var(--jx-text-md)",
54294
54615
  color: "var(--danger)"
@@ -54444,7 +54765,7 @@ var settings_css_vars_default = {
54444
54765
  {
54445
54766
  tagName: "div",
54446
54767
  attributes: {
54447
- part: "error-slot",
54768
+ part: "section-error-slot",
54448
54769
  role: "none"
54449
54770
  },
54450
54771
  $switch: {
@@ -54454,7 +54775,7 @@ var settings_css_vars_default = {
54454
54775
  true: {
54455
54776
  tagName: "p",
54456
54777
  attributes: {
54457
- part: "error",
54778
+ part: "section-error",
54458
54779
  role: "alert"
54459
54780
  },
54460
54781
  textContent: "${state.error}"
@@ -57934,8 +58255,8 @@ var settings_extensions_default = {
57934
58255
  fontSize: "var(--jx-text-md)",
57935
58256
  color: "var(--fg-dim)"
57936
58257
  },
57937
- '& [part="error"]': {
57938
- $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.',
57939
58260
  margin: "6px 0 0",
57940
58261
  fontSize: "var(--jx-text-md)",
57941
58262
  color: "var(--danger)"
@@ -57952,7 +58273,7 @@ var settings_extensions_default = {
57952
58273
  color: "var(--fg)"
57953
58274
  },
57954
58275
  '& [part="row"]': {
57955
- $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.",
57956
58277
  display: "flex",
57957
58278
  flexWrap: "wrap",
57958
58279
  alignItems: "flex-start",
@@ -58028,7 +58349,7 @@ var settings_extensions_default = {
58028
58349
  flexShrink: "0"
58029
58350
  },
58030
58351
  '& [part="empty"]': {
58031
- $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.',
58032
58353
  display: "flex",
58033
58354
  flexDirection: "column",
58034
58355
  alignItems: "flex-start",
@@ -58065,7 +58386,7 @@ var settings_extensions_default = {
58065
58386
  {
58066
58387
  tagName: "div",
58067
58388
  attributes: {
58068
- part: "error-slot",
58389
+ part: "section-error-slot",
58069
58390
  role: "none"
58070
58391
  },
58071
58392
  style: {
@@ -58078,7 +58399,7 @@ var settings_extensions_default = {
58078
58399
  true: {
58079
58400
  tagName: "p",
58080
58401
  attributes: {
58081
- part: "error",
58402
+ part: "section-error",
58082
58403
  role: "alert"
58083
58404
  },
58084
58405
  textContent: "${state.error}"
@@ -58620,8 +58941,8 @@ var settings_deploy_default = {
58620
58941
  fontWeight: "600",
58621
58942
  color: "var(--fg)"
58622
58943
  },
58623
- '& [part="error"]': {
58624
- $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.',
58625
58946
  margin: "0 0 16px",
58626
58947
  fontSize: "var(--jx-text-md)",
58627
58948
  color: "var(--danger)"
@@ -58642,7 +58963,7 @@ var settings_deploy_default = {
58642
58963
  {
58643
58964
  tagName: "div",
58644
58965
  attributes: {
58645
- part: "error-slot",
58966
+ part: "section-error-slot",
58646
58967
  role: "none"
58647
58968
  },
58648
58969
  style: {
@@ -58655,7 +58976,7 @@ var settings_deploy_default = {
58655
58976
  true: {
58656
58977
  tagName: "p",
58657
58978
  attributes: {
58658
- part: "error",
58979
+ part: "section-error",
58659
58980
  role: "alert"
58660
58981
  },
58661
58982
  textContent: "${state.error}"
@@ -60616,8 +60937,8 @@ var settings_locales_default = {
60616
60937
  fontSize: "var(--jx-text-md)",
60617
60938
  color: "var(--fg-dim)"
60618
60939
  },
60619
- '& [part="error"]': {
60620
- $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.",
60621
60942
  margin: "6px 0 0",
60622
60943
  fontSize: "var(--jx-text-md)",
60623
60944
  color: "var(--danger)"
@@ -60691,7 +61012,7 @@ var settings_locales_default = {
60691
61012
  {
60692
61013
  tagName: "div",
60693
61014
  attributes: {
60694
- part: "error-slot",
61015
+ part: "section-error-slot",
60695
61016
  role: "none"
60696
61017
  },
60697
61018
  $switch: {
@@ -60701,7 +61022,7 @@ var settings_locales_default = {
60701
61022
  true: {
60702
61023
  tagName: "p",
60703
61024
  attributes: {
60704
- part: "error",
61025
+ part: "section-error",
60705
61026
  role: "alert"
60706
61027
  },
60707
61028
  textContent: "${state.error}"
@@ -61297,7 +61618,7 @@ async function openProjectSettings(section) {
61297
61618
  showSettingsDocument();
61298
61619
  notifySettingsDocument2();
61299
61620
  try {
61300
- const { syncExtensionSettingsSections } = await (__preload("2mrptyek"), import("./chunks/extension-sections-2mrptyek.js"));
61621
+ const { syncExtensionSettingsSections } = await (__preload("r73an5s6"), import("./chunks/extension-sections-r73an5s6.js"));
61301
61622
  await syncExtensionSettingsSections();
61302
61623
  } catch (error) {
61303
61624
  notify2.warn(`Could not load extension settings sections — ${errorMessage2(error)}`, {
@@ -61434,9 +61755,11 @@ var about_default = {
61434
61755
  color: "var(--fg-dim)"
61435
61756
  },
61436
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.",
61437
61759
  margin: "0",
61438
61760
  color: "var(--fg)",
61439
- wordBreak: "break-word"
61761
+ wordBreak: "break-word",
61762
+ userSelect: "text"
61440
61763
  },
61441
61764
  '& [part="links"]': {
61442
61765
  display: "flex",
@@ -61474,10 +61797,12 @@ var about_default = {
61474
61797
  gap: "4px"
61475
61798
  },
61476
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.",
61477
61801
  display: "flex",
61478
61802
  justifyContent: "space-between",
61479
61803
  gap: "12px",
61480
- fontSize: "var(--jx-text-md)"
61804
+ fontSize: "var(--jx-text-md)",
61805
+ userSelect: "text"
61481
61806
  },
61482
61807
  '& [part="package-name"]': {
61483
61808
  color: "var(--fg-dim)"
@@ -61707,9 +62032,9 @@ function openAboutSurface(options) {
61707
62032
 
61708
62033
  // src/version.ts
61709
62034
  var APP_NAME = "Jx Studio";
61710
- var VERSION = "5.3.0";
61711
- var BUILD_DATE = "2026-09-22T01:30:39.991Z";
61712
- var GIT_COMMIT = "2928bf2";
62035
+ var VERSION = "5.4.0";
62036
+ var BUILD_DATE = "2026-09-29T19:41:26.105Z";
62037
+ var GIT_COMMIT = "fd18dbe";
61713
62038
  var LINKS = {
61714
62039
  github: "https://github.com/jxsuite/jx",
61715
62040
  docs: "https://github.com/jxsuite/jx#readme",
@@ -62117,9 +62442,9 @@ function withExtension(path, ext) {
62117
62442
  return current === "" ? `${path}${ext}` : `${path.slice(0, path.length - current.length)}${ext}`;
62118
62443
  }
62119
62444
  async function readDocument(path, source) {
62120
- const { formatForPath } = await (__preload("1k4qrcqd"), import("./chunks/format-host-1k4qrcqd.js"));
62445
+ const { formatForPath } = await (__preload("mym8qnd0"), import("./chunks/format-host-mym8qnd0.js"));
62121
62446
  if (formatForPath(path)) {
62122
- const { parseSourceForPath } = await (__preload("knfqshrm"), import("./chunks/file-ops-knfqshrm.js"));
62447
+ const { parseSourceForPath } = await (__preload("3xan8mnj"), import("./chunks/file-ops-3xan8mnj.js"));
62123
62448
  const parsed = await parseSourceForPath(path, source);
62124
62449
  return { document: parsed.document, frontmatter: parsed.frontmatter };
62125
62450
  }
@@ -62130,7 +62455,7 @@ async function writeDocument(targetExt, document2, frontmatter) {
62130
62455
  if (targetExt === ".json") {
62131
62456
  return serializeJson(merged, null);
62132
62457
  }
62133
- const { formatByExtension, formatSerialize } = await (__preload("1k4qrcqd"), import("./chunks/format-host-1k4qrcqd.js"));
62458
+ const { formatByExtension, formatSerialize } = await (__preload("mym8qnd0"), import("./chunks/format-host-mym8qnd0.js"));
62134
62459
  const format = formatByExtension(targetExt, "serialize");
62135
62460
  if (!format) {
62136
62461
  throw new Error(`No installed format writes ${targetExt} files.`);
@@ -62185,7 +62510,7 @@ async function buildPlan(path, source, targetExt) {
62185
62510
  if (!await readsBackIdentically(targetPath, text)) {
62186
62511
  lines.push("This file will not read back identically after conversion.");
62187
62512
  }
62188
- const { loadUsages, usageWarning } = await (__preload("h69xp9zz"), import("./chunks/references-h69xp9zz.js"));
62513
+ const { loadUsages, usageWarning } = await (__preload("f7bdv3z5"), import("./chunks/references-f7bdv3z5.js"));
62189
62514
  const sentence = usageWarning(await loadUsages({ path }), "convert");
62190
62515
  if (sentence !== null) {
62191
62516
  lines.push(sentence);
@@ -62200,14 +62525,14 @@ async function convertFile(path, targetExt) {
62200
62525
  notify.error(message, { detail, path, source: "Convert" });
62201
62526
  return null;
62202
62527
  };
62203
- const { loadFormats } = await (__preload("1k4qrcqd"), import("./chunks/format-host-1k4qrcqd.js"));
62528
+ const { loadFormats } = await (__preload("mym8qnd0"), import("./chunks/format-host-mym8qnd0.js"));
62204
62529
  await loadFormats();
62205
62530
  const targets = convertTargets(path);
62206
62531
  if (targets.length === 0) {
62207
62532
  return fail(`${path} cannot be converted to another format.`, CANNOT_CONVERT);
62208
62533
  }
62209
- const { workspace } = await (__preload("np10mayv"), import("./chunks/workspace-np10mayv.js"));
62210
- 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"));
62211
62536
  const openTabEntry = [...workspace.tabs.values()].find((tab) => tab.documentPath === path);
62212
62537
  if (openTabEntry) {
62213
62538
  await commitTabBuffers(openTabEntry);
@@ -62222,7 +62547,7 @@ async function convertFile(path, targetExt) {
62222
62547
  if (!targets.some((target) => target.ext === chosen)) {
62223
62548
  return fail(`${path} cannot be converted to ${chosen}.`, CANNOT_CONVERT);
62224
62549
  }
62225
- const { getPlatform } = await (__preload("ts9cg1a4"), import("./chunks/platform-ts9cg1a4.js"));
62550
+ const { getPlatform } = await (__preload("fnchmhy7"), import("./chunks/platform-fnchmhy7.js"));
62226
62551
  const platform = getPlatform();
62227
62552
  const targetPath = withExtension(path, chosen);
62228
62553
  try {
@@ -62244,7 +62569,7 @@ async function convertFile(path, targetExt) {
62244
62569
  if (plan.blocker !== null) {
62245
62570
  return fail(`${path} cannot be converted to ${chosen}.`, plan.blocker);
62246
62571
  }
62247
- const { showConfirmDialog } = await (__preload("jnvdbqva"), import("./chunks/layers-jnvdbqva.js"));
62572
+ const { showConfirmDialog } = await (__preload("s6mc01ry"), import("./chunks/layers-s6mc01ry.js"));
62248
62573
  const { html } = await (__preload("m244w9nh"), import("./chunks/lit-html-m244w9nh.js"));
62249
62574
  const confirmed = await showConfirmDialog("Convert Format", html`<span
62250
62575
  >Convert <strong>${path.split("/").pop()}</strong> to
@@ -62253,8 +62578,8 @@ async function convertFile(path, targetExt) {
62253
62578
  if (!confirmed) {
62254
62579
  return null;
62255
62580
  }
62256
- const { markLocalMutation } = await (__preload("3y6y0nrm"), import("./chunks/fs-events-3y6y0nrm.js"));
62257
- const { notifyMoveOutcome, settleRename } = await (__preload("y6fr4he9"), import("./chunks/files-y6fr4he9.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"));
62258
62583
  markLocalMutation(path, plan.targetPath);
62259
62584
  try {
62260
62585
  await platform.writeFile(path, plan.text);
@@ -62279,8 +62604,8 @@ async function convertFile(path, targetExt) {
62279
62604
  return plan.targetPath;
62280
62605
  }
62281
62606
  async function rebuildTab(from, to, text) {
62282
- const { openTab, renameTab } = await (__preload("np10mayv"), import("./chunks/workspace-np10mayv.js"));
62283
- const { formatForPath } = await (__preload("1k4qrcqd"), import("./chunks/format-host-1k4qrcqd.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"));
62284
62609
  renameTab(from, to, to);
62285
62610
  const format = formatForPath(to);
62286
62611
  let document2;
@@ -62288,7 +62613,7 @@ async function rebuildTab(from, to, text) {
62288
62613
  let layout = null;
62289
62614
  try {
62290
62615
  if (format) {
62291
- const { parseSourceForPath } = await (__preload("knfqshrm"), import("./chunks/file-ops-knfqshrm.js"));
62616
+ const { parseSourceForPath } = await (__preload("3xan8mnj"), import("./chunks/file-ops-3xan8mnj.js"));
62292
62617
  ({ document: document2, frontmatter } = await parseSourceForPath(to, text));
62293
62618
  } else {
62294
62619
  ({ document: document2, layout } = parseJsonDocument(text));
@@ -62317,7 +62642,7 @@ async function pickTarget(path, targets) {
62317
62642
  if (targets.length === 1) {
62318
62643
  return targets[0].ext;
62319
62644
  }
62320
- const { showPromptDialog } = await (__preload("jnvdbqva"), import("./chunks/layers-jnvdbqva.js"));
62645
+ const { showPromptDialog } = await (__preload("s6mc01ry"), import("./chunks/layers-s6mc01ry.js"));
62321
62646
  let picked = targets[0].ext;
62322
62647
  const answer = await showPromptDialog("Convert Format", {
62323
62648
  choice: {
@@ -62442,7 +62767,7 @@ function publishCommands() {
62442
62767
  when: (ctx) => ctx.project.open,
62443
62768
  enablement: () => platformSupportsPublish(),
62444
62769
  run: async () => {
62445
- const { openPublishPanel } = await (__preload("z1zd2da1"), import("./chunks/publish-panel-z1zd2da1.js"));
62770
+ const { openPublishPanel } = await (__preload("ak6hxfma"), import("./chunks/publish-panel-ak6hxfma.js"));
62446
62771
  openPublishPanel();
62447
62772
  },
62448
62773
  title: "Set Up Publishing"
@@ -63143,7 +63468,7 @@ function i18nCommands() {
63143
63468
  });
63144
63469
  return;
63145
63470
  }
63146
- const { openFileInTab } = await (__preload("y6fr4he9"), import("./chunks/files-y6fr4he9.js"));
63471
+ const { openFileInTab } = await (__preload("egsb0ry8"), import("./chunks/files-egsb0ry8.js"));
63147
63472
  await openFileInTab(path);
63148
63473
  },
63149
63474
  title: "Open Translation"
@@ -63188,7 +63513,7 @@ function i18nCommands() {
63188
63513
  return;
63189
63514
  }
63190
63515
  const cut = path.lastIndexOf("/");
63191
- const { createFileIn, openFileInTab } = await (__preload("y6fr4he9"), import("./chunks/files-y6fr4he9.js"));
63516
+ const { createFileIn, openFileInTab } = await (__preload("egsb0ry8"), import("./chunks/files-egsb0ry8.js"));
63192
63517
  const created = await createFileIn({
63193
63518
  content,
63194
63519
  dir: cut === -1 ? "." : path.slice(0, cut),
@@ -63359,6 +63684,293 @@ registerSurface("shell", shell_default);
63359
63684
  function mountShellSurface(host) {
63360
63685
  return mountSurface("shell", {}, host);
63361
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
+ }
63362
63974
 
63363
63975
  // src/shell/tree.ts
63364
63976
  function overlayLayers() {
@@ -63393,6 +64005,12 @@ async function mountShellTree(host = document.body) {
63393
64005
  await mountShellSurface(root);
63394
64006
  render4(overlayLayers(), root);
63395
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
+ }
63396
64014
  function shellRoot(host) {
63397
64015
  const existing = host.querySelector?.("#shell-root");
63398
64016
  if (existing) {
@@ -63444,7 +64062,14 @@ async function navigateToComponent(componentPath) {
63444
64062
  }
63445
64063
  initCssData(webdata_default);
63446
64064
  if (!hasPlatform2()) {
63447
- 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
+ }
63448
64073
  }
63449
64074
  mountResizeEdges();
63450
64075
  registerKit();
@@ -63471,6 +64096,7 @@ mount5(primaryCell?.chrome ?? document.createElement("div"), {
63471
64096
  parseMediaEntries,
63472
64097
  setCanvasMode
63473
64098
  });
64099
+ setDiffChromeRepaint(render7);
63474
64100
  mount4({
63475
64101
  isEditing,
63476
64102
  renderBlockActionBar
@@ -63552,11 +64178,8 @@ initWelcome({
63552
64178
  }
63553
64179
  },
63554
64180
  cloneRepository: () => cloneRepository2({ openRecentProject }),
63555
- openNewProject: async (options) => {
63556
- const result = await openNewProjectModal(options);
63557
- if (result) {
63558
- openRecentProject(result.root);
63559
- }
64181
+ openNewProject: (options) => {
64182
+ openNewProjectModal(options);
63560
64183
  },
63561
64184
  openProject: () => openProject(),
63562
64185
  openRecentProject: (root) => openRecentProject(root)
@@ -64084,5 +64707,5 @@ export {
64084
64707
  hasUnsavedTabs
64085
64708
  };
64086
64709
 
64087
- //# debugId=596158A5181569F864756E2164756E21
64710
+ //# debugId=17BBB037A446E70F64756E2164756E21
64088
64711
  //# sourceMappingURL=studio.js.map