@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
@@ -20,9 +20,11 @@
20
20
  */
21
21
 
22
22
  import { SETTINGS } from "./settings/definitions";
23
+ import type { SettingDefinition } from "./settings/definitions";
23
24
  import {
24
25
  clearSettings,
25
26
  hasSetting,
27
+ normalizeSetting,
26
28
  readStoredSetting,
27
29
  setSetting,
28
30
  setSettings,
@@ -82,6 +84,13 @@ export function setModel(modelId: string) {
82
84
  setSetting(SETTINGS.aiModel, modelId || "");
83
85
  }
84
86
 
87
+ /** The provider's three values together — what a credentials form drafts and what Save stores. */
88
+ export interface AiProvider {
89
+ apiKey: string;
90
+ baseUrl: string;
91
+ model: string;
92
+ }
93
+
85
94
  /**
86
95
  * Store the provider's key, endpoint and model as ONE change.
87
96
  *
@@ -89,9 +98,9 @@ export function setModel(modelId: string) {
89
98
  * anyway, but they announce three times, and each intermediate announcement describes a provider
90
99
  * the user never asked for — a new key against the old endpoint, then against no model.
91
100
  *
92
- * @param {{ apiKey: string; baseUrl: string; model: string }} provider
101
+ * @param {AiProvider} provider
93
102
  */
94
- export function saveAiProvider(provider: { apiKey: string; baseUrl: string; model: string }) {
103
+ export function saveAiProvider(provider: AiProvider) {
95
104
  setSettings([
96
105
  [SETTINGS.aiOpenAiKey, provider.apiKey],
97
106
  [SETTINGS.aiBaseUrl, provider.baseUrl],
@@ -99,6 +108,50 @@ export function saveAiProvider(provider: { apiKey: string; baseUrl: string; mode
99
108
  ]);
100
109
  }
101
110
 
111
+ /**
112
+ * Whether {@link saveAiProvider} with these values would change what is stored.
113
+ *
114
+ * What a credentials form's Save and Cancel are drawn from: at rest there is nothing to keep and
115
+ * nothing to abandon. Each value is compared as the store WOULD hold it — through its definition's
116
+ * own `normalize` — so a pasted key's whitespace or an endpoint's trailing slash is not an edit,
117
+ * because Save would store exactly what is already there. An absent setting reads as `""`, as every
118
+ * getter here does, so a blank draft over nothing is no change either.
119
+ *
120
+ * **BOTH sides are normalised, not just the draft.** Only the setters normalise on the way in;
121
+ * `hydrateSettings` and `adoptRemoteSettings` put a backend's or another window's values into the
122
+ * cache as they found them, and a hand-edited `settings.json` carries whatever was typed. So the
123
+ * store legitimately holds `http://localhost:11434/v1/` with the trailing slash the docs print, and
124
+ * comparing a normalised draft against that raw value reported an edit nobody had made: Save and
125
+ * Cancel appeared on a form at rest, and Cancel could not clear them, because reloading the drafts
126
+ * loaded the same unnormalised value again. Normalising the stored side is the comparison the
127
+ * sentence above always claimed to be making.
128
+ *
129
+ * @param {AiProvider} provider The drafts.
130
+ * @returns {boolean} `true` when at least one of the three would move.
131
+ */
132
+ export function aiProviderDiffers(provider: AiProvider): boolean {
133
+ return (
134
+ !storedEquals(SETTINGS.aiOpenAiKey, provider.apiKey, getOpenAiKey()) ||
135
+ !storedEquals(SETTINGS.aiBaseUrl, provider.baseUrl, getBaseUrl()) ||
136
+ !storedEquals(SETTINGS.aiModel, provider.model, storedModel())
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Whether a draft and a stored value are the same setting, judged as the store would hold each.
142
+ *
143
+ * Exported because a form's "has the user touched this field?" has to ask the same question as
144
+ * {@link aiProviderDiffers}, or the two disagree about a whitespace-only draft — which is how a
145
+ * revoked key survived a Disconnect (`ui/ai-credentials-form.ts`).
146
+ */
147
+ export function storedEquals(
148
+ definition: SettingDefinition,
149
+ draft: string,
150
+ stored: string,
151
+ ): boolean {
152
+ return normalizeSetting(definition, draft) === normalizeSetting(definition, stored);
153
+ }
154
+
102
155
  /**
103
156
  * Forget the provider entirely — key, endpoint and model.
104
157
  *
@@ -3,27 +3,30 @@
3
3
  *
4
4
  * Concrete `.jx` AST tools registered into a `@jxsuite/ai` ToolRegistry. Each tool wraps an
5
5
  * existing `transactDoc()` mutation helper so AI edits get the same undo/redo history as manual
6
- * edits (ADR docs/ai-assistant-decision.md §5 — optimistic apply + undo).
6
+ * edits (specs/ai.md §3.1 and §3.2 — optimistic apply, undo as the backstop).
7
7
  *
8
8
  * @license MIT
9
9
  */
10
10
 
11
11
  import { createToolDefinition } from "@jxsuite/ai/tools";
12
- import type { ToolRegistry, ToolResult } from "@jxsuite/ai/tools";
12
+ import type { ToolRegistry, ToolResult, WriteLedger } from "@jxsuite/ai/tools";
13
13
  import type { JxMutableNode, JxPath, JxStateDefinition } from "@jxsuite/schema/types";
14
14
  import { getNodeAtPath } from "../state";
15
15
  import type { Tab } from "../tabs/tab";
16
16
  import {
17
+ mutateAddDef,
17
18
  mutateInsertNode,
18
19
  mutateMoveNode,
20
+ mutateRemoveDef,
19
21
  mutateUpdateProperty,
20
22
  mutateUpdateStyle,
21
23
  transactDoc,
22
24
  } from "../tabs/transact";
23
25
  import type { JxNodeValue } from "../tabs/transact";
26
+ import type { JsonValue } from "../types";
24
27
  import { validateDoc } from "./jx-validate";
25
- import { recordWrite } from "./ai-writes";
26
- import { serializeJson } from "../files/json-layout";
28
+ import { invalidPathResult, normalizeRelPath } from "./project-path";
29
+ import { serializeJson } from "@jxsuite/schema/json-layout";
27
30
  import {
28
31
  reportDocumentWrite,
29
32
  snapshotBeforeWrite,
@@ -52,11 +55,12 @@ function noDocError(): ToolResult {
52
55
 
53
56
  /**
54
57
  * Apply a mutation, then hand the verdict to `ai-write-report.ts`: only the schema errors the edit
55
- * NEWLY introduced are reported (the eval signal — ADR §6b), the render check runs when a
56
- * renderCheck was provided and rendering worked before, and a success carries the token hints. The
57
- * change stays applied either way (optimistic apply + undo, ADR §5); reporting the errors lets the
58
- * agent loop self-correct on the next round.
58
+ * NEWLY introduced are reported, the render check runs when a renderCheck was provided and
59
+ * rendering worked before, and a success carries the token hints. The change stays applied either
60
+ * way (optimistic apply + undo); reporting the errors lets the agent loop self-correct on the next
61
+ * round. Both rules are specs/ai.md §3.1.
59
62
  *
63
+ * @param {WriteLedger} ledger - The call's ledger (`ctx.ledger`)
60
64
  * @param {import("../tabs/tab").Tab} tab
61
65
  * @param {(t: import("../tabs/tab").Tab) => void} mutationFn
62
66
  * @param {string} summary
@@ -66,6 +70,7 @@ function noDocError(): ToolResult {
66
70
  * @returns {Promise<import("@jxsuite/ai/tools").ToolResult>}
67
71
  */
68
72
  async function applyAndValidate(
73
+ ledger: WriteLedger,
69
74
  tab: Tab,
70
75
  mutationFn: (t: Tab) => void,
71
76
  summary: string,
@@ -91,9 +96,9 @@ async function applyAndValidate(
91
96
  };
92
97
  }
93
98
  /* One ledger entry per mutation, so the panel's "Changed N files" counts documents the model
94
- touched rather than sentences it wrote (§7.4). `disk: false` is the load-bearing half: this
95
- went through transactDoc, so the tab's history — and "Restore to here" — can reach it. */
96
- recordWrite({ disk: false, ok: true, path: tab.documentPath ?? "(untitled)", tool: summary });
99
+ touched rather than sentences it wrote (`ai.md` §3.2). `disk: false` is the load-bearing half:
100
+ this went through transactDoc, so the tab's history — and "Restore to here" — can reach it. */
101
+ ledger.record({ disk: false, ok: true, path: tab.documentPath ?? "(untitled)", tool: summary });
97
102
 
98
103
  return reportDocumentWrite(tab, before, summary, deps);
99
104
  }
@@ -225,11 +230,11 @@ export function registerAiTools(
225
230
  },
226
231
  /*
227
232
  * "value" omitted from required: passing null / omitting it means "remove the property",
228
- * but the registry rejects null on required args (tools.js:181). See §14.
233
+ * but the registry rejects null on required args (tools.js:181). See `ai.md` §3.6.
229
234
  */
230
235
  required: ["path", "key"],
231
236
  },
232
- async execute(args) {
237
+ async execute(args, ctx) {
233
238
  const tab = getTab();
234
239
  if (!tab) {
235
240
  return noDocError();
@@ -240,6 +245,7 @@ export function registerAiTools(
240
245
  return { success: false, error: `No node exists at path ${JSON.stringify(path)}.` };
241
246
  }
242
247
  return applyAndValidate(
248
+ ctx.ledger,
243
249
  tab,
244
250
  (t) => mutateUpdateProperty(t, path, key, value ?? undefined),
245
251
  `Set "${key}" at ${JSON.stringify(path)}.`,
@@ -275,7 +281,7 @@ export function registerAiTools(
275
281
  },
276
282
  required: ["parentPath", "index", "node"],
277
283
  },
278
- async execute(args) {
284
+ async execute(args, ctx) {
279
285
  const tab = getTab();
280
286
  if (!tab) {
281
287
  return noDocError();
@@ -318,6 +324,7 @@ export function registerAiTools(
318
324
  };
319
325
  }
320
326
  return applyAndValidate(
327
+ ctx.ledger,
321
328
  tab,
322
329
  (t) => mutateInsertNode(t, parentPath, index, childNode),
323
330
  `Inserted node at ${JSON.stringify([...parentPath, "children", index])}.`,
@@ -357,11 +364,11 @@ export function registerAiTools(
357
364
  },
358
365
  /*
359
366
  * "value" omitted from required so null / omitted = remove (registry rejects null on
360
- * required args, tools.js:181). See §14.
367
+ * required args, tools.js:181). See `ai.md` §3.6.
361
368
  */
362
369
  required: ["path", "property"],
363
370
  },
364
- async execute(args) {
371
+ async execute(args, ctx) {
365
372
  const tab = getTab();
366
373
  if (!tab) {
367
374
  return noDocError();
@@ -377,6 +384,7 @@ export function registerAiTools(
377
384
  const prop = property;
378
385
  const val = value == null ? undefined : String(value);
379
386
  return applyAndValidate(
387
+ ctx.ledger,
380
388
  tab,
381
389
  (t) => mutateUpdateStyle(t, path, prop, val),
382
390
  `Set style "${prop}" at ${JSON.stringify(path)}.`,
@@ -407,7 +415,7 @@ export function registerAiTools(
407
415
  },
408
416
  required: ["path", "value"],
409
417
  },
410
- async execute(args) {
418
+ async execute(args, ctx) {
411
419
  const tab = getTab();
412
420
  if (!tab) {
413
421
  return noDocError();
@@ -417,11 +425,14 @@ export function registerAiTools(
417
425
  return { success: false, error: `No node exists at path ${JSON.stringify(path)}.` };
418
426
  }
419
427
  return applyAndValidate(
428
+ ctx.ledger,
420
429
  tab,
430
+ /* Through the recording mutators, as two `set-key` ops, rather than by writing the node
431
+ directly. An unrecorded write reaches the canvas only as a full re-render, history only
432
+ as a whole-document snapshot, and collaborators only as a diff. */
421
433
  (t) => {
422
- const node = getNodeAtPath(t.doc.document, path);
423
- delete node.textContent;
424
- node.children = [value];
434
+ mutateUpdateProperty(t, path, "textContent");
435
+ mutateUpdateProperty(t, path, "children", [value]);
425
436
  },
426
437
  `Set text at ${JSON.stringify(path)}.`,
427
438
  validate,
@@ -453,7 +464,7 @@ export function registerAiTools(
453
464
  },
454
465
  required: ["key", "value"],
455
466
  },
456
- async execute(args) {
467
+ async execute(args, ctx) {
457
468
  const tab = getTab();
458
469
  if (!tab) {
459
470
  return noDocError();
@@ -466,18 +477,12 @@ export function registerAiTools(
466
477
  };
467
478
  }
468
479
  return applyAndValidate(
480
+ ctx.ledger,
469
481
  tab,
470
- (t) => {
471
- // Ensure the state object exists before setting a key on it.
472
- if (!t.doc.document.state) {
473
- t.doc.document.state = {};
474
- }
475
- /*
476
- * Directly mutate — bypass mutateUpdateProperty because its "" → delete behaviour
477
- * (transact.ts:248) is wrong for state defaults (e.g. "title": "").
478
- */
479
- t.doc.document.state[key] = value;
480
- },
482
+ /* `mutateAddDef` rather than `mutateUpdateProperty`: the latter deletes on "", which is
483
+ wrong for a state default (`"title": ""`). It also records the op, so the canvas, the
484
+ history and collaborators each get the edit as an edit. */
485
+ (t) => mutateAddDef(t, key, value as Record<string, JsonValue>),
481
486
  `Added state "${key}".`,
482
487
  validate,
483
488
  renderCheck,
@@ -505,11 +510,11 @@ export function registerAiTools(
505
510
  },
506
511
  /*
507
512
  * "value" omitted from required so null = remove (registry rejects null on required
508
- * args, tools.js:181). See §14.
513
+ * args, tools.js:181). See `ai.md` §3.6.
509
514
  */
510
515
  required: ["key"],
511
516
  },
512
- async execute(args) {
517
+ async execute(args, ctx) {
513
518
  const tab = getTab();
514
519
  if (!tab) {
515
520
  return noDocError();
@@ -522,20 +527,15 @@ export function registerAiTools(
522
527
  };
523
528
  }
524
529
  return applyAndValidate(
530
+ ctx.ledger,
525
531
  tab,
532
+ /* The recording def mutators, for the reason `add_state` gives. Removing the last key
533
+ drops the empty `state` object, as the Inspector's own removal does. */
526
534
  (t) => {
527
- /*
528
- * Directly mutate — bypass mutateUpdateProperty because its "" → delete behaviour
529
- * (transact.ts:248) is wrong for state defaults (e.g. "title": "").
530
- */
531
- const { state } = t.doc.document;
532
- if (!state) {
533
- return;
534
- }
535
535
  if (value == null) {
536
- delete state[key];
536
+ mutateRemoveDef(t, key);
537
537
  } else {
538
- state[key] = value;
538
+ mutateAddDef(t, key, value as Record<string, JsonValue>);
539
539
  }
540
540
  },
541
541
  value == null ? `Removed state "${key}".` : `Updated state "${key}".`,
@@ -575,7 +575,7 @@ export function registerAiTools(
575
575
  },
576
576
  required: ["fromPath", "toParentPath", "toIndex"],
577
577
  },
578
- async execute(args) {
578
+ async execute(args, ctx) {
579
579
  const tab = getTab();
580
580
  if (!tab) {
581
581
  return noDocError();
@@ -601,6 +601,7 @@ export function registerAiTools(
601
601
  };
602
602
  }
603
603
  return applyAndValidate(
604
+ ctx.ledger,
604
605
  tab,
605
606
  (t) => mutateMoveNode(t, fromPath, toParentPath, toIndex),
606
607
  `Moved node from ${JSON.stringify(fromPath)} to ${JSON.stringify([...toParentPath, "children", toIndex])}.`,
@@ -636,14 +637,19 @@ export function registerAiTools(
636
637
  },
637
638
  required: ["path", "content"],
638
639
  },
639
- async execute(args) {
640
+ async execute(args, ctx) {
640
641
  if (!saveFile) {
641
642
  return {
642
643
  success: false,
643
644
  error: "File operations are not available in this environment.",
644
645
  };
645
646
  }
646
- const { path: relPath, content } = args as { path: string; content: object };
647
+ const { path, content } = args as { path: string; content: object };
648
+ // Inside the project or not at all (ai.md §4): the platform write is given this path.
649
+ const relPath = normalizeRelPath(path);
650
+ if (relPath === null) {
651
+ return invalidPathResult(path);
652
+ }
647
653
  const dirtyError = dirtyTabError(relPath);
648
654
  if (dirtyError) {
649
655
  return dirtyError;
@@ -666,17 +672,17 @@ export function registerAiTools(
666
672
  }
667
673
  }
668
674
  try {
669
- // The save serializer, layout-less (`files/json-layout.ts`): the assistant supplied a
675
+ // The save serializer, layout-less (`@jxsuite/schema/json-layout`): the assistant supplied a
670
676
  // Value, not a text, so the formatter's layout for fresh output is the right one.
671
677
  await saveFile(relPath, serializeJson(content, null));
672
- recordWrite({ disk: true, ok: true, path: relPath, tool: "create_component" });
678
+ ctx.ledger.record({ disk: true, ok: true, path: relPath, tool: "create_component" });
673
679
  return {
674
680
  success: true,
675
681
  summary: await reconcileAfterWrite(relPath, `Created component at "${relPath}".`),
676
682
  };
677
683
  } catch (error) {
678
684
  const message = error instanceof Error ? error.message : String(error);
679
- recordWrite({
685
+ ctx.ledger.record({
680
686
  disk: true,
681
687
  error: message,
682
688
  ok: false,
@@ -715,14 +721,18 @@ export function registerAiTools(
715
721
  },
716
722
  required: ["path", "content"],
717
723
  },
718
- async execute(args) {
724
+ async execute(args, ctx) {
719
725
  if (!saveFile) {
720
726
  return {
721
727
  success: false,
722
728
  error: "File operations are not available in this environment.",
723
729
  };
724
730
  }
725
- const { path: relPath, content } = args as { path: string; content: object };
731
+ const { path, content } = args as { path: string; content: object };
732
+ const relPath = normalizeRelPath(path);
733
+ if (relPath === null) {
734
+ return invalidPathResult(path);
735
+ }
726
736
  const dirtyError = dirtyTabError(relPath);
727
737
  if (dirtyError) {
728
738
  return dirtyError;
@@ -747,14 +757,14 @@ export function registerAiTools(
747
757
  try {
748
758
  // As `create_component`: a value the assistant supplied, written in the formatter's layout.
749
759
  await saveFile(relPath, serializeJson(content, null));
750
- recordWrite({ disk: true, ok: true, path: relPath, tool: "create_page" });
760
+ ctx.ledger.record({ disk: true, ok: true, path: relPath, tool: "create_page" });
751
761
  return {
752
762
  success: true,
753
763
  summary: await reconcileAfterWrite(relPath, `Created page at "${relPath}".`),
754
764
  };
755
765
  } catch (error) {
756
766
  const message = error instanceof Error ? error.message : String(error);
757
- recordWrite({
767
+ ctx.ledger.record({
758
768
  disk: true,
759
769
  error: message,
760
770
  ok: false,
@@ -773,9 +783,10 @@ export function registerAiTools(
773
783
  /*
774
784
  * `open_document` is NOT here any more. It is the projection of the `document.open` record
775
785
  * (`workspace/workspace.ts`'s `tabCommands`), so the person's palette row and the model's tool
776
- * are one `run` and one gate (§12.4). The hand tool read `getTab()` after the open and called
777
- * whatever it found a success — a missing file left the previous document active and reported
778
- * "Switched to" — and it re-anchored the undo batch itself, which `tool-executor.ts` has done
779
- * after EVERY tool since project adoption started replacing tabs mid-loop.
786
+ * are one `run` and one gate (`studio-ui-guidelines.md` §12.4). The hand tool read `getTab()`
787
+ * after the open and called whatever it found a success — a missing file left the previous
788
+ * document active and reported "Switched to" — and it re-anchored the undo batch itself, which
789
+ * `tool-executor.ts` has done after EVERY tool since project adoption started replacing tabs
790
+ * mid-loop.
780
791
  */
781
792
  }
@@ -8,14 +8,14 @@
8
8
  * with:
9
9
  *
10
10
  * 1. {@link snapshotBeforeWrite} reads the document's schema errors and render state BEFORE the
11
- * write, because the eval signal (ADR docs/ai-assistant-decision.md §6b) is "what did this edit
12
- * newly introduce", not "is the document valid": a page that was already broken must not make
13
- * every subsequent edit read as the culprit.
11
+ * write, because the loop's signal (specs/ai.md §3.1) is "what did this edit newly introduce",
12
+ * not "is the document valid": a page that was already broken must not make every subsequent
13
+ * edit read as the culprit.
14
14
  * 2. {@link reportDocumentWrite} compares AFTER the write, translating each new schema error into a
15
15
  * fix hint the model can act on, running the render check only when rendering worked before,
16
16
  * and appending the soft token-discipline hints to a success.
17
17
  *
18
- * The change stays applied either way (optimistic apply + undo, ADR §5); reporting the errors lets
18
+ * The change stays applied either way (optimistic apply + undo, specs/ai.md §3.1); reporting the errors lets
19
19
  * the agent loop self-correct on the next round.
20
20
  *
21
21
  * @license MIT
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Ai-writes.ts — what the assistant changed, per turn, and whether you can take it back.
3
3
  *
4
- * §7.4 (AI honesty). Two facts about an assistant turn were legible only to the model:
4
+ * `ai.md` §3.2 (the accountable turn). Two facts about an assistant turn were legible only to the
5
+ * model:
5
6
  *
6
7
  * 1. **Which files it changed.** The loop collected `result.summary` strings and fed them back into
7
8
  * the conversation; the human got a paragraph of prose in which "I updated the hero" and "I
@@ -17,27 +18,15 @@
17
18
  *
18
19
  * The tools record; the loop bounds the turn; the panel renders. Nothing here knows about lit, and
19
20
  * nothing here imports the loop, so a tool can be honest without either.
21
+ *
22
+ * @docs studio/ai/chat
20
23
  */
21
24
 
22
- /** One change the assistant made, as recorded by the tool that made it. */
23
- export interface AiWrite {
24
- /** Project-relative path, or the document path for an in-editor mutation. */
25
- path: string;
26
- /** The tool that did it — the command title the chip renders. */
27
- tool: string;
28
- /**
29
- * True when the change went to disk with no transaction behind it.
30
- *
31
- * This is the undo caveat, as a fact rather than a sentence: a `disk` write is NOT reachable by
32
- * ⌘Z, by the tab's history, or by "Restore to here". The panel says so, in those words, to the
33
- * person who would otherwise press ⌘Z and believe it worked.
34
- */
35
- disk: boolean;
36
- /** False when the tool reported a failure — a listed attempt that changed nothing. */
37
- ok: boolean;
38
- /** Why it failed, when it did. */
39
- error?: string;
40
- }
25
+ import type { Message } from "@jxsuite/ai/chat-state";
26
+ import { createLedger } from "@jxsuite/ai/tools";
27
+ import type { AiWrite, WriteLedger } from "@jxsuite/ai/tools";
28
+
29
+ export type { AiWrite } from "@jxsuite/ai/tools";
41
30
 
42
31
  /** Every write recorded during one assistant turn, in the order the tools made them. */
43
32
  export interface AiTurn {
@@ -52,52 +41,42 @@ const turns: AiTurn[] = [];
52
41
  /** How many turns of ledger are kept. Older ones drop their summary; the messages remain. */
53
42
  export const MAX_TURNS = 50;
54
43
 
55
- let open: AiTurn | null = null;
56
-
57
- /**
58
- * Start recording. Called by the agent loop once per user turn.
59
- *
60
- * Idempotent on the same id so a nested or re-entered loop cannot split one turn's writes across
61
- * two ledgers — which is the same class of bug as the batch that was opened against whichever tab
62
- * happened to be active.
63
- *
64
- * @param {string} id
65
- */
66
- export function beginTurn(id: string): void {
67
- if (open?.id === id) {
68
- return;
69
- }
70
- open = { id, writes: [] };
44
+ /** One turn's ledger, named for the turn it records. */
45
+ export interface TurnLedger extends WriteLedger {
46
+ readonly turnId: string;
71
47
  }
72
48
 
73
49
  /**
74
- * Record one change. A no-op when no turn is open, so a tool invoked outside the loop — a command,
75
- * a test — costs nothing and reports nothing.
50
+ * A fresh ledger for one turn. The loop opens it when the turn starts, hands it to every call as
51
+ * `ctx.ledger`, and files it with {@link fileTurn} when the turn ends.
52
+ *
53
+ * Per turn, not a module slot: the slot this replaced was one ledger for the whole window, so a
54
+ * tool run outside the loop recorded into whichever turn happened to be open, and two turns could
55
+ * not each keep their own.
76
56
  *
77
- * @param {AiWrite} write
57
+ * @param {string} turnId - The turn it records, which also names the actor its calls run as
58
+ * @returns {TurnLedger}
78
59
  */
79
- export function recordWrite(write: AiWrite): void {
80
- open?.writes.push(write);
60
+ export function openTurnLedger(turnId: string): TurnLedger {
61
+ return Object.assign(createLedger(), { turnId });
81
62
  }
82
63
 
83
64
  /**
84
- * Close the open turn and file it under `id`.
65
+ * File a finished turn's writes under `anchor`, the message the transcript draws its summary under.
85
66
  *
86
- * The id arrives at the END because it is the assistant message's id, which does not exist when the
87
- * turn starts — the loop opens the turn before it knows what the model will say.
67
+ * The anchor arrives at the END because it is an assistant message's id, which does not exist when
68
+ * the turn starts. A turn with no drawn message is filed under `""`, which no message has, so the
69
+ * transcript draws its changes nowhere. A turn that wrote nothing files nothing.
88
70
  *
89
- * @param {string} id - The assistant message the turn produced
90
- * @returns {AiWrite[]} What was recorded
71
+ * @param {string} anchor - The message the turn's changes are drawn under
72
+ * @param {readonly AiWrite[]} writes - The turn's ledger
73
+ * @returns {AiWrite[]} What was filed
91
74
  */
92
- export function endTurn(id: string): AiWrite[] {
93
- if (!open) {
94
- return [];
95
- }
96
- const turn: AiTurn = { id, writes: open.writes };
97
- open = null;
98
- if (turn.writes.length === 0) {
75
+ export function fileTurn(anchor: string, writes: readonly AiWrite[]): AiWrite[] {
76
+ if (writes.length === 0) {
99
77
  return [];
100
78
  }
79
+ const turn: AiTurn = { id: anchor, writes: [...writes] };
101
80
  turns.push(turn);
102
81
  while (turns.length > MAX_TURNS) {
103
82
  turns.shift();
@@ -147,8 +126,48 @@ export function summarizeWrites(writes: AiWrite[]): string {
147
126
  return parts.join(" · ");
148
127
  }
149
128
 
150
- /** Drop every ledger. For tests and for the "new chat" / "close project" paths. */
129
+ /**
130
+ * Drop every ledger: New Chat (the messages they are drawn under are gone), leaving a project
131
+ * (their paths name the project left behind; `setWorkspaceProject`), and tests.
132
+ */
151
133
  export function resetAiWrites(): void {
152
134
  turns.splice(0);
153
- open = null;
135
+ }
136
+
137
+ /**
138
+ * The message a turn's changes are filed under: its last DRAWN assistant message, which is the one
139
+ * the transcript renders the changed-files summary beneath (panels/ai-chat/chat-view.ts), and so
140
+ * the id {@link fileTurn} is given.
141
+ *
142
+ * Not simply the last message. A turn can end on a `tool` reply (a Stop during the last call, or a
143
+ * cap reached with nothing applied), on a final round that said nothing, or after a stream error
144
+ * removed its round's partial; filed under any of those, the summary and Restore were drawn under
145
+ * nothing and the author never saw what the turn changed. The cap message counts: it is drawn.
146
+ *
147
+ * **An anchor is only ever this turn's.** The scan must MEET the message the turn answers, and only
148
+ * what it passed on the way counts. A transcript that no longer holds that message was replaced
149
+ * under the turn (another chat opened from Chat History while it waited on a tool), and every
150
+ * message in it belongs to some other conversation: filing there would draw "Changed 1 file", and
151
+ * Restore, under a reply that changed nothing.
152
+ *
153
+ * @param {readonly Message[]} messages
154
+ * @param {string} [userId] - The id of the user message this turn answers; without one, the whole
155
+ * transcript is the turn
156
+ * @returns {string | null} Null when the turn drew nothing (so it ran no tool either), or when its
157
+ * user message has left the transcript
158
+ */
159
+ export function turnAnchor(messages: readonly Message[], userId?: string): string | null {
160
+ let anchor: string | null = null;
161
+ for (let index = messages.length - 1; index >= 0; index--) {
162
+ const message = messages[index]!;
163
+ if (message.id === userId) {
164
+ return anchor;
165
+ }
166
+ const drawn =
167
+ message.role === "assistant" && (message.content || (message.toolCalls?.length ?? 0) > 0);
168
+ if (anchor === null && drawn) {
169
+ anchor = message.id;
170
+ }
171
+ }
172
+ return userId === undefined ? anchor : null;
154
173
  }