@jxsuite/studio 5.2.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (398) hide show
  1. package/data/css-meta.json +28 -27
  2. package/dist/chunks/{cf-account-picker-h4dab9bq.js → cf-account-picker-3f08rry6.js} +21 -18
  3. package/dist/chunks/{cf-settings-adg74qsk.js → cf-settings-m79r7p5y.js} +4 -4
  4. package/dist/chunks/{components-5y78e05d.js → components-xm826zjw.js} +3 -3
  5. package/dist/chunks/{content-source-sgg1sv6n.js → content-source-jt7rbcbc.js} +22 -22
  6. package/dist/chunks/{convert-to-repeater-mf1cxgfn.js → convert-to-repeater-vg38bq99.js} +22 -22
  7. package/dist/chunks/{editor-3qhbg51x.js → editor-ddy019sn.js} +2 -2
  8. package/dist/chunks/entry-commands-64nb2a9a.js +42 -0
  9. package/dist/chunks/{extension-sections-c6zwzbre.js → extension-sections-r73an5s6.js} +31 -30
  10. package/dist/chunks/{file-ops-vrpq1jjk.js → file-ops-3xan8mnj.js} +21 -21
  11. package/dist/chunks/{files-msaf95y6.js → files-egsb0ry8.js} +30 -29
  12. package/dist/chunks/{format-host-f64fnesp.js → format-host-mym8qnd0.js} +3 -3
  13. package/dist/chunks/{fs-events-3y6y0nrm.js → fs-events-z2gg8qjt.js} +4 -4
  14. package/dist/chunks/{git-panel-bv5e22j0.js → git-panel-a05atk6q.js} +20 -20
  15. package/dist/chunks/{grid-view-1p6hsyjx.css → grid-view-3g1h2re8.css} +956 -139
  16. package/dist/chunks/{grid-view-paj67nr5.js → grid-view-qaqer9kr.js} +24 -24
  17. package/dist/chunks/{icons-yy10wk9r.js → icons-drxq9q10.js} +8 -2
  18. package/dist/chunks/{iframe-host-dh5np8vk.js → iframe-host-psetcv20.js} +11 -11
  19. package/dist/chunks/{index-n4j7qxyr.js → index-9yjae3hz.js} +4 -4
  20. package/dist/chunks/{javascript-4abzjcy9.css → javascript-bc9z461d.css} +688 -54
  21. package/dist/chunks/{javascript-xvrfrpkb.js → javascript-hr5sg57n.js} +2 -2
  22. package/dist/chunks/{jsonMode-vvmpmk34.css → jsonMode-qqd9nm1t.css} +1120 -303
  23. package/dist/chunks/{jsonMode-tyx2kj6p.js → jsonMode-zs88v0wr.js} +4 -4
  24. package/dist/chunks/{jx-validate-j0wxk72w.js → jx-validate-epp2z3s3.js} +2 -2
  25. package/dist/chunks/layers-panel-83a16ytx.js +41 -0
  26. package/dist/chunks/{layers-1vcfdbet.js → layers-s6mc01ry.js} +3 -3
  27. package/dist/chunks/{listbox-6v2ac08j.js → listbox-b7jxnhq1.js} +19 -3
  28. package/dist/chunks/{media-picker-p49nhqhs.js → media-picker-nhn6rv4w.js} +11 -11
  29. package/dist/chunks/{menu-qht48vnc.js → menu-8j4qxjrp.js} +4 -4
  30. package/dist/chunks/{monaco-binding-sw6en4dw.js → monaco-binding-e8d9wzyk.js} +3 -3
  31. package/dist/chunks/{monaco-buffer-pykwbm92.js → monaco-buffer-jaq1t9zj.js} +2 -2
  32. package/dist/chunks/{monaco-setup-8vbyqnhr.css → monaco-setup-ey9cjcm3.css} +954 -137
  33. package/dist/chunks/{monaco-setup-8r62fj6y.js → monaco-setup-gsjdysze.js} +7 -7
  34. package/dist/chunks/{platform-ts9cg1a4.js → platform-fnchmhy7.js} +5 -3
  35. package/dist/chunks/publish-panel-ak6hxfma.js +27 -0
  36. package/dist/chunks/quick-search-c63mkt3b.js +47 -0
  37. package/dist/chunks/{references-h69xp9zz.js → references-f7bdv3z5.js} +3 -3
  38. package/dist/chunks/right-panel-t5n8wttg.js +45 -0
  39. package/dist/chunks/{split-g7nm1rgb.js → split-4cz4b6ef.js} +14 -3
  40. package/dist/chunks/{studio-sar8g54p.js → studio-01269reb.js} +8 -8
  41. package/dist/chunks/{studio-rnyf3f8e.js → studio-0a4gncx0.js} +3 -3
  42. package/dist/chunks/{studio-e1f24eff.js → studio-0d4p4wxj.js} +6778 -2993
  43. package/dist/chunks/{studio-4wbb9ct0.js → studio-1tejxvba.js} +3006 -1855
  44. package/dist/chunks/{studio-p6pmwkvh.js → studio-3nprwbd9.js} +147 -28
  45. package/dist/chunks/{studio-4c041r8y.js → studio-40z39dme.js} +3 -3
  46. package/dist/chunks/{studio-68fydsrj.js → studio-482eeazk.js} +20 -20
  47. package/dist/chunks/{studio-y4f9fdge.js → studio-4998spet.js} +10 -9
  48. package/dist/chunks/{studio-nq5yf5mp.js → studio-557bmn0x.js} +2 -2
  49. package/dist/chunks/{studio-hdc2jt3z.js → studio-5w26wza5.js} +3 -3
  50. package/dist/chunks/{studio-6nzaktvy.js → studio-6pwn9j8c.js} +6 -3
  51. package/dist/chunks/{studio-wnsyffg1.js → studio-6vg0x5fz.js} +3 -2
  52. package/dist/chunks/{studio-vn5hske5.js → studio-7d3c1m59.js} +12 -5
  53. package/dist/chunks/{studio-28vs5k2y.js → studio-9vsvz7w7.js} +7 -7
  54. package/dist/chunks/{studio-t3adrr56.js → studio-9wm0xzpf.js} +19 -19
  55. package/dist/chunks/{studio-38emantv.js → studio-a5h5j33a.js} +19 -5
  56. package/dist/chunks/{studio-ympe0n7a.js → studio-ag3z6shb.js} +10 -4
  57. package/dist/chunks/studio-aj9drheq.js +177 -0
  58. package/dist/chunks/{studio-vt0tj32j.js → studio-apv9x70c.js} +189 -130
  59. package/dist/chunks/{studio-c04nzbby.js → studio-b9q3znz8.js} +2 -2
  60. package/dist/chunks/{studio-2mdbbc76.js → studio-bh47w0cy.js} +9 -10
  61. package/dist/chunks/{studio-ez11dmdh.js → studio-dgdm6vvt.js} +264 -52
  62. package/dist/chunks/{studio-5nz00bhh.js → studio-dwdcr2e8.js} +4 -4
  63. package/dist/chunks/{studio-nqgawfks.js → studio-fbcyz234.js} +146 -139
  64. package/dist/chunks/{studio-kcgff4r0.js → studio-fgmfksz8.js} +2 -2
  65. package/dist/chunks/{studio-x72j58vw.js → studio-fqpkpq1t.js} +7 -4
  66. package/dist/chunks/{studio-15f3kxqe.js → studio-fy320dfc.js} +62 -68
  67. package/dist/chunks/{studio-zjjde71a.js → studio-gjj4bbfx.js} +2 -2
  68. package/dist/chunks/{studio-66arbpc2.js → studio-jfhxy8h3.js} +16 -15
  69. package/dist/chunks/{studio-d0300636.js → studio-ks2zzth2.js} +17 -2
  70. package/dist/chunks/{studio-c07s5ckd.js → studio-kwewmhhq.js} +4 -4
  71. package/dist/chunks/{studio-01jeck2y.js → studio-m58xn8at.js} +26 -25
  72. package/dist/chunks/{studio-mpzwze9x.js → studio-mfkh1q8d.js} +17 -17
  73. package/dist/chunks/{studio-etvx9s2g.js → studio-mrh0a92f.js} +17 -17
  74. package/dist/chunks/{studio-qa7fzxan.js → studio-mt79tn56.js} +3 -3
  75. package/dist/chunks/{studio-hhskq2mr.js → studio-ne593z1j.js} +39 -32
  76. package/dist/chunks/{studio-h4nvnjvz.js → studio-ptfcxkz5.js} +418 -232
  77. package/dist/chunks/{studio-4xmjcrg3.js → studio-qxye31h7.js} +7 -7
  78. package/dist/chunks/{studio-a1pswps3.js → studio-rav5bpmp.js} +81 -59
  79. package/dist/chunks/{studio-ncn9e189.js → studio-rwmmbrth.js} +6 -6
  80. package/dist/chunks/{studio-en82c639.js → studio-sceg8ws0.js} +8 -8
  81. package/dist/chunks/{studio-wjpgxywa.js → studio-sjas5b3a.js} +19 -13
  82. package/dist/chunks/{studio-4r14yzyy.js → studio-vd67b66d.js} +3 -3
  83. package/dist/chunks/{studio-14d9tq4d.js → studio-w475cbf9.js} +10 -10
  84. package/dist/chunks/{studio-ctm7bhx5.js → studio-w7gpcvfj.js} +9 -9
  85. package/dist/chunks/{studio-9j6sfeba.js → studio-xrbfdxgk.js} +7 -7
  86. package/dist/chunks/{studio-x10fp4tp.js → studio-y49b6mjv.js} +35 -35
  87. package/dist/chunks/{studio-5t7ena5z.js → studio-ygvr9e4q.js} +10 -10
  88. package/dist/chunks/{studio-z2smx5ce.js → studio-yxmmjy2a.js} +9 -6
  89. package/dist/chunks/{tsMode-jy4m86rv.js → tsMode-ay4c78ne.js} +4 -4
  90. package/dist/chunks/{tsMode-hnxxb73h.css → tsMode-c72wyz27.css} +954 -137
  91. package/dist/chunks/{workspace-bnw54cha.js → workspace-b9k2frks.js} +5 -5
  92. package/dist/chunks/{ws-client-ny4zc51r.js → ws-client-erze4czt.js} +356 -20
  93. package/dist/codicon.ttf +0 -0
  94. package/dist/iframe-entry.js +20 -18
  95. package/dist/studio.css +956 -139
  96. package/dist/studio.js +1511 -851
  97. package/dist/workers/editor.worker.js +196 -107
  98. package/dist/workers/json.worker.js +196 -107
  99. package/dist/workers/ts.worker.js +196 -107
  100. package/package.json +18 -17
  101. package/src/about/about-modal.ts +4 -3
  102. package/src/browse/library-commands.ts +15 -13
  103. package/src/browse/library-pane.ts +2 -2
  104. package/src/canvas/canvas-render.ts +97 -83
  105. package/src/canvas/canvas-surface.ts +2 -2
  106. package/src/canvas/canvas-utils.ts +42 -32
  107. package/src/canvas/diff-toolbar.ts +21 -2
  108. package/src/canvas/diff-view.ts +30 -6
  109. package/src/canvas/iframe-entry.ts +2 -2
  110. package/src/canvas/iframe-host.ts +5 -5
  111. package/src/canvas/iframe-interaction.ts +7 -6
  112. package/src/canvas/iframe-overlay.ts +1 -1
  113. package/src/canvas/iframe-protocol.ts +6 -5
  114. package/src/canvas/iframe-render.ts +3 -3
  115. package/src/canvas/popover-state.ts +1 -1
  116. package/src/canvas/stage-scrollbar.ts +112 -0
  117. package/src/canvas/surface-registry.ts +9 -2
  118. package/src/collab/collab-commands.ts +8 -6
  119. package/src/collab/collab-session.ts +4 -4
  120. package/src/collab/collab-state.ts +4 -4
  121. package/src/collab/presence-chips.ts +3 -3
  122. package/src/commands/app-commands.ts +7 -7
  123. package/src/commands/budget.ts +17 -16
  124. package/src/commands/command-args.ts +10 -9
  125. package/src/commands/context.ts +20 -10
  126. package/src/commands/defaults.ts +30 -29
  127. package/src/commands/levels.ts +8 -6
  128. package/src/commands/live-context.ts +12 -10
  129. package/src/commands/reference.ts +5 -5
  130. package/src/commands/registry.ts +2 -2
  131. package/src/content/draft-state.ts +9 -8
  132. package/src/content/entry-commands.ts +16 -14
  133. package/src/content/entry-editor.ts +2 -2
  134. package/src/content/entry-fields.ts +1 -1
  135. package/src/editor/context-menu.ts +4 -4
  136. package/src/editor/convert-to-component.ts +6 -5
  137. package/src/editor/inline-edit.ts +3 -3
  138. package/src/editor/shortcuts.ts +88 -20
  139. package/src/editor/slash-menu.ts +11 -10
  140. package/src/files/elements.ts +4 -5
  141. package/src/files/file-ops.ts +1 -1
  142. package/src/files/files.ts +21 -17
  143. package/src/files/serialize-document.ts +1 -1
  144. package/src/format/convert-file.ts +5 -5
  145. package/src/github/github-auth-status.ts +107 -0
  146. package/src/github/github-auth.ts +18 -82
  147. package/src/github/github-publish.ts +7 -6
  148. package/src/grid/cell-editors.ts +8 -7
  149. package/src/grid/grid-layout.ts +1 -1
  150. package/src/grid/grid-open.ts +6 -5
  151. package/src/grid/grid-panel.ts +13 -12
  152. package/src/grid/redirects-grid.ts +14 -11
  153. package/src/grid/redirects.ts +10 -5
  154. package/src/grid/sources/content-source.ts +10 -9
  155. package/src/hosting/document.ts +28 -1
  156. package/src/hosting/stage.ts +15 -4
  157. package/src/i18n/i18n-commands.ts +9 -7
  158. package/src/new-project/location-fields.ts +16 -3
  159. package/src/new-project/new-project-modal.ts +23 -7
  160. package/src/page-params.ts +6 -6
  161. package/src/panels/activity-panel.ts +11 -10
  162. package/src/panels/ai-chat/chat-view.ts +27 -14
  163. package/src/panels/ai-chat/composer.ts +10 -3
  164. package/src/panels/ai-panel.ts +87 -29
  165. package/src/panels/block-action-bar.ts +59 -33
  166. package/src/panels/bottom-dock.ts +22 -22
  167. package/src/panels/chat-panel.ts +40 -5
  168. package/src/panels/data-explorer.ts +9 -9
  169. package/src/panels/data-grid.ts +1 -1
  170. package/src/panels/editors.ts +8 -8
  171. package/src/panels/elements-panel.ts +7 -8
  172. package/src/panels/empty-state.ts +1 -1
  173. package/src/panels/events-panel.ts +13 -12
  174. package/src/panels/formula-workspace.ts +8 -5
  175. package/src/panels/frontmatter-fields.ts +4 -1
  176. package/src/panels/frontmatter-panel.ts +18 -18
  177. package/src/panels/git-panel.ts +19 -13
  178. package/src/panels/head-panel.ts +9 -10
  179. package/src/panels/i18n-panel.ts +6 -4
  180. package/src/panels/imports-panel.ts +1 -1
  181. package/src/panels/jump-bar.ts +20 -18
  182. package/src/panels/layers-panel.ts +19 -17
  183. package/src/panels/left-panel.ts +3 -3
  184. package/src/panels/navigator-panels.ts +8 -8
  185. package/src/panels/overlays.ts +1 -1
  186. package/src/panels/pane-context.ts +56 -28
  187. package/src/panels/pane-grid.ts +9 -8
  188. package/src/panels/panel-registry.ts +47 -19
  189. package/src/panels/problems-panel.ts +11 -12
  190. package/src/panels/properties-panel.ts +21 -20
  191. package/src/panels/provenance.ts +3 -3
  192. package/src/panels/quick-search.ts +15 -11
  193. package/src/panels/right-panel.ts +4 -3
  194. package/src/panels/seo-modal.ts +5 -4
  195. package/src/panels/settings-menu.ts +9 -9
  196. package/src/panels/settings-pane.ts +3 -3
  197. package/src/panels/signals-panel.ts +16 -9
  198. package/src/panels/statement-editor.ts +12 -9
  199. package/src/panels/style-panel.ts +45 -41
  200. package/src/panels/stylebook-panel.ts +0 -1
  201. package/src/panels/tab-strip.ts +26 -23
  202. package/src/platform-errors.ts +2 -2
  203. package/src/platform.ts +44 -4
  204. package/src/platforms/cloud.ts +11 -1
  205. package/src/platforms/default-platform.ts +141 -4
  206. package/src/platforms/devserver.ts +23 -3
  207. package/src/publish/deploy-checklist.ts +11 -10
  208. package/src/publish/publish-commands.ts +9 -8
  209. package/src/services/a11y-report.ts +1 -1
  210. package/src/services/ai-ask.ts +39 -16
  211. package/src/services/ai-command-tools.ts +28 -14
  212. package/src/services/ai-import-tools.ts +43 -29
  213. package/src/services/ai-project-tools.ts +20 -38
  214. package/src/services/ai-settings.ts +55 -2
  215. package/src/services/ai-tools.ts +69 -58
  216. package/src/services/ai-write-report.ts +4 -4
  217. package/src/services/ai-writes.ts +74 -55
  218. package/src/services/automation.ts +12 -12
  219. package/src/services/clock.ts +2 -1
  220. package/src/services/connection.ts +2 -2
  221. package/src/services/context-manager.ts +69 -6
  222. package/src/services/directory-picker.ts +93 -27
  223. package/src/services/document-assistant.ts +86 -17
  224. package/src/services/gated-registry.ts +4 -3
  225. package/src/services/idle.ts +5 -4
  226. package/src/services/live-surfaces.ts +2 -1
  227. package/src/services/module-preloads.ts +67 -0
  228. package/src/services/notify.ts +9 -7
  229. package/src/services/popover-report.ts +3 -3
  230. package/src/services/preview-eval.ts +1 -1
  231. package/src/services/project-adoption.ts +34 -1
  232. package/src/services/project-path.ts +53 -0
  233. package/src/services/settings/kernel.ts +13 -2
  234. package/src/services/tool-executor.ts +135 -30
  235. package/src/services/tool-outcomes.ts +143 -0
  236. package/src/settings/contexts-section.ts +8 -8
  237. package/src/settings/contributed-section.ts +6 -6
  238. package/src/settings/css-vars-editor.ts +6 -5
  239. package/src/settings/extension-commands.ts +19 -18
  240. package/src/settings/extension-sections.ts +7 -7
  241. package/src/settings/extensions-section.ts +3 -3
  242. package/src/settings/general-settings.ts +21 -18
  243. package/src/settings/preferences-accounts.ts +3 -3
  244. package/src/settings/preferences-dialog.ts +9 -8
  245. package/src/settings/preferences-sections.ts +1 -1
  246. package/src/settings/project-sections.ts +2 -2
  247. package/src/settings/section-registry.ts +7 -6
  248. package/src/settings/settings-document.ts +41 -43
  249. package/src/shell/tree.ts +29 -0
  250. package/src/shell.ts +63 -55
  251. package/src/site-context.ts +1 -1
  252. package/src/store.ts +2 -2
  253. package/src/studio.ts +52 -28
  254. package/src/style/live-preview.ts +8 -8
  255. package/src/style/project-styles.ts +8 -9
  256. package/src/style/token-ref.ts +2 -2
  257. package/src/surfaces/about.json +6 -2
  258. package/src/surfaces/add-repo.json +2 -2
  259. package/src/surfaces/add-repo.ts +2 -2
  260. package/src/surfaces/ai-chat.json +14 -6
  261. package/src/surfaces/ai-chat.ts +4 -2
  262. package/src/surfaces/ai-credentials-form.json +53 -49
  263. package/src/surfaces/ai-credentials-form.ts +18 -4
  264. package/src/surfaces/ai-managed-connect.json +4 -4
  265. package/src/surfaces/block-action-bar.json +4 -4
  266. package/src/surfaces/block-action-bar.ts +3 -3
  267. package/src/surfaces/boot-failure.json +228 -0
  268. package/src/surfaces/boot-failure.ts +156 -0
  269. package/src/surfaces/bottom-dock.json +1 -1
  270. package/src/surfaces/canvas-stage.json +9 -40
  271. package/src/surfaces/canvas-stage.ts +9 -9
  272. package/src/surfaces/cf-account-picker.json +11 -8
  273. package/src/surfaces/commandbar.ts +11 -10
  274. package/src/surfaces/convert-repeater.ts +3 -2
  275. package/src/surfaces/dialog.json +7 -1
  276. package/src/surfaces/dialog.ts +2 -2
  277. package/src/surfaces/doc-header.json +6 -13
  278. package/src/surfaces/doc-header.ts +4 -5
  279. package/src/surfaces/entry-editor.json +1 -1
  280. package/src/surfaces/entry-editor.ts +2 -1
  281. package/src/surfaces/expression-editor.json +5 -5
  282. package/src/surfaces/files-panel.json +4 -3
  283. package/src/surfaces/files-panel.ts +1 -1
  284. package/src/surfaces/formula-palette.json +10 -9
  285. package/src/surfaces/formula-palette.ts +11 -7
  286. package/src/surfaces/git-panel.json +167 -109
  287. package/src/surfaces/git-panel.ts +2 -2
  288. package/src/surfaces/github-auth.json +2 -1
  289. package/src/surfaces/github-publish.ts +3 -3
  290. package/src/surfaces/grid-open.json +1 -1
  291. package/src/surfaces/grid-panel.json +2 -2
  292. package/src/surfaces/grid-views.json +1 -1
  293. package/src/surfaces/inspector-dock.json +2 -2
  294. package/src/surfaces/jump-bar.json +1 -1
  295. package/src/surfaces/jump-bar.ts +1 -1
  296. package/src/surfaces/library-pane.ts +2 -1
  297. package/src/surfaces/logic-panel.json +7 -7
  298. package/src/surfaces/logic-panel.ts +6 -4
  299. package/src/surfaces/logic-workspace.json +2 -2
  300. package/src/surfaces/logic-workspace.ts +2 -2
  301. package/src/surfaces/media-browser.ts +3 -3
  302. package/src/surfaces/media-pane.ts +2 -1
  303. package/src/surfaces/menu.ts +5 -5
  304. package/src/surfaces/new-project.json +1 -1
  305. package/src/surfaces/palette.json +8 -7
  306. package/src/surfaces/pane-context.json +86 -57
  307. package/src/surfaces/pane-context.ts +2 -1
  308. package/src/surfaces/pane-grid.json +3 -3
  309. package/src/surfaces/panel-activity.json +2 -1
  310. package/src/surfaces/panel-deploy-checklist.json +1 -1
  311. package/src/surfaces/panel-elements.ts +2 -1
  312. package/src/surfaces/panel-i18n.ts +2 -1
  313. package/src/surfaces/panel-outline.json +1 -1
  314. package/src/surfaces/panel-outline.ts +4 -3
  315. package/src/surfaces/panel-page.json +2 -2
  316. package/src/surfaces/panel-page.ts +4 -1
  317. package/src/surfaces/panel-problems.json +4 -3
  318. package/src/surfaces/panel-problems.ts +2 -2
  319. package/src/surfaces/panel-signals.json +23 -8
  320. package/src/surfaces/panel-signals.ts +5 -1
  321. package/src/surfaces/panel-stylebook-layers.json +1 -1
  322. package/src/surfaces/panel-stylebook-layers.ts +2 -2
  323. package/src/surfaces/preferences.json +21 -8
  324. package/src/surfaces/preferences.ts +9 -1
  325. package/src/surfaces/properties-panel.json +4 -4
  326. package/src/surfaces/properties-panel.ts +2 -2
  327. package/src/surfaces/publish.json +1 -1
  328. package/src/surfaces/publish.ts +2 -1
  329. package/src/surfaces/push-plan.json +4 -3
  330. package/src/surfaces/rail.json +2 -2
  331. package/src/surfaces/rail.ts +10 -4
  332. package/src/surfaces/reference-field.json +1 -1
  333. package/src/surfaces/schema-form.json +2 -2
  334. package/src/surfaces/schema-form.ts +1 -1
  335. package/src/surfaces/seo.json +1 -1
  336. package/src/surfaces/seo.ts +2 -2
  337. package/src/surfaces/settings-contributed.ts +2 -1
  338. package/src/surfaces/settings-css-vars.json +4 -4
  339. package/src/surfaces/settings-deploy.json +4 -4
  340. package/src/surfaces/settings-deploy.ts +2 -1
  341. package/src/surfaces/settings-extensions.json +6 -6
  342. package/src/surfaces/settings-extensions.ts +6 -2
  343. package/src/surfaces/settings-locales.json +4 -4
  344. package/src/surfaces/settings-overview.ts +2 -1
  345. package/src/surfaces/settings-pane.ts +3 -2
  346. package/src/surfaces/settings-rawjson.ts +1 -1
  347. package/src/surfaces/slash-menu.ts +4 -4
  348. package/src/surfaces/statements.json +3 -3
  349. package/src/surfaces/statusbar.ts +23 -23
  350. package/src/surfaces/style-panel.json +9 -5
  351. package/src/surfaces/style-panel.ts +3 -3
  352. package/src/surfaces/tab-strip.json +2 -2
  353. package/src/surfaces/target-line.json +1 -1
  354. package/src/surfaces/target-line.ts +12 -10
  355. package/src/surfaces/welcome.json +7 -2
  356. package/src/tabs/doc-op-apply.ts +5 -5
  357. package/src/tabs/patch-ops.ts +3 -3
  358. package/src/tabs/project-config.ts +10 -10
  359. package/src/tabs/selection.ts +1 -1
  360. package/src/tabs/tab.ts +13 -14
  361. package/src/tabs/transact.ts +18 -24
  362. package/src/types.ts +27 -3
  363. package/src/ui/ai-credentials-form.ts +74 -16
  364. package/src/ui/ai-managed-connect.ts +68 -19
  365. package/src/ui/color-selector.ts +1 -1
  366. package/src/ui/expression-editor.ts +3 -3
  367. package/src/ui/form-controls.ts +6 -5
  368. package/src/ui/formula-catalog.ts +4 -4
  369. package/src/ui/formula-chips.ts +2 -2
  370. package/src/ui/layers.ts +20 -18
  371. package/src/ui/panel-resize.ts +8 -28
  372. package/src/ui/popover-surface.ts +2 -2
  373. package/src/ui/progress-modal.ts +3 -3
  374. package/src/ui/regions.ts +14 -9
  375. package/src/ui/schema-form.ts +10 -9
  376. package/src/ui/timing.ts +9 -7
  377. package/src/ui/value-source.ts +4 -4
  378. package/src/ui/virtual-window.ts +58 -5
  379. package/src/utils/base64.ts +20 -1
  380. package/src/utils/canvas-media.ts +1 -1
  381. package/src/utils/studio-utils.ts +0 -31
  382. package/src/view.ts +4 -3
  383. package/src/workspace/pane-derive.ts +14 -13
  384. package/src/workspace/session.ts +7 -6
  385. package/src/workspace/workspace.ts +49 -39
  386. package/styles/canvas.css +1 -27
  387. package/styles/inspector.css +4 -3
  388. package/styles/overlays.css +1 -1
  389. package/styles/tokens.css +17 -1
  390. package/styles/tokens.json +6 -1
  391. package/dist/chunks/entry-commands-w9qsxa6q.js +0 -42
  392. package/dist/chunks/layers-panel-fksbdg4q.js +0 -40
  393. package/dist/chunks/publish-panel-vz23wwmg.js +0 -27
  394. package/dist/chunks/quick-search-e9rp74m8.js +0 -46
  395. package/dist/chunks/right-panel-j97t2yvy.js +0 -45
  396. package/dist/chunks/studio-727xh0e3.js +0 -76
  397. package/src/files/json-layout.ts +0 -474
  398. package/src/services/ai-turn-signal.ts +0 -70
@@ -6,9 +6,9 @@
6
6
  * exactly three members (spec studio.md §13.5):
7
7
  *
8
8
  * - `run(id, args)` — **is** `registry.run(id, args)`, behind {@link isScriptable}. There is no
9
- * second action table. A hand-maintained parallel list of what the app can do is what plan §2
10
- * principle 1 defines as a defect, and this module used to be 39 entries of exactly that, 16 of
11
- * them holding raw XPath into the shell and three of them matching RENDERED TEXT.
9
+ * second action table. A hand-maintained parallel list of what the app can do is what §13 defines
10
+ * as a defect, and this module used to be 39 entries of exactly that, 16 of them holding raw
11
+ * XPath into the shell and three of them matching RENDERED TEXT.
12
12
  * - `seed(id, args)` — the {@link SEEDS} registry, under the **Remote Rule**: a seed may only write
13
13
  * state whose real writer is a network or IPC boundary. It stands in for a remote, never for a
14
14
  * user. {@link REFUSED_SEEDS} names what that excludes and why.
@@ -16,7 +16,7 @@
16
16
  * `state()` (the full `CommandContext`), `commands()`, `seeds()`, and `pointAt`/`revealPath`,
17
17
  * which compose the app's own transforms and answer in top-document coordinates.
18
18
  *
19
- * **What is deliberately absent**, each a normative refusal in spec §13.5:
19
+ * **What is deliberately absent**, each a normative refusal in studio.md §13.5:
20
20
  *
21
21
  * 1. No method that takes a selector. If a script cannot say it in command ids and `JxPath`s, it
22
22
  * cannot say it here.
@@ -97,7 +97,7 @@ export function isScriptable(command: AnyCommand): boolean {
97
97
  *
98
98
  * - `command` — a thing a user does, so it becomes a registry record. The value names who lands it.
99
99
  * - `seed` — a remote's state, addressed through {@link AutomationApi.seed}.
100
- * - `refused` — §13.3 says the app will never provide it; the manifest step is deleted.
100
+ * - `refused` — §13.5 says the app will never provide it; the manifest step is deleted.
101
101
  */
102
102
  export type GapDisposition = "command" | "seed" | "refused";
103
103
 
@@ -118,8 +118,8 @@ export interface ManifestId {
118
118
  * — only to explain, when the registry refuses an id, which of the three fates that id has.
119
119
  *
120
120
  * This is a countdown, in the idiom of the checker's own `TOGGLE_DEBT`: it may only shrink. It went
121
- * 39 → 8 when the manifest converted (S2), and 8 → 7 when `element.insertData` became `insert.data`
122
- * (P5, `canvas/canvas-render.ts`). What left: every `seed` entry, now read off {@link seedIds} and
121
+ * 39 → 8 when the manifest converted, and 8 → 7 when `element.insertData` became `insert.data`
122
+ * (`canvas/canvas-render.ts`). What left: every `seed` entry, now read off {@link seedIds} and
123
123
  * answered from the live registry rather than from a hand-kept list; every `toggle*` entry, which
124
124
  * {@link TOGGLE_ID} refuses before this map is ever consulted; and every `command` entry whose
125
125
  * record has since landed.
@@ -127,7 +127,7 @@ export interface ManifestId {
127
127
  * What is left is two kinds. Two `command` entries are the manifest's real registry gaps — each
128
128
  * reached today by an `input` step clicking a hand-stamped region, and each of those steps carries
129
129
  * an `unstable` hatch naming the phase that lands the record, so this map and that hatch count are
130
- * the same debt. Five `refused` entries are NOT a countdown: they are §13.3's normative refusals,
130
+ * the same debt. Five `refused` entries are NOT a countdown: they are §13.5's normative refusals,
131
131
  * and they stay so that a caller reaching for one gets the reason instead of "unknown command".
132
132
  */
133
133
  export const AUTOMATION_COMMANDS: Readonly<Record<string, ManifestId>> = {
@@ -137,9 +137,9 @@ export const AUTOMATION_COMMANDS: Readonly<Record<string, ManifestId>> = {
137
137
  },
138
138
  "layers.contextMenu": {
139
139
  disposition: "refused",
140
- note: "matched RENDERED TEXT, which §13's R1 forbids; P5.5 stamps data-jx-path on rows",
140
+ note: "matched RENDERED TEXT, which R1 of scripts/screenshots/README.md forbids; a row is addressed by its data-jx-path",
141
141
  },
142
- "media.browse": { disposition: "command", note: "P7.5 — media.browse" },
142
+ "media.browse": { disposition: "command", note: "media.browse, site-architecture.md §9.4" },
143
143
  "project.showWelcome": {
144
144
  disposition: "refused",
145
145
  note: "cold start is a startup profile (?profile=fresh), not an action",
@@ -176,7 +176,7 @@ export const REFUSED_SEEDS: Readonly<Record<string, string>> = {
176
176
  setActivity: 'a user picks a Navigator panel — run("navigator.showPanel", { panel })',
177
177
  setRightTab: 'a user picks an Inspector tab — run("inspector.setTab", { tab })',
178
178
  setStatus:
179
- "the status bar reports what the app just did; staging it is the one lie §13.3 names outright",
179
+ "the status bar reports what the app just did; staging it is the one lie the Remote Rule (studio.md §13.5) names outright",
180
180
  setZoom: 'a user zooms — run("canvas.setZoom", { zoom })',
181
181
  };
182
182
 
@@ -315,7 +315,7 @@ export interface ScriptableCommand {
315
315
  args?: object;
316
316
  }
317
317
 
318
- /** What `pointAt` can address. Region ids join `path` when P3.8's region registry lands. */
318
+ /** What `pointAt` can address. Region ids join `path` when a region registry lands. */
319
319
  export interface AutomationTarget {
320
320
  path: JxPath;
321
321
  }
@@ -8,7 +8,8 @@
8
8
  * 1. **They were untestable.** A formatter that reads the wall clock can only be asserted against an
9
9
  * offset from the real present, so "yesterday" and "over a year ago" were never covered.
10
10
  * 2. **They drift.** Two captures of the same state minutes apart legitimately read `01:59 PM` and
11
- * `02:04 PM` (UX-REDESIGN-PLAN §13.4 measured exactly that on the git-panel shot).
11
+ * `02:04 PM` (the shot contract's Determinism section measured exactly that on the git-panel
12
+ * shot).
12
13
  *
13
14
  * This is deliberately NOT a global `Date.now` override. Monaco, every `setTimeout`, the collab
14
15
  * clock and the transaction log all read the real clock and must keep reading it — pinning time for
@@ -11,8 +11,8 @@
11
11
  * The transport now rejects those calls, and this is the other half: a person needs to be told
12
12
  * which of "it is broken" and "it is coming back" they are in. One keyed problem so a flapping
13
13
  * connection cannot stack up a column of identical rows, dismissed on recovery so a fixed thing
14
- * does not stay on the Problems list (§16.1's third tier, and §16.6's rule that a record is retired
15
- * by whoever owns its life).
14
+ * does not stay on the Problems list (§16.1's second and third rules: a record is retired when what
15
+ * it names is fixed, and a repeat is not a new row).
16
16
  *
17
17
  * A platform without `subscribeConnection` — the dev server, electrobun — installs nothing. There
18
18
  * is no connection to lose there, and a notice about one would be a lie.
@@ -2,7 +2,7 @@
2
2
  * Context-manager.js — Token budget management for the AI assistant
3
3
  *
4
4
  * Estimates token usage and trims conversation history before each send to keep the
5
- * context window within safe limits (ADR docs/ai-assistant-decision.md §11.4).
5
+ * context window within safe limits (specs/ai.md §2 and §2.1).
6
6
  *
7
7
  * Strategy (MVP): keep the system prompt + the most recent messages, dropping the oldest
8
8
  * messages when the estimated total exceeds the configured token budget. Dropped messages
@@ -30,7 +30,8 @@ const CHARS_PER_TOKEN = 4;
30
30
 
31
31
  /**
32
32
  * Approximate context windows (in tokens) per model id, longest-prefix matched. Used to derive a
33
- * model-aware budget instead of a flat cap (ADR §14.2). Conservative fallback for unknown models.
33
+ * model-aware budget instead of a flat cap (specs/ai.md §2.1). Conservative fallback for unknown
34
+ * models.
34
35
  */
35
36
  const MODEL_CONTEXT_WINDOWS: [string, number][] = [
36
37
  ["gpt-4o", 128_000],
@@ -123,6 +124,64 @@ function estimateTokens(input: string | MessageArrayEntry[]): number {
123
124
  return total;
124
125
  }
125
126
 
127
+ /** The wire-shaped entry {@link estimateTokens} reads, for one transcript message. */
128
+ function wireEntry(msg: ReturnType<typeof createChatState>["messages"][number]): MessageArrayEntry {
129
+ const entry: MessageArrayEntry = { content: msg.content, role: msg.role };
130
+ if (msg.toolCalls) {
131
+ entry.tool_calls = msg.toolCalls.map((tc) => ({
132
+ function: { arguments: tc.arguments, name: tc.name },
133
+ }));
134
+ }
135
+ return entry;
136
+ }
137
+
138
+ /**
139
+ * The context's size in tokens, as close to the provider's own count as the transcript allows.
140
+ *
141
+ * When the provider reported a count (`chatState.usage`) for a transcript this one still extends
142
+ * (the message at the count's boundary is the one it was taken at), that count covers everything up
143
+ * to it exactly, tool schemas included, so only the messages added since are estimated, plus
144
+ * whatever the system prompt has grown by. Without one, or once the transcript has been trimmed,
145
+ * rewound or repaired beneath it, the whole history is estimated at four characters per token,
146
+ * which is what every budget used before the count was forwarded.
147
+ */
148
+ function contextTokens(
149
+ chatState: ReturnType<typeof createChatState>,
150
+ systemTokens: number,
151
+ ): number {
152
+ const { usage, messages } = chatState;
153
+ const boundary = usage ? messages[usage.messageCount - 1] : undefined;
154
+ if (usage && boundary && boundary.id === usage.lastMessageId) {
155
+ /* Only what will be SENT: `toMessagesArray` drops an assistant turn carrying neither text nor
156
+ tool calls — the placeholder `sendMessage` pushes is always one — so it is not counted. */
157
+ const since = messages
158
+ .slice(usage.messageCount)
159
+ .filter((msg) => msg.role !== "assistant" || msg.content || (msg.toolCalls?.length ?? 0) > 0)
160
+ .map((msg) => wireEntry(msg));
161
+ /* The system prompt is rebuilt on every send from live state (the document outline, the file
162
+ inventory), so the one the count covered is not the one about to go out. What it has grown
163
+ or shrunk by is added, estimated the same way on both sides. */
164
+ const promptDrift = usage.systemTokens === undefined ? 0 : systemTokens - usage.systemTokens;
165
+ const reused = Math.max(0, usage.contextTokens + promptDrift) + estimateTokens(since);
166
+ // A count this module cannot trust is no count at all.
167
+ if (Number.isFinite(reused)) {
168
+ return reused;
169
+ }
170
+ }
171
+ return systemTokens + estimateTokens(chatState.toMessagesArray() as MessageArrayEntry[]);
172
+ }
173
+
174
+ /**
175
+ * The estimate of a system prompt, in the same units {@link trimContext} budgets in — what the loop
176
+ * records beside a provider's count so the next budget can measure the prompt's drift.
177
+ *
178
+ * @param {string} systemPrompt
179
+ * @returns {number}
180
+ */
181
+ export function estimatePromptTokens(systemPrompt: string): number {
182
+ return estimateTokens(systemPrompt);
183
+ }
184
+
126
185
  // ─── Trim ───────────────────────────────────────────────────────────────────
127
186
 
128
187
  /**
@@ -146,8 +205,7 @@ export function trimContext(
146
205
 
147
206
  const systemTokens = estimateTokens(systemPrompt);
148
207
  const allMessages = chatState.messages;
149
- const messageTokens = estimateTokens(chatState.toMessagesArray());
150
- const total = systemTokens + messageTokens;
208
+ const total = contextTokens(chatState, systemTokens);
151
209
 
152
210
  if (total <= maxTokens) {
153
211
  chatState.setTokenCount(total);
@@ -194,6 +252,8 @@ export function trimContext(
194
252
 
195
253
  const droppedCount = keepFrom;
196
254
  allMessages.splice(0, keepFrom);
255
+ // The provider counted a transcript that no longer exists; the estimate is all there is now.
256
+ chatState.clearUsage();
197
257
 
198
258
  // Insert a summary note so the model knows context was trimmed.
199
259
  allMessages.unshift({
@@ -212,8 +272,11 @@ export function trimContext(
212
272
 
213
273
  // ─── Orphaned tool calls ────────────────────────────────────────────────────
214
274
 
215
- /** What a synthesized result says, so a reader and the model see the same reason. */
216
- const UNANSWERED_TOOL_RESULT = JSON.stringify({
275
+ /**
276
+ * What a synthesized result says, so a reader and the model see the same reason. Exported so a
277
+ * restore can tell a seal from a reply (services/tool-outcomes.ts).
278
+ */
279
+ export const UNANSWERED_TOOL_RESULT = JSON.stringify({
217
280
  success: false,
218
281
  error:
219
282
  "This tool call was never completed — the session was reloaded or the history was trimmed.",
@@ -12,9 +12,17 @@
12
12
  * drop a hidden {@link LOCATION_ID_FILE} holding a freshly generated id and ask the backend which
13
13
  * directory holds that exact id. A fixed filename with the identity in its _contents_ is what makes
14
14
  * the match unambiguous: two folders may share a basename, and a stale file from a crashed session
15
- * may still be lying around, but only one file anywhere holds this id. The backend deletes the file
16
- * as soon as it matches, and this module removes it too, so neither a lost tab nor a failed lookup
17
- * leaves anything behind in the user's new project folder.
15
+ * may still be lying around, but only one file anywhere holds this id. The file lives in the chosen
16
+ * **Location** (the parent the project will be created in), never in the new project folder. The
17
+ * backend deletes it as soon as it matches, and this module removes it on every path the page
18
+ * survives, including a write that failed after the empty file was created. Only a tab closed
19
+ * mid-lookup can leave one behind, and a stale id can never match a later pick.
20
+ *
21
+ * **`null` means the user dismissed the chooser, and nothing else does.** Every other failure
22
+ * rejects with a sentence fit to show under the Location field: the chooser could not open, the
23
+ * folder cannot be written, the backend could not be reached, or it found no folder holding the id.
24
+ * This used to resolve `null` for all of them, which made a broken lookup indistinguishable from a
25
+ * cancel, and Browse… a button that silently did nothing.
18
26
  *
19
27
  * Only the plain dev-server browser session needs this. Every packaged build has a real native
20
28
  * dialog that returns a path directly and keeps it — electrobun's `Utils.openFileDialog`, and the
@@ -24,6 +32,7 @@
24
32
  */
25
33
 
26
34
  import { LOCATION_ID_FILE } from "@jxsuite/protocol/routes";
35
+ import { errorMessage } from "@jxsuite/schema/parse";
27
36
 
28
37
  /** Re-exported so a caller wiring up `locate` need not also reach into the protocol package. */
29
38
  export { LOCATION_ID_FILE } from "@jxsuite/protocol/routes";
@@ -62,54 +71,111 @@ function newLocationId(): string {
62
71
  return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
63
72
  }
64
73
 
74
+ /**
75
+ * A thrown value's `name`, read structurally. A DOMException from another realm (an iframe's
76
+ * `showDirectoryPicker`, a test double) is not `instanceof Error` here, but it still says what it
77
+ * is.
78
+ */
79
+ function nameOf(error: unknown): string {
80
+ return typeof error === "object" &&
81
+ error !== null &&
82
+ "name" in error &&
83
+ typeof error.name === "string"
84
+ ? error.name
85
+ : "";
86
+ }
87
+
65
88
  /**
66
89
  * Open the browser's native folder chooser and resolve the chosen folder's absolute path.
67
90
  *
68
- * Resolves `null` when the user cancels, when the API is unavailable, or when the backend cannot
69
- * place the folder (e.g. it lives outside the home directory the backend scans) — all three are "no
70
- * destination chosen", which the caller already handles by leaving the Location field alone.
91
+ * Resolves `null` only when the user dismisses the chooser (an `AbortError`, which is also what a
92
+ * declined readwrite grant reaches the page as: a user choice either way). Everything else rejects
93
+ * with an Error whose message is a plain sentence the caller can show as is:
94
+ *
95
+ * - No `showDirectoryPicker` at all (the dev-server adapter never offers Browse… then, so only an
96
+ * embedder wiring this helper by hand reaches it);
97
+ * - The chooser refused to open (a `SecurityError` included: this runs synchronously from the click,
98
+ * so a lost gesture means the environment is wrong, and silence would be a dead button);
99
+ * - The folder cannot be written, so it cannot be tagged, and a project could not be created there
100
+ * either;
101
+ * - `locate` threw (backend unreachable, non-ok), or answered `null` (no folder holds the id).
71
102
  *
72
103
  * MUST be called synchronously from a user gesture; `showDirectoryPicker()` is invoked before this
73
104
  * function awaits anything so a click handler that calls it directly keeps the gesture.
74
105
  *
75
- * @param locate Ask the backend which directory holds `id`, given the handle's `name`.
106
+ * @param locate Ask the backend which directory holds `id`, given the handle's `name`. Resolve the
107
+ * path, resolve `null` for "no directory holds this id", and throw on a transport or backend
108
+ * failure. The not-found sentence assumes a backend that searches the home folder, as the dev
109
+ * server does.
76
110
  */
77
111
  export async function pickDirectoryPath(
78
112
  locate: (query: { name: string; id: string }) => Promise<string | null>,
79
113
  ): Promise<string | null> {
80
114
  const show = (globalThis as { showDirectoryPicker?: ShowDirectoryPicker }).showDirectoryPicker;
81
115
  if (typeof show !== "function") {
82
- return null;
116
+ throw new TypeError(
117
+ "This browser has no folder chooser. Type the folder's path into Location instead.",
118
+ );
83
119
  }
84
120
 
85
121
  let handle: DirectoryHandle;
86
122
  try {
87
123
  // `id` makes Chrome reopen at the last folder chosen for this purpose across sessions.
88
124
  handle = await show({ id: "jx-new-project-location", mode: "readwrite", startIn: "documents" });
89
- } catch {
90
- // AbortError on cancel; a SecurityError if the gesture was lost. Neither is worth surfacing.
91
- return null;
125
+ } catch (error) {
126
+ if (nameOf(error) === "AbortError") {
127
+ return null;
128
+ }
129
+ throw new Error(
130
+ `The folder chooser could not open (${errorMessage(error)}). Type the folder's path into Location instead.`,
131
+ { cause: error },
132
+ );
92
133
  }
93
134
 
135
+ const { name } = handle;
94
136
  const id = newLocationId();
137
+ /* The `getFileHandle({ create: true })` call puts an empty file on disk before anything is
138
+ written, so a write that fails after it still leaves a tag to clean up. Only remove what this
139
+ call created. */
140
+ let tagged = false;
95
141
  try {
96
- const file = await handle.getFileHandle(LOCATION_ID_FILE, { create: true });
97
- const writable = await file.createWritable();
98
- await writable.write(id);
99
- await writable.close();
100
- } catch {
101
- // Read-only grant or a folder we cannot write: not a usable project destination anyway.
102
- return null;
103
- }
104
- try {
105
- return await locate({ id, name: handle.name });
106
- } catch {
107
- return null;
142
+ try {
143
+ const file = await handle.getFileHandle(LOCATION_ID_FILE, { create: true });
144
+ tagged = true;
145
+ const writable = await file.createWritable();
146
+ await writable.write(id);
147
+ await writable.close();
148
+ } catch (error) {
149
+ /* The readwrite grant was just issued, so this is the filesystem refusing: a read-only
150
+ mount, OS permissions, a full disk. Creating the project there would fail the same way. */
151
+ throw new Error(
152
+ `Jx Studio can't write to "${name}" (${errorMessage(error)}), so it can't create a project there. Choose a folder you can write to.`,
153
+ { cause: error },
154
+ );
155
+ }
156
+
157
+ let path: string | null;
158
+ try {
159
+ path = await locate({ id, name });
160
+ } catch (error) {
161
+ throw new Error(
162
+ `Jx Studio could not look up where "${name}" is: ${errorMessage(error)}. Type the folder's path into Location instead.`,
163
+ { cause: error },
164
+ );
165
+ }
166
+ if (!path) {
167
+ throw new Error(
168
+ `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.`,
169
+ );
170
+ }
171
+ return path;
108
172
  } finally {
109
173
  // The backend removes the file the moment it matches; this covers every path where it did not
110
- // (no match, a network failure, a backend that never ran the lookup).
111
- await handle.removeEntry(LOCATION_ID_FILE).catch(() => {
112
- /* Already gone — the backend beat us to it. */
113
- });
174
+ // (no match, a network failure, a backend that never ran the lookup, a half-finished write).
175
+ if (tagged) {
176
+ await handle.removeEntry(LOCATION_ID_FILE).catch(() => {
177
+ /* Already gone — the backend beat us to it. */
178
+ });
179
+ }
114
180
  }
115
181
  }
@@ -4,16 +4,17 @@
4
4
  *
5
5
  * Wires the @jxsuite/ai infrastructure (chat-state, proxy streaming client, tool registry) to
6
6
  * the active Jx document via `transactDoc()`-backed tools, and drives the error-correction
7
- * agent loop. See docs/ai-assistant-decision.md.
7
+ * agent loop. See specs/ai.md §3.
8
8
  *
9
9
  * @license MIT
10
10
  */
11
11
 
12
12
  import { createChatState, createProxyStreamingClient, createToolRegistry } from "@jxsuite/ai";
13
+ import { createSessionFacts } from "@jxsuite/ai/tools";
13
14
  import type { ProjectConfig } from "@jxsuite/schema/types";
14
15
  import { getPlatform } from "../platform";
15
16
  import { activeTab, workspace } from "../workspace/workspace";
16
- import { toRaw } from "../reactivity";
17
+ import { shallowRef, toRaw } from "../reactivity";
17
18
  import { projectState } from "../store";
18
19
  import { adoptProjectConfig } from "../tabs/project-config";
19
20
  import type { Tab } from "../tabs/tab";
@@ -27,10 +28,11 @@ import {
27
28
  createCommandToolRegistry,
28
29
  } from "./ai-command-tools";
29
30
  import { registerProjectTools } from "./ai-project-tools";
30
- import { registerImportTools, resetImportGuard } from "./ai-import-tools";
31
+ import { registerImportTools } from "./ai-import-tools";
31
32
  import { createGatedToolRegistry } from "./gated-registry";
32
33
  import type { ToolAvailability } from "./gated-registry";
33
34
  import { adoptProject } from "./project-adoption";
35
+ import { resetAiWrites } from "./ai-writes";
34
36
  import { abortImportRun, resetImportRuns } from "./import-run";
35
37
  import { runAgentLoop } from "./tool-executor";
36
38
  import { AI_TOOL_TIERS, buildSystemPrompt, toolActive } from "./ai-system-prompt";
@@ -43,10 +45,12 @@ import { validateDoc } from "./jx-validate";
43
45
  import { reloadFileInTab } from "../files/files";
44
46
  import { getExtensionCatalog, refreshExtensionUi } from "../format/format-host";
45
47
  import * as sessionStore from "./ai-session-store";
48
+ import { backfillToolResults, stripToolResults } from "./tool-outcomes";
46
49
 
47
50
  /**
48
- * Project root scoping the session store (ADR §11.5 / §14.2 — conversations don't bleed across
49
- * projects). Falls back to a shared unscoped store when no project is open.
51
+ * Project root scoping the session store, so conversations don't bleed across projects (the history
52
+ * section of docs/studio/ai/chat.md). Falls back to a shared unscoped store when no project is
53
+ * open.
50
54
  *
51
55
  * @returns {string}
52
56
  */
@@ -73,6 +77,9 @@ export function createDocumentAssistant() {
73
77
 
74
78
  /** The persisted session backing the live chat; null = fresh unsaved chat. */
75
79
  let sessionId: string | null = null;
80
+ /* What the conversation's tools remember across its turns (an import already ran): every call's
81
+ `ctx.session`. New Chat starts over; a new session id keeps what the conversation knew. */
82
+ let sessionFacts = createSessionFacts();
76
83
 
77
84
  const getProjectStyle = () =>
78
85
  (workspace.projectConfig as ProjectConfig | null)?.style as Record<string, string> | undefined;
@@ -88,7 +95,10 @@ export function createDocumentAssistant() {
88
95
 
89
96
  const innerRegistry = createToolRegistry();
90
97
  // Ungated: a question is not a function of what happens to be open.
91
- registerAskTool(innerRegistry);
98
+ /* A question suspends the turn on the author, for as long as they like, and the conversation is
99
+ otherwise saved only when a turn starts and ends. Saving here is what lets a reload find the
100
+ question to restore as open rather than losing the whole unfinished turn. */
101
+ registerAskTool(innerRegistry, { onPending: () => persistChat() });
92
102
  registerAiTools(innerRegistry, {
93
103
  getTab: () => activeTab.value,
94
104
  saveFile: async (relPath: string, content: string) => {
@@ -222,6 +232,19 @@ export function createDocumentAssistant() {
222
232
 
223
233
  let controller: AbortController | null = null;
224
234
 
235
+ /**
236
+ * Whether a turn is in flight: true from the moment a send is accepted until its loop has ended.
237
+ *
238
+ * Not the chat's `status`, which belongs to the token stream: it reads idle while a round's tools
239
+ * run, while a question waits on the author and during an import. Guarding on it let a second
240
+ * send start a second turn beside the first, and let the composer offer Send in the middle of
241
+ * one. One window runs one turn, and this is the fact that says whether it is running.
242
+ */
243
+ const turnActive = shallowRef(false);
244
+
245
+ /** Settles when the turn in flight has ended, finally included; already settled when none is. */
246
+ let turnEnded: Promise<void> = Promise.resolve();
247
+
225
248
  function buildPrompt() {
226
249
  const tab = activeTab.value;
227
250
  const inventory = projectState
@@ -264,7 +287,7 @@ export function createDocumentAssistant() {
264
287
  }
265
288
 
266
289
  async function sendMessage(text: string) {
267
- if (!text.trim() || chatState.status === "streaming") {
290
+ if (!text.trim() || turnActive.value || chatState.status === "streaming") {
268
291
  return;
269
292
  }
270
293
 
@@ -290,14 +313,25 @@ export function createDocumentAssistant() {
290
313
  // Persist after trimming so the saved history reflects what's actually sent.
291
314
  persistChat();
292
315
 
316
+ /* Stop is armed before the send's first wait. The turn's signal used to be created after
317
+ `await plat.aiChatUrl()`, which on desktop is an IPC round trip while the chat already reads
318
+ as streaming: a Stop pressed in that window found no controller, so the turn went on to
319
+ stream and run its tools, into a transcript the Stop had already cleared. The URL is now
320
+ resolved inside the first request, on the armed signal. */
321
+ controller = new AbortController();
322
+ const { signal } = controller;
323
+ turnActive.value = true;
324
+ let settleTurnEnded!: () => void;
325
+ turnEnded = new Promise<void>((settle) => {
326
+ settleTurnEnded = settle;
327
+ });
293
328
  try {
294
329
  const plat = getPlatform();
295
- const chatUrl = await Promise.resolve(plat.aiChatUrl());
296
330
  // Re-read the persisted model each send: the session is constructed once at module load
297
331
  // (before the user sets a key/model), so the picker's choice must be picked up here.
298
332
  chatState.setModel(preferredModel());
299
333
  const streamingClient = createProxyStreamingClient({
300
- chatUrl,
334
+ chatUrl: () => plat.aiChatUrl(),
301
335
  model: chatState.model,
302
336
  // Sent as X-Api-Key; the proxy falls back to the server's OPENAI_API_KEY when empty.
303
337
  apiKey: getOpenAiKey() || undefined,
@@ -305,29 +339,52 @@ export function createDocumentAssistant() {
305
339
  baseUrl: getBaseUrl() || undefined,
306
340
  });
307
341
 
308
- controller = new AbortController();
342
+ if (sessionFacts.sessionId !== sessionId) {
343
+ sessionFacts = createSessionFacts(sessionId, sessionFacts.toJSON());
344
+ }
309
345
  await runAgentLoop({
310
346
  chatState,
311
347
  streamingClient,
312
348
  toolRegistry,
313
349
  systemPrompt: buildPrompt(),
314
- signal: controller.signal,
350
+ signal,
351
+ session: sessionFacts,
315
352
  getTab: () => activeTab.value,
316
353
  });
317
354
  } catch (error) {
318
355
  /*
319
- * Synchronous failure (e.g. platform not registered, network unreachable before the
320
- * stream starts). Set the error so the panel can display it.
356
+ * A failure outside the stream's own error frames: the platform not registered, or its chat
357
+ * URL lookup rejecting inside the first request (a lookup that fails after a Stop ends the
358
+ * stream as cancelled instead). Set the error so the panel can display it.
321
359
  */
322
360
  chatState.setError(error instanceof Error ? error.message : String(error));
323
361
  } finally {
324
362
  controller = null;
363
+ turnActive.value = false;
325
364
  // Persist again once the stream settled so the completed reply (or the state
326
365
  // After an error/abort cleanup) survives a reload without another send.
327
366
  persistChat();
367
+ settleTurnEnded();
328
368
  }
329
369
  }
330
370
 
371
+ /** Whether a turn is in flight, tools and questions included. Reactive: read it in an effect. */
372
+ function isTurnActive(): boolean {
373
+ return turnActive.value;
374
+ }
375
+
376
+ /**
377
+ * Resolves once the turn in flight, if any, has ended.
378
+ *
379
+ * Stop, New Chat and Open Session end a turn but cannot finish it: they abort its signal, and the
380
+ * loop unwinds afterwards, at least a microtask later and as late as the call it was running
381
+ * allows. Until then the window still holds that turn, so a send made in the meantime is refused.
382
+ * A caller that means to start the next turn waits here first.
383
+ */
384
+ function whenTurnEnds(): Promise<void> {
385
+ return turnEnded;
386
+ }
387
+
331
388
  function stop() {
332
389
  controller?.abort();
333
390
  /* The abort settles an outstanding question through the turn signal, but only when the turn
@@ -342,7 +399,11 @@ export function createDocumentAssistant() {
342
399
  stop();
343
400
  resetAsk();
344
401
  resetImportRuns();
345
- resetImportGuard();
402
+ /* The record of what each turn changed is drawn under that turn's message; with the messages
403
+ gone there is nothing to draw it under, and a Restore offered from it would undo whatever
404
+ document is active now (ai.md §3.2). A stopped turn still unwinding files under no message. */
405
+ resetAiWrites();
406
+ sessionFacts = createSessionFacts();
346
407
  chatState.clearChat();
347
408
  sessionId = null;
348
409
  sessionStore.setActiveSession(projectRoot(), null);
@@ -395,12 +456,18 @@ export function createDocumentAssistant() {
395
456
  const msgs = chatState.messages.filter(
396
457
  (m) => m.role !== "assistant" || m.content || (m.toolCalls?.length ?? 0) > 0,
397
458
  );
398
- sessionStore.saveSession(projectRoot(), sessionId, msgs);
459
+ /* Without each call's `result`: the tool message that answers it is the outcome's one copy,
460
+ and a restore reads it back from there (services/tool-outcomes.ts). */
461
+ sessionStore.saveSession(projectRoot(), sessionId, stripToolResults(msgs));
399
462
  }
400
463
 
401
- /** Push persisted messages into chat state, synthesizing ids where missing. */
464
+ /**
465
+ * Push persisted messages into chat state, synthesizing ids where missing, with each tool call's
466
+ * outcome backfilled from the tool message that answered it — so a restored chip shows how its
467
+ * call ended, and an answered question shows its answer.
468
+ */
402
469
  function pushRestoredMessages(msgs: sessionStore.PersistedMessage[]) {
403
- for (const m of msgs) {
470
+ for (const m of backfillToolResults(msgs)) {
404
471
  chatState.messages.push({
405
472
  ...m,
406
473
  id: m.id || `restored_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
@@ -428,7 +495,9 @@ export function createDocumentAssistant() {
428
495
 
429
496
  return {
430
497
  chatState,
498
+ isTurnActive,
431
499
  sendMessage,
500
+ whenTurnEnds,
432
501
  stop,
433
502
  newChat,
434
503
  listSessions,
@@ -13,7 +13,7 @@
13
13
  * @license MIT
14
14
  */
15
15
 
16
- import type { ToolDefinition, ToolRegistry } from "@jxsuite/ai/tools";
16
+ import type { ToolContext, ToolDefinition, ToolRegistry } from "@jxsuite/ai/tools";
17
17
 
18
18
  export interface ToolAvailability {
19
19
  /** Whether the tool is currently advertised/executable. */
@@ -58,7 +58,7 @@ export function createGatedToolRegistry(
58
58
  validate(toolName: string, args: object) {
59
59
  return inner.validate(toolName, args);
60
60
  },
61
- async execute(toolName: string, args: object) {
61
+ async execute(toolName: string, args: object, ctx?: ToolContext) {
62
62
  if (inner.getDefinition(toolName) && !isAvailable(toolName)) {
63
63
  const gate = availability.get(toolName);
64
64
  return {
@@ -66,7 +66,8 @@ export function createGatedToolRegistry(
66
66
  error: `Tool "${toolName}" is not available right now — it requires ${gate?.requires ?? "a different studio state"}.`,
67
67
  };
68
68
  }
69
- return inner.execute(toolName, args);
69
+ // The call's context reaches the tool unchanged: its signal, id, ledger and session.
70
+ return inner.execute(toolName, args, ctx);
70
71
  },
71
72
  };
72
73
  }