@jxsuite/studio 8.1.0 → 10.0.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 (266) hide show
  1. package/dist/chunks/{2020-dg097k3e.js → 2020-j56903f1.js} +3 -3
  2. package/dist/chunks/adapter-wc5zvg9a.js +4 -0
  3. package/dist/chunks/add-new-flow-snx1s4b0.js +4 -0
  4. package/dist/chunks/cf-account-picker-xyqw0jex.js +4 -0
  5. package/dist/chunks/cf-settings-hp1pjm0n.js +4 -0
  6. package/dist/chunks/color-04361ttk.js +4 -0
  7. package/dist/chunks/{color-area-vh9cew51.js → color-area-h8dbasp1.js} +2 -2
  8. package/dist/chunks/{color-field-e78f16bs.js → color-field-qv5r2b16.js} +3 -3
  9. package/dist/chunks/{color-slider-fg4yhvk6.js → color-slider-nz2mrzky.js} +2 -2
  10. package/dist/chunks/components-nkwphrx3.js +4 -0
  11. package/dist/chunks/content-source-6a253wmk.js +4 -0
  12. package/dist/chunks/{convert-to-repeater-grnmea6c.js → convert-to-repeater-fhdwyg14.js} +3 -3
  13. package/dist/chunks/editor-wqrc68ca.js +4 -0
  14. package/dist/chunks/entry-commands-nef0wrmp.js +4 -0
  15. package/dist/chunks/{extension-sections-rp7fkega.js → extension-sections-8ez2rrk8.js} +3 -3
  16. package/dist/chunks/file-ops-y3w71k1n.js +4 -0
  17. package/dist/chunks/files-de90apn9.js +4 -0
  18. package/dist/chunks/format-host-zfjz4rzm.js +4 -0
  19. package/dist/chunks/fs-events-77c0mr0x.js +4 -0
  20. package/dist/chunks/grid-open-4n4fjfpf.js +4 -0
  21. package/dist/chunks/{grid-view-x26rfpz1.js → grid-view-ka55z38f.js} +10 -10
  22. package/dist/chunks/{iframe-entry-pkgmcgp0.js → iframe-entry-2x9bn5ng.js} +4 -4
  23. package/dist/chunks/{iframe-entry-02wzcyvy.js → iframe-entry-btck0e9s.js} +4 -4
  24. package/dist/chunks/iframe-host-na2mywz4.js +4 -0
  25. package/dist/chunks/index-ewmgdb7c.js +4 -0
  26. package/dist/chunks/{index-203h71wy.js → index-qb98xhse.js} +3 -3
  27. package/dist/chunks/{javascript-ww5pv5ae.js → javascript-ces13wny.js} +2 -2
  28. package/dist/chunks/{jsonMode-vwbdwwq3.js → jsonMode-6md16d4v.js} +6 -6
  29. package/dist/chunks/jx-validate-b309c020.js +4 -0
  30. package/dist/chunks/layers-8rn8y6qv.js +4 -0
  31. package/dist/chunks/layers-panel-eyah7h9z.js +4 -0
  32. package/dist/chunks/lit-html-wyqw04y2.js +4 -0
  33. package/dist/chunks/media-library-gs3np47y.js +4 -0
  34. package/dist/chunks/media-picker-tt7dk4tb.js +4 -0
  35. package/dist/chunks/{monaco-binding-63tqze27.js → monaco-binding-p8j80s4w.js} +3 -3
  36. package/dist/chunks/monaco-buffer-wsf6cmnq.js +4 -0
  37. package/dist/chunks/{monaco-setup-zyyynnj8.js → monaco-setup-wasktzfg.js} +3 -3
  38. package/dist/chunks/notify-qwst108r.js +4 -0
  39. package/dist/chunks/parse-n8dfxnqf.js +4 -0
  40. package/dist/chunks/platform-c16mqvmp.js +4 -0
  41. package/dist/chunks/publish-panel-d34bhvwj.js +4 -0
  42. package/dist/chunks/{publish-quick-ajzdhhs6.js → publish-quick-vr695j26.js} +3 -3
  43. package/dist/chunks/publish-workspace-2yr60mt4.js +4 -0
  44. package/dist/chunks/quick-search-e6xsdv0d.js +4 -0
  45. package/dist/chunks/references-gsj6trdv.js +4 -0
  46. package/dist/chunks/right-panel-33jv1knc.js +4 -0
  47. package/dist/chunks/source-control-bj9bejjv.js +4 -0
  48. package/dist/chunks/studio-03mqh8ax.js +5 -0
  49. package/dist/chunks/{studio-957jg8g3.js → studio-06j4en5h.js} +4 -4
  50. package/dist/chunks/studio-0ydxv2mr.js +7 -0
  51. package/dist/chunks/{studio-v2ek69cb.js → studio-1e8dw2w2.js} +4 -4
  52. package/dist/chunks/studio-2hsqbq7b.js +7 -0
  53. package/dist/chunks/studio-2j86y34y.js +6 -0
  54. package/dist/chunks/studio-2ks6ka4g.js +5 -0
  55. package/dist/chunks/studio-34jrz12k.js +5 -0
  56. package/dist/chunks/{studio-1meqbxns.js → studio-35vrtpdq.js} +4 -4
  57. package/dist/chunks/studio-38g0jdz7.js +5 -0
  58. package/dist/chunks/{studio-25a5by3h.js → studio-396srjbk.js} +4 -4
  59. package/dist/chunks/{studio-zm7dnr9g.js → studio-3c8pp7pb.js} +4 -4
  60. package/dist/chunks/{studio-x3ge4rts.js → studio-3v5cagy9.js} +4 -4
  61. package/dist/chunks/studio-3w35qvay.js +5 -0
  62. package/dist/chunks/studio-3zge48z4.js +7 -0
  63. package/dist/chunks/studio-45gyxpcq.js +5 -0
  64. package/dist/chunks/{studio-hw60n5dh.js → studio-4kzbrr5t.js} +3 -3
  65. package/dist/chunks/{studio-533py3yy.js → studio-5tzgq6fx.js} +4 -4
  66. package/dist/chunks/studio-5wzdy1jc.js +16 -0
  67. package/dist/chunks/studio-732mw0g8.js +5 -0
  68. package/dist/chunks/{studio-3q6n9qvq.js → studio-771k2n12.js} +4 -4
  69. package/dist/chunks/{studio-z42kysg7.js → studio-7dzyz9n8.js} +137 -137
  70. package/dist/chunks/{studio-fqs1a22a.js → studio-7xag4na8.js} +4 -4
  71. package/dist/chunks/{studio-jqrg9xxg.js → studio-881z1xrx.js} +4 -4
  72. package/dist/chunks/studio-88bmt9rh.js +5 -0
  73. package/dist/chunks/{studio-8nyw10dp.js → studio-9b7177jq.js} +4 -4
  74. package/dist/chunks/{studio-nm9qev4p.js → studio-9cxmp61c.js} +5 -5
  75. package/dist/chunks/{studio-4dsdnan2.js → studio-9dz76yqc.js} +5 -5
  76. package/dist/chunks/{studio-a6x058ew.js → studio-agyvzfm4.js} +4 -4
  77. package/dist/chunks/{studio-2b8443ms.js → studio-ajw4k8hp.js} +3 -3
  78. package/dist/chunks/studio-c1pxszat.js +5 -0
  79. package/dist/chunks/studio-cpkamjdn.js +14 -0
  80. package/dist/chunks/studio-d038kqbs.js +5 -0
  81. package/dist/chunks/studio-d83m0wyq.js +5 -0
  82. package/dist/chunks/studio-dbxv7w4s.js +5 -0
  83. package/dist/chunks/studio-de1s2x9v.js +5 -0
  84. package/dist/chunks/{studio-94ppxeqy.js → studio-dx73md4n.js} +8 -8
  85. package/dist/chunks/studio-ekgtrndg.js +5 -0
  86. package/dist/chunks/studio-epenqmvk.js +7 -0
  87. package/dist/chunks/studio-et2q4s8n.js +5 -0
  88. package/dist/chunks/{studio-txkx6586.js → studio-ewarnzm4.js} +7 -7
  89. package/dist/chunks/{studio-bf0rgyf7.js → studio-fy7jzj9j.js} +4 -4
  90. package/dist/chunks/{studio-332hesdk.js → studio-gr36vnte.js} +4 -4
  91. package/dist/chunks/{studio-rdnezzkd.js → studio-gwrhcrss.js} +5 -5
  92. package/dist/chunks/{studio-9wyqyfqy.js → studio-h310mbqm.js} +4 -4
  93. package/dist/chunks/{studio-zht0x0v3.js → studio-h5tpz5ke.js} +4 -4
  94. package/dist/chunks/studio-h7weqf2q.js +5 -0
  95. package/dist/chunks/{studio-1gdfsr49.js → studio-hcsba201.js} +7 -7
  96. package/dist/chunks/studio-hwejq03c.js +5 -0
  97. package/dist/chunks/{studio-wcakp5bt.js → studio-hy4ye0dx.js} +7 -7
  98. package/dist/chunks/{studio-02phtmqn.js → studio-jttyqrmh.js} +4 -4
  99. package/dist/chunks/{studio-j5jqvyn3.js → studio-jw0q5j6k.js} +4 -4
  100. package/dist/chunks/studio-k3a8rp3d.js +5 -0
  101. package/dist/chunks/{studio-hnx1h1pr.js → studio-k3bcy02e.js} +4 -4
  102. package/dist/chunks/studio-m48v13s3.js +4 -0
  103. package/dist/chunks/{studio-bqgs90r8.js → studio-ndskgqef.js} +4 -4
  104. package/dist/chunks/studio-p1n2xjda.js +5 -0
  105. package/dist/chunks/studio-pgm83a2f.js +6 -0
  106. package/dist/chunks/studio-pqqq6v85.js +5 -0
  107. package/dist/chunks/studio-psh8mwnw.js +4 -0
  108. package/dist/chunks/{studio-cpdzrdgp.js → studio-r17asgs0.js} +4 -4
  109. package/dist/chunks/{studio-6vrxzkba.js → studio-ry81j4q1.js} +4 -4
  110. package/dist/chunks/studio-rzg8hnkf.js +5 -0
  111. package/dist/chunks/{studio-nsv7g1qn.js → studio-s0fg2wav.js} +4 -4
  112. package/dist/chunks/{studio-v0gsjav0.js → studio-tj2s0tks.js} +4 -4
  113. package/dist/chunks/studio-tyw7tk0x.js +5 -0
  114. package/dist/chunks/studio-v9ywz2se.js +6 -0
  115. package/dist/chunks/{studio-dmn7gpba.js → studio-vswcpzk7.js} +5 -5
  116. package/dist/chunks/studio-vzgmyt0v.js +5 -0
  117. package/dist/chunks/{studio-2dtc90zs.js → studio-wxdcd42g.js} +6 -6
  118. package/dist/chunks/{studio-c5dj3dzk.js → studio-wxhy1yh7.js} +4 -4
  119. package/dist/chunks/studio-x08a3m0a.js +5 -0
  120. package/dist/chunks/studio-x3mjr5cc.js +5 -0
  121. package/dist/chunks/{studio-xddgbwhe.js → studio-x97ay0v4.js} +4 -4
  122. package/dist/chunks/{studio-mmqr9awv.js → studio-xdpz9rpe.js} +33 -33
  123. package/dist/chunks/studio-xh683qzy.js +5 -0
  124. package/dist/chunks/{studio-hxq1ebp0.js → studio-xkxetm65.js} +6 -6
  125. package/dist/chunks/{studio-73wz63nn.js → studio-y26hx9re.js} +4 -4
  126. package/dist/chunks/{studio-kdseew9m.js → studio-yg3djpnm.js} +4 -4
  127. package/dist/chunks/studio-yx8hzqja.js +5 -0
  128. package/dist/chunks/studio-zg16eeq1.js +5 -0
  129. package/dist/chunks/studio-zgx7svjd.js +9 -0
  130. package/dist/chunks/studio-zkq3r91p.js +5 -0
  131. package/dist/chunks/{toast-host-1yg5ar8d.js → toast-host-g3r2s0kn.js} +2 -2
  132. package/dist/chunks/toast-w11qpkyz.js +4 -0
  133. package/dist/chunks/{tsMode-vra2hh7y.js → tsMode-h6tkz0ea.js} +5 -5
  134. package/dist/chunks/workspace-home-zkkrxq3r.js +4 -0
  135. package/dist/chunks/workspace-tdqqjqaa.js +4 -0
  136. package/dist/chunks/{ws-client-72x5n99j.js → ws-client-e951my95.js} +3 -3
  137. package/dist/iframe-entry.js +13 -13
  138. package/dist/manifest.json +77 -77
  139. package/dist/studio.js +91 -92
  140. package/package.json +11 -12
  141. package/src/canvas/iframe-editable-root.ts +32 -2
  142. package/src/collab/collab-session.ts +6 -2
  143. package/src/commands/context.ts +4 -0
  144. package/src/commands/defaults.ts +2 -0
  145. package/src/commands/live-context.ts +2 -0
  146. package/src/files/files.ts +6 -3
  147. package/src/format/format-host.ts +1 -1
  148. package/src/github/github-auth-status.ts +2 -2
  149. package/src/media/media-library.ts +73 -3
  150. package/src/new-project/new-project-modal.ts +1 -4
  151. package/src/packages/ensure-deps.ts +5 -5
  152. package/src/packages/pull-package-sync.ts +4 -4
  153. package/src/panels/activity-panel.ts +1 -1
  154. package/src/panels/ai-panel.ts +27 -34
  155. package/src/platform.ts +20 -0
  156. package/src/platforms/cloud.ts +48 -34
  157. package/src/platforms/devserver.ts +2 -120
  158. package/src/services/ai-ask.ts +29 -171
  159. package/src/services/ai-command-tools.ts +40 -50
  160. package/src/services/ai-import-tools.ts +4 -5
  161. package/src/services/ai-project-tools.ts +13 -3
  162. package/src/services/ai-tools.ts +26 -564
  163. package/src/services/ai-writes.ts +84 -2
  164. package/src/services/document-assistant.ts +29 -58
  165. package/src/services/harness/assistant-tools.ts +68 -0
  166. package/src/services/harness/interaction-store.ts +118 -0
  167. package/src/services/harness/studio-documents.ts +105 -0
  168. package/src/services/harness/turn-hooks.ts +1 -6
  169. package/src/services/project-adoption.ts +8 -16
  170. package/src/services/tool-executor.ts +25 -22
  171. package/src/settings/extension-commands.ts +1 -1
  172. package/src/settings/extension-rows.ts +1 -6
  173. package/src/settings/extensions-section.ts +3 -6
  174. package/src/state.ts +3 -13
  175. package/src/surfaces/block-action-bar.json +1 -3
  176. package/src/surfaces/files-panel.json +12 -0
  177. package/src/surfaces/files-panel.ts +5 -2
  178. package/src/surfaces/media-library.json +35 -0
  179. package/src/surfaces/media-library.ts +2 -0
  180. package/src/surfaces/menu.ts +62 -1
  181. package/src/surfaces/settings-packages.ts +1 -1
  182. package/src/surfaces/welcome.ts +4 -1
  183. package/src/tabs/selection.ts +14 -109
  184. package/src/tabs/tab.ts +10 -1
  185. package/src/tabs/transact.ts +316 -124
  186. package/src/types.ts +23 -16
  187. package/src/ui/layers.ts +1 -1
  188. package/src/ui/progress-modal.ts +3 -3
  189. package/dist/chunks/adapter-tck81zft.js +0 -4
  190. package/dist/chunks/add-new-flow-a1w0bnm6.js +0 -4
  191. package/dist/chunks/cf-account-picker-ce3kp3x1.js +0 -4
  192. package/dist/chunks/cf-settings-7ye6x7g2.js +0 -4
  193. package/dist/chunks/color-4gj49xq6.js +0 -4
  194. package/dist/chunks/components-qce0v93f.js +0 -4
  195. package/dist/chunks/content-source-jjx8kgzr.js +0 -4
  196. package/dist/chunks/editor-bkja5dbv.js +0 -4
  197. package/dist/chunks/entry-commands-1wh0btvg.js +0 -4
  198. package/dist/chunks/file-ops-6pe4z7an.js +0 -4
  199. package/dist/chunks/files-ybpp3j38.js +0 -4
  200. package/dist/chunks/format-host-3mtbhxk1.js +0 -4
  201. package/dist/chunks/fs-events-xfanfxds.js +0 -4
  202. package/dist/chunks/grid-open-dg5qmnaj.js +0 -4
  203. package/dist/chunks/iframe-host-j2bk2xkc.js +0 -4
  204. package/dist/chunks/index-fkrm7pdk.js +0 -4
  205. package/dist/chunks/jx-validate-w7v32f6a.js +0 -4
  206. package/dist/chunks/layers-g7j732cm.js +0 -4
  207. package/dist/chunks/layers-panel-gjrsxcck.js +0 -4
  208. package/dist/chunks/lit-html-jdfz9rhc.js +0 -4
  209. package/dist/chunks/media-library-9j0v4wmk.js +0 -4
  210. package/dist/chunks/media-picker-cpyyt2m0.js +0 -4
  211. package/dist/chunks/monaco-buffer-9wcme176.js +0 -4
  212. package/dist/chunks/notify-rahnnnv4.js +0 -4
  213. package/dist/chunks/parse-v77xtwv7.js +0 -4
  214. package/dist/chunks/platform-b2aqv7v5.js +0 -4
  215. package/dist/chunks/publish-panel-4yk2461b.js +0 -4
  216. package/dist/chunks/publish-workspace-mxestzcf.js +0 -4
  217. package/dist/chunks/quick-search-9g6z65b0.js +0 -4
  218. package/dist/chunks/references-c26enpxs.js +0 -4
  219. package/dist/chunks/right-panel-9d0e0yg7.js +0 -4
  220. package/dist/chunks/source-control-xyn3a8ds.js +0 -4
  221. package/dist/chunks/studio-0khd0xdw.js +0 -5
  222. package/dist/chunks/studio-1fvzq9vs.js +0 -5
  223. package/dist/chunks/studio-1yhz0s8p.js +0 -5
  224. package/dist/chunks/studio-2c2mjyzj.js +0 -6
  225. package/dist/chunks/studio-3pndhat0.js +0 -9
  226. package/dist/chunks/studio-4fjqdzrb.js +0 -6
  227. package/dist/chunks/studio-5abge3wy.js +0 -5
  228. package/dist/chunks/studio-74nzv280.js +0 -5
  229. package/dist/chunks/studio-7cg75gtj.js +0 -14
  230. package/dist/chunks/studio-89cwmnae.js +0 -5
  231. package/dist/chunks/studio-8m1t83kn.js +0 -7
  232. package/dist/chunks/studio-a3evp7f6.js +0 -6
  233. package/dist/chunks/studio-ana67wax.js +0 -5
  234. package/dist/chunks/studio-b4xhpcwy.js +0 -4
  235. package/dist/chunks/studio-c4rcaftm.js +0 -5
  236. package/dist/chunks/studio-cgjt1zce.js +0 -5
  237. package/dist/chunks/studio-cq87hree.js +0 -5
  238. package/dist/chunks/studio-d140sy8e.js +0 -5
  239. package/dist/chunks/studio-e1g64q43.js +0 -5
  240. package/dist/chunks/studio-e8pzjjhf.js +0 -5
  241. package/dist/chunks/studio-f8kz3krj.js +0 -7
  242. package/dist/chunks/studio-fw3jp9m7.js +0 -5
  243. package/dist/chunks/studio-g6ysm05p.js +0 -7
  244. package/dist/chunks/studio-gmhd0j3f.js +0 -5
  245. package/dist/chunks/studio-gqpty626.js +0 -5
  246. package/dist/chunks/studio-grp684wx.js +0 -5
  247. package/dist/chunks/studio-h98520qr.js +0 -5
  248. package/dist/chunks/studio-hvxt8z79.js +0 -5
  249. package/dist/chunks/studio-j4e627w1.js +0 -5
  250. package/dist/chunks/studio-kt2j51sa.js +0 -5
  251. package/dist/chunks/studio-m4w6sygj.js +0 -16
  252. package/dist/chunks/studio-nqxqf8p2.js +0 -4
  253. package/dist/chunks/studio-p89w12s2.js +0 -7
  254. package/dist/chunks/studio-pbagym4j.js +0 -5
  255. package/dist/chunks/studio-pr23qhrw.js +0 -5
  256. package/dist/chunks/studio-r4ya3sb2.js +0 -5
  257. package/dist/chunks/studio-rm9m3z60.js +0 -5
  258. package/dist/chunks/studio-tpmnbgmt.js +0 -5
  259. package/dist/chunks/studio-ttxhq5yt.js +0 -5
  260. package/dist/chunks/studio-y1x65h5r.js +0 -5
  261. package/dist/chunks/studio-zf188hnd.js +0 -5
  262. package/dist/chunks/toast-1gv0h53h.js +0 -4
  263. package/dist/chunks/workspace-3nt93k48.js +0 -4
  264. package/dist/chunks/workspace-home-zbgyqqws.js +0 -4
  265. package/src/services/directory-picker.ts +0 -187
  266. package/src/services/gated-registry.ts +0 -77
@@ -11,9 +11,8 @@
11
11
 
12
12
  import { initProjectRepo } from "../files/files";
13
13
  import { getPlatform, hasPlatform } from "../platform";
14
- import { beginBatch, endBatch, isBatching } from "../tabs/transact";
14
+ import { flushOpenBatches } from "../tabs/transact";
15
15
  import { workspace } from "../workspace/workspace";
16
- import type { Tab } from "../tabs/tab";
17
16
 
18
17
  let _adopter: ((root: string) => Promise<void>) | null = null;
19
18
 
@@ -52,8 +51,6 @@ export interface AdoptOutcome {
52
51
 
53
52
  /** The seams {@link adoptCreatedProject} runs through, so each caller keeps the one it already had. */
54
53
  export interface AdoptCreatedOptions {
55
- /** The active tab, read AFTER adoption to re-anchor the agent loop's undo batch. */
56
- getTab: () => Tab | null;
57
54
  /** The project-open flow. Defaults to this module's late-bound {@link adoptProject}. */
58
55
  adopt?: (root: string) => Promise<void>;
59
56
  /** Version-control init. Defaults to `files/files.ts`'s `initProjectRepo`. */
@@ -76,9 +73,8 @@ export interface AdoptCreatedOptions {
76
73
  * - **Verifying.** `openRecentProject` swallows its failures into a status message, so a resolved
77
74
  * promise is not proof of adoption. The caller must be told which of the two happened.
78
75
  *
79
- * The batch dance is the third: the agent loop may hold an undo batch on the pre-adoption tab, and
80
- * adoption replaces every tab in the workspace. Flush it, adopt, re-open one on whatever tab
81
- * adoption left active.
76
+ * The third is the undo batch: the agent loop's lane may hold segments on pre-adoption tabs, and
77
+ * adoption replaces every tab in the workspace, so they are closed first (specs/ai.md §3.3).
82
78
  *
83
79
  * @param {string} root - Absolute root of the project that was just created.
84
80
  * @param {AdoptCreatedOptions} opts
@@ -86,7 +82,7 @@ export interface AdoptCreatedOptions {
86
82
  */
87
83
  export async function adoptCreatedProject(
88
84
  root: string,
89
- { getTab, adopt = adoptProject, initRepo = initProjectRepo }: AdoptCreatedOptions,
85
+ { adopt = adoptProject, initRepo = initProjectRepo }: AdoptCreatedOptions = {},
90
86
  ): Promise<AdoptOutcome> {
91
87
  // Never fails the create: a project that was written stays written (initProjectRepo notifies).
92
88
  await initRepo(root);
@@ -117,19 +113,15 @@ export async function adoptCreatedProject(
117
113
  }
118
114
  }
119
115
 
120
- const wasBatching = isBatching();
121
- if (wasBatching) {
122
- endBatch();
123
- }
116
+ /* The tabs an open lane has segments on are about to be replaced: close each now, against the
117
+ document it was written into. The lane stays open, so the turn's next write opens a segment on
118
+ whatever document it writes in the adopted project. */
119
+ flushOpenBatches();
124
120
  let adoptionError: string | null = null;
125
121
  try {
126
122
  await adopt(root);
127
123
  } catch (error) {
128
124
  adoptionError = error instanceof Error ? error.message : String(error);
129
125
  }
130
- if (wasBatching) {
131
- beginBatch(getTab());
132
- }
133
-
134
126
  return { adopted: workspace.projectRoot === root, error: adoptionError, openedElsewhere };
135
127
  }
@@ -9,17 +9,19 @@
9
9
  * happen (`services/harness/chat-reducer.ts`), and each round's request is projected from it, as
10
10
  * it always was, so the provider sees the conversation the panel shows, with whatever the send
11
11
  * path trimmed or sealed before the turn began.
12
- * - **A batch belongs to the tab it edits** (§3.3). The undo batch opens on the active tab and is
13
- * re-anchored after every call whose tools moved to another document, so a turn that edits two
14
- * documents gives each its own history step and collab publish.
12
+ * - **The batch belongs to the assistant** (§3.3). The turn opens a batch lane owned by its actor,
13
+ * and every write the turn's tools make joins it, one segment per document written: each document
14
+ * gets one history step and one collab publish, whichever tab is active, and a person's edit
15
+ * during the turn closes the segment on that document rather than being folded into it.
15
16
  * - **The ledger is filed under the last message the turn drew** (§3.2), in this module's `finally`,
16
17
  * so the transcript draws the turn's changed-files summary under it.
17
18
  *
18
19
  * @license MIT
19
20
  */
20
21
 
22
+ import type { InteractionStore } from "./harness/interaction-store";
21
23
  import type { createChatState } from "@jxsuite/ai/chat-state";
22
- import { DEFAULT_TURN_POLICY, runTurn } from "@jxsuite/ai/harness";
24
+ import { DEFAULT_TURN_POLICY, assistantActorId, runTurn } from "@jxsuite/ai/harness";
23
25
  import type { TurnInput, TurnRun } from "@jxsuite/ai/harness";
24
26
  import { toChatMessages, toOpenAITools } from "@jxsuite/ai/messages";
25
27
  import type { StreamingClient } from "@jxsuite/ai/streaming-client";
@@ -27,7 +29,7 @@ import { createSessionFacts } from "@jxsuite/ai/tools";
27
29
  import type { SessionFacts, ToolRegistry } from "@jxsuite/ai/tools";
28
30
 
29
31
  import type { Tab } from "../tabs/tab";
30
- import { batchTab, beginBatch, endBatch } from "../tabs/transact";
32
+ import { openBatchLane } from "../tabs/transact";
31
33
  import { estimatePromptTokens } from "./context-manager";
32
34
  import { fileTurn, openTurnLedger, turnAnchor } from "./ai-writes";
33
35
  import { applyHarnessEvent } from "./harness/chat-reducer";
@@ -48,6 +50,8 @@ interface RunAgentLoopOptions {
48
50
  getTab?: () => Tab | null;
49
51
  /** The conversation's facts, which every call's context carries; a fresh set when omitted. */
50
52
  session?: SessionFacts;
53
+ /** Where a call that needs the author waits on them. Without one, such a call is refused. */
54
+ interactions?: InteractionStore;
51
55
  }
52
56
 
53
57
  /**
@@ -62,26 +66,23 @@ export async function runAgentLoop({
62
66
  signal,
63
67
  getTab,
64
68
  session = createSessionFacts(),
69
+ interactions,
65
70
  }: RunAgentLoopOptions): Promise<void> {
66
71
  /* The ledger is filed under the turn's last DRAWN assistant message (see services/ai-writes), so
67
72
  the scan starts at the message this turn answers. */
68
73
  const turnUserId = chatState.messages.findLast((m) => m.role === "user")?.id;
69
74
  const ledger = openTurnLedger(`turn:${chatState.messages.length}`);
70
75
 
71
- // Batch all tool-call mutations into a single undo step, anchored on the tab being edited.
72
- if (getTab) {
73
- beginBatch(getTab());
74
- }
75
- const reanchorBatch = () => {
76
- if (!getTab) {
77
- return;
78
- }
79
- const current = getTab();
80
- if (current !== batchTab()) {
81
- endBatch();
82
- beginBatch(current);
83
- }
84
- };
76
+ /* One undo step per document the turn writes. A loop with no `getTab` has no document of its own
77
+ (the evals), so it gets no lane and each tool is its own entry. The owner's id is the one each
78
+ call's context will carry, so a tool's write joins the lane by naming `ctx.actor`. */
79
+ const lane = getTab
80
+ ? openBatchLane({
81
+ id: assistantActorId(session.sessionId, ledger.turnId),
82
+ kind: "assistant",
83
+ turnId: ledger.turnId,
84
+ })
85
+ : null;
85
86
 
86
87
  /* The placeholder `sendMessage` pushed is round one's message, so the turn answers into it; the
87
88
  conversation the turn continues is everything before it. */
@@ -124,7 +125,6 @@ export async function runAgentLoop({
124
125
  superseded = true;
125
126
  run?.cancel("superseded");
126
127
  },
127
- reanchor: reanchorBatch,
128
128
  }),
129
129
  session,
130
130
  ledger,
@@ -137,13 +137,16 @@ export async function runAgentLoop({
137
137
  if (signal) {
138
138
  input.signal = signal;
139
139
  }
140
+ if (interactions) {
141
+ input.interactions = interactions.strategy;
142
+ }
140
143
  if (placeholder) {
141
144
  input.firstMessageId = placeholder.id;
142
145
  }
143
146
  run = runTurn(input);
144
147
  await run.outcome;
145
148
  } finally {
146
- endBatch();
147
- fileTurn(turnAnchor(chatState.messages, turnUserId) ?? "", ledger.writes);
149
+ const batches = lane?.close() ?? [];
150
+ fileTurn(turnAnchor(chatState.messages, turnUserId) ?? "", ledger.writes, batches);
148
151
  }
149
152
  }
@@ -135,7 +135,7 @@ export async function enableExtension(specifier: string): Promise<void> {
135
135
  let installed = false;
136
136
  notifySettingsDocument();
137
137
  try {
138
- if (!row.installed && !row.bundled) {
138
+ if (!row.installed) {
139
139
  /*
140
140
  * `beginActivity`, not `showProgressModal`. `studio-ui-guidelines.md` §13.3 reserves blocking
141
141
  * for an operation that cannot proceed while the author edits, and this one can — the only
@@ -32,8 +32,6 @@ export interface ExtensionRow {
32
32
  origin: ExtensionOrigin;
33
33
  enabled: boolean;
34
34
  installed: boolean;
35
- /** The host resolves it without a project install, so enabling is a config write alone. */
36
- bundled: boolean;
37
35
  /** Why this backend cannot run it, as one sentence. Absent means it can. */
38
36
  unavailable?: string | undefined;
39
37
  /** Enabled but not installed — the state that fails the next build. */
@@ -69,11 +67,9 @@ export function buildRows(packages: PackageInfo[] | null = null): ExtensionRow[]
69
67
  for (const entry of getExtensionCatalog()) {
70
68
  const specifier = entry.specifier ?? entry.name;
71
69
  const here = entry.installed ?? installed.has(entry.name);
72
- const bundled = entry.bundled ?? false;
73
70
  const info = resolved.get(entry.name);
74
71
  const row: ExtensionRow = {
75
- broken: enabled.has(specifier) && !here && !bundled,
76
- bundled,
72
+ broken: enabled.has(specifier) && !here,
77
73
  enabled: enabled.has(specifier),
78
74
  installed: here,
79
75
  name: entry.name,
@@ -102,7 +98,6 @@ export function buildRows(packages: PackageInfo[] | null = null): ExtensionRow[]
102
98
  const info = resolved.get(specifier);
103
99
  const row: ExtensionRow = {
104
100
  broken: !installed.has(specifier) && info === undefined,
105
- bundled: false,
106
101
  enabled: true,
107
102
  installed: installed.has(specifier),
108
103
  name: specifier,
@@ -105,12 +105,9 @@ function noteFor(row: ExtensionRow): RowNote {
105
105
  noteTone: "warn",
106
106
  };
107
107
  }
108
- if (!row.installed && !row.bundled) {
108
+ if (!row.installed) {
109
109
  return { note: "Not installed. Turning this on installs it first.", noteTone: "note" };
110
110
  }
111
- if (row.bundled) {
112
- return { note: "Ships with this backend, so it needs no install.", noteTone: "note" };
113
- }
114
111
  return { note: "", noteTone: "none" };
115
112
  }
116
113
 
@@ -131,7 +128,7 @@ function blockedReason(row: ExtensionRow): string {
131
128
  * Refused while the extension is still enabled, and DISABLED with the reason rather than hidden
132
129
  * (`studio-ui-guidelines.md` §10): removing the package while `project.json` still names it
133
130
  * manufactures exactly the broken state this section exists to eliminate. It is absent only when
134
- * there is nothing to remove — a package the project never installed, or one the backend bundles.
131
+ * there is nothing to remove — a package the project never installed.
135
132
  */
136
133
  function removeFor(
137
134
  row: ExtensionRow,
@@ -140,7 +137,7 @@ function removeFor(
140
137
  ? `Turn ${row.title} off first. Removing the package while project.json still names it would fail the next build.`
141
138
  : "";
142
139
  return {
143
- canRemove: row.installed && !row.bundled,
140
+ canRemove: row.installed,
144
141
  removeDisabled: refusal !== "" || extensionOpInFlight() !== null,
145
142
  removeHint: refusal === "" ? `Remove ${row.name}` : refusal,
146
143
  removeLabel: `Remove ${row.name}`,
package/src/state.ts CHANGED
@@ -154,19 +154,9 @@ export function pathsEqual(a: JxPath | null, b: JxPath | null) {
154
154
  return a.every((v, i) => v === b[i]);
155
155
  }
156
156
 
157
- /**
158
- * Returns true if `path` is an ancestor of (or equal to) `descendant`.
159
- *
160
- * @param {JxPath} path
161
- * @param {JxPath} descendant
162
- * @returns {boolean}
163
- */
164
- export function isAncestor(path: JxPath, descendant: JxPath) {
165
- if (path.length > descendant.length) {
166
- return false;
167
- }
168
- return path.every((v, i) => v === descendant[i]);
169
- }
157
+ /* Lives beside the doc-op vocabulary (`@jxsuite/schema/doc-ops`), with the other structural-path
158
+ helpers; re-exported for the many callers that import it from here. */
159
+ export { isAncestor } from "@jxsuite/schema/doc-ops";
170
160
 
171
161
  // ─── Tree flattening (for layer panel) ────────────────────────────────────────
172
162
 
@@ -104,9 +104,7 @@
104
104
  "display": "contents"
105
105
  },
106
106
  "& [part=\"link\"]": {
107
- "display": "flex",
108
- "gap": "var(--jx-space-2)",
109
- "alignItems": "center",
107
+ "$description": "No display here. A closed popover is hidden by the platform's own `[popover]:not(:popover-open)` rule, which the kit lets through with `display: revert-layer`; a `display` declared on the part beats it, and the closed panel stayed laid out at the viewport origin, invisible at opacity 0 and still taking every click and the text cursor meant for the workspace tabs beneath it.",
110
108
  "padding": "var(--jx-space-3)"
111
109
  },
112
110
  "& [part=\"link-field\"]": {
@@ -136,6 +136,18 @@
136
136
  }
137
137
  ]
138
138
  },
139
+ "address": {
140
+ "tagName": "div",
141
+ "attributes": {
142
+ "part": "empty"
143
+ },
144
+ "children": [
145
+ {
146
+ "tagName": "p",
147
+ "textContent": "Add ?project= and a project folder's absolute path to this page's address to open it."
148
+ }
149
+ ]
150
+ },
139
151
  "welcome": {
140
152
  "tagName": "div",
141
153
  "attributes": {
@@ -81,8 +81,11 @@ export interface FileRowView {
81
81
 
82
82
  /** Everything the document draws, as one value. */
83
83
  export interface FilesPanelValues {
84
- /** Which of three things the panel is: no project, a monorepo root, or a tree. */
85
- view: "none" | "welcome" | "tree";
84
+ /**
85
+ * Which of four things the panel is: no project, a monorepo root on a host that can open a
86
+ * project, the same root on one that opens from the address (`?project=`), or a tree.
87
+ */
88
+ view: "none" | "welcome" | "address" | "tree";
86
89
  headerState: "hidden" | "shown";
87
90
  projectName: string;
88
91
  query: string;
@@ -1027,6 +1027,41 @@
1027
1027
  }
1028
1028
  ]
1029
1029
  },
1030
+ "oncontextmenu": {
1031
+ "$prototype": "Function",
1032
+ "body": [
1033
+ {
1034
+ "$switch": {
1035
+ "$ref": "#/state/mode"
1036
+ },
1037
+ "cases": {
1038
+ "manage": [
1039
+ {
1040
+ "preventDefault": true
1041
+ },
1042
+ {
1043
+ "operator": "call",
1044
+ "target": {
1045
+ "$ref": "#/state/contextMenu"
1046
+ },
1047
+ "value": [
1048
+ {
1049
+ "$ref": "$map/item/file"
1050
+ },
1051
+ {
1052
+ "$ref": "event#/clientX"
1053
+ },
1054
+ {
1055
+ "$ref": "event#/clientY"
1056
+ }
1057
+ ]
1058
+ }
1059
+ ]
1060
+ }
1061
+ }
1062
+ ],
1063
+ "description": "Manage mode only: the detail pane's actions at the pointer. Pick mode's dialog is modal, and a menu in the popover layer outside it would be inert, so its right-click is left to the platform."
1064
+ },
1030
1065
  "children": [
1031
1066
  {
1032
1067
  "tagName": "div",
@@ -118,6 +118,8 @@ export interface MediaLibraryActions {
118
118
  activate: (file: string) => void;
119
119
  /** Enter on the grid: the same, for the selected file. */
120
120
  activateSelected: () => void;
121
+ /** A right-click on a tile in manage mode: select it and open its actions at the pointer. */
122
+ contextMenu: (file: string, x: number, y: number) => void;
121
123
  key: (key: string, ctrl: boolean) => void;
122
124
  measure: (file: string, width: number, height: number) => void;
123
125
  copyRef: () => void;
@@ -5,7 +5,8 @@
5
5
  * `menu.json` is the markup, ARIA and style, and this adapter is the decisions — which records,
6
6
  * what they print, where the panel opens, what runs on select. The kit owns the menu contract
7
7
  * (roving focus, typeahead, submenus, light dismissal, Escape) through the platform's own popover,
8
- * so nothing here binds a key or a document listener.
8
+ * so nothing here binds a key, and the one window listener is the record of which pointers are held
9
+ * ({@link pointersReleased}): a menu asked for mid-press is shown on the release.
9
10
  *
10
11
  * Every menu opens in a `getLayerSlot("popover", id)` slot, which is what carries the
11
12
  * `overlay.menu:<id>` region the screenshot pipeline addresses, and is torn down when the popover
@@ -309,6 +310,10 @@ export function openMenu(options: OpenMenuOptions): MenuHandle {
309
310
  if (aborted()) {
310
311
  return;
311
312
  }
313
+ await pointersReleased(controller.signal);
314
+ if (aborted()) {
315
+ return;
316
+ }
312
317
  const source =
313
318
  options.opener instanceof HTMLElement && options.opener !== document.body
314
319
  ? options.opener
@@ -368,6 +373,62 @@ export function openMenu(options: OpenMenuOptions): MenuHandle {
368
373
  return handle;
369
374
  }
370
375
 
376
+ /**
377
+ * The pointers pressed in this document and not yet released, by `pointerId`.
378
+ *
379
+ * Read by {@link pointersReleased}. Window listeners in the capture phase, so the record is current
380
+ * before any handler in the document runs, a `contextmenu` handler included. A press in the canvas
381
+ * iframe is never seen here, and needs not be: that release lands in the iframe's document, which
382
+ * cannot light-dismiss a popover in this one. Bound at load, so the press that asks for the first
383
+ * menu is already recorded; not at all without a window, where the docs build imports this module
384
+ * for the command catalog and nothing is ever pressed.
385
+ */
386
+ const pressedPointers = new Set<number>();
387
+ if (typeof window !== "undefined") {
388
+ window.addEventListener("pointerdown", (event) => pressedPointers.add(event.pointerId), true);
389
+ window.addEventListener("pointerup", (event) => pressedPointers.delete(event.pointerId), true);
390
+ window.addEventListener(
391
+ "pointercancel",
392
+ (event) => pressedPointers.delete(event.pointerId),
393
+ true,
394
+ );
395
+ // A release outside a window that lost focus may never arrive.
396
+ window.addEventListener("blur", () => pressedPointers.clear());
397
+ }
398
+
399
+ /**
400
+ * Settle once no pointer is held down in this document, or when `signal` aborts.
401
+ *
402
+ * A menu shown while the button that asked for it is still down is light-dismissed by that button's
403
+ * release. On Linux and macOS `contextmenu` fires on the PRESS, so every right-click menu opened
404
+ * with the button held, and the release that followed is a `pointerup` outside the popover. The
405
+ * HTML light-dismiss algorithm hides every `auto` popover on it, before the event reaches any
406
+ * listener, so it cannot be cancelled. Chromium 156 exempts a non-primary button and Electron 43's
407
+ * Chromium does not: in the desktop app the menu flashed and vanished. Waiting for the release is
408
+ * what Windows does natively, where `contextmenu` fires on it; a menu opened by a click or a key
409
+ * has nothing held and is shown at once.
410
+ */
411
+ function pointersReleased(signal: AbortSignal): Promise<void> {
412
+ if (pressedPointers.size === 0 || signal.aborted) {
413
+ return Promise.resolve();
414
+ }
415
+ return new Promise((resolve) => {
416
+ const waiting = new AbortController();
417
+ const settle = (): void => {
418
+ if (pressedPointers.size === 0 || signal.aborted) {
419
+ waiting.abort();
420
+ resolve();
421
+ }
422
+ };
423
+ // Registered after the record's own listeners, so each sees the record already updated.
424
+ const options = { capture: true, signal: waiting.signal };
425
+ window.addEventListener("pointerup", settle, options);
426
+ window.addEventListener("pointercancel", settle, options);
427
+ window.addEventListener("blur", settle, { signal: waiting.signal });
428
+ signal.addEventListener("abort", settle, { signal: waiting.signal });
429
+ });
430
+ }
431
+
371
432
  /**
372
433
  * Give the keyboard back to the opener BEFORE a chosen row runs, while the menu still holds it.
373
434
  *
@@ -73,7 +73,7 @@ export interface PackagesActions {
73
73
  /** Move every package that is behind one. */
74
74
  updateAll: () => void;
75
75
  remove: (name: string) => void;
76
- /** Run `bun install` over the project as it stands. */
76
+ /** Install the project's dependencies as it stands. */
77
77
  reinstall: () => void;
78
78
  }
79
79
 
@@ -22,6 +22,7 @@
22
22
  import { getAccountStatus, getBannerNotices, needsAppInstall } from "../account-status";
23
23
  import { runOfferedAction } from "../account/action-flow";
24
24
  import { platformSupportsAddRepo } from "../new-project/add-repo-modal";
25
+ import { canOpenProject, getPlatform } from "../platform";
25
26
  import { getProjectList } from "../project-list";
26
27
  import { reactive } from "../reactivity";
27
28
  import { MINUTE, now } from "../services/clock";
@@ -226,7 +227,9 @@ function project(): Omit<WelcomeScope, "run" | "open" | "remove" | "clear" | "no
226
227
  // One entry, not two: the starter gallery IS the first step of New Project now, so the
227
228
  // Separate "Start from an Example…" button it used to hide behind is gone.
228
229
  { icon: "plus", id: "new", title: "New Project…" },
229
- { icon: "folder-open", id: "open", title: "Open Project…" },
230
+ ...(canOpenProject(getPlatform())
231
+ ? [{ icon: "folder-open", id: "open", title: "Open Project…" } as const]
232
+ : []),
230
233
  ...(platformSupportsClone()
231
234
  ? [{ icon: "download-simple", id: "clone", title: "Clone Git Repository…" } as const]
232
235
  : []),
@@ -42,10 +42,22 @@
42
42
  * standing between a mixed selection and a half-applied, unrecorded delete.
43
43
  */
44
44
 
45
- import { isNumber } from "@jxsuite/schema/guards";
45
+ import { isAncestor } from "@jxsuite/schema/doc-ops";
46
46
 
47
47
  import type { JxPath } from "../state";
48
- import { isAncestor, pathsEqual } from "../state";
48
+ import { pathsEqual } from "../state";
49
+
50
+ /* The structural-path helpers live beside the doc-op vocabulary, so any host that edits a document
51
+ the way Studio does orders and filters a batch the same way. Re-exported here, where every
52
+ Studio caller already imports them. */
53
+ export {
54
+ comparePaths,
55
+ isSpliceablePath,
56
+ spliceCoordinate,
57
+ structuralBatch,
58
+ topLevelSelection,
59
+ } from "@jxsuite/schema/doc-ops";
60
+ export type { SpliceCoordinate } from "@jxsuite/schema/doc-ops";
49
61
 
50
62
  /**
51
63
  * The path single-target surfaces address — the most recently added, or `null` when empty.
@@ -171,113 +183,6 @@ export function pruneSelection(selection: readonly JxPath[], removed: JxPath): J
171
183
  return clonePaths(selection.filter((p) => !isAncestor(removed, p)));
172
184
  }
173
185
 
174
- /**
175
- * Order two paths the way the document reads: parents before children, siblings by index.
176
- *
177
- * Segments are compared pairwise; numbers numerically (so `children/10` follows `children/9`), and
178
- * anything else by string order. A prefix sorts before what extends it.
179
- *
180
- * @param {JxPath} a
181
- * @param {JxPath} b
182
- * @returns {number}
183
- */
184
- export function comparePaths(a: JxPath, b: JxPath): number {
185
- const len = Math.min(a.length, b.length);
186
- for (let i = 0; i < len; i++) {
187
- const x = a[i]!;
188
- const y = b[i]!;
189
- if (x === y) {
190
- continue;
191
- }
192
- if (isNumber(x) && isNumber(y)) {
193
- return x - y;
194
- }
195
- return String(x) < String(y) ? -1 : 1;
196
- }
197
- return a.length - b.length;
198
- }
199
-
200
- /**
201
- * Drop any path whose ancestor is also selected.
202
- *
203
- * Deleting a `<section>` and one of its own paragraphs is one deletion, not two: the paragraph goes
204
- * with its parent, and a second splice into coordinates that no longer exist is how a batch
205
- * corrupts a document.
206
- *
207
- * @param {readonly JxPath[]} selection
208
- * @returns {JxPath[]}
209
- */
210
- export function topLevelSelection(selection: readonly JxPath[]): JxPath[] {
211
- return clonePaths(
212
- selection.filter((p) => !selection.some((q) => !pathsEqual(q, p) && isAncestor(q, p))),
213
- );
214
- }
215
-
216
- /**
217
- * Whether `path` names a position a structural verb can splice — a tail of `"children"` + index.
218
- *
219
- * Every structural mutator (`mutateRemoveNode`, `mutateDuplicateNode`, `mutateWrapNode`) reaches
220
- * its parent with `parentElementPath` — drop the last two segments — and its position with
221
- * `childIndex` — take the last. That produces a splice coordinate for exactly one path shape, and
222
- * the Outline emits three others as first-class rows: the document element (`[]`), a repeater's map
223
- * template (`[…, "children", 1, "map"]`) and a `$switch` case (`[…, "cases", "warn"]`). For the
224
- * template, `parentElementPath` resolves to the enclosing children ARRAY, whose `.children` is
225
- * `undefined`, so the splice throws mid-transaction; for a case it splices at `NaN` and takes the
226
- * wrong child.
227
- *
228
- * Selecting one of those beside an ordinary element is a single ctrl-click, so this is the
229
- * invariant that keeps a batch from mutating the document and then throwing before anything is
230
- * recorded.
231
- *
232
- * @param {JxPath} path
233
- * @returns {boolean}
234
- */
235
- export function isSpliceablePath(path: JxPath): boolean {
236
- return spliceCoordinate(path) !== null;
237
- }
238
-
239
- /** Where a structural verb splices: the parent element's path and the index in its children. */
240
- export interface SpliceCoordinate {
241
- parentPath: JxPath;
242
- index: number;
243
- }
244
-
245
- /**
246
- * The splice coordinate `path` names, or `null` for no path and for every path
247
- * {@link isSpliceablePath} refuses.
248
- *
249
- * @param {JxPath | null | undefined} path
250
- * @returns {SpliceCoordinate | null}
251
- */
252
- export function spliceCoordinate(path: JxPath | null | undefined): SpliceCoordinate | null {
253
- const index = path?.at(-1);
254
- return path && path.at(-2) === "children" && isNumber(index)
255
- ? { index, parentPath: path.slice(0, -2) }
256
- : null;
257
- }
258
-
259
- /**
260
- * The order a structural batch must run in: contained paths dropped, unspliceable paths dropped,
261
- * then LAST node first.
262
- *
263
- * Descending document order is what makes a loop of splices safe. Removing or duplicating at index
264
- * `n` renumbers every sibling after `n`, so a batch that starts at the end never touches a
265
- * coordinate a later step still needs. It is also why duplicating a multi-selection inserts each
266
- * clone directly after its own original instead of walking the whole run forward.
267
- *
268
- * Containment is decided BEFORE spliceability, on the selection the user actually made: a selected
269
- * repeater template still shadows its own descendants, so selecting the template and something
270
- * inside it yields nothing rather than quietly deleting the child the user never pointed at.
271
- *
272
- * @param {readonly JxPath[]} selection
273
- * @returns {JxPath[]} Deepest/last first, every entry a splice coordinate.
274
- */
275
- export function structuralBatch(selection: readonly JxPath[]): JxPath[] {
276
- return topLevelSelection(selection)
277
- .filter((path) => isSpliceablePath(path))
278
- .toSorted((a, b) => comparePaths(b, a));
279
- }
280
-
281
186
  /**
282
187
  * Deduplicate a list of paths, keeping the FIRST occurrence of each.
283
188
  *
package/src/tabs/tab.ts CHANGED
@@ -132,7 +132,7 @@ export interface StoredLivePreview {
132
132
  error: string | null;
133
133
  }
134
134
 
135
- interface HistorySnapshot {
135
+ export interface HistorySnapshot {
136
136
  /** Full document at this state (checkpoint), or null when the ops describe the transition. */
137
137
  document: JxMutableNode | null;
138
138
  /** Selection after this state's transaction — the whole set, so a batch undoes back to it. */
@@ -153,6 +153,13 @@ interface HistorySnapshot {
153
153
  * back through the prose one pause at a time instead of undoing the edit.
154
154
  */
155
155
  coalesceKey?: string | null;
156
+ /**
157
+ * Who made this step, when it is not the person at the keyboard: the id of the actor whose batch
158
+ * lane closed it (specs/ai.md §3.3). Absent on a person's own edits.
159
+ */
160
+ actor?: string;
161
+ /** The turn whose batch this step is, beside {@link HistorySnapshot.actor}. */
162
+ turnId?: string;
156
163
  }
157
164
 
158
165
  /**
@@ -186,6 +193,8 @@ export interface Tab {
186
193
  scope: {
187
194
  stop: () => void;
188
195
  run: <T>(fn: () => T) => T | undefined;
196
+ /** False once {@link disposeTab} stopped it: how a record that outlives the tab can tell. */
197
+ readonly active?: boolean;
189
198
  };
190
199
  doc: {
191
200
  document: JxMutableNode;