@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
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import { initProjectRepo } from "../files/files";
|
|
13
13
|
import { getPlatform, hasPlatform } from "../platform";
|
|
14
|
-
import {
|
|
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
|
|
80
|
-
* adoption replaces every tab in the workspace
|
|
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
|
-
{
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
* - **
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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 {
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
/**
|
|
85
|
-
|
|
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;
|
package/src/surfaces/menu.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
76
|
+
/** Install the project's dependencies as it stands. */
|
|
77
77
|
reinstall: () => void;
|
|
78
78
|
}
|
|
79
79
|
|
package/src/surfaces/welcome.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
: []),
|
package/src/tabs/selection.ts
CHANGED
|
@@ -42,10 +42,22 @@
|
|
|
42
42
|
* standing between a mixed selection and a half-applied, unrecorded delete.
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
-
import {
|
|
45
|
+
import { isAncestor } from "@jxsuite/schema/doc-ops";
|
|
46
46
|
|
|
47
47
|
import type { JxPath } from "../state";
|
|
48
|
-
import {
|
|
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;
|