@jxsuite/studio 8.1.0 → 9.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 (247) hide show
  1. package/dist/chunks/{2020-dg097k3e.js → 2020-89fz3pfy.js} +3 -3
  2. package/dist/chunks/adapter-ay13gema.js +4 -0
  3. package/dist/chunks/add-new-flow-nmkqpjym.js +4 -0
  4. package/dist/chunks/cf-account-picker-2e06qxey.js +4 -0
  5. package/dist/chunks/cf-settings-5y04810c.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-3vf75n83.js +4 -0
  11. package/dist/chunks/content-source-4f2raf4a.js +4 -0
  12. package/dist/chunks/{convert-to-repeater-grnmea6c.js → convert-to-repeater-j9hfcj4f.js} +3 -3
  13. package/dist/chunks/editor-645krvtz.js +4 -0
  14. package/dist/chunks/entry-commands-3qmv8wkr.js +4 -0
  15. package/dist/chunks/{extension-sections-rp7fkega.js → extension-sections-thfke054.js} +3 -3
  16. package/dist/chunks/file-ops-kkhp9ma7.js +4 -0
  17. package/dist/chunks/files-yswdhxrf.js +4 -0
  18. package/dist/chunks/format-host-4g8ex6bz.js +4 -0
  19. package/dist/chunks/fs-events-z9gpvc6t.js +4 -0
  20. package/dist/chunks/grid-open-fm4j183c.js +4 -0
  21. package/dist/chunks/{grid-view-x26rfpz1.js → grid-view-7d2nx83y.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-q7fedsmr.js +4 -0
  25. package/dist/chunks/{index-203h71wy.js → index-jxmt7vh8.js} +3 -3
  26. package/dist/chunks/index-pt4wh98x.js +4 -0
  27. package/dist/chunks/{javascript-ww5pv5ae.js → javascript-vmqp56ke.js} +2 -2
  28. package/dist/chunks/{jsonMode-vwbdwwq3.js → jsonMode-n3wmpant.js} +6 -6
  29. package/dist/chunks/jx-validate-19y62knt.js +4 -0
  30. package/dist/chunks/layers-panel-1qvc83y9.js +4 -0
  31. package/dist/chunks/layers-qf0bc7ze.js +4 -0
  32. package/dist/chunks/lit-html-wyqw04y2.js +4 -0
  33. package/dist/chunks/media-library-f9d5qcjb.js +4 -0
  34. package/dist/chunks/media-picker-rsb1fmk2.js +4 -0
  35. package/dist/chunks/{monaco-binding-63tqze27.js → monaco-binding-wfhmz89d.js} +3 -3
  36. package/dist/chunks/monaco-buffer-ak0raadw.js +4 -0
  37. package/dist/chunks/{monaco-setup-zyyynnj8.js → monaco-setup-fty7a8vg.js} +3 -3
  38. package/dist/chunks/notify-4qj70q0x.js +4 -0
  39. package/dist/chunks/parse-n8dfxnqf.js +4 -0
  40. package/dist/chunks/platform-ehg6t9vd.js +4 -0
  41. package/dist/chunks/publish-panel-yhmda3cd.js +4 -0
  42. package/dist/chunks/{publish-quick-ajzdhhs6.js → publish-quick-12hh8926.js} +3 -3
  43. package/dist/chunks/publish-workspace-hx8qdv9p.js +4 -0
  44. package/dist/chunks/quick-search-r955m3q1.js +4 -0
  45. package/dist/chunks/references-8gxv0x7t.js +4 -0
  46. package/dist/chunks/right-panel-jarwqjpy.js +4 -0
  47. package/dist/chunks/source-control-84qt25m9.js +4 -0
  48. package/dist/chunks/studio-0nefxhar.js +7 -0
  49. package/dist/chunks/studio-0rg30pvs.js +5 -0
  50. package/dist/chunks/{studio-3q6n9qvq.js → studio-14hh60j8.js} +4 -4
  51. package/dist/chunks/studio-1cfyd1ys.js +5 -0
  52. package/dist/chunks/studio-2bq7hheq.js +5 -0
  53. package/dist/chunks/{studio-mmqr9awv.js → studio-32d8wyxq.js} +33 -33
  54. package/dist/chunks/studio-3hpcawh8.js +4 -0
  55. package/dist/chunks/{studio-zm7dnr9g.js → studio-4j35hgsw.js} +4 -4
  56. package/dist/chunks/{studio-bqgs90r8.js → studio-4qg61g9j.js} +4 -4
  57. package/dist/chunks/studio-4rgem12q.js +5 -0
  58. package/dist/chunks/studio-6k9djnxw.js +5 -0
  59. package/dist/chunks/studio-6rbaedw2.js +9 -0
  60. package/dist/chunks/studio-732mw0g8.js +5 -0
  61. package/dist/chunks/studio-79r8msnr.js +7 -0
  62. package/dist/chunks/studio-7q6jdd4h.js +5 -0
  63. package/dist/chunks/{studio-jqrg9xxg.js → studio-881z1xrx.js} +4 -4
  64. package/dist/chunks/{studio-9wyqyfqy.js → studio-8hf8qcm1.js} +4 -4
  65. package/dist/chunks/{studio-6vrxzkba.js → studio-8pne0rx4.js} +4 -4
  66. package/dist/chunks/{studio-wcakp5bt.js → studio-9179bsm7.js} +7 -7
  67. package/dist/chunks/{studio-xddgbwhe.js → studio-9ff0vqff.js} +3 -3
  68. package/dist/chunks/{studio-25a5by3h.js → studio-9gzvypw4.js} +4 -4
  69. package/dist/chunks/studio-9ndgd55w.js +5 -0
  70. package/dist/chunks/studio-9ra9e7m4.js +5 -0
  71. package/dist/chunks/studio-a6e5fzve.js +5 -0
  72. package/dist/chunks/studio-a9k86nq6.js +5 -0
  73. package/dist/chunks/studio-ae7a69jc.js +6 -0
  74. package/dist/chunks/studio-aghsg0ds.js +7 -0
  75. package/dist/chunks/{studio-2b8443ms.js → studio-ajw4k8hp.js} +3 -3
  76. package/dist/chunks/{studio-957jg8g3.js → studio-apapy17g.js} +4 -4
  77. package/dist/chunks/{studio-hnx1h1pr.js → studio-by8x0jvc.js} +4 -4
  78. package/dist/chunks/{studio-hxq1ebp0.js → studio-c88gq88j.js} +6 -6
  79. package/dist/chunks/{studio-a6x058ew.js → studio-cg54vtet.js} +4 -4
  80. package/dist/chunks/studio-dvhxxxbd.js +5 -0
  81. package/dist/chunks/{studio-1meqbxns.js → studio-e4rq6x0q.js} +4 -4
  82. package/dist/chunks/{studio-v2ek69cb.js → studio-ekzbgnjp.js} +4 -4
  83. package/dist/chunks/{studio-2dtc90zs.js → studio-fg4fj9ec.js} +6 -6
  84. package/dist/chunks/studio-fj6jzgxw.js +5 -0
  85. package/dist/chunks/{studio-txkx6586.js → studio-g45r5vm8.js} +7 -7
  86. package/dist/chunks/{studio-v0gsjav0.js → studio-gczve8zf.js} +4 -4
  87. package/dist/chunks/studio-ge08zwpv.js +5 -0
  88. package/dist/chunks/{studio-1gdfsr49.js → studio-gk9yk8dj.js} +7 -7
  89. package/dist/chunks/{studio-cpdzrdgp.js → studio-gskkgfe3.js} +4 -4
  90. package/dist/chunks/{studio-94ppxeqy.js → studio-h4jbgne2.js} +8 -8
  91. package/dist/chunks/{studio-kdseew9m.js → studio-hcy9zeac.js} +4 -4
  92. package/dist/chunks/studio-ht81xjkr.js +5 -0
  93. package/dist/chunks/{studio-73wz63nn.js → studio-jr30g4ae.js} +4 -4
  94. package/dist/chunks/{studio-fqs1a22a.js → studio-k5n4es74.js} +4 -4
  95. package/dist/chunks/{studio-j5jqvyn3.js → studio-k6hbx2fc.js} +4 -4
  96. package/dist/chunks/{studio-332hesdk.js → studio-kqh7fxn6.js} +4 -4
  97. package/dist/chunks/{studio-8nyw10dp.js → studio-kyw47pet.js} +4 -4
  98. package/dist/chunks/studio-mkw17ngg.js +5 -0
  99. package/dist/chunks/{studio-02phtmqn.js → studio-myb4mf0h.js} +4 -4
  100. package/dist/chunks/{studio-d140sy8e.js → studio-n4qq1n57.js} +4 -4
  101. package/dist/chunks/{studio-bf0rgyf7.js → studio-nfmpmkqb.js} +4 -4
  102. package/dist/chunks/studio-nfwacw31.js +5 -0
  103. package/dist/chunks/{studio-z42kysg7.js → studio-nm86874b.js} +136 -136
  104. package/dist/chunks/studio-nqexty8x.js +16 -0
  105. package/dist/chunks/studio-nr0kcqc6.js +5 -0
  106. package/dist/chunks/{studio-dmn7gpba.js → studio-nvfa137z.js} +5 -5
  107. package/dist/chunks/studio-psh8mwnw.js +4 -0
  108. package/dist/chunks/{studio-4dsdnan2.js → studio-qd0cycm4.js} +5 -5
  109. package/dist/chunks/{studio-zht0x0v3.js → studio-qevf5z9d.js} +4 -4
  110. package/dist/chunks/studio-qs5fmhtj.js +14 -0
  111. package/dist/chunks/studio-ry4789ng.js +7 -0
  112. package/dist/chunks/studio-s4fq9jht.js +5 -0
  113. package/dist/chunks/studio-sc35ttdk.js +5 -0
  114. package/dist/chunks/studio-sm2f0sc6.js +6 -0
  115. package/dist/chunks/{studio-rdnezzkd.js → studio-spza69tb.js} +5 -5
  116. package/dist/chunks/studio-t6dqnnft.js +5 -0
  117. package/dist/chunks/{studio-hw60n5dh.js → studio-tfmhnwyr.js} +3 -3
  118. package/dist/chunks/studio-tn7v8gey.js +6 -0
  119. package/dist/chunks/{studio-nsv7g1qn.js → studio-trcfer2b.js} +4 -4
  120. package/dist/chunks/studio-tyw7tk0x.js +5 -0
  121. package/dist/chunks/{studio-533py3yy.js → studio-tyzgte21.js} +4 -4
  122. package/dist/chunks/studio-vtey9w98.js +5 -0
  123. package/dist/chunks/studio-ws2r1d7e.js +5 -0
  124. package/dist/chunks/studio-x2trkhk5.js +5 -0
  125. package/dist/chunks/{studio-c5dj3dzk.js → studio-xzgp0cp7.js} +4 -4
  126. package/dist/chunks/studio-y47090vq.js +5 -0
  127. package/dist/chunks/{studio-nm9qev4p.js → studio-yjh9gddd.js} +5 -5
  128. package/dist/chunks/studio-ywf9y1e2.js +5 -0
  129. package/dist/chunks/{studio-x3ge4rts.js → studio-z1p91z5a.js} +4 -4
  130. package/dist/chunks/studio-ztppbam0.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-byje4a01.js} +5 -5
  134. package/dist/chunks/workspace-7q4arnt3.js +4 -0
  135. package/dist/chunks/workspace-home-ge027kr6.js +4 -0
  136. package/dist/chunks/{ws-client-72x5n99j.js → ws-client-h09h6sqg.js} +3 -3
  137. package/dist/iframe-entry.js +13 -13
  138. package/dist/manifest.json +77 -77
  139. package/dist/studio.js +89 -89
  140. package/package.json +11 -12
  141. package/src/collab/collab-session.ts +6 -2
  142. package/src/commands/context.ts +4 -0
  143. package/src/commands/defaults.ts +2 -0
  144. package/src/commands/live-context.ts +2 -0
  145. package/src/files/files.ts +6 -3
  146. package/src/new-project/new-project-modal.ts +1 -4
  147. package/src/panels/ai-panel.ts +27 -34
  148. package/src/platform.ts +20 -0
  149. package/src/platforms/devserver.ts +2 -120
  150. package/src/services/ai-ask.ts +29 -171
  151. package/src/services/ai-command-tools.ts +40 -50
  152. package/src/services/ai-import-tools.ts +4 -5
  153. package/src/services/ai-project-tools.ts +13 -3
  154. package/src/services/ai-tools.ts +26 -564
  155. package/src/services/ai-writes.ts +84 -2
  156. package/src/services/document-assistant.ts +29 -58
  157. package/src/services/harness/assistant-tools.ts +68 -0
  158. package/src/services/harness/interaction-store.ts +118 -0
  159. package/src/services/harness/studio-documents.ts +105 -0
  160. package/src/services/harness/turn-hooks.ts +1 -6
  161. package/src/services/project-adoption.ts +8 -16
  162. package/src/services/tool-executor.ts +25 -22
  163. package/src/state.ts +3 -13
  164. package/src/surfaces/files-panel.json +12 -0
  165. package/src/surfaces/files-panel.ts +5 -2
  166. package/src/surfaces/welcome.ts +4 -1
  167. package/src/tabs/selection.ts +14 -109
  168. package/src/tabs/tab.ts +10 -1
  169. package/src/tabs/transact.ts +316 -124
  170. package/src/types.ts +13 -8
  171. package/dist/chunks/adapter-tck81zft.js +0 -4
  172. package/dist/chunks/add-new-flow-a1w0bnm6.js +0 -4
  173. package/dist/chunks/cf-account-picker-ce3kp3x1.js +0 -4
  174. package/dist/chunks/cf-settings-7ye6x7g2.js +0 -4
  175. package/dist/chunks/color-4gj49xq6.js +0 -4
  176. package/dist/chunks/components-qce0v93f.js +0 -4
  177. package/dist/chunks/content-source-jjx8kgzr.js +0 -4
  178. package/dist/chunks/editor-bkja5dbv.js +0 -4
  179. package/dist/chunks/entry-commands-1wh0btvg.js +0 -4
  180. package/dist/chunks/file-ops-6pe4z7an.js +0 -4
  181. package/dist/chunks/files-ybpp3j38.js +0 -4
  182. package/dist/chunks/format-host-3mtbhxk1.js +0 -4
  183. package/dist/chunks/fs-events-xfanfxds.js +0 -4
  184. package/dist/chunks/grid-open-dg5qmnaj.js +0 -4
  185. package/dist/chunks/iframe-host-j2bk2xkc.js +0 -4
  186. package/dist/chunks/index-fkrm7pdk.js +0 -4
  187. package/dist/chunks/jx-validate-w7v32f6a.js +0 -4
  188. package/dist/chunks/layers-g7j732cm.js +0 -4
  189. package/dist/chunks/layers-panel-gjrsxcck.js +0 -4
  190. package/dist/chunks/lit-html-jdfz9rhc.js +0 -4
  191. package/dist/chunks/media-library-9j0v4wmk.js +0 -4
  192. package/dist/chunks/media-picker-cpyyt2m0.js +0 -4
  193. package/dist/chunks/monaco-buffer-9wcme176.js +0 -4
  194. package/dist/chunks/notify-rahnnnv4.js +0 -4
  195. package/dist/chunks/parse-v77xtwv7.js +0 -4
  196. package/dist/chunks/platform-b2aqv7v5.js +0 -4
  197. package/dist/chunks/publish-panel-4yk2461b.js +0 -4
  198. package/dist/chunks/publish-workspace-mxestzcf.js +0 -4
  199. package/dist/chunks/quick-search-9g6z65b0.js +0 -4
  200. package/dist/chunks/references-c26enpxs.js +0 -4
  201. package/dist/chunks/right-panel-9d0e0yg7.js +0 -4
  202. package/dist/chunks/source-control-xyn3a8ds.js +0 -4
  203. package/dist/chunks/studio-0khd0xdw.js +0 -5
  204. package/dist/chunks/studio-1fvzq9vs.js +0 -5
  205. package/dist/chunks/studio-1yhz0s8p.js +0 -5
  206. package/dist/chunks/studio-2c2mjyzj.js +0 -6
  207. package/dist/chunks/studio-3pndhat0.js +0 -9
  208. package/dist/chunks/studio-4fjqdzrb.js +0 -6
  209. package/dist/chunks/studio-5abge3wy.js +0 -5
  210. package/dist/chunks/studio-74nzv280.js +0 -5
  211. package/dist/chunks/studio-7cg75gtj.js +0 -14
  212. package/dist/chunks/studio-89cwmnae.js +0 -5
  213. package/dist/chunks/studio-8m1t83kn.js +0 -7
  214. package/dist/chunks/studio-a3evp7f6.js +0 -6
  215. package/dist/chunks/studio-ana67wax.js +0 -5
  216. package/dist/chunks/studio-b4xhpcwy.js +0 -4
  217. package/dist/chunks/studio-c4rcaftm.js +0 -5
  218. package/dist/chunks/studio-cgjt1zce.js +0 -5
  219. package/dist/chunks/studio-cq87hree.js +0 -5
  220. package/dist/chunks/studio-e1g64q43.js +0 -5
  221. package/dist/chunks/studio-e8pzjjhf.js +0 -5
  222. package/dist/chunks/studio-f8kz3krj.js +0 -7
  223. package/dist/chunks/studio-fw3jp9m7.js +0 -5
  224. package/dist/chunks/studio-g6ysm05p.js +0 -7
  225. package/dist/chunks/studio-gmhd0j3f.js +0 -5
  226. package/dist/chunks/studio-gqpty626.js +0 -5
  227. package/dist/chunks/studio-grp684wx.js +0 -5
  228. package/dist/chunks/studio-h98520qr.js +0 -5
  229. package/dist/chunks/studio-hvxt8z79.js +0 -5
  230. package/dist/chunks/studio-j4e627w1.js +0 -5
  231. package/dist/chunks/studio-kt2j51sa.js +0 -5
  232. package/dist/chunks/studio-m4w6sygj.js +0 -16
  233. package/dist/chunks/studio-nqxqf8p2.js +0 -4
  234. package/dist/chunks/studio-p89w12s2.js +0 -7
  235. package/dist/chunks/studio-pbagym4j.js +0 -5
  236. package/dist/chunks/studio-pr23qhrw.js +0 -5
  237. package/dist/chunks/studio-r4ya3sb2.js +0 -5
  238. package/dist/chunks/studio-rm9m3z60.js +0 -5
  239. package/dist/chunks/studio-tpmnbgmt.js +0 -5
  240. package/dist/chunks/studio-ttxhq5yt.js +0 -5
  241. package/dist/chunks/studio-y1x65h5r.js +0 -5
  242. package/dist/chunks/studio-zf188hnd.js +0 -5
  243. package/dist/chunks/toast-1gv0h53h.js +0 -4
  244. package/dist/chunks/workspace-3nt93k48.js +0 -4
  245. package/dist/chunks/workspace-home-zbgyqqws.js +0 -4
  246. package/src/services/directory-picker.ts +0 -187
  247. package/src/services/gated-registry.ts +0 -77
@@ -25,6 +25,9 @@
25
25
  import type { LiveMessage } from "@jxsuite/ai/messages";
26
26
  import { createLedger } from "@jxsuite/ai/tools";
27
27
  import type { AiWrite, WriteLedger } from "@jxsuite/ai/tools";
28
+ import { undo } from "../tabs/transact";
29
+ import type { BatchReceipt } from "../tabs/transact";
30
+ import type { Tab } from "../tabs/tab";
28
31
 
29
32
  export type { AiWrite } from "@jxsuite/ai/tools";
30
33
 
@@ -33,6 +36,8 @@ export interface AiTurn {
33
36
  /** The id of the assistant message the turn produced — how the panel finds its own ledger. */
34
37
  id: string;
35
38
  writes: AiWrite[];
39
+ /** Where each of the turn's undo steps landed, oldest first: what Restore takes back. */
40
+ batches: BatchReceipt[];
36
41
  }
37
42
 
38
43
  /** Completed turns, oldest first. Bounded: a session's history is the session store's job. */
@@ -70,13 +75,18 @@ export function openTurnLedger(turnId: string): TurnLedger {
70
75
  *
71
76
  * @param {string} anchor - The message the turn's changes are drawn under
72
77
  * @param {readonly AiWrite[]} writes - The turn's ledger
78
+ * @param {readonly BatchReceipt[]} [batches] - The receipts its batch lane returned on close
73
79
  * @returns {AiWrite[]} What was filed
74
80
  */
75
- export function fileTurn(anchor: string, writes: readonly AiWrite[]): AiWrite[] {
81
+ export function fileTurn(
82
+ anchor: string,
83
+ writes: readonly AiWrite[],
84
+ batches: readonly BatchReceipt[] = [],
85
+ ): AiWrite[] {
76
86
  if (writes.length === 0) {
77
87
  return [];
78
88
  }
79
- const turn: AiTurn = { id: anchor, writes: [...writes] };
89
+ const turn: AiTurn = { batches: [...batches], id: anchor, writes: [...writes] };
80
90
  turns.push(turn);
81
91
  while (turns.length > MAX_TURNS) {
82
92
  turns.shift();
@@ -95,6 +105,78 @@ export function writesForTurn(id: string): AiWrite[] {
95
105
  return turns.find((turn) => turn.id === id)?.writes ?? [];
96
106
  }
97
107
 
108
+ /**
109
+ * Undo every step one turn left, across every document it wrote, or nothing at all.
110
+ *
111
+ * `ai.md` §3.2. The turn's batch lane closed one step per document it wrote (two when a person's
112
+ * edit split it), and each receipt names its entry. A step is taken back only while it is the top
113
+ * of its tab's history, newest first, so Restore never undoes an edit that is not the turn's: a
114
+ * person's edit made since, a step already undone, a closed tab or a shared document whose undo the
115
+ * session owns each refuse the whole Restore with a sentence naming the document. Every receipt is
116
+ * checked before anything is undone, because a Restore that took back SOME of a turn would be worse
117
+ * than one that refused.
118
+ *
119
+ * @param {string} id - The message the turn is filed under
120
+ * @returns {string | Tab[]} Why it refused, or the tabs it undid
121
+ */
122
+ export function restoreTurn(id: string): string | Tab[] {
123
+ const turn = turns.find((filed) => filed.id === id);
124
+ const lost = "There is no longer a record of what that turn changed.";
125
+ if (!turn) {
126
+ return lost;
127
+ }
128
+ const disk = turn.writes.filter((w) => w.disk).map((w) => w.path);
129
+ if (disk.length > 0) {
130
+ return (
131
+ `Cannot restore: ${disk.join(", ")} ${disk.length === 1 ? "was" : "were"} written straight ` +
132
+ "to disk, which undo cannot reach."
133
+ );
134
+ }
135
+ if (turn.batches.length === 0) {
136
+ return lost;
137
+ }
138
+ const byTab = new Map<Tab, BatchReceipt[]>();
139
+ for (const receipt of turn.batches) {
140
+ byTab.set(receipt.tab, [...(byTab.get(receipt.tab) ?? []), receipt]);
141
+ }
142
+ for (const [tab, receipts] of byTab) {
143
+ const refusal = refuseTab(tab, receipts);
144
+ if (refusal) {
145
+ return refusal;
146
+ }
147
+ }
148
+ for (const [tab, receipts] of byTab) {
149
+ for (const _ of receipts) {
150
+ undo(tab);
151
+ }
152
+ }
153
+ return [...byTab.keys()];
154
+ }
155
+
156
+ /** Why `tab`'s steps from one turn cannot be undone, or null when they are its newest entries. */
157
+ function refuseTab(tab: Tab, receipts: readonly BatchReceipt[]): string | null {
158
+ const path = tab.documentPath ?? "(untitled)";
159
+ if (tab.scope.active === false) {
160
+ return `Cannot restore: ${path} was closed after that turn.`;
161
+ }
162
+ if (receipts.some((receipt) => receipt.entry === null)) {
163
+ return (
164
+ `Cannot restore: ${path} is shared live, and Restore cannot check its undo history. ` +
165
+ "Undo it in the document instead."
166
+ );
167
+ }
168
+ const { index, snapshots } = tab.history;
169
+ const top = snapshots.slice(index - receipts.length + 1, index + 1);
170
+ const matches =
171
+ index - receipts.length >= 0 && receipts.every((receipt, at) => top[at] === receipt.entry);
172
+ if (matches) {
173
+ return null;
174
+ }
175
+ return snapshots.indexOf(receipts.at(-1)!.entry!) > index
176
+ ? `Cannot restore: that turn's change to ${path} was already undone.`
177
+ : `Cannot restore: ${path} has edits made since that turn began, which Restore would undo too.`;
178
+ }
179
+
98
180
  /**
99
181
  * The one-line summary the panel puts above the expander.
100
182
  *
@@ -22,21 +22,17 @@ import type { Tab } from "../tabs/tab";
22
22
  import { componentRegistry } from "../files/components";
23
23
  import { activeRegistry } from "../commands/active-registry";
24
24
  import { registerAiTools } from "./ai-tools";
25
- import { cancelAsk, registerAskTool, resetAsk } from "./ai-ask";
26
- import {
27
- commandToolBlurbs,
28
- composeToolRegistries,
29
- createCommandToolRegistry,
30
- } from "./ai-command-tools";
25
+ import { registerAskTool } from "./ai-ask";
26
+ import { createInteractionStore } from "./harness/interaction-store";
27
+ import { commandToolBlurbs, createCommandToolRegistry } from "./ai-command-tools";
31
28
  import { registerProjectTools } from "./ai-project-tools";
32
29
  import { registerImportTools } from "./ai-import-tools";
33
- import { createGatedToolRegistry } from "./gated-registry";
34
- import type { ToolAvailability } from "./gated-registry";
30
+ import { assistantToolRegistry } from "./harness/assistant-tools";
35
31
  import { adoptProject } from "./project-adoption";
36
32
  import { resetAiWrites } from "./ai-writes";
37
33
  import { abortImportRun, resetImportRuns } from "./import-run";
38
34
  import { runAgentLoop } from "./tool-executor";
39
- import { AI_TOOL_TIERS, buildSystemPrompt, toolActive } from "./ai-system-prompt";
35
+ import { buildSystemPrompt } from "./ai-system-prompt";
40
36
  import type { ExtensionCatalogSummary } from "./ai-system-prompt";
41
37
  import { getBaseUrl, getOpenAiKey } from "./ai-settings";
42
38
  import { preferredModel } from "./ai-models";
@@ -79,6 +75,7 @@ const probeRender = renderCheck as (doc: CandidateDocument) => Promise<RenderVer
79
75
  *
80
76
  * @returns {{
81
77
  * chatState: ReturnType<typeof createChatState>;
78
+ * interactions: import("./harness/interaction-store").InteractionStore;
82
79
  * sendMessage: (text: string) => Promise<void>;
83
80
  * stop: () => void;
84
81
  * newChat: () => void;
@@ -111,12 +108,18 @@ export function createDocumentAssistant() {
111
108
  return null;
112
109
  };
113
110
 
111
+ /* Where a turn waits on the author: a question is put here, and the panel answers it. A question
112
+ suspends the turn for as long as they like, and the conversation is otherwise saved only when a
113
+ turn starts and ends; saving once it is pending is what lets a reload find the question to
114
+ restore as open rather than losing the whole unfinished turn. */
115
+ const interactions = createInteractionStore({ onPending: () => persistChat() });
116
+
114
117
  const innerRegistry = createToolRegistry();
115
118
  // Ungated: a question is not a function of what happens to be open.
116
119
  /* A question suspends the turn on the author, for as long as they like, and the conversation is
117
120
  otherwise saved only when a turn starts and ends. Saving here is what lets a reload find the
118
121
  question to restore as open rather than losing the whole unfinished turn. */
119
- registerAskTool(innerRegistry, { onPending: () => persistChat() });
122
+ registerAskTool(innerRegistry);
120
123
  registerAiTools(innerRegistry, {
121
124
  getTab: () => activeTab.value,
122
125
  saveFile: async (relPath: string, content: string) => {
@@ -130,7 +133,6 @@ export function createDocumentAssistant() {
130
133
  });
131
134
  registerImportTools(innerRegistry, {
132
135
  adoptProject,
133
- getTab: () => activeTab.value,
134
136
  // Same re-keying as `create_project`: an import bootstraps a project, so the bootstrap
135
137
  // Conversation has to follow it out of the unscoped store.
136
138
  onProjectAdopted: (root: string) => {
@@ -140,7 +142,6 @@ export function createDocumentAssistant() {
140
142
  },
141
143
  });
142
144
  registerProjectTools(innerRegistry, {
143
- getTab: () => activeTab.value,
144
145
  renderCheck: probeRender,
145
146
  getProjectStyle,
146
147
  findOpenTab,
@@ -169,24 +170,6 @@ export function createDocumentAssistant() {
169
170
  },
170
171
  });
171
172
 
172
- /*
173
- * Gate tool advertisement/execution on live studio state, derived from the same tier table the
174
- * system prompt renders — the model is never shown a tool it cannot execute. listForLLM() runs
175
- * every agent-loop round, so a mid-loop create_project unlocks the higher tiers immediately.
176
- */
177
- const TIER_REQUIREMENTS = {
178
- /* Never refused, so the sentence is unreachable — it exists because the map is derived from
179
- the tier union, and a tier without one would not compile. */
180
- always: "nothing",
181
- "no-project": "no project to be open (it bootstraps one)",
182
- project: "an open project",
183
- document: "an open document (use open_document first)",
184
- "document-tree":
185
- "an open document whose element tree the canvas is editing — Project Settings and the " +
186
- "grid editors are documents, but not trees to restructure, and a page left in Code view " +
187
- "returns to the canvas with set_canvas_mode",
188
- } as const;
189
-
190
173
  /**
191
174
  * The HUMAN's own gate, read rather than reimplemented.
192
175
  *
@@ -203,26 +186,6 @@ export function createDocumentAssistant() {
203
186
  // The agent than for the person is the divergence this is here to remove.
204
187
  return registry ? registry.context().editor.kind === "canvas" : true;
205
188
  };
206
- const availability = new Map<string, ToolAvailability>(
207
- AI_TOOL_TIERS.map((t) => [
208
- t.name,
209
- {
210
- when: () =>
211
- toolActive(t, {
212
- /* The platform's own answer, read live rather than captured: the assistant is
213
- constructed at module load, before a platform is registered in some hosts. */
214
- canImport: Boolean(getPlatform().importSite),
215
- hasDocument: Boolean(activeTab.value),
216
- hasProject: Boolean(workspace.projectRoot),
217
- treeEditable: treeEditable(),
218
- }),
219
- requires:
220
- t.capability === "importSite"
221
- ? "a platform with a site-import backend"
222
- : TIER_REQUIREMENTS[t.tier],
223
- },
224
- ]),
225
- );
226
189
  /*
227
190
  * The other kind of tool: every command record that declares `aiTool`, as a VIEW over the active
228
191
  * registry (`services/ai-command-tools.ts`). Nothing is registered — the record IS the tool, its
@@ -237,10 +200,16 @@ export function createDocumentAssistant() {
237
200
  renderCheck: probeRender,
238
201
  validate: validateDoc,
239
202
  });
240
- const toolRegistry = composeToolRegistries(
241
- createGatedToolRegistry(innerRegistry, availability),
242
- commandTools,
243
- );
203
+ /* The hand tools gated on live studio state, then the command tools. The state is read on every
204
+ listing, so a mid-loop create_project unlocks the higher tiers the very next round. */
205
+ const toolRegistry = assistantToolRegistry(innerRegistry, commandTools, () => ({
206
+ /* The platform's own answer, read live rather than captured: the assistant is constructed at
207
+ module load, before a platform is registered in some hosts. */
208
+ canImport: Boolean(getPlatform().importSite),
209
+ hasDocument: Boolean(activeTab.value),
210
+ hasProject: Boolean(workspace.projectRoot),
211
+ treeEditable: treeEditable(),
212
+ }));
244
213
 
245
214
  let controller: AbortController | null = null;
246
215
 
@@ -378,6 +347,7 @@ export function createDocumentAssistant() {
378
347
  signal,
379
348
  session: sessionFacts,
380
349
  getTab: () => activeTab.value,
350
+ interactions,
381
351
  });
382
352
  } catch (error) {
383
353
  /*
@@ -419,14 +389,14 @@ export function createDocumentAssistant() {
419
389
  /* The abort settles an outstanding question through the turn signal, but only when the turn
420
390
  HAS one — the evals runner and several tests drive the loop without. Settling here as well
421
391
  is what makes Stop terminal in every case rather than in most of them. */
422
- cancelAsk();
392
+ interactions.cancel();
423
393
  abortImportRun();
424
394
  chatState.cancelStream();
425
395
  }
426
396
 
427
397
  function newChat() {
428
398
  stop();
429
- resetAsk();
399
+ interactions.reset();
430
400
  resetImportRuns();
431
401
  /* The record of what each turn changed is drawn under that turn's message; with the messages
432
402
  gone there is nothing to draw it under, and a Restore offered from it would undo whatever
@@ -452,7 +422,7 @@ export function createDocumentAssistant() {
452
422
  return;
453
423
  }
454
424
  stop();
455
- resetAsk();
425
+ interactions.reset();
456
426
  chatState.clearChat();
457
427
  pushRestoredMessages(msgs);
458
428
  sessionId = id;
@@ -464,7 +434,7 @@ export function createDocumentAssistant() {
464
434
  sessionStore.deleteSession(projectRoot(), id);
465
435
  if (sessionId === id) {
466
436
  stop();
467
- resetAsk();
437
+ interactions.reset();
468
438
  chatState.clearChat();
469
439
  sessionId = null;
470
440
  }
@@ -527,6 +497,7 @@ export function createDocumentAssistant() {
527
497
 
528
498
  return {
529
499
  chatState,
500
+ interactions,
530
501
  isTurnActive,
531
502
  sendMessage,
532
503
  whenTurnEnds,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Assistant-tools.ts — the one registry the assistant's turn reads its tools from.
3
+ *
4
+ * Two kinds of tool reach the model: the hand-registered ones, gated by the tier table
5
+ * (`AI_TOOL_TIERS`), and the projections of every command record that declares `aiTool`, gated by
6
+ * the record (`services/ai-command-tools.ts`). This is where they meet, for the document assistant
7
+ * and for the eval runner alike, so both offer the model the same tools in the same state.
8
+ *
9
+ * @license MIT
10
+ */
11
+
12
+ import { catalogRegistry, createCatalog } from "@jxsuite/ai/catalog";
13
+ import type { ToolRegistry } from "@jxsuite/ai/tools";
14
+ import type { CommandToolRegistry } from "../ai-command-tools";
15
+ import { AI_TOOL_TIERS, toolActive } from "../ai-system-prompt";
16
+ import type { AiToolState, AiToolTier } from "../ai-system-prompt";
17
+
18
+ /**
19
+ * What each tier requires, as the sentence a refused call ends with. The model reads it, so it is
20
+ * phrased as what to do as much as what is missing.
21
+ */
22
+ export const TIER_REQUIREMENTS: Readonly<Record<AiToolTier, string>> = {
23
+ /* Never refused, so the sentence is unreachable — it exists because the map is derived from the
24
+ tier union, and a tier without one would not compile. */
25
+ always: "nothing",
26
+ "no-project": "no project to be open (it bootstraps one)",
27
+ project: "an open project",
28
+ document: "an open document (use open_document first)",
29
+ "document-tree":
30
+ "an open document whose element tree the canvas is editing — Project Settings and the " +
31
+ "grid editors are documents, but not trees to restructure, and a page left in Code view " +
32
+ "returns to the canvas with set_canvas_mode",
33
+ };
34
+
35
+ /**
36
+ * The assistant's tools as one catalog (specs/ai.md §3.10): the hand tools, then the command
37
+ * records' projections. Membership is every tool either side has, rebuilt per call because the
38
+ * command side is a view over whichever command registry is active. A hand tool is gated on live
39
+ * studio state by the same tier table the system prompt renders, so the model is never shown a tool
40
+ * it cannot execute; a command tool is offered while its record is, and its record refuses a call
41
+ * itself. The state is read on every listing, and `listForLLM()` runs every agent-loop round, so a
42
+ * mid-loop `create_project` unlocks the higher tiers immediately.
43
+ *
44
+ * @param {ToolRegistry} hand - The hand-registered tools
45
+ * @param {CommandToolRegistry} commands - The command records' projections
46
+ * @param {() => AiToolState} state - The studio facts a tier is decided by, read live
47
+ * @returns {ToolRegistry}
48
+ */
49
+ export function assistantToolRegistry(
50
+ hand: ToolRegistry,
51
+ commands: CommandToolRegistry,
52
+ state: () => AiToolState,
53
+ ): ToolRegistry {
54
+ const tiers = new Map(AI_TOOL_TIERS.map((tool) => [tool.name, tool]));
55
+ return catalogRegistry(() => createCatalog(hand.list(), commands.members()), {
56
+ advertise: (entry) =>
57
+ hand.getDefinition(entry.name) !== undefined || commands.advertises(entry.name),
58
+ gate(entry) {
59
+ const tool = tiers.get(entry.name);
60
+ if (!tool || toolActive(tool, state())) {
61
+ return null;
62
+ }
63
+ return tool.capability === "importSite"
64
+ ? "a platform with a site-import backend"
65
+ : TIER_REQUIREMENTS[tool.tier];
66
+ },
67
+ });
68
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Interaction-store.ts — where a turn waits on the author (specs/ai.md §3.4 and §3.11).
3
+ *
4
+ * A call that needs a person (`ask_user`'s question) never runs a tool body that awaits them: the
5
+ * invoker answers `needs_input`, the turn engine hands the request to this store's inline strategy,
6
+ * and the turn waits on the promise it returns. The panel reads `pending` to draw the question in
7
+ * its chip and the composer, and settles it with `answer`, `skip` or, on Stop, `cancel`. The answer
8
+ * goes back to the invoker, which gives it to the tool's `settle`, so it travels as the tool result
9
+ * and never as a user message.
10
+ *
11
+ * Exactly one interaction can be pending, because a turn runs its calls one at a time and a window
12
+ * runs one turn.
13
+ *
14
+ * @license MIT
15
+ */
16
+
17
+ import type { InteractionStrategy } from "@jxsuite/ai/harness";
18
+ import type { InteractionRequest, InteractionResponse } from "@jxsuite/ai/tools";
19
+ import { reactive } from "../../reactivity";
20
+
21
+ /** What a turn is waiting on. `callId` is the call's id, so the chip and the request agree. */
22
+ export interface PendingInteraction {
23
+ readonly callId: string;
24
+ readonly request: InteractionRequest;
25
+ }
26
+
27
+ /** The author's side of a turn that is waiting on them. */
28
+ export interface InteractionStore {
29
+ /** What the turn is waiting on, or null. Reactive: read it inside an `effect()`. */
30
+ readonly pending: PendingInteraction | null;
31
+ /** The strategy a turn resolves its interactions through. */
32
+ readonly strategy: InteractionStrategy;
33
+ /** Answer the pending question. False when nothing is pending. */
34
+ readonly answer: (text: string) => boolean;
35
+ /** Decline to answer ("you decide"), which settles as a success. False when nothing is pending. */
36
+ readonly skip: () => boolean;
37
+ /** Settle the pending interaction as unanswered, as a stopped turn does. */
38
+ readonly cancel: () => void;
39
+ /** Drop the pending interaction, settling its turn as unanswered. */
40
+ readonly reset: () => void;
41
+ }
42
+
43
+ /** What the host may hook. */
44
+ export interface InteractionStoreOptions {
45
+ /**
46
+ * Called once a request is pending, before the turn waits on it. A host saves the conversation
47
+ * here: a turn waiting on a person may wait for as long as they like, and a reload in the
48
+ * meantime must still find the question to restore as open.
49
+ */
50
+ readonly onPending?: () => void;
51
+ }
52
+
53
+ /** The answer a stopped turn gives: a question unanswered and not skipped, an approval refused. */
54
+ function unanswered(request: InteractionRequest): InteractionResponse {
55
+ return request.kind === "question"
56
+ ? { answer: null, kind: "question", skipped: false }
57
+ : { approved: false, kind: "approval" };
58
+ }
59
+
60
+ /**
61
+ * A store with nothing pending.
62
+ *
63
+ * @param {InteractionStoreOptions} [options]
64
+ * @returns {InteractionStore}
65
+ */
66
+ export function createInteractionStore({
67
+ onPending,
68
+ }: InteractionStoreOptions = {}): InteractionStore {
69
+ const state = reactive<{ pending: PendingInteraction | null }>({ pending: null });
70
+ /** The settler for the pending request, kept out of the reactive object (it is not render data). */
71
+ let settle: ((response: InteractionResponse) => void) | null = null;
72
+ /** Torn down with the request, so a stopped turn leaves no listener on a dead signal. */
73
+ let detachAbort: (() => void) | null = null;
74
+
75
+ /** Clear the pending request and settle its turn with `response`; false when none is pending. */
76
+ function finish(response: (request: InteractionRequest) => InteractionResponse): boolean {
77
+ const resolve = settle;
78
+ const { pending } = state;
79
+ if (!resolve || !pending) {
80
+ return false;
81
+ }
82
+ state.pending = null;
83
+ settle = null;
84
+ detachAbort?.();
85
+ detachAbort = null;
86
+ resolve(response(pending.request));
87
+ return true;
88
+ }
89
+
90
+ function cancel(): void {
91
+ finish(unanswered);
92
+ }
93
+
94
+ return {
95
+ get pending() {
96
+ return state.pending;
97
+ },
98
+ strategy: {
99
+ mode: "inline",
100
+ resolve: (request, { callId, signal }) =>
101
+ new Promise<InteractionResponse>((resolve) => {
102
+ if (signal.aborted) {
103
+ resolve(unanswered(request));
104
+ return;
105
+ }
106
+ state.pending = { callId, request };
107
+ settle = resolve;
108
+ signal.addEventListener("abort", cancel, { once: true });
109
+ detachAbort = () => signal.removeEventListener("abort", cancel);
110
+ onPending?.();
111
+ }),
112
+ },
113
+ answer: (text) => finish(() => ({ answer: text, kind: "question", skipped: false })),
114
+ skip: () => finish(() => ({ answer: null, kind: "question", skipped: true })),
115
+ cancel,
116
+ reset: cancel,
117
+ };
118
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Studio-documents.ts — the open tab as a `DocumentHost` for the portable tree tools (specs/ai.md
3
+ * §3.9).
4
+ *
5
+ * The producers in `@jxsuite/ai/jx-tools` plan an edit as doc ops; this host resolves the call to
6
+ * the active tab, hands the producer the raw document, and applies its ops through
7
+ * `applyDocOpsAsUser`, so an assistant edit is an ordinary transaction: the gate, the turn's batch
8
+ * lane, history, the canvas patch and the collab observer all see it the way they see the
9
+ * person's.
10
+ *
11
+ * @license MIT
12
+ */
13
+
14
+ import type { DocumentHost, DocumentRef, WriteReporter } from "@jxsuite/ai/jx-tools";
15
+ import { FROZEN_SENTENCE } from "@jxsuite/ai/jx-tools";
16
+ import { toRaw } from "../../reactivity";
17
+ import type { Tab } from "../../tabs/tab";
18
+ import { applyDocOpsAsUser } from "../../tabs/transact";
19
+ import { reportDocumentWrite, snapshotBeforeWrite } from "../ai-write-report";
20
+ import type { WriteReportDeps, WriteSnapshot } from "../ai-write-report";
21
+
22
+ /** What a tree tool reads when no document is open. */
23
+ export const NO_DOCUMENT_SENTENCE =
24
+ "No document is open — use open_document(path) to open one on the canvas, or " +
25
+ "read_file/write_file to work with project files directly.";
26
+
27
+ /** A {@link DocumentHost} over Studio's tabs, which can also name the tab behind a ref. */
28
+ export interface StudioDocumentHost extends DocumentHost {
29
+ /** The tab `ref` was resolved to, or null for a ref this host never issued. */
30
+ readonly tabOf: (ref: DocumentRef) => Tab | null;
31
+ }
32
+
33
+ /**
34
+ * The host the tree tools edit through. A call is pinned to the tab active when it began: a ref
35
+ * holds that tab for the whole call, so a person switching tabs during the call's validation does
36
+ * not redirect its write.
37
+ *
38
+ * @param {{ getTab: () => Tab | null }} deps
39
+ * @returns {StudioDocumentHost}
40
+ */
41
+ export function studioDocumentHost({ getTab }: { getTab: () => Tab | null }): StudioDocumentHost {
42
+ const tabs = new WeakMap<DocumentRef, Tab>();
43
+
44
+ function tabOf(ref: DocumentRef): Tab | null {
45
+ return tabs.get(ref) ?? null;
46
+ }
47
+
48
+ return {
49
+ edit(ref, produce, { actor }) {
50
+ const tab = tabOf(ref);
51
+ if (!tab) {
52
+ return { applied: false, message: NO_DOCUMENT_SENTENCE, reason: "not-open" };
53
+ }
54
+ // Planned and applied in one synchronous step, against the raw tree (never the proxy).
55
+ const edit = produce(toRaw(tab.doc.document));
56
+ if (!edit.ok) {
57
+ return { applied: false, message: edit.error, reason: "refused" };
58
+ }
59
+ /* Named, not inferred: the write joins the turn's batch lane by its actor (specs/ai.md §3.3).
60
+ A refusal is the collab gate pausing structural edits while source is canonical, which the
61
+ model must be told rather than congratulated on. */
62
+ const outcome = applyDocOpsAsUser(tab, edit.ops, { actor, selection: edit.selection });
63
+ if (outcome === "refused") {
64
+ return { applied: false, message: FROZEN_SENTENCE, reason: "frozen" };
65
+ }
66
+ return { applied: true, changed: outcome === "applied" };
67
+ },
68
+ resolve() {
69
+ const tab = getTab();
70
+ if (!tab) {
71
+ return NO_DOCUMENT_SENTENCE;
72
+ }
73
+ const ref: DocumentRef = { id: tab.id, path: tab.documentPath ?? null };
74
+ tabs.set(ref, tab);
75
+ return ref;
76
+ },
77
+ snapshot(ref) {
78
+ /* SAFETY: every ref a tree tool snapshots came from `resolve` on this host in the same call,
79
+ and `resolve` registers the tab before it returns the ref. */
80
+ return toRaw(tabOf(ref)!.doc.document);
81
+ },
82
+ tabOf,
83
+ undoable: "document",
84
+ };
85
+ }
86
+
87
+ /**
88
+ * The write verdict (`ai-write-report.ts`) as a reporter: the schema errors and render state before
89
+ * the write, and the new errors, the render check and the token hints after it.
90
+ *
91
+ * @param {StudioDocumentHost} host
92
+ * @param {WriteReportDeps} deps
93
+ * @returns {WriteReporter<WriteSnapshot>}
94
+ */
95
+ export function studioWriteReporter(
96
+ host: StudioDocumentHost,
97
+ deps: WriteReportDeps,
98
+ ): WriteReporter<WriteSnapshot> {
99
+ return {
100
+ /* SAFETY (both): the entries bracket only a write whose ref `resolve` issued in the same call,
101
+ so the host still holds its tab. */
102
+ after: (ref, before, summary) => reportDocumentWrite(host.tabOf(ref)!, before, summary, deps),
103
+ before: (ref) => snapshotBeforeWrite(host.tabOf(ref)!, deps),
104
+ };
105
+ }
@@ -13,8 +13,6 @@ export interface StudioTurnHookDeps {
13
13
  superseded: () => boolean;
14
14
  /** Called once a call settles into a transcript that replaced the turn's own. */
15
15
  onSuperseded: () => void;
16
- /** Re-anchor the undo batch on the document the tools now edit (specs/ai.md §3.3). */
17
- reanchor: () => void;
18
16
  }
19
17
 
20
18
  /**
@@ -22,8 +20,7 @@ export interface StudioTurnHookDeps {
22
20
  *
23
21
  * - **After each call**, a turn whose conversation has been replaced stops there: its reply belongs
24
22
  * to a request that is no longer on screen, and writing it would land a stray `tool` message in
25
- * the other conversation. Otherwise the undo batch follows the document the tools moved to, which
26
- * a tool, project adoption or the author clicking another tab can each change.
23
+ * the other conversation.
27
24
  * - **On a stream error**, a lapsed hosted grant (`cf_reconnect_required`) drops the model reading
28
25
  * and re-probes (specs/ai.md §2.1): the probe settles once at boot, so without this every gate
29
26
  * keeps offering an assistant that cannot answer, and the failed send is the only evidence.
@@ -36,9 +33,7 @@ export function studioTurnHooks(deps: StudioTurnHookDeps): TurnHooks {
36
33
  afterTool() {
37
34
  if (deps.superseded()) {
38
35
  deps.onSuperseded();
39
- return;
40
36
  }
41
- deps.reanchor();
42
37
  },
43
38
  onStreamError(error) {
44
39
  if (error.code === "cf_reconnect_required") {