@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.
- package/dist/chunks/{2020-dg097k3e.js → 2020-j56903f1.js} +3 -3
- package/dist/chunks/adapter-wc5zvg9a.js +4 -0
- package/dist/chunks/add-new-flow-snx1s4b0.js +4 -0
- package/dist/chunks/cf-account-picker-xyqw0jex.js +4 -0
- package/dist/chunks/cf-settings-hp1pjm0n.js +4 -0
- package/dist/chunks/color-04361ttk.js +4 -0
- package/dist/chunks/{color-area-vh9cew51.js → color-area-h8dbasp1.js} +2 -2
- package/dist/chunks/{color-field-e78f16bs.js → color-field-qv5r2b16.js} +3 -3
- package/dist/chunks/{color-slider-fg4yhvk6.js → color-slider-nz2mrzky.js} +2 -2
- package/dist/chunks/components-nkwphrx3.js +4 -0
- package/dist/chunks/content-source-6a253wmk.js +4 -0
- package/dist/chunks/{convert-to-repeater-grnmea6c.js → convert-to-repeater-fhdwyg14.js} +3 -3
- package/dist/chunks/editor-wqrc68ca.js +4 -0
- package/dist/chunks/entry-commands-nef0wrmp.js +4 -0
- package/dist/chunks/{extension-sections-rp7fkega.js → extension-sections-8ez2rrk8.js} +3 -3
- package/dist/chunks/file-ops-y3w71k1n.js +4 -0
- package/dist/chunks/files-de90apn9.js +4 -0
- package/dist/chunks/format-host-zfjz4rzm.js +4 -0
- package/dist/chunks/fs-events-77c0mr0x.js +4 -0
- package/dist/chunks/grid-open-4n4fjfpf.js +4 -0
- package/dist/chunks/{grid-view-x26rfpz1.js → grid-view-ka55z38f.js} +10 -10
- package/dist/chunks/{iframe-entry-pkgmcgp0.js → iframe-entry-2x9bn5ng.js} +4 -4
- package/dist/chunks/{iframe-entry-02wzcyvy.js → iframe-entry-btck0e9s.js} +4 -4
- package/dist/chunks/iframe-host-na2mywz4.js +4 -0
- package/dist/chunks/index-ewmgdb7c.js +4 -0
- package/dist/chunks/{index-203h71wy.js → index-qb98xhse.js} +3 -3
- package/dist/chunks/{javascript-ww5pv5ae.js → javascript-ces13wny.js} +2 -2
- package/dist/chunks/{jsonMode-vwbdwwq3.js → jsonMode-6md16d4v.js} +6 -6
- package/dist/chunks/jx-validate-b309c020.js +4 -0
- package/dist/chunks/layers-8rn8y6qv.js +4 -0
- package/dist/chunks/layers-panel-eyah7h9z.js +4 -0
- package/dist/chunks/lit-html-wyqw04y2.js +4 -0
- package/dist/chunks/media-library-gs3np47y.js +4 -0
- package/dist/chunks/media-picker-tt7dk4tb.js +4 -0
- package/dist/chunks/{monaco-binding-63tqze27.js → monaco-binding-p8j80s4w.js} +3 -3
- package/dist/chunks/monaco-buffer-wsf6cmnq.js +4 -0
- package/dist/chunks/{monaco-setup-zyyynnj8.js → monaco-setup-wasktzfg.js} +3 -3
- package/dist/chunks/notify-qwst108r.js +4 -0
- package/dist/chunks/parse-n8dfxnqf.js +4 -0
- package/dist/chunks/platform-c16mqvmp.js +4 -0
- package/dist/chunks/publish-panel-d34bhvwj.js +4 -0
- package/dist/chunks/{publish-quick-ajzdhhs6.js → publish-quick-vr695j26.js} +3 -3
- package/dist/chunks/publish-workspace-2yr60mt4.js +4 -0
- package/dist/chunks/quick-search-e6xsdv0d.js +4 -0
- package/dist/chunks/references-gsj6trdv.js +4 -0
- package/dist/chunks/right-panel-33jv1knc.js +4 -0
- package/dist/chunks/source-control-bj9bejjv.js +4 -0
- package/dist/chunks/studio-03mqh8ax.js +5 -0
- package/dist/chunks/{studio-957jg8g3.js → studio-06j4en5h.js} +4 -4
- package/dist/chunks/studio-0ydxv2mr.js +7 -0
- package/dist/chunks/{studio-v2ek69cb.js → studio-1e8dw2w2.js} +4 -4
- package/dist/chunks/studio-2hsqbq7b.js +7 -0
- package/dist/chunks/studio-2j86y34y.js +6 -0
- package/dist/chunks/studio-2ks6ka4g.js +5 -0
- package/dist/chunks/studio-34jrz12k.js +5 -0
- package/dist/chunks/{studio-1meqbxns.js → studio-35vrtpdq.js} +4 -4
- package/dist/chunks/studio-38g0jdz7.js +5 -0
- package/dist/chunks/{studio-25a5by3h.js → studio-396srjbk.js} +4 -4
- package/dist/chunks/{studio-zm7dnr9g.js → studio-3c8pp7pb.js} +4 -4
- package/dist/chunks/{studio-x3ge4rts.js → studio-3v5cagy9.js} +4 -4
- package/dist/chunks/studio-3w35qvay.js +5 -0
- package/dist/chunks/studio-3zge48z4.js +7 -0
- package/dist/chunks/studio-45gyxpcq.js +5 -0
- package/dist/chunks/{studio-hw60n5dh.js → studio-4kzbrr5t.js} +3 -3
- package/dist/chunks/{studio-533py3yy.js → studio-5tzgq6fx.js} +4 -4
- package/dist/chunks/studio-5wzdy1jc.js +16 -0
- package/dist/chunks/studio-732mw0g8.js +5 -0
- package/dist/chunks/{studio-3q6n9qvq.js → studio-771k2n12.js} +4 -4
- package/dist/chunks/{studio-z42kysg7.js → studio-7dzyz9n8.js} +137 -137
- package/dist/chunks/{studio-fqs1a22a.js → studio-7xag4na8.js} +4 -4
- package/dist/chunks/{studio-jqrg9xxg.js → studio-881z1xrx.js} +4 -4
- package/dist/chunks/studio-88bmt9rh.js +5 -0
- package/dist/chunks/{studio-8nyw10dp.js → studio-9b7177jq.js} +4 -4
- package/dist/chunks/{studio-nm9qev4p.js → studio-9cxmp61c.js} +5 -5
- package/dist/chunks/{studio-4dsdnan2.js → studio-9dz76yqc.js} +5 -5
- package/dist/chunks/{studio-a6x058ew.js → studio-agyvzfm4.js} +4 -4
- package/dist/chunks/{studio-2b8443ms.js → studio-ajw4k8hp.js} +3 -3
- package/dist/chunks/studio-c1pxszat.js +5 -0
- package/dist/chunks/studio-cpkamjdn.js +14 -0
- package/dist/chunks/studio-d038kqbs.js +5 -0
- package/dist/chunks/studio-d83m0wyq.js +5 -0
- package/dist/chunks/studio-dbxv7w4s.js +5 -0
- package/dist/chunks/studio-de1s2x9v.js +5 -0
- package/dist/chunks/{studio-94ppxeqy.js → studio-dx73md4n.js} +8 -8
- package/dist/chunks/studio-ekgtrndg.js +5 -0
- package/dist/chunks/studio-epenqmvk.js +7 -0
- package/dist/chunks/studio-et2q4s8n.js +5 -0
- package/dist/chunks/{studio-txkx6586.js → studio-ewarnzm4.js} +7 -7
- package/dist/chunks/{studio-bf0rgyf7.js → studio-fy7jzj9j.js} +4 -4
- package/dist/chunks/{studio-332hesdk.js → studio-gr36vnte.js} +4 -4
- package/dist/chunks/{studio-rdnezzkd.js → studio-gwrhcrss.js} +5 -5
- package/dist/chunks/{studio-9wyqyfqy.js → studio-h310mbqm.js} +4 -4
- package/dist/chunks/{studio-zht0x0v3.js → studio-h5tpz5ke.js} +4 -4
- package/dist/chunks/studio-h7weqf2q.js +5 -0
- package/dist/chunks/{studio-1gdfsr49.js → studio-hcsba201.js} +7 -7
- package/dist/chunks/studio-hwejq03c.js +5 -0
- package/dist/chunks/{studio-wcakp5bt.js → studio-hy4ye0dx.js} +7 -7
- package/dist/chunks/{studio-02phtmqn.js → studio-jttyqrmh.js} +4 -4
- package/dist/chunks/{studio-j5jqvyn3.js → studio-jw0q5j6k.js} +4 -4
- package/dist/chunks/studio-k3a8rp3d.js +5 -0
- package/dist/chunks/{studio-hnx1h1pr.js → studio-k3bcy02e.js} +4 -4
- package/dist/chunks/studio-m48v13s3.js +4 -0
- package/dist/chunks/{studio-bqgs90r8.js → studio-ndskgqef.js} +4 -4
- package/dist/chunks/studio-p1n2xjda.js +5 -0
- package/dist/chunks/studio-pgm83a2f.js +6 -0
- package/dist/chunks/studio-pqqq6v85.js +5 -0
- package/dist/chunks/studio-psh8mwnw.js +4 -0
- package/dist/chunks/{studio-cpdzrdgp.js → studio-r17asgs0.js} +4 -4
- package/dist/chunks/{studio-6vrxzkba.js → studio-ry81j4q1.js} +4 -4
- package/dist/chunks/studio-rzg8hnkf.js +5 -0
- package/dist/chunks/{studio-nsv7g1qn.js → studio-s0fg2wav.js} +4 -4
- package/dist/chunks/{studio-v0gsjav0.js → studio-tj2s0tks.js} +4 -4
- package/dist/chunks/studio-tyw7tk0x.js +5 -0
- package/dist/chunks/studio-v9ywz2se.js +6 -0
- package/dist/chunks/{studio-dmn7gpba.js → studio-vswcpzk7.js} +5 -5
- package/dist/chunks/studio-vzgmyt0v.js +5 -0
- package/dist/chunks/{studio-2dtc90zs.js → studio-wxdcd42g.js} +6 -6
- package/dist/chunks/{studio-c5dj3dzk.js → studio-wxhy1yh7.js} +4 -4
- package/dist/chunks/studio-x08a3m0a.js +5 -0
- package/dist/chunks/studio-x3mjr5cc.js +5 -0
- package/dist/chunks/{studio-xddgbwhe.js → studio-x97ay0v4.js} +4 -4
- package/dist/chunks/{studio-mmqr9awv.js → studio-xdpz9rpe.js} +33 -33
- package/dist/chunks/studio-xh683qzy.js +5 -0
- package/dist/chunks/{studio-hxq1ebp0.js → studio-xkxetm65.js} +6 -6
- package/dist/chunks/{studio-73wz63nn.js → studio-y26hx9re.js} +4 -4
- package/dist/chunks/{studio-kdseew9m.js → studio-yg3djpnm.js} +4 -4
- package/dist/chunks/studio-yx8hzqja.js +5 -0
- package/dist/chunks/studio-zg16eeq1.js +5 -0
- package/dist/chunks/studio-zgx7svjd.js +9 -0
- package/dist/chunks/studio-zkq3r91p.js +5 -0
- package/dist/chunks/{toast-host-1yg5ar8d.js → toast-host-g3r2s0kn.js} +2 -2
- package/dist/chunks/toast-w11qpkyz.js +4 -0
- package/dist/chunks/{tsMode-vra2hh7y.js → tsMode-h6tkz0ea.js} +5 -5
- package/dist/chunks/workspace-home-zkkrxq3r.js +4 -0
- package/dist/chunks/workspace-tdqqjqaa.js +4 -0
- package/dist/chunks/{ws-client-72x5n99j.js → ws-client-e951my95.js} +3 -3
- package/dist/iframe-entry.js +13 -13
- package/dist/manifest.json +77 -77
- package/dist/studio.js +91 -92
- package/package.json +11 -12
- package/src/canvas/iframe-editable-root.ts +32 -2
- package/src/collab/collab-session.ts +6 -2
- package/src/commands/context.ts +4 -0
- package/src/commands/defaults.ts +2 -0
- package/src/commands/live-context.ts +2 -0
- package/src/files/files.ts +6 -3
- package/src/format/format-host.ts +1 -1
- package/src/github/github-auth-status.ts +2 -2
- package/src/media/media-library.ts +73 -3
- package/src/new-project/new-project-modal.ts +1 -4
- package/src/packages/ensure-deps.ts +5 -5
- package/src/packages/pull-package-sync.ts +4 -4
- package/src/panels/activity-panel.ts +1 -1
- package/src/panels/ai-panel.ts +27 -34
- package/src/platform.ts +20 -0
- package/src/platforms/cloud.ts +48 -34
- package/src/platforms/devserver.ts +2 -120
- package/src/services/ai-ask.ts +29 -171
- package/src/services/ai-command-tools.ts +40 -50
- package/src/services/ai-import-tools.ts +4 -5
- package/src/services/ai-project-tools.ts +13 -3
- package/src/services/ai-tools.ts +26 -564
- package/src/services/ai-writes.ts +84 -2
- package/src/services/document-assistant.ts +29 -58
- package/src/services/harness/assistant-tools.ts +68 -0
- package/src/services/harness/interaction-store.ts +118 -0
- package/src/services/harness/studio-documents.ts +105 -0
- package/src/services/harness/turn-hooks.ts +1 -6
- package/src/services/project-adoption.ts +8 -16
- package/src/services/tool-executor.ts +25 -22
- package/src/settings/extension-commands.ts +1 -1
- package/src/settings/extension-rows.ts +1 -6
- package/src/settings/extensions-section.ts +3 -6
- package/src/state.ts +3 -13
- package/src/surfaces/block-action-bar.json +1 -3
- package/src/surfaces/files-panel.json +12 -0
- package/src/surfaces/files-panel.ts +5 -2
- package/src/surfaces/media-library.json +35 -0
- package/src/surfaces/media-library.ts +2 -0
- package/src/surfaces/menu.ts +62 -1
- package/src/surfaces/settings-packages.ts +1 -1
- package/src/surfaces/welcome.ts +4 -1
- package/src/tabs/selection.ts +14 -109
- package/src/tabs/tab.ts +10 -1
- package/src/tabs/transact.ts +316 -124
- package/src/types.ts +23 -16
- package/src/ui/layers.ts +1 -1
- package/src/ui/progress-modal.ts +3 -3
- package/dist/chunks/adapter-tck81zft.js +0 -4
- package/dist/chunks/add-new-flow-a1w0bnm6.js +0 -4
- package/dist/chunks/cf-account-picker-ce3kp3x1.js +0 -4
- package/dist/chunks/cf-settings-7ye6x7g2.js +0 -4
- package/dist/chunks/color-4gj49xq6.js +0 -4
- package/dist/chunks/components-qce0v93f.js +0 -4
- package/dist/chunks/content-source-jjx8kgzr.js +0 -4
- package/dist/chunks/editor-bkja5dbv.js +0 -4
- package/dist/chunks/entry-commands-1wh0btvg.js +0 -4
- package/dist/chunks/file-ops-6pe4z7an.js +0 -4
- package/dist/chunks/files-ybpp3j38.js +0 -4
- package/dist/chunks/format-host-3mtbhxk1.js +0 -4
- package/dist/chunks/fs-events-xfanfxds.js +0 -4
- package/dist/chunks/grid-open-dg5qmnaj.js +0 -4
- package/dist/chunks/iframe-host-j2bk2xkc.js +0 -4
- package/dist/chunks/index-fkrm7pdk.js +0 -4
- package/dist/chunks/jx-validate-w7v32f6a.js +0 -4
- package/dist/chunks/layers-g7j732cm.js +0 -4
- package/dist/chunks/layers-panel-gjrsxcck.js +0 -4
- package/dist/chunks/lit-html-jdfz9rhc.js +0 -4
- package/dist/chunks/media-library-9j0v4wmk.js +0 -4
- package/dist/chunks/media-picker-cpyyt2m0.js +0 -4
- package/dist/chunks/monaco-buffer-9wcme176.js +0 -4
- package/dist/chunks/notify-rahnnnv4.js +0 -4
- package/dist/chunks/parse-v77xtwv7.js +0 -4
- package/dist/chunks/platform-b2aqv7v5.js +0 -4
- package/dist/chunks/publish-panel-4yk2461b.js +0 -4
- package/dist/chunks/publish-workspace-mxestzcf.js +0 -4
- package/dist/chunks/quick-search-9g6z65b0.js +0 -4
- package/dist/chunks/references-c26enpxs.js +0 -4
- package/dist/chunks/right-panel-9d0e0yg7.js +0 -4
- package/dist/chunks/source-control-xyn3a8ds.js +0 -4
- package/dist/chunks/studio-0khd0xdw.js +0 -5
- package/dist/chunks/studio-1fvzq9vs.js +0 -5
- package/dist/chunks/studio-1yhz0s8p.js +0 -5
- package/dist/chunks/studio-2c2mjyzj.js +0 -6
- package/dist/chunks/studio-3pndhat0.js +0 -9
- package/dist/chunks/studio-4fjqdzrb.js +0 -6
- package/dist/chunks/studio-5abge3wy.js +0 -5
- package/dist/chunks/studio-74nzv280.js +0 -5
- package/dist/chunks/studio-7cg75gtj.js +0 -14
- package/dist/chunks/studio-89cwmnae.js +0 -5
- package/dist/chunks/studio-8m1t83kn.js +0 -7
- package/dist/chunks/studio-a3evp7f6.js +0 -6
- package/dist/chunks/studio-ana67wax.js +0 -5
- package/dist/chunks/studio-b4xhpcwy.js +0 -4
- package/dist/chunks/studio-c4rcaftm.js +0 -5
- package/dist/chunks/studio-cgjt1zce.js +0 -5
- package/dist/chunks/studio-cq87hree.js +0 -5
- package/dist/chunks/studio-d140sy8e.js +0 -5
- package/dist/chunks/studio-e1g64q43.js +0 -5
- package/dist/chunks/studio-e8pzjjhf.js +0 -5
- package/dist/chunks/studio-f8kz3krj.js +0 -7
- package/dist/chunks/studio-fw3jp9m7.js +0 -5
- package/dist/chunks/studio-g6ysm05p.js +0 -7
- package/dist/chunks/studio-gmhd0j3f.js +0 -5
- package/dist/chunks/studio-gqpty626.js +0 -5
- package/dist/chunks/studio-grp684wx.js +0 -5
- package/dist/chunks/studio-h98520qr.js +0 -5
- package/dist/chunks/studio-hvxt8z79.js +0 -5
- package/dist/chunks/studio-j4e627w1.js +0 -5
- package/dist/chunks/studio-kt2j51sa.js +0 -5
- package/dist/chunks/studio-m4w6sygj.js +0 -16
- package/dist/chunks/studio-nqxqf8p2.js +0 -4
- package/dist/chunks/studio-p89w12s2.js +0 -7
- package/dist/chunks/studio-pbagym4j.js +0 -5
- package/dist/chunks/studio-pr23qhrw.js +0 -5
- package/dist/chunks/studio-r4ya3sb2.js +0 -5
- package/dist/chunks/studio-rm9m3z60.js +0 -5
- package/dist/chunks/studio-tpmnbgmt.js +0 -5
- package/dist/chunks/studio-ttxhq5yt.js +0 -5
- package/dist/chunks/studio-y1x65h5r.js +0 -5
- package/dist/chunks/studio-zf188hnd.js +0 -5
- package/dist/chunks/toast-1gv0h53h.js +0 -4
- package/dist/chunks/workspace-3nt93k48.js +0 -4
- package/dist/chunks/workspace-home-zbgyqqws.js +0 -4
- package/src/services/directory-picker.ts +0 -187
- 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(
|
|
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 {
|
|
26
|
-
import {
|
|
27
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
392
|
+
interactions.cancel();
|
|
423
393
|
abortImportRun();
|
|
424
394
|
chatState.cancelStream();
|
|
425
395
|
}
|
|
426
396
|
|
|
427
397
|
function newChat() {
|
|
428
398
|
stop();
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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") {
|