@jxsuite/studio 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (389) hide show
  1. package/data/css-meta.json +28 -27
  2. package/dist/chunks/{cf-account-picker-nwnwdm6p.js → cf-account-picker-3f08rry6.js} +21 -18
  3. package/dist/chunks/{cf-settings-adg74qsk.js → cf-settings-m79r7p5y.js} +4 -4
  4. package/dist/chunks/{components-5y78e05d.js → components-xm826zjw.js} +3 -3
  5. package/dist/chunks/{content-source-4rzvmp0m.js → content-source-jt7rbcbc.js} +22 -22
  6. package/dist/chunks/{convert-to-repeater-jxsv62v7.js → convert-to-repeater-vg38bq99.js} +22 -22
  7. package/dist/chunks/{editor-3qhbg51x.js → editor-ddy019sn.js} +2 -2
  8. package/dist/chunks/entry-commands-64nb2a9a.js +42 -0
  9. package/dist/chunks/{extension-sections-2mrptyek.js → extension-sections-r73an5s6.js} +30 -29
  10. package/dist/chunks/{file-ops-knfqshrm.js → file-ops-3xan8mnj.js} +21 -21
  11. package/dist/chunks/{files-y6fr4he9.js → files-egsb0ry8.js} +28 -28
  12. package/dist/chunks/{format-host-1k4qrcqd.js → format-host-mym8qnd0.js} +3 -3
  13. package/dist/chunks/{fs-events-3y6y0nrm.js → fs-events-z2gg8qjt.js} +4 -4
  14. package/dist/chunks/{git-panel-2rkrsjva.js → git-panel-a05atk6q.js} +20 -20
  15. package/dist/chunks/{grid-view-1p6hsyjx.css → grid-view-3g1h2re8.css} +956 -139
  16. package/dist/chunks/{grid-view-a7b87n2c.js → grid-view-qaqer9kr.js} +24 -24
  17. package/dist/chunks/{icons-yy10wk9r.js → icons-drxq9q10.js} +8 -2
  18. package/dist/chunks/{iframe-host-scpw01h8.js → iframe-host-psetcv20.js} +11 -11
  19. package/dist/chunks/{index-n4j7qxyr.js → index-9yjae3hz.js} +4 -4
  20. package/dist/chunks/{javascript-4abzjcy9.css → javascript-bc9z461d.css} +688 -54
  21. package/dist/chunks/{javascript-xvrfrpkb.js → javascript-hr5sg57n.js} +2 -2
  22. package/dist/chunks/{jsonMode-vvmpmk34.css → jsonMode-qqd9nm1t.css} +1120 -303
  23. package/dist/chunks/{jsonMode-tyx2kj6p.js → jsonMode-zs88v0wr.js} +4 -4
  24. package/dist/chunks/layers-panel-83a16ytx.js +41 -0
  25. package/dist/chunks/{layers-jnvdbqva.js → layers-s6mc01ry.js} +3 -3
  26. package/dist/chunks/{media-picker-f66nav16.js → media-picker-nhn6rv4w.js} +11 -11
  27. package/dist/chunks/{menu-1dfhry4s.js → menu-8j4qxjrp.js} +4 -4
  28. package/dist/chunks/{monaco-binding-sw6en4dw.js → monaco-binding-e8d9wzyk.js} +3 -3
  29. package/dist/chunks/{monaco-buffer-pykwbm92.js → monaco-buffer-jaq1t9zj.js} +2 -2
  30. package/dist/chunks/{monaco-setup-8vbyqnhr.css → monaco-setup-ey9cjcm3.css} +954 -137
  31. package/dist/chunks/{monaco-setup-km6abc6q.js → monaco-setup-gsjdysze.js} +6 -6
  32. package/dist/chunks/{platform-ts9cg1a4.js → platform-fnchmhy7.js} +5 -3
  33. package/dist/chunks/publish-panel-ak6hxfma.js +27 -0
  34. package/dist/chunks/quick-search-c63mkt3b.js +47 -0
  35. package/dist/chunks/{references-h69xp9zz.js → references-f7bdv3z5.js} +3 -3
  36. package/dist/chunks/right-panel-t5n8wttg.js +45 -0
  37. package/dist/chunks/{studio-ze46gzjh.js → studio-01269reb.js} +7 -7
  38. package/dist/chunks/{studio-rnyf3f8e.js → studio-0a4gncx0.js} +3 -3
  39. package/dist/chunks/{studio-e1f24eff.js → studio-0d4p4wxj.js} +6778 -2993
  40. package/dist/chunks/{studio-4wbb9ct0.js → studio-1tejxvba.js} +3006 -1855
  41. package/dist/chunks/{studio-d1n2bykn.js → studio-3nprwbd9.js} +117 -25
  42. package/dist/chunks/{studio-4c041r8y.js → studio-40z39dme.js} +3 -3
  43. package/dist/chunks/{studio-151w0xnh.js → studio-482eeazk.js} +20 -20
  44. package/dist/chunks/{studio-cgprcdxn.js → studio-4998spet.js} +10 -9
  45. package/dist/chunks/{studio-nq5yf5mp.js → studio-557bmn0x.js} +2 -2
  46. package/dist/chunks/{studio-rtskkm43.js → studio-5w26wza5.js} +3 -3
  47. package/dist/chunks/{studio-6nzaktvy.js → studio-6pwn9j8c.js} +6 -3
  48. package/dist/chunks/{studio-wnsyffg1.js → studio-6vg0x5fz.js} +3 -2
  49. package/dist/chunks/{studio-vn5hske5.js → studio-7d3c1m59.js} +12 -5
  50. package/dist/chunks/{studio-6gryj0sg.js → studio-9vsvz7w7.js} +6 -6
  51. package/dist/chunks/{studio-50wfwy44.js → studio-9wm0xzpf.js} +19 -19
  52. package/dist/chunks/{studio-0j7r7s9g.js → studio-ag3z6shb.js} +10 -4
  53. package/dist/chunks/studio-aj9drheq.js +177 -0
  54. package/dist/chunks/{studio-xajk70w0.js → studio-apv9x70c.js} +189 -130
  55. package/dist/chunks/{studio-c04nzbby.js → studio-b9q3znz8.js} +2 -2
  56. package/dist/chunks/{studio-nfx354t1.js → studio-bh47w0cy.js} +9 -10
  57. package/dist/chunks/{studio-gk2twyh4.js → studio-dgdm6vvt.js} +264 -52
  58. package/dist/chunks/{studio-gtfq1dkz.js → studio-dwdcr2e8.js} +4 -4
  59. package/dist/chunks/{studio-nqgawfks.js → studio-fbcyz234.js} +146 -139
  60. package/dist/chunks/{studio-27q131j3.js → studio-fqpkpq1t.js} +7 -4
  61. package/dist/chunks/{studio-c0qgc3tn.js → studio-fy320dfc.js} +62 -68
  62. package/dist/chunks/{studio-zjjde71a.js → studio-gjj4bbfx.js} +2 -2
  63. package/dist/chunks/{studio-g33v4fe9.js → studio-jfhxy8h3.js} +7 -7
  64. package/dist/chunks/{studio-ab87phj1.js → studio-kwewmhhq.js} +4 -4
  65. package/dist/chunks/{studio-7rrb1esn.js → studio-m58xn8at.js} +25 -24
  66. package/dist/chunks/{studio-fzjx96t2.js → studio-mfkh1q8d.js} +17 -17
  67. package/dist/chunks/{studio-drg8qj9k.js → studio-mrh0a92f.js} +17 -17
  68. package/dist/chunks/{studio-jt27ta6x.js → studio-mt79tn56.js} +3 -3
  69. package/dist/chunks/{studio-b66rsh78.js → studio-ne593z1j.js} +38 -31
  70. package/dist/chunks/{studio-h4nvnjvz.js → studio-ptfcxkz5.js} +418 -232
  71. package/dist/chunks/{studio-dd1qqkry.js → studio-qxye31h7.js} +7 -7
  72. package/dist/chunks/{studio-ye9drbk8.js → studio-rav5bpmp.js} +73 -52
  73. package/dist/chunks/{studio-6kvgrkrp.js → studio-rwmmbrth.js} +6 -6
  74. package/dist/chunks/{studio-b7xacrwe.js → studio-sceg8ws0.js} +8 -8
  75. package/dist/chunks/{studio-msc1ee1m.js → studio-sjas5b3a.js} +19 -13
  76. package/dist/chunks/{studio-a65w20ex.js → studio-vd67b66d.js} +3 -3
  77. package/dist/chunks/{studio-7144epdr.js → studio-w475cbf9.js} +10 -10
  78. package/dist/chunks/{studio-4f3psrk1.js → studio-w7gpcvfj.js} +9 -9
  79. package/dist/chunks/{studio-nsgw7dbk.js → studio-xrbfdxgk.js} +7 -7
  80. package/dist/chunks/{studio-ke09tn9k.js → studio-y49b6mjv.js} +35 -35
  81. package/dist/chunks/{studio-j0w60gft.js → studio-ygvr9e4q.js} +10 -10
  82. package/dist/chunks/{studio-z2smx5ce.js → studio-yxmmjy2a.js} +9 -6
  83. package/dist/chunks/{tsMode-jy4m86rv.js → tsMode-ay4c78ne.js} +4 -4
  84. package/dist/chunks/{tsMode-hnxxb73h.css → tsMode-c72wyz27.css} +954 -137
  85. package/dist/chunks/{workspace-np10mayv.js → workspace-b9k2frks.js} +5 -5
  86. package/dist/chunks/{ws-client-ny4zc51r.js → ws-client-erze4czt.js} +356 -20
  87. package/dist/codicon.ttf +0 -0
  88. package/dist/iframe-entry.js +18 -16
  89. package/dist/studio.css +956 -139
  90. package/dist/studio.js +1453 -830
  91. package/dist/workers/editor.worker.js +196 -107
  92. package/dist/workers/json.worker.js +196 -107
  93. package/dist/workers/ts.worker.js +196 -107
  94. package/package.json +15 -14
  95. package/src/about/about-modal.ts +4 -3
  96. package/src/browse/library-commands.ts +15 -13
  97. package/src/browse/library-pane.ts +2 -2
  98. package/src/canvas/canvas-render.ts +97 -83
  99. package/src/canvas/canvas-surface.ts +2 -2
  100. package/src/canvas/canvas-utils.ts +42 -32
  101. package/src/canvas/diff-toolbar.ts +21 -2
  102. package/src/canvas/diff-view.ts +30 -6
  103. package/src/canvas/iframe-entry.ts +2 -2
  104. package/src/canvas/iframe-host.ts +5 -5
  105. package/src/canvas/iframe-interaction.ts +7 -6
  106. package/src/canvas/iframe-overlay.ts +1 -1
  107. package/src/canvas/iframe-protocol.ts +6 -5
  108. package/src/canvas/iframe-render.ts +3 -3
  109. package/src/canvas/popover-state.ts +1 -1
  110. package/src/canvas/stage-scrollbar.ts +112 -0
  111. package/src/canvas/surface-registry.ts +9 -2
  112. package/src/collab/collab-commands.ts +8 -6
  113. package/src/collab/collab-session.ts +4 -4
  114. package/src/collab/collab-state.ts +4 -4
  115. package/src/collab/presence-chips.ts +3 -3
  116. package/src/commands/app-commands.ts +7 -7
  117. package/src/commands/budget.ts +17 -16
  118. package/src/commands/command-args.ts +10 -9
  119. package/src/commands/context.ts +20 -10
  120. package/src/commands/defaults.ts +30 -29
  121. package/src/commands/levels.ts +8 -6
  122. package/src/commands/live-context.ts +12 -10
  123. package/src/commands/reference.ts +5 -5
  124. package/src/commands/registry.ts +2 -2
  125. package/src/content/draft-state.ts +9 -8
  126. package/src/content/entry-commands.ts +16 -14
  127. package/src/content/entry-editor.ts +2 -2
  128. package/src/content/entry-fields.ts +1 -1
  129. package/src/editor/context-menu.ts +4 -4
  130. package/src/editor/convert-to-component.ts +6 -5
  131. package/src/editor/inline-edit.ts +3 -3
  132. package/src/editor/shortcuts.ts +88 -20
  133. package/src/editor/slash-menu.ts +11 -10
  134. package/src/files/elements.ts +4 -5
  135. package/src/files/file-ops.ts +1 -1
  136. package/src/files/files.ts +21 -17
  137. package/src/files/serialize-document.ts +1 -1
  138. package/src/format/convert-file.ts +5 -5
  139. package/src/github/github-auth-status.ts +107 -0
  140. package/src/github/github-auth.ts +18 -82
  141. package/src/github/github-publish.ts +7 -6
  142. package/src/grid/cell-editors.ts +8 -7
  143. package/src/grid/grid-layout.ts +1 -1
  144. package/src/grid/grid-open.ts +6 -5
  145. package/src/grid/grid-panel.ts +13 -12
  146. package/src/grid/redirects-grid.ts +14 -11
  147. package/src/grid/redirects.ts +10 -5
  148. package/src/grid/sources/content-source.ts +10 -9
  149. package/src/hosting/document.ts +28 -1
  150. package/src/hosting/stage.ts +15 -4
  151. package/src/i18n/i18n-commands.ts +9 -7
  152. package/src/new-project/location-fields.ts +16 -3
  153. package/src/new-project/new-project-modal.ts +23 -7
  154. package/src/page-params.ts +6 -6
  155. package/src/panels/activity-panel.ts +11 -10
  156. package/src/panels/ai-chat/chat-view.ts +27 -14
  157. package/src/panels/ai-chat/composer.ts +10 -3
  158. package/src/panels/ai-panel.ts +87 -29
  159. package/src/panels/block-action-bar.ts +59 -33
  160. package/src/panels/bottom-dock.ts +22 -22
  161. package/src/panels/data-explorer.ts +9 -9
  162. package/src/panels/data-grid.ts +1 -1
  163. package/src/panels/editors.ts +8 -8
  164. package/src/panels/elements-panel.ts +7 -8
  165. package/src/panels/empty-state.ts +1 -1
  166. package/src/panels/events-panel.ts +13 -12
  167. package/src/panels/formula-workspace.ts +8 -5
  168. package/src/panels/frontmatter-fields.ts +4 -1
  169. package/src/panels/frontmatter-panel.ts +18 -18
  170. package/src/panels/git-panel.ts +19 -13
  171. package/src/panels/head-panel.ts +9 -10
  172. package/src/panels/i18n-panel.ts +6 -4
  173. package/src/panels/imports-panel.ts +1 -1
  174. package/src/panels/jump-bar.ts +20 -18
  175. package/src/panels/layers-panel.ts +19 -17
  176. package/src/panels/left-panel.ts +3 -3
  177. package/src/panels/navigator-panels.ts +8 -8
  178. package/src/panels/overlays.ts +1 -1
  179. package/src/panels/pane-context.ts +56 -28
  180. package/src/panels/pane-grid.ts +9 -8
  181. package/src/panels/panel-registry.ts +47 -19
  182. package/src/panels/problems-panel.ts +11 -12
  183. package/src/panels/properties-panel.ts +21 -20
  184. package/src/panels/provenance.ts +3 -3
  185. package/src/panels/quick-search.ts +15 -11
  186. package/src/panels/right-panel.ts +4 -3
  187. package/src/panels/seo-modal.ts +5 -4
  188. package/src/panels/settings-menu.ts +9 -9
  189. package/src/panels/settings-pane.ts +3 -3
  190. package/src/panels/signals-panel.ts +16 -9
  191. package/src/panels/statement-editor.ts +12 -9
  192. package/src/panels/style-panel.ts +45 -41
  193. package/src/panels/stylebook-panel.ts +0 -1
  194. package/src/panels/tab-strip.ts +26 -23
  195. package/src/platform-errors.ts +2 -2
  196. package/src/platform.ts +44 -4
  197. package/src/platforms/cloud.ts +11 -1
  198. package/src/platforms/default-platform.ts +141 -4
  199. package/src/platforms/devserver.ts +23 -3
  200. package/src/publish/deploy-checklist.ts +11 -10
  201. package/src/publish/publish-commands.ts +9 -8
  202. package/src/services/a11y-report.ts +1 -1
  203. package/src/services/ai-ask.ts +39 -16
  204. package/src/services/ai-command-tools.ts +28 -14
  205. package/src/services/ai-import-tools.ts +43 -29
  206. package/src/services/ai-project-tools.ts +20 -38
  207. package/src/services/ai-settings.ts +55 -2
  208. package/src/services/ai-tools.ts +69 -58
  209. package/src/services/ai-write-report.ts +4 -4
  210. package/src/services/ai-writes.ts +74 -55
  211. package/src/services/automation.ts +12 -12
  212. package/src/services/clock.ts +2 -1
  213. package/src/services/connection.ts +2 -2
  214. package/src/services/context-manager.ts +69 -6
  215. package/src/services/directory-picker.ts +93 -27
  216. package/src/services/document-assistant.ts +86 -17
  217. package/src/services/gated-registry.ts +4 -3
  218. package/src/services/idle.ts +5 -4
  219. package/src/services/live-surfaces.ts +2 -1
  220. package/src/services/notify.ts +9 -7
  221. package/src/services/popover-report.ts +3 -3
  222. package/src/services/preview-eval.ts +1 -1
  223. package/src/services/project-adoption.ts +34 -1
  224. package/src/services/project-path.ts +53 -0
  225. package/src/services/settings/kernel.ts +13 -2
  226. package/src/services/tool-executor.ts +135 -30
  227. package/src/services/tool-outcomes.ts +143 -0
  228. package/src/settings/contexts-section.ts +8 -8
  229. package/src/settings/contributed-section.ts +6 -6
  230. package/src/settings/css-vars-editor.ts +6 -5
  231. package/src/settings/extension-commands.ts +19 -18
  232. package/src/settings/extension-sections.ts +7 -7
  233. package/src/settings/extensions-section.ts +3 -3
  234. package/src/settings/general-settings.ts +21 -18
  235. package/src/settings/preferences-accounts.ts +3 -3
  236. package/src/settings/preferences-dialog.ts +9 -8
  237. package/src/settings/preferences-sections.ts +1 -1
  238. package/src/settings/project-sections.ts +2 -2
  239. package/src/settings/section-registry.ts +7 -6
  240. package/src/settings/settings-document.ts +41 -43
  241. package/src/shell/tree.ts +29 -0
  242. package/src/shell.ts +63 -55
  243. package/src/site-context.ts +1 -1
  244. package/src/store.ts +2 -2
  245. package/src/studio.ts +46 -28
  246. package/src/style/live-preview.ts +8 -8
  247. package/src/style/project-styles.ts +8 -9
  248. package/src/style/token-ref.ts +2 -2
  249. package/src/surfaces/about.json +6 -2
  250. package/src/surfaces/add-repo.json +2 -2
  251. package/src/surfaces/add-repo.ts +2 -2
  252. package/src/surfaces/ai-chat.json +14 -6
  253. package/src/surfaces/ai-chat.ts +4 -2
  254. package/src/surfaces/ai-credentials-form.json +53 -49
  255. package/src/surfaces/ai-credentials-form.ts +18 -4
  256. package/src/surfaces/ai-managed-connect.json +4 -4
  257. package/src/surfaces/block-action-bar.json +4 -4
  258. package/src/surfaces/block-action-bar.ts +3 -3
  259. package/src/surfaces/boot-failure.json +228 -0
  260. package/src/surfaces/boot-failure.ts +156 -0
  261. package/src/surfaces/bottom-dock.json +1 -1
  262. package/src/surfaces/canvas-stage.json +9 -40
  263. package/src/surfaces/canvas-stage.ts +9 -9
  264. package/src/surfaces/cf-account-picker.json +11 -8
  265. package/src/surfaces/commandbar.ts +11 -10
  266. package/src/surfaces/convert-repeater.ts +3 -2
  267. package/src/surfaces/dialog.json +7 -1
  268. package/src/surfaces/dialog.ts +2 -2
  269. package/src/surfaces/doc-header.json +6 -13
  270. package/src/surfaces/doc-header.ts +4 -5
  271. package/src/surfaces/entry-editor.json +1 -1
  272. package/src/surfaces/entry-editor.ts +2 -1
  273. package/src/surfaces/expression-editor.json +5 -5
  274. package/src/surfaces/files-panel.json +4 -3
  275. package/src/surfaces/files-panel.ts +1 -1
  276. package/src/surfaces/formula-palette.json +2 -2
  277. package/src/surfaces/formula-palette.ts +1 -1
  278. package/src/surfaces/git-panel.json +167 -109
  279. package/src/surfaces/git-panel.ts +2 -2
  280. package/src/surfaces/github-auth.json +2 -1
  281. package/src/surfaces/github-publish.ts +3 -3
  282. package/src/surfaces/grid-open.json +1 -1
  283. package/src/surfaces/grid-panel.json +2 -2
  284. package/src/surfaces/grid-views.json +1 -1
  285. package/src/surfaces/inspector-dock.json +2 -2
  286. package/src/surfaces/jump-bar.json +1 -1
  287. package/src/surfaces/jump-bar.ts +1 -1
  288. package/src/surfaces/library-pane.ts +2 -1
  289. package/src/surfaces/logic-panel.json +7 -7
  290. package/src/surfaces/logic-panel.ts +6 -4
  291. package/src/surfaces/logic-workspace.json +2 -2
  292. package/src/surfaces/logic-workspace.ts +2 -2
  293. package/src/surfaces/media-browser.ts +3 -3
  294. package/src/surfaces/media-pane.ts +2 -1
  295. package/src/surfaces/menu.ts +5 -5
  296. package/src/surfaces/new-project.json +1 -1
  297. package/src/surfaces/pane-context.json +86 -57
  298. package/src/surfaces/pane-context.ts +2 -1
  299. package/src/surfaces/pane-grid.json +3 -3
  300. package/src/surfaces/panel-activity.json +2 -1
  301. package/src/surfaces/panel-deploy-checklist.json +1 -1
  302. package/src/surfaces/panel-elements.ts +2 -1
  303. package/src/surfaces/panel-i18n.ts +2 -1
  304. package/src/surfaces/panel-outline.json +1 -1
  305. package/src/surfaces/panel-outline.ts +4 -3
  306. package/src/surfaces/panel-page.json +2 -2
  307. package/src/surfaces/panel-page.ts +4 -1
  308. package/src/surfaces/panel-problems.json +4 -3
  309. package/src/surfaces/panel-problems.ts +2 -2
  310. package/src/surfaces/panel-signals.json +23 -8
  311. package/src/surfaces/panel-signals.ts +5 -1
  312. package/src/surfaces/panel-stylebook-layers.json +1 -1
  313. package/src/surfaces/panel-stylebook-layers.ts +2 -2
  314. package/src/surfaces/preferences.json +21 -8
  315. package/src/surfaces/preferences.ts +9 -1
  316. package/src/surfaces/properties-panel.json +4 -4
  317. package/src/surfaces/properties-panel.ts +2 -2
  318. package/src/surfaces/publish.json +1 -1
  319. package/src/surfaces/publish.ts +2 -1
  320. package/src/surfaces/push-plan.json +4 -3
  321. package/src/surfaces/rail.json +2 -2
  322. package/src/surfaces/rail.ts +10 -4
  323. package/src/surfaces/reference-field.json +1 -1
  324. package/src/surfaces/schema-form.json +2 -2
  325. package/src/surfaces/schema-form.ts +1 -1
  326. package/src/surfaces/seo.json +1 -1
  327. package/src/surfaces/seo.ts +2 -2
  328. package/src/surfaces/settings-contributed.ts +2 -1
  329. package/src/surfaces/settings-css-vars.json +4 -4
  330. package/src/surfaces/settings-deploy.json +4 -4
  331. package/src/surfaces/settings-deploy.ts +2 -1
  332. package/src/surfaces/settings-extensions.json +6 -6
  333. package/src/surfaces/settings-extensions.ts +6 -2
  334. package/src/surfaces/settings-locales.json +4 -4
  335. package/src/surfaces/settings-overview.ts +2 -1
  336. package/src/surfaces/settings-pane.ts +3 -2
  337. package/src/surfaces/settings-rawjson.ts +1 -1
  338. package/src/surfaces/slash-menu.ts +4 -4
  339. package/src/surfaces/statements.json +3 -3
  340. package/src/surfaces/statusbar.ts +23 -23
  341. package/src/surfaces/style-panel.json +9 -5
  342. package/src/surfaces/style-panel.ts +3 -3
  343. package/src/surfaces/tab-strip.json +2 -2
  344. package/src/surfaces/target-line.json +1 -1
  345. package/src/surfaces/target-line.ts +12 -10
  346. package/src/surfaces/welcome.json +7 -2
  347. package/src/tabs/doc-op-apply.ts +5 -5
  348. package/src/tabs/patch-ops.ts +3 -3
  349. package/src/tabs/project-config.ts +10 -10
  350. package/src/tabs/selection.ts +1 -1
  351. package/src/tabs/tab.ts +13 -14
  352. package/src/tabs/transact.ts +18 -24
  353. package/src/types.ts +27 -3
  354. package/src/ui/ai-credentials-form.ts +74 -16
  355. package/src/ui/ai-managed-connect.ts +68 -19
  356. package/src/ui/color-selector.ts +1 -1
  357. package/src/ui/expression-editor.ts +3 -3
  358. package/src/ui/form-controls.ts +6 -5
  359. package/src/ui/formula-catalog.ts +4 -4
  360. package/src/ui/formula-chips.ts +2 -2
  361. package/src/ui/layers.ts +20 -18
  362. package/src/ui/panel-resize.ts +5 -5
  363. package/src/ui/popover-surface.ts +2 -2
  364. package/src/ui/progress-modal.ts +3 -3
  365. package/src/ui/regions.ts +14 -9
  366. package/src/ui/schema-form.ts +10 -9
  367. package/src/ui/timing.ts +9 -7
  368. package/src/ui/value-source.ts +4 -4
  369. package/src/ui/virtual-window.ts +1 -1
  370. package/src/utils/base64.ts +20 -1
  371. package/src/utils/canvas-media.ts +1 -1
  372. package/src/utils/studio-utils.ts +0 -31
  373. package/src/view.ts +4 -3
  374. package/src/workspace/pane-derive.ts +14 -13
  375. package/src/workspace/session.ts +7 -6
  376. package/src/workspace/workspace.ts +49 -39
  377. package/styles/canvas.css +1 -27
  378. package/styles/inspector.css +4 -3
  379. package/styles/overlays.css +1 -1
  380. package/styles/tokens.css +17 -1
  381. package/styles/tokens.json +6 -1
  382. package/dist/chunks/entry-commands-1wnsqn5d.js +0 -42
  383. package/dist/chunks/layers-panel-73gfkcga.js +0 -41
  384. package/dist/chunks/publish-panel-z1zd2da1.js +0 -27
  385. package/dist/chunks/quick-search-zdv7h8ht.js +0 -47
  386. package/dist/chunks/right-panel-889vbnm5.js +0 -45
  387. package/dist/chunks/studio-727xh0e3.js +0 -76
  388. package/src/files/json-layout.ts +0 -474
  389. package/src/services/ai-turn-signal.ts +0 -70
@@ -3,7 +3,8 @@
3
3
  * Idle.ts — "settled" is a predicate, and it can fail.
4
4
  *
5
5
  * One question — _has Studio finished reacting?_ — asked of the five subsystems that can still be
6
- * mid-flight, over two consecutive animation frames (spec studio.md §13.5, plan §13.4).
6
+ * mid-flight, over two consecutive animation frames (spec studio.md §13.5, and "Determinism" in
7
+ * `scripts/screenshots/README.md`).
7
8
  *
8
9
  * **The rejection is the point.** The thing this replaces is 115 `wait: {ms}` steps totalling 73
9
10
  * seconds, and a sleep cannot fail: a subsystem that was slow that day got answered with `+500 ms`
@@ -29,7 +30,7 @@
29
30
  * all thirty-odd `git*`/fs/fetch methods pass through.
30
31
  * 4. `layers.overlayIdleBlockers()` — overlays still in transition. Only the SETTLING window counts: a
31
32
  * resting toast is not a blocker, which is what lets `toastsAreHeld()` hold one open for a
32
- * capture without `probeIdle()` waiting forever alongside it (plan §13.7's named exception).
33
+ * capture without `probeIdle()` waiting forever alongside it (§13.5's toast exception).
33
34
  * 5. `activity-panel.activityIdleBlockers()` — operations still RUNNING in the Activity tab. Not a
34
35
  * duplicate of `platform`: an activity spans a whole operation (an install, a deploy, a clone)
35
36
  * across many PAL calls, so the gaps between them look quiet at the seam while the operation is
@@ -38,8 +39,8 @@
38
39
  * selection range `selectableRange: 1` gives them. Nothing else covered Tabulator: a grid
39
40
  * command resolves when the panel mounts, which is several frames before the table is drawn.
40
41
  *
41
- * The consumers are the `packages/studio:verify` skill, the screenshot runner and P4.2's Activity
42
- * tracker (this is a read-only projection of the same in-flight set that dock renders).
42
+ * The consumers are the `packages/studio:verify` skill, the screenshot runner and the Activity
43
+ * tracker of §16.4 (this is a read-only projection of the same in-flight set that dock renders).
43
44
  */
44
45
 
45
46
  import { rendersInFlight } from "../store";
@@ -17,7 +17,8 @@
17
17
  * outside Studio reaches the shell on the next reload, as it always did, because a watcher is a
18
18
  * second writer racing the tab. And it does not diff: a saved document is re-registered whole and
19
19
  * every root re-mounted, because a surface's host scope is reused (`SurfaceMount.remount`) and only
20
- * the surface-local state resets, which is the contract §9.3 gives the registry.
20
+ * the surface-local state resets, which is the contract `studio-ui-guidelines.md` §9.3 gives the
21
+ * registry.
21
22
  *
22
23
  * A kit redefinition reaches the canvas frames too, with the FILE's URL under the project as its
23
24
  * base: the frame draws a project's elements from the project's own files, so a sibling `$ref` in
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Notify.ts — the third record type (plan §1), and the app's only way to say what happened.
2
+ * Notify.ts — the third record type, beside `Command` and `Panel` (§16), and the app's only way to
3
+ * say what happened.
3
4
  *
4
5
  * What this replaces: 78 `statusMessage()` call sites — 26 failures and 52 successes — rendered in
5
6
  * identical 11px grey text in a 24px strip and destroyed after 3000 ms. A failure and a save looked
@@ -7,20 +8,20 @@
7
8
  * read them. `statusMessage` is deleted, not wrapped; `scripts/check-styles.ts` bans the name from
8
9
  * `src/` so it cannot regrow.
9
10
  *
10
- * **A notification is a record, and its lifetime is chosen by the action it requires** (§7.1):
11
+ * **A notification is a record, and its lifetime is chosen by the action it requires** (§16.1):
11
12
  *
12
13
  * | Tier | Host | Lifetime | Use when |
13
14
  * | --------- | -------------------------- | ------------------ | ------------------------------ |
14
15
  * | `toast` | the fourth overlay layer | timed, dismissible | reversible, or needs no action |
15
16
  * | `problem` | the Bottom dock's Problems | until it is fixed | it must be fixed |
16
17
  *
17
- * §7.1 names a third tier — **inline**, the refusal a field draws at the control (`jx-field`'s own
18
+ * §16.1 names a third tier — **inline**, the refusal a field draws at the control (`jx-field`'s own
18
19
  * `[part="error"]`, projected by whichever inspector document owns the row). It is deliberately NOT
19
20
  * a member of {@link NotificationTier}: an inline error is a value a control renders next to the
20
21
  * field the user is editing, not a record the app posts to a host, and it has no store to live in.
21
22
  * Declaring a tier here whose only possible outcome is being dropped on the floor would be exactly
22
23
  * the silence this module exists to end, so the union has two members and the third tier arrives as
23
- * a field's own projected value (P4.4).
24
+ * a field's own projected value (§16.5).
24
25
  *
25
26
  * **Recovery is a command id, not a per-call-site closure.** `notify.error("Save failed", { action:
26
27
  * "file.save" })` gives the toast and the Problems row a Retry button whose label, availability,
@@ -64,8 +65,9 @@ const DEFAULT_TIER: Readonly<Record<Severity, NotificationTier>> = {
64
65
  /**
65
66
  * How long a toast rests before it is taken away, by severity.
66
67
  *
67
- * §7.1 caps the band at 4–8s. A warning that nobody has to act on still deserves the long end of
68
- * it, because it is the one a reader is most likely to have looked away from.
68
+ * `studio-ui-guidelines.md` §13.2 caps the band at 4–8s. A warning that nobody has to act on still
69
+ * deserves the long end of it, because it is the one a reader is most likely to have looked away
70
+ * from.
69
71
  */
70
72
  export const TOAST_LIFETIME_MS: Readonly<Record<Severity, number>> = {
71
73
  error: 8000,
@@ -214,7 +216,7 @@ export function notify(
214
216
  return record;
215
217
  }
216
218
 
217
- /** `notify.success(msg, opts)` and its three siblings — the shape §7.1 names. */
219
+ /** `notify.success(msg, opts)` and its three siblings — the shape §16.1 names. */
218
220
  function severityHelper(severity: Severity) {
219
221
  return (message: string, options: NotifyOptions = {}) => notify(severity, message, options);
220
222
  }
@@ -113,9 +113,9 @@ export function reportPopoverProblems(doc: JxElement, path?: string): number {
113
113
  ...(path === undefined ? {} : { path }),
114
114
  });
115
115
  }
116
- /* The dialog and invoker-command rules (spec §8.7) file under the same source: one report for
117
- everything the platform overlays. They carry no repair button yet — the popover repairs move
118
- `display` into `:popover-open`, and a dialog's open rule is a different selector. */
116
+ /* The dialog and invoker-command rules (`spec.md` §8.7) file under the same source: one report
117
+ for everything the platform overlays. They carry no repair button yet — the popover repairs
118
+ move `display` into `:popover-open`, and a dialog's open rule is a different selector. */
119
119
  const dialogDefects = findDialogDefects(doc, KIT_SCOPE);
120
120
  for (const defect of dialogDefects) {
121
121
  notify(defect.severity, defect.message, {
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Parent-side live expression preview (spec §19.9).
3
+ * Parent-side live expression preview (`spec.md` §19.9).
4
4
  *
5
5
  * Evaluates an expression node against the canvas iframe's last dataScope snapshot
6
6
  * (tab.session.canvas.scope) on a structuredClone, so mutating operators are safe — they mutate the
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { initProjectRepo } from "../files/files";
13
+ import { getPlatform, hasPlatform } from "../platform";
13
14
  import { beginBatch, endBatch, isBatching } from "../tabs/transact";
14
15
  import { workspace } from "../workspace/workspace";
15
16
  import type { Tab } from "../tabs/tab";
@@ -41,6 +42,12 @@ export interface AdoptOutcome {
41
42
  adopted: boolean;
42
43
  /** Why adoption failed, when it threw. Null when it did not (including when it silently did not). */
43
44
  error: string | null;
45
+ /**
46
+ * `adopted` is false BY DESIGN: a different project was already showing here, so the open flow
47
+ * below put the new one in a window of its own instead of replacing what this one is showing. Not
48
+ * a failure — a caller that only checks `adopted` would otherwise report a success as one.
49
+ */
50
+ openedElsewhere: boolean;
44
51
  }
45
52
 
46
53
  /** The seams {@link adoptCreatedProject} runs through, so each caller keeps the one it already had. */
@@ -84,6 +91,32 @@ export async function adoptCreatedProject(
84
91
  // Never fails the create: a project that was written stays written (initProjectRepo notifies).
85
92
  await initRepo(root);
86
93
 
94
+ /* `initRepo` ran git init through THIS window's backend — `platform.createProject` re-roots it to
95
+ `root` as a side effect of scaffolding, because git init has no explicit-root variant of its
96
+ own. That is fine when this window is about to show `root` too, but wrong when a DIFFERENT
97
+ project was already showing here: `adopt` below (`openRecentProject`) would open `root` in a
98
+ new window on exactly that condition, and that new-window flow dedupes by asking the backend
99
+ which window already has `root` open — which, thanks to the reroot above, now reads as THIS
100
+ one. Left alone, that either hands `root` back to this same window (skipping the new window
101
+ entirely) or opens a second window while leaving this one's backend silently pointed at `root`
102
+ even though its own UI still names the project it had before. Putting this window's backend
103
+ back where its UI says it is — before `adopt` runs — is what keeps both promises: the window
104
+ that already had a project keeps that project, backend included, and `root` gets a window of
105
+ its own. */
106
+ const platform = hasPlatform() ? getPlatform() : null;
107
+ const shownRoot = workspace.projectRoot;
108
+ const openedElsewhere = Boolean(
109
+ platform && shownRoot && shownRoot !== root && platform.openProjectInNewWindow,
110
+ );
111
+ if (openedElsewhere && shownRoot && platform?.setWindowProject) {
112
+ try {
113
+ await platform.setWindowProject(shownRoot);
114
+ } catch {
115
+ /* Best-effort restore — `adopt` below still runs, and a failed restore here is no worse than
116
+ the mismatch this guards against. */
117
+ }
118
+ }
119
+
87
120
  const wasBatching = isBatching();
88
121
  if (wasBatching) {
89
122
  endBatch();
@@ -98,5 +131,5 @@ export async function adoptCreatedProject(
98
131
  beginBatch(getTab());
99
132
  }
100
133
 
101
- return { adopted: workspace.projectRoot === root, error: adoptionError };
134
+ return { adopted: workspace.projectRoot === root, error: adoptionError, openedElsewhere };
102
135
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Project-path.ts — the containment check the assistant's file tools run before they touch disk.
3
+ *
4
+ * `ai.md` §4: a file tool refuses a path outside the open project itself, rather than leaving the
5
+ * server to. `list_files`, `read_file` and `write_file` always refused one; `create_component`
6
+ * and `create_page` took the model's path as given and handed it to the platform write. One module,
7
+ * so the two sets of tools cannot disagree about what "inside the project" means, and a leaf,
8
+ * because the document tools and the project tools already import each other's neighbours.
9
+ *
10
+ * @docs studio/ai/chat
11
+ * @license MIT
12
+ */
13
+
14
+ import type { ToolResult } from "@jxsuite/ai/tools";
15
+
16
+ /**
17
+ * Normalize a project-relative path, or return null when it escapes the project (absolute paths,
18
+ * `..` segments, drive letters, a home directory). The server re-checks; this keeps the error
19
+ * actionable.
20
+ *
21
+ * @param {unknown} path
22
+ * @returns {string | null}
23
+ */
24
+ export function normalizeRelPath(path: unknown): string | null {
25
+ if (typeof path !== "string" || !path.trim()) {
26
+ return null;
27
+ }
28
+ let p = path.trim().replaceAll("\\", "/");
29
+ while (p.startsWith("./")) {
30
+ p = p.slice(2);
31
+ }
32
+ if (p.startsWith("/") || p.startsWith("~") || /^[A-Za-z]:/.test(p)) {
33
+ return null;
34
+ }
35
+ if (p.split("/").includes("..")) {
36
+ return null;
37
+ }
38
+ return p;
39
+ }
40
+
41
+ /**
42
+ * The refusal a tool returns for a path {@link normalizeRelPath} rejected, quoting what the model
43
+ * sent so it can see what to change.
44
+ *
45
+ * @param {unknown} path
46
+ * @returns {ToolResult}
47
+ */
48
+ export function invalidPathResult(path: unknown): ToolResult {
49
+ return {
50
+ success: false,
51
+ error: `Invalid path ${JSON.stringify(path)} — use a path relative to the project root (no leading "/", no "..").`,
52
+ };
53
+ }
@@ -184,9 +184,20 @@ function apply(patch: SettingsPatch): string[] {
184
184
  return moved;
185
185
  }
186
186
 
187
+ /**
188
+ * What a write of `value` would store: the definition's own `normalize`, or the value as given.
189
+ *
190
+ * The one home of that rule, and exported for a reason: a form that asks "would Save change
191
+ * anything?" has to compare what the store WOULD hold, not what was typed — or a pasted key's
192
+ * trailing newline, or an endpoint's trailing slash, reads as an edit the store would erase.
193
+ */
194
+ export function normalizeSetting(definition: SettingDefinition, value: string): string {
195
+ return definition.normalize ? definition.normalize(value) : value;
196
+ }
197
+
187
198
  /** Store a value. An empty string is a VALUE; {@link clearSetting} is the only deletion. */
188
199
  export function setSetting(definition: SettingDefinition, value: string): void {
189
- const next = definition.normalize ? definition.normalize(value) : value;
200
+ const next = normalizeSetting(definition, value);
190
201
  const moved = apply({ [definition.key]: next });
191
202
  if (moved.length > 0) {
192
203
  queue.enqueue({ [definition.key]: next });
@@ -204,7 +215,7 @@ export function setSetting(definition: SettingDefinition, value: string): void {
204
215
  export function setSettings(entries: readonly [SettingDefinition, string][]): void {
205
216
  const patch: SettingsPatch = {};
206
217
  for (const [definition, value] of entries) {
207
- patch[definition.key] = definition.normalize ? definition.normalize(value) : value;
218
+ patch[definition.key] = normalizeSetting(definition, value);
208
219
  }
209
220
  const moved = apply(patch);
210
221
  if (moved.length > 0) {
@@ -3,16 +3,22 @@
3
3
  *
4
4
  * Streams a chat round, executes any tool calls the model makes (via a ToolRegistry backed by
5
5
  * `transactDoc()`), feeds the results back as `tool` messages, and re-streams — up to a capped
6
- * number of rounds (spec §10.2, ADR docs/ai-assistant-decision.md §6a).
6
+ * number of rounds (specs/ai.md §3.2 to §3.4).
7
7
  *
8
- * Two §7.4 honesty rules live here rather than in the panel, because they are properties of the RUN
9
- * and a panel can only render what the run recorded:
8
+ * Two `ai.md` §3.2 honesty rules live here rather than in the panel, because they are properties of
9
+ * the RUN and a panel can only render what the run recorded:
10
10
  *
11
11
  * - **A partial success is not a failure.** Running out of rounds called `setError`, which paints
12
12
  * the turn red and — in `chat-state.ts` — DELETES the streaming message, so a turn that applied
13
13
  * four edits and then hit the cap reported as an error that had also erased its own account of
14
14
  * the four edits. The cap is now an ordinary assistant message whenever anything was applied, and
15
- * an error only when nothing was.
15
+ * an error only when nothing was. **Applied means it wrote**: a call counts when it succeeded with
16
+ * a summary AND the write ledger grew by an `ok` write while it ran, not merely when it returned a
17
+ * summary, because a read returns a summary too, and a turn that only looked around was reported
18
+ * as having changed things.
19
+ * - **A turn that drew nothing says so.** A model that answered with neither text nor a tool call
20
+ * left the author's message sitting there with no reply at all. That turn ends on an error row,
21
+ * with Retry, instead.
16
22
  * - **A batch belongs to the tab it edits.** `beginBatch(getTab())` ran once, against whichever tab
17
23
  * happened to be active when the loop started. A turn that then moved to a second document closed
18
24
  * its batch against the FIRST tab, so the second document's edits got neither a history snapshot
@@ -23,16 +29,22 @@
23
29
 
24
30
  import type { createChatState } from "@jxsuite/ai/chat-state";
25
31
  import type { StreamingClient } from "@jxsuite/ai/streaming-client";
26
- import type { ToolRegistry } from "@jxsuite/ai/tools";
32
+ import { createSessionFacts, createToolContext, linkCallSignal } from "@jxsuite/ai/tools";
33
+ import type { Actor, SessionFacts, ToolRegistry } from "@jxsuite/ai/tools";
27
34
 
28
35
  import type { Tab } from "../tabs/tab";
36
+ import type { ImportProgressEvent } from "../types";
29
37
  import { batchTab, beginBatch, endBatch } from "../tabs/transact";
30
38
  import { ensureProxyProbe, resetModelCache } from "./ai-models";
31
- import { beginTurn, endTurn } from "./ai-writes";
32
- import { beginToolCall, beginTurnSignal, endTurnSignal } from "./ai-turn-signal";
39
+ import { estimatePromptTokens } from "./context-manager";
40
+ import { fileTurn, openTurnLedger, turnAnchor } from "./ai-writes";
41
+ import { recordImportProgress } from "./import-run";
33
42
 
34
43
  const MAX_ROUNDS = 5;
35
44
 
45
+ /** What a turn that drew nothing says: the model answered with neither text nor a tool call. */
46
+ export const EMPTY_TURN_TEXT = "The model sent back an empty reply.";
47
+
36
48
  /**
37
49
  * Hard ceiling on rounds of every kind, so the loop terminates whatever the model does.
38
50
  *
@@ -44,9 +56,6 @@ const MAX_ROUNDS = 5;
44
56
  */
45
57
  const MAX_TOTAL_ROUNDS = 25;
46
58
 
47
- /** Tools that suspend the turn on a human rather than doing work. */
48
- const INTERACTIVE_TOOLS = new Set(["ask_user"]);
49
-
50
59
  interface RunAgentLoopOptions {
51
60
  chatState: ReturnType<typeof createChatState>;
52
61
  streamingClient: StreamingClient;
@@ -54,6 +63,8 @@ interface RunAgentLoopOptions {
54
63
  systemPrompt: string;
55
64
  signal?: AbortSignal;
56
65
  getTab?: () => Tab | null;
66
+ /** The conversation's facts, which every call's context carries; a fresh set when omitted. */
67
+ session?: SessionFacts;
57
68
  }
58
69
 
59
70
  /**
@@ -67,18 +78,24 @@ export async function runAgentLoop({
67
78
  systemPrompt,
68
79
  signal,
69
80
  getTab,
81
+ session = createSessionFacts(),
70
82
  }: RunAgentLoopOptions): Promise<void> {
71
83
  const allErrors: string[] = [];
72
84
  const appliedSummaries: string[] = [];
73
85
 
74
- /** The ledger is filed under the assistant message the turn ends on — see services/ai-writes. */
75
- const turnId = () => chatState.messages.at(-1)?.id ?? "";
76
- beginTurn(`turn:${chatState.messages.length}`);
77
-
78
- /* The signal, published for the tools rather than passed to them: `ToolRegistry.execute` takes
79
- none. Without it a tool that waits for a human never learns the turn was stopped, and the
80
- `await` below is the whole loop. See services/ai-turn-signal.ts. */
81
- beginTurnSignal(signal);
86
+ /* The ledger is filed under the turn's last DRAWN assistant message (see services/ai-writes), so
87
+ the scan starts at the message this turn answers. */
88
+ const turnUserId = chatState.messages.findLast((m) => m.role === "user")?.id;
89
+ /* What every call's context carries (specs/ai.md §3.7): this turn's ledger, the conversation's
90
+ facts, and who the calls act as. The signal and the call id are the call's own, below. */
91
+ const ledger = openTurnLedger(`turn:${chatState.messages.length}`);
92
+ const actor: Actor = {
93
+ id: `assistant:${session.sessionId ?? "local"}:${ledger.turnId}`,
94
+ kind: "assistant",
95
+ model: chatState.model,
96
+ sessionId: session.sessionId,
97
+ turnId: ledger.turnId,
98
+ };
82
99
 
83
100
  // Batch all tool-call mutations into a single undo step, anchored on the tab being edited.
84
101
  if (getTab) {
@@ -149,6 +166,12 @@ export async function runAgentLoop({
149
166
  chatState.appendToolCallEnd(event.id);
150
167
  break;
151
168
  }
169
+ case "usage": {
170
+ /* The provider's own count replaces the four-characters-per-token estimate. The prompt
171
+ it covered is recorded with it, because the next send rebuilds the prompt. */
172
+ chatState.recordUsage(event, { systemTokens: estimatePromptTokens(systemPrompt) });
173
+ break;
174
+ }
152
175
  case "done": {
153
176
  ({ stopReason } = event);
154
177
  break;
@@ -164,8 +187,6 @@ export async function runAgentLoop({
164
187
  }
165
188
  }
166
189
 
167
- chatState.finishStream(stopReason);
168
-
169
190
  if (streamError) {
170
191
  /* A lapsed hosted grant is the one stream error that makes the app's OWN reading wrong.
171
192
  The probe settles once at boot, so without this every gate keeps offering an assistant
@@ -175,36 +196,91 @@ export async function runAgentLoop({
175
196
  resetModelCache();
176
197
  ensureProxyProbe();
177
198
  }
199
+ /* `setError` alone, never after `finishStream`: it removes the round's partial message,
200
+ and only while that message is still the streaming one. Finishing first let go of it, so
201
+ the partial (a tool call cut off mid-arguments, as often as not) stayed in the
202
+ transcript and went out on the next send. */
178
203
  chatState.setError(streamError);
179
204
  return;
180
205
  }
181
206
 
182
- if (stopReason !== "tool_calls" || toolCalls.size === 0) {
207
+ /* A turn that has drawn nothing by the end of a round with no calls ends here, as an error
208
+ the author can see and retry, rather than as silence under their message. `setError`
209
+ before `finishStream`, so it removes the empty reply. A stopped round is not empty: the
210
+ author ended it. */
211
+ const stopped = stopReason === "cancelled" || signal?.aborted === true;
212
+ if (toolCalls.size === 0 && !stopped && turnAnchor(chatState.messages, turnUserId) === null) {
213
+ chatState.setError(EMPTY_TURN_TEXT);
214
+ return;
215
+ }
216
+
217
+ chatState.finishStream(stopReason);
218
+
219
+ /* The calls the model streamed decide whether tools run, not the finish reason the provider
220
+ reported beside them. Some OpenAI-compatible backends (Workers AI among them) end a
221
+ tool-calling turn with `finish_reason: "stop"`, and gating on `tool_calls` dropped those
222
+ calls unrun — the turn ended looking finished, and the next send sealed them as failures.
223
+ A call cut off by `length` still runs: its arguments fail to parse, and the model reads
224
+ that as a tool error it can correct.
225
+
226
+ Cancellation is the exception, and it is not a finish reason at all: it is the author
227
+ pressing Stop. A stopped round runs nothing it streamed, including calls whose arguments
228
+ were already complete, because Stop is the one control that must not be outrun. */
229
+ if (toolCalls.size === 0 || stopReason === "cancelled" || signal?.aborted) {
183
230
  return;
184
231
  }
185
232
 
186
233
  let didWork = false;
187
234
  for (const [id, call] of toolCalls) {
235
+ /* A Stop that lands between calls (while `ask_user` waits, or during a long import) stops
236
+ the calls after it too. They stay unanswered, and the send path seals them. */
237
+ if (signal?.aborted) {
238
+ return;
239
+ }
188
240
  let result;
189
- // Published, not passed: `ToolRegistry.execute` takes the args and nothing else.
190
- beginToolCall(id);
241
+ // The ledger's length before the call, so the call can be judged by what it recorded.
242
+ const writesBefore = ledger.writes.length;
243
+ /* The call's own signal: it aborts with the turn, so a tool waiting on a person or a crawl
244
+ learns of a Stop, and it is unlinked once the call settles, so a Stop later in the turn
245
+ reaches nothing that has already finished. */
246
+ const link = signal ? linkCallSignal(signal) : null;
247
+ const ctx = createToolContext({
248
+ actor,
249
+ callId: id,
250
+ ledger,
251
+ progress: (event) => {
252
+ recordImportProgress(id, event as unknown as ImportProgressEvent);
253
+ },
254
+ session,
255
+ ...(link ? { signal: link.signal } : {}),
256
+ });
191
257
  try {
192
- const args = call.arguments ? (JSON.parse(call.arguments) as object) : {};
193
- result = await toolRegistry.execute(call.name, args);
258
+ result = await toolRegistry.execute(call.name, parseToolArguments(call.arguments), ctx);
194
259
  } catch (error) {
195
260
  result = {
196
261
  success: false,
197
262
  error: `Failed to parse arguments: ${(error as Error).message}`,
198
263
  };
264
+ } finally {
265
+ link?.release();
199
266
  }
200
- if (!INTERACTIVE_TOOLS.has(call.name)) {
267
+ /* The transcript was replaced while the call ran: another chat was opened from Chat
268
+ History, which stops the turn. The reply belongs to a request that is no longer there,
269
+ so writing it would land a stray `tool` message in the other conversation, and could
270
+ overwrite a restored record's result where a provider reuses call ids. */
271
+ if (turnUserId !== undefined && !chatState.messages.some((m) => m.id === turnUserId)) {
272
+ return;
273
+ }
274
+ // A call that suspends the turn on a person does not spend its work budget.
275
+ if (toolRegistry.getDefinition(call.name)?.interactive !== true) {
201
276
  didWork = true;
202
277
  }
203
278
  reanchorBatch();
204
279
  if (!result.success && result.error) {
205
280
  allErrors.push(result.error);
206
281
  }
207
- if (result.success && result.summary) {
282
+ const wrote = ledger.writes.slice(writesBefore).some((write) => write.ok);
283
+ if (result.success && result.summary && wrote) {
208
284
  appliedSummaries.push(result.summary);
209
285
  }
210
286
  chatState.appendToolResult(id, result);
@@ -214,6 +290,12 @@ export async function runAgentLoop({
214
290
  if (didWork) {
215
291
  workRounds += 1;
216
292
  }
293
+ /* A Stop that landed during the round's last call. Nothing is left to stop in this round, so
294
+ without this the loop opened the next one: a placeholder pushed after the author stopped,
295
+ and a request streamed on the aborted signal. */
296
+ if (signal?.aborted) {
297
+ return;
298
+ }
217
299
  if (workRounds < MAX_ROUNDS && round < MAX_TOTAL_ROUNDS) {
218
300
  chatState.beginAssistantTurn();
219
301
  }
@@ -222,7 +304,7 @@ export async function runAgentLoop({
222
304
  /*
223
305
  * The round cap. Surface the actual errors so the user knows what went wrong, not just a
224
306
  * generic "I couldn't do it" — and, when anything was applied, say it as the ASSISTANT rather
225
- * than as a failure (§7.4). A partial success is not a failure.
307
+ * than as a failure (`ai.md` §3.2). A partial success is not a failure.
226
308
  */
227
309
  const uniqueErrors = [...new Set(allErrors)];
228
310
  const applied =
@@ -244,8 +326,31 @@ export async function runAgentLoop({
244
326
  }
245
327
  chatState.setError(tail);
246
328
  } finally {
247
- endTurnSignal();
248
329
  endBatch();
249
- endTurn(turnId());
330
+ fileTurn(turnAnchor(chatState.messages, turnUserId) ?? "", ledger.writes);
331
+ }
332
+ }
333
+
334
+ /**
335
+ * A call's arguments as the object a tool takes.
336
+ *
337
+ * An empty string is no arguments. JSON that parses to something other than an object (`null`, an
338
+ * array, a number) is refused here with its own sentence, rather than reaching the registry, where
339
+ * a validator reading a property of `null` threw a message about the validator. The prefix is the
340
+ * one a JSON syntax error already carries, so the model reads both as one kind of mistake.
341
+ *
342
+ * @param {string} text - The call's accumulated argument text
343
+ * @returns {object}
344
+ * @throws {SyntaxError | TypeError} Caught by the loop and returned to the model as a tool error
345
+ */
346
+ function parseToolArguments(text: string): object {
347
+ if (!text) {
348
+ return {};
349
+ }
350
+ const parsed = JSON.parse(text) as unknown;
351
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
352
+ const type = parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed;
353
+ throw new TypeError(`arguments must be a JSON object, got ${type}`);
250
354
  }
355
+ return parsed;
251
356
  }