@jxsuite/studio 5.1.0 → 5.3.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/{cf-account-picker-qwkm9zrf.js → cf-account-picker-nwnwdm6p.js} +10 -10
- package/dist/chunks/{cf-settings-71bv5zn9.js → cf-settings-adg74qsk.js} +4 -4
- package/dist/chunks/{components-vj72gqtq.js → components-5y78e05d.js} +4 -4
- package/dist/chunks/content-source-4rzvmp0m.js +46 -0
- package/dist/chunks/{convert-to-repeater-n64h8znc.js → convert-to-repeater-jxsv62v7.js} +22 -22
- package/dist/chunks/entry-commands-1wnsqn5d.js +42 -0
- package/dist/chunks/{extension-sections-3hpcambp.js → extension-sections-2mrptyek.js} +27 -27
- package/dist/chunks/{file-ops-8yhd83xr.js → file-ops-knfqshrm.js} +22 -22
- package/dist/chunks/{files-hnxqxtsg.js → files-y6fr4he9.js} +32 -30
- package/dist/chunks/{format-host-779sfp2n.js → format-host-1k4qrcqd.js} +2 -2
- package/dist/chunks/{fs-events-b88z7g4h.js → fs-events-3y6y0nrm.js} +4 -4
- package/dist/chunks/{git-panel-xwj8fpab.js → git-panel-2rkrsjva.js} +23 -22
- package/dist/chunks/grid-view-1p6hsyjx.css +8406 -0
- package/dist/chunks/grid-view-a7b87n2c.js +15322 -0
- package/dist/chunks/{iframe-host-syr0f9sb.js → iframe-host-scpw01h8.js} +13 -13
- package/dist/chunks/{jx-validate-j0wxk72w.js → jx-validate-epp2z3s3.js} +2 -2
- package/dist/chunks/{layers-ff81wthn.js → layers-jnvdbqva.js} +5 -5
- package/dist/chunks/layers-panel-73gfkcga.js +41 -0
- package/dist/chunks/{listbox-6v2ac08j.js → listbox-b7jxnhq1.js} +19 -3
- package/dist/chunks/{media-picker-cqhhxhhe.js → media-picker-f66nav16.js} +13 -13
- package/dist/chunks/menu-1dfhry4s.js +15 -0
- package/dist/chunks/{monaco-buffer-bhy3hb6m.js → monaco-buffer-pykwbm92.js} +3 -3
- package/dist/chunks/{monaco-setup-8r62fj6y.js → monaco-setup-km6abc6q.js} +2 -2
- package/dist/chunks/{notify-628t0anh.js → notify-42gk8ha1.js} +2 -2
- package/dist/chunks/{parse-0zx14y15.js → parse-reg9eevt.js} +2 -2
- package/dist/chunks/publish-panel-z1zd2da1.js +27 -0
- package/dist/chunks/quick-search-zdv7h8ht.js +47 -0
- package/dist/chunks/{references-qvvxej7h.js → references-h69xp9zz.js} +2 -2
- package/dist/chunks/right-panel-889vbnm5.js +45 -0
- package/dist/chunks/{split-g7nm1rgb.js → split-4cz4b6ef.js} +14 -3
- package/dist/chunks/{studio-vfq8qpjt.js → studio-0j7r7s9g.js} +3 -3
- package/dist/chunks/{studio-b4s9eykd.js → studio-151w0xnh.js} +15 -15
- package/dist/chunks/studio-1tbjfqv4.js +146 -0
- package/dist/chunks/{studio-8scdew0e.js → studio-27q131j3.js} +4 -4
- package/dist/chunks/{studio-7r42c6ca.js → studio-3ntvq6cg.js} +5 -5
- package/dist/chunks/{studio-g3jevyjt.js → studio-4c041r8y.js} +2 -2
- package/dist/chunks/{studio-js8nv53n.js → studio-4f3psrk1.js} +14 -14
- package/dist/chunks/{studio-pxbgypmg.js → studio-50wfwy44.js} +17 -14
- package/dist/chunks/{studio-vw7mzp6z.js → studio-6gryj0sg.js} +5 -5
- package/dist/chunks/{studio-e1pgga3k.js → studio-6kvgrkrp.js} +7 -7
- package/dist/chunks/{studio-wqz6dggm.js → studio-7144epdr.js} +9 -9
- package/dist/chunks/{studio-b0k5tqgp.js → studio-7rrb1esn.js} +32 -28
- package/dist/chunks/{studio-38emantv.js → studio-a5h5j33a.js} +19 -5
- package/dist/chunks/{studio-3773086z.js → studio-a65w20ex.js} +4 -4
- package/dist/chunks/{studio-nzzpvzx9.js → studio-ab87phj1.js} +4 -4
- package/dist/chunks/{studio-r6nvacqs.js → studio-b66rsh78.js} +20 -20
- package/dist/chunks/{studio-jxp12rnj.js → studio-b7xacrwe.js} +6 -6
- package/dist/chunks/{studio-6xa21ej9.js → studio-c04nzbby.js} +2 -2
- package/dist/chunks/{studio-try13h8c.js → studio-c0qgc3tn.js} +18 -18
- package/dist/chunks/{studio-tqgryxwg.js → studio-cgprcdxn.js} +7 -7
- package/dist/chunks/{studio-kkqkcp3d.js → studio-d1n2bykn.js} +40 -13
- package/dist/chunks/{studio-p1dw8wfw.js → studio-d8z5bajd.js} +14 -10
- package/dist/chunks/{studio-3rz5md06.js → studio-dd1qqkry.js} +6 -6
- package/dist/chunks/{studio-1z1kcjv4.js → studio-drg8qj9k.js} +11 -11
- package/dist/chunks/{studio-kcgff4r0.js → studio-fgmfksz8.js} +2 -2
- package/dist/chunks/{studio-qk90fgqm.js → studio-fzjx96t2.js} +7 -7
- package/dist/chunks/{studio-a79k90n9.js → studio-g33v4fe9.js} +17 -16
- package/dist/chunks/{studio-k75n7vhp.js → studio-gk2twyh4.js} +74 -36
- package/dist/chunks/{studio-syg2tadj.js → studio-gtfq1dkz.js} +3 -3
- package/dist/chunks/{studio-1knfqgy7.js → studio-j0w60gft.js} +8 -8
- package/dist/chunks/{studio-aj37eer3.js → studio-jt27ta6x.js} +4 -4
- package/dist/chunks/{studio-gb13095a.js → studio-ke09tn9k.js} +59 -20
- package/dist/chunks/{studio-d0300636.js → studio-ks2zzth2.js} +17 -2
- package/dist/chunks/{studio-6ewqv390.js → studio-m1p9b6c3.js} +5 -5
- package/dist/chunks/{studio-qk4bfsq2.js → studio-msc1ee1m.js} +4 -4
- package/dist/chunks/{studio-93zafjcm.js → studio-nfx354t1.js} +10 -10
- package/dist/chunks/{studio-mhhyamxf.js → studio-nsgw7dbk.js} +6 -6
- package/dist/chunks/{studio-qhhm6hbt.js → studio-rtskkm43.js} +4 -4
- package/dist/chunks/studio-t171hxhe.js +23 -0
- package/dist/chunks/{studio-hhvs4sdg.js → studio-vn5hske5.js} +10 -4
- package/dist/chunks/{studio-q3x06vmj.js → studio-wnsyffg1.js} +2 -2
- package/dist/chunks/{studio-5wt9nvhd.js → studio-xajk70w0.js} +29 -42
- package/dist/chunks/{studio-7xnmgv66.js → studio-ye9drbk8.js} +45 -44
- package/dist/chunks/{studio-y9zedyjn.js → studio-z2smx5ce.js} +2 -2
- package/dist/chunks/{studio-sbjyc8bt.js → studio-ze46gzjh.js} +9 -9
- package/dist/chunks/{studio-zdn2g6dt.js → studio-zjjde71a.js} +2 -2
- package/dist/chunks/{workspace-t55jdjbx.js → workspace-np10mayv.js} +18 -10
- package/dist/iframe-entry.js +9 -5
- package/dist/studio.css +1 -2
- package/dist/studio.js +910 -15705
- package/package.json +10 -10
- package/src/files/files.ts +38 -15
- package/src/grid/grid-lazy.ts +59 -0
- package/src/grid/grid-open.ts +20 -3
- package/src/grid/grid-panel.ts +43 -9
- package/src/media/media-open.ts +14 -2
- package/src/panels/chat-panel.ts +40 -5
- package/src/panels/pane-context.ts +5 -4
- package/src/panels/pane-grid.ts +100 -10
- package/src/panels/tab-drop.ts +220 -0
- package/src/panels/tab-strip.ts +235 -30
- package/src/services/ai-models.ts +13 -0
- package/src/services/module-preloads.ts +67 -0
- package/src/studio.ts +16 -6
- package/src/surfaces/commandbar.json +26 -16
- package/src/surfaces/formula-palette.json +8 -7
- package/src/surfaces/formula-palette.ts +10 -6
- package/src/surfaces/palette.json +8 -7
- package/src/surfaces/pane-grid.json +35 -3
- package/src/surfaces/pane-grid.ts +26 -4
- package/src/surfaces/tab-strip.json +16 -39
- package/src/surfaces/tab-strip.ts +76 -12
- package/src/ui/ai-credentials-form.ts +12 -1
- package/src/ui/panel-resize.ts +3 -23
- package/src/ui/virtual-window.ts +57 -4
- package/src/workspace/pane-derive.ts +7 -5
- package/src/workspace/session.ts +3 -3
- package/src/workspace/workspace.ts +160 -43
- package/styles/shell-frame.css +8 -0
- package/styles/shell-frame.json +4 -0
- package/dist/chunks/content-source-5mex4x1h.js +0 -46
- package/dist/chunks/entry-commands-234hjnqw.js +0 -42
- package/dist/chunks/layers-panel-pnnghw12.js +0 -40
- package/dist/chunks/menu-9p2w46gj.js +0 -15
- package/dist/chunks/publish-panel-5zt840sy.js +0 -27
- package/dist/chunks/quick-search-3748564r.js +0 -45
- package/dist/chunks/right-panel-eccz92qt.js +0 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -59,31 +59,31 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@atlaskit/pragmatic-drag-and-drop": "^3.1.0",
|
|
62
|
-
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.2.
|
|
63
|
-
"@jxsuite/ai": "^0.37.
|
|
62
|
+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.2.2",
|
|
63
|
+
"@jxsuite/ai": "^0.37.2",
|
|
64
64
|
"@jxsuite/collab": "^0.9.0",
|
|
65
65
|
"@jxsuite/create": "^1.3.11",
|
|
66
66
|
"@jxsuite/formulas": "^0.0.18",
|
|
67
67
|
"@jxsuite/markup": "^0.4.10",
|
|
68
|
-
"@jxsuite/protocol": "^2.3.
|
|
68
|
+
"@jxsuite/protocol": "^2.3.1",
|
|
69
69
|
"@jxsuite/runtime": "^4.0.1",
|
|
70
70
|
"@jxsuite/schema": "^2.2.0",
|
|
71
71
|
"@jxsuite/site": "^2.0.1",
|
|
72
|
-
"@jxsuite/ui": "^0.
|
|
73
|
-
"@vue/reactivity": "3.5.
|
|
72
|
+
"@jxsuite/ui": "^0.3.0",
|
|
73
|
+
"@vue/reactivity": "3.5.43",
|
|
74
74
|
"ajv": "^8.20.0",
|
|
75
75
|
"ajv-formats": "^3.0.1",
|
|
76
76
|
"lit-html": "^3.3.3",
|
|
77
77
|
"monaco-editor": "^0.56.0",
|
|
78
|
-
"tabulator-tables": "^6.5.
|
|
78
|
+
"tabulator-tables": "^6.5.3",
|
|
79
79
|
"yaml": "^2.9.1"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@happy-dom/global-registrator": "^20.14.5",
|
|
83
83
|
"@types/mdast": "^4.0.4",
|
|
84
|
-
"@webref/css": "^8.7.
|
|
85
|
-
"@webref/elements": "^2.
|
|
86
|
-
"@webref/idl": "^3.
|
|
84
|
+
"@webref/css": "^8.7.5",
|
|
85
|
+
"@webref/elements": "^2.9.0",
|
|
86
|
+
"@webref/idl": "^3.84.0"
|
|
87
87
|
},
|
|
88
88
|
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
89
89
|
}
|
package/src/files/files.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
61
61
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
62
62
|
import {
|
|
63
|
+
PRIMARY_PANE,
|
|
63
64
|
activateTab,
|
|
64
65
|
moveTabToPane,
|
|
65
66
|
openTab,
|
|
@@ -405,7 +406,7 @@ export async function openLastSessionOrHome(): Promise<boolean> {
|
|
|
405
406
|
if (session) {
|
|
406
407
|
const opened = await restoreSession(session, {
|
|
407
408
|
ensureSecondPane: () => {
|
|
408
|
-
receivingPane();
|
|
409
|
+
receivingPane(PRIMARY_PANE);
|
|
409
410
|
},
|
|
410
411
|
openFile: (path, paneId) => openFileInTab(path, { focus: false, paneId }),
|
|
411
412
|
});
|
|
@@ -1525,6 +1526,11 @@ function fileRowFacts(entry: { path: string; type: string }): Record<string, unk
|
|
|
1525
1526
|
}
|
|
1526
1527
|
return facts;
|
|
1527
1528
|
}
|
|
1529
|
+
/* Every file row states `file`, because `document.openToSide` takes one and the row is the only
|
|
1530
|
+
thing that knows it — the same reason `content.openEntry`'s `path` is stated here. A command
|
|
1531
|
+
whose required arguments this row cannot answer is skipped, so without this fact the
|
|
1532
|
+
"Open to the Side" row would never render on any file. */
|
|
1533
|
+
facts.file = entry.path;
|
|
1528
1534
|
if (collectionOfPath(entry.path)) {
|
|
1529
1535
|
facts.path = entry.path;
|
|
1530
1536
|
}
|
|
@@ -2238,23 +2244,36 @@ export interface OpenFileOpts {
|
|
|
2238
2244
|
/**
|
|
2239
2245
|
* Bring an ALREADY-OPEN tab to where the caller asked for it.
|
|
2240
2246
|
*
|
|
2241
|
-
* Three cases, and the third is the one a
|
|
2247
|
+
* Three cases, and the third is the one a cross-pane open depends on:
|
|
2242
2248
|
*
|
|
2243
|
-
* | the tab is…
|
|
2244
|
-
* |
|
|
2245
|
-
* |
|
|
2246
|
-
* | elsewhere, and **
|
|
2247
|
-
* |
|
|
2249
|
+
* | the tab is… | behaviour |
|
|
2250
|
+
* | ---------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
2251
|
+
* | elsewhere, and **not** its pane's active tab | move it — one tab is one document in one strip |
|
|
2252
|
+
* | elsewhere, and **is** its pane's active tab, browsing (`focus: false`) | **nothing.** You are already looking at it |
|
|
2253
|
+
* | already in the requested pane, or committing (moving/staying, focus follows) | activate it there, honouring `focus` |
|
|
2248
2254
|
*
|
|
2249
|
-
* The
|
|
2255
|
+
* The first exists because moving it would oscillate: a derivation that re-resolves to the document
|
|
2250
2256
|
* the author is editing would yank it out of their pane and into the assistant one, and the follow
|
|
2251
2257
|
* would then re-resolve against whatever landed in its place.
|
|
2258
|
+
*
|
|
2259
|
+
* The third is the first's other half, and it is what "Open to the Side" needs: the tab is
|
|
2260
|
+
* elsewhere and IS its pane's active tab, but the caller asked for it in a DIFFERENT pane with the
|
|
2261
|
+
* focus following — the author asked to go there, so "you are already looking at it" is the wrong
|
|
2262
|
+
* answer. The move is a `moveTabToPane`, which activates nothing and moves no focus; the
|
|
2263
|
+
* `activateTab` below then points the keyboard at the pane it landed in.
|
|
2264
|
+
*
|
|
2265
|
+
* **The guard reads `wanted !== undefined && holder` — not also `holder.id !== wanted`.** A tab
|
|
2266
|
+
* already in the requested pane still passes through: `moveTabToPane` is idempotent for a tab
|
|
2267
|
+
* already in `paneId` (its own doc, next to this call), so entering the block and calling it anyway
|
|
2268
|
+
* costs nothing and reaches the identical final `activateTab`. Naming the extra condition would ask
|
|
2269
|
+
* a reader to verify a distinction that changes nothing — the redundant branch this replaced is
|
|
2270
|
+
* `check-lens-mutants.ts`'s own "delete rather than assert" rule, applied to itself.
|
|
2252
2271
|
*/
|
|
2253
2272
|
function revealOpenTab(tabId: string, opts: OpenFileOpts): void {
|
|
2254
2273
|
const wanted = opts.paneId;
|
|
2255
2274
|
const holder = paneOfTab(tabId);
|
|
2256
|
-
if (wanted !== undefined && holder
|
|
2257
|
-
if (holder.activeTabId === tabId) {
|
|
2275
|
+
if (wanted !== undefined && holder) {
|
|
2276
|
+
if (holder.activeTabId === tabId && opts.focus === false) {
|
|
2258
2277
|
return;
|
|
2259
2278
|
}
|
|
2260
2279
|
moveTabToPane(tabId, wanted);
|
|
@@ -2293,7 +2312,7 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
|
|
|
2293
2312
|
* why a branch anywhere else would send a restored media tab straight back down the error path.
|
|
2294
2313
|
*/
|
|
2295
2314
|
if (isViewableMedia(path)) {
|
|
2296
|
-
openMediaTab(path);
|
|
2315
|
+
openMediaTab(path, opts);
|
|
2297
2316
|
if (follows) {
|
|
2298
2317
|
requireProjectState().selectedPath = path;
|
|
2299
2318
|
}
|
|
@@ -2308,8 +2327,10 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
|
|
|
2308
2327
|
// CSV files open in the grid editor (source mode remains as the raw-text alternate).
|
|
2309
2328
|
if (path.toLowerCase().endsWith(".csv")) {
|
|
2310
2329
|
try {
|
|
2311
|
-
await openCsvGridTab(path);
|
|
2312
|
-
|
|
2330
|
+
await openCsvGridTab(path, opts);
|
|
2331
|
+
if (follows) {
|
|
2332
|
+
requireProjectState().selectedPath = path;
|
|
2333
|
+
}
|
|
2313
2334
|
trackRecentFile({
|
|
2314
2335
|
name: path.split("/").pop() || path,
|
|
2315
2336
|
path,
|
|
@@ -2395,8 +2416,10 @@ export async function openFileInTab(path: string, opts: OpenFileOpts = {}) {
|
|
|
2395
2416
|
* Open a file into a NAMED pane, browsing rather than committing, leaving the keyboard behind.
|
|
2396
2417
|
*
|
|
2397
2418
|
* The same body as {@link openFileInTab} with the three options a side-open always wants, given a
|
|
2398
|
-
* signature that says which pane in the first parameter. It is what
|
|
2399
|
-
*
|
|
2419
|
+
* signature that says which pane in the first parameter. It is what the READS mean by "open it
|
|
2420
|
+
* beside this" — following a layout, `pane.compareWith`. The gestures that open something
|
|
2421
|
+
* (drill-in, Open to the Side) call {@link openFileInTab} directly, because the pane they open into
|
|
2422
|
+
* takes the keyboard.
|
|
2400
2423
|
*
|
|
2401
2424
|
* @param {string} paneId
|
|
2402
2425
|
* @param {string} path
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy grid engine.
|
|
3
|
+
*
|
|
4
|
+
* `grid-view.ts` is the only module that imports `tabulator-tables`, and as of 6.5.3 Tabulator's
|
|
5
|
+
* `Row` class runs `document.createElement` in a static field, so the CLASS DEFINITION — not the
|
|
6
|
+
* table's construction — needs a DOM to exist. That makes `grid-view.ts` unsafe to reach with a
|
|
7
|
+
* static value-import from any module that must load with no DOM: `app-commands.ts` pulls
|
|
8
|
+
* `gridViewCommands` from `grid-panel.ts` for the command set `scripts/check-command-levels.ts` and
|
|
9
|
+
* its siblings read in a bare Bun process (see `commands/defaults.ts`'s note on
|
|
10
|
+
* `selection.convertToComponent` for the sibling convention this follows).
|
|
11
|
+
*
|
|
12
|
+
* So `grid-panel.ts` never has a static value-import of `./grid-view`; it loads the engine through
|
|
13
|
+
* here instead, exactly as `services/monaco-lazy.ts` defers Monaco. Real usage pays no cost for it
|
|
14
|
+
* — a grid tab cannot exist before `document` does — and the two accessors mirror Monaco's: one for
|
|
15
|
+
* code that MOUNTS a grid (async is free there), one for code that can only run with a grid engine
|
|
16
|
+
* already loaded.
|
|
17
|
+
*/
|
|
18
|
+
import type * as gridViewModule from "./grid-view";
|
|
19
|
+
|
|
20
|
+
export type GridEngine = typeof gridViewModule;
|
|
21
|
+
|
|
22
|
+
let _engine: GridEngine | null = null;
|
|
23
|
+
let _loading: Promise<GridEngine> | null = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load the Tabulator-backed grid engine. Memoized — concurrent callers share one import.
|
|
27
|
+
*
|
|
28
|
+
* @returns {Promise<GridEngine>}
|
|
29
|
+
*/
|
|
30
|
+
export function loadGridEngine(): Promise<GridEngine> {
|
|
31
|
+
if (_engine) {
|
|
32
|
+
return Promise.resolve(_engine);
|
|
33
|
+
}
|
|
34
|
+
// Not `async`: an async function wraps the memo in a fresh promise per call, so concurrent
|
|
35
|
+
// Callers could not observe that they share one in-flight load.
|
|
36
|
+
_loading ??= import("./grid-view").then((engine) => {
|
|
37
|
+
_engine = engine;
|
|
38
|
+
return engine;
|
|
39
|
+
});
|
|
40
|
+
return _loading;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The loaded engine, or null before the first grid mount.
|
|
45
|
+
*
|
|
46
|
+
* For call sites that can run synchronously once a grid already exists in the session — they must
|
|
47
|
+
* not force the module to load, only read it if it is already there.
|
|
48
|
+
*
|
|
49
|
+
* @returns {GridEngine | null}
|
|
50
|
+
*/
|
|
51
|
+
export function loadedGridEngine(): GridEngine | null {
|
|
52
|
+
return _engine;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Reset the memo (tests only). */
|
|
56
|
+
export function resetGridLazy(): void {
|
|
57
|
+
_engine = null;
|
|
58
|
+
_loading = null;
|
|
59
|
+
}
|
package/src/grid/grid-open.ts
CHANGED
|
@@ -30,11 +30,25 @@ import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
|
30
30
|
/** Placeholder document for grid tabs — the grid never reads it; save routes to the controller. */
|
|
31
31
|
const GRID_STUB_DOCUMENT = { children: [], tagName: "div" };
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Open (or activate) a `.csv` file as a grid tab.
|
|
35
|
+
*
|
|
36
|
+
* The options are `openFileInTab`'s own, passed straight through: `paneId` says which pane the tab
|
|
37
|
+
* lands in (defaulting to the focused one), `focus: false` browses without moving the keyboard, and
|
|
38
|
+
* `preview` opens a disposable tab. An existing tab is activated with the caller's `focus` — the
|
|
39
|
+
* same rule the document path follows.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} path - Project-relative path of the `.csv`
|
|
42
|
+
* @param {{ paneId?: string; focus?: boolean; preview?: boolean }} [opts]
|
|
43
|
+
* @returns {Promise<Tab>}
|
|
44
|
+
*/
|
|
45
|
+
export async function openCsvGridTab(
|
|
46
|
+
path: string,
|
|
47
|
+
opts: { paneId?: string; focus?: boolean; preview?: boolean } = {},
|
|
48
|
+
): Promise<Tab> {
|
|
35
49
|
const existing = workspace.tabs.get(path);
|
|
36
50
|
if (existing) {
|
|
37
|
-
activateTab(path);
|
|
51
|
+
activateTab(path, { focus: opts.focus !== false });
|
|
38
52
|
return existing;
|
|
39
53
|
}
|
|
40
54
|
|
|
@@ -52,6 +66,9 @@ export async function openCsvGridTab(path: string): Promise<Tab> {
|
|
|
52
66
|
documentPath: path,
|
|
53
67
|
id: path,
|
|
54
68
|
sourceFormat: formatForPath(path)?.name ?? null,
|
|
69
|
+
...(opts.paneId !== undefined && { paneId: opts.paneId }),
|
|
70
|
+
...(opts.preview === true && { preview: true }),
|
|
71
|
+
...(opts.focus === false && { focus: false }),
|
|
55
72
|
});
|
|
56
73
|
const controller = createGridController(tab, source);
|
|
57
74
|
void controller.load();
|
package/src/grid/grid-panel.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { rectOf } from "../utils/geometry";
|
|
|
39
39
|
import { activeTab, workspace } from "../workspace/workspace";
|
|
40
40
|
import { createGridController, getGridController } from "./grid-controller";
|
|
41
41
|
import { createCsvFileSource } from "./sources/csv-file-source";
|
|
42
|
-
import {
|
|
42
|
+
import { loadedGridEngine, loadGridEngine } from "./grid-lazy";
|
|
43
43
|
import { parseGridTabId } from "./grid-source";
|
|
44
44
|
import { mountGridPanelSurface } from "../surfaces/grid-panel";
|
|
45
45
|
import { openGridViewsSurface } from "../surfaces/grid-views";
|
|
@@ -473,12 +473,45 @@ export function renderGridMode(surface: CanvasSurface, tab: Tab) {
|
|
|
473
473
|
const engine = controller;
|
|
474
474
|
let hostEl: HTMLElement | null = null;
|
|
475
475
|
|
|
476
|
+
/**
|
|
477
|
+
* (Re)build the Tabulator engine over `host`, destroying whatever view is live first.
|
|
478
|
+
*
|
|
479
|
+
* Synchronous when `tabulator-tables` is already loaded — true for every mount after the
|
|
480
|
+
* session's first, so every existing caller keeps seeing `panel.view` set before this returns. On
|
|
481
|
+
* a cold first load it goes through `loadGridEngine()` instead (see `grid-lazy.ts`) and `onReady`
|
|
482
|
+
* — and, symmetrically with the `ready` handling below, `panel.bump()` — run once the import
|
|
483
|
+
* resolves, guarded against the pane having moved on in the meantime.
|
|
484
|
+
*/
|
|
485
|
+
function rebuildView(onReady?: (view: GridView) => void) {
|
|
486
|
+
panel.view?.destroy();
|
|
487
|
+
panel.view = null;
|
|
488
|
+
if (!hostEl) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const requestedHost = hostEl;
|
|
492
|
+
const loaded = loadedGridEngine();
|
|
493
|
+
if (loaded) {
|
|
494
|
+
const view = loaded.createGridView(requestedHost, engine);
|
|
495
|
+
panel.view = view;
|
|
496
|
+
onReady?.(view);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
void loadGridEngine().then(({ createGridView }) => {
|
|
500
|
+
if (activeIn(paneId) !== panel || hostEl !== requestedHost || panel.view) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const view = createGridView(requestedHost, engine);
|
|
504
|
+
panel.view = view;
|
|
505
|
+
onReady?.(view);
|
|
506
|
+
panel.bump();
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
476
510
|
const panel: ActiveGridPanel = {
|
|
477
511
|
applyLayout(layout) {
|
|
478
512
|
void engine.setSort(layout?.sort ?? null);
|
|
479
513
|
engine.setGrouping(layout?.groupBy ?? null);
|
|
480
|
-
|
|
481
|
-
panel.view?.setSearch(layout?.filter ?? "");
|
|
514
|
+
rebuildView((view) => view.setSearch(layout?.filter ?? ""));
|
|
482
515
|
panel.bump();
|
|
483
516
|
},
|
|
484
517
|
bump() {
|
|
@@ -486,8 +519,7 @@ export function renderGridMode(surface: CanvasSurface, tab: Tab) {
|
|
|
486
519
|
},
|
|
487
520
|
paneId,
|
|
488
521
|
remount() {
|
|
489
|
-
|
|
490
|
-
panel.view = hostEl ? createGridView(hostEl, engine) : null;
|
|
522
|
+
rebuildView();
|
|
491
523
|
},
|
|
492
524
|
replace: null,
|
|
493
525
|
scope,
|
|
@@ -576,14 +608,16 @@ export function renderGridMode(surface: CanvasSurface, tab: Tab) {
|
|
|
576
608
|
|
|
577
609
|
// Create the engine once the columns exist and the host div is in the DOM.
|
|
578
610
|
if (!panel.view && !engine.state.loading && engine.state.columns.length > 0 && hostEl) {
|
|
579
|
-
|
|
580
|
-
|
|
611
|
+
rebuildView((view) => {
|
|
612
|
+
if (restored) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
581
615
|
restored = true;
|
|
582
616
|
const layout = loadGridLayout(engine.source.id);
|
|
583
617
|
void engine.setSort(layout?.sort ?? null);
|
|
584
618
|
engine.setGrouping(layout?.groupBy ?? null);
|
|
585
|
-
|
|
586
|
-
}
|
|
619
|
+
view.setSearch(layout?.filter ?? "");
|
|
620
|
+
});
|
|
587
621
|
}
|
|
588
622
|
});
|
|
589
623
|
});
|
package/src/media/media-open.ts
CHANGED
|
@@ -39,13 +39,22 @@ export function mediaTabModes(path: string): string[] {
|
|
|
39
39
|
/**
|
|
40
40
|
* Open (or activate) a media file's viewer tab.
|
|
41
41
|
*
|
|
42
|
+
* The options are `openFileInTab`'s own, passed straight through: `paneId` says which pane the tab
|
|
43
|
+
* lands in (defaulting to the focused one), `focus: false` browses without moving the keyboard, and
|
|
44
|
+
* `preview` opens a disposable tab. An existing tab is activated with the caller's `focus` — the
|
|
45
|
+
* same rule the document path follows.
|
|
46
|
+
*
|
|
42
47
|
* @param {string} path - Project-relative path of the file
|
|
48
|
+
* @param {{ paneId?: string; focus?: boolean; preview?: boolean }} [opts]
|
|
43
49
|
* @returns {Tab}
|
|
44
50
|
*/
|
|
45
|
-
export function openMediaTab(
|
|
51
|
+
export function openMediaTab(
|
|
52
|
+
path: string,
|
|
53
|
+
opts: { paneId?: string; focus?: boolean; preview?: boolean } = {},
|
|
54
|
+
): Tab {
|
|
46
55
|
const existing = workspace.tabs.get(path);
|
|
47
56
|
if (existing) {
|
|
48
|
-
activateTab(path);
|
|
57
|
+
activateTab(path, { focus: opts.focus !== false });
|
|
49
58
|
return existing;
|
|
50
59
|
}
|
|
51
60
|
return openTab({
|
|
@@ -54,5 +63,8 @@ export function openMediaTab(path: string): Tab {
|
|
|
54
63
|
documentPath: path,
|
|
55
64
|
id: path,
|
|
56
65
|
sourceFormat: null,
|
|
66
|
+
...(opts.paneId !== undefined && { paneId: opts.paneId }),
|
|
67
|
+
...(opts.preview === true && { preview: true }),
|
|
68
|
+
...(opts.focus === false && { focus: false }),
|
|
57
69
|
});
|
|
58
70
|
}
|
package/src/panels/chat-panel.ts
CHANGED
|
@@ -12,10 +12,19 @@
|
|
|
12
12
|
* in `ui/regions.ts`'s shell table — the assistant no longer has a shell host to name. Three
|
|
13
13
|
* screenshot shots address that id and none of them changed.
|
|
14
14
|
*
|
|
15
|
-
* Hosts the assistant
|
|
16
|
-
* project but no open document, and with a document open. The
|
|
17
|
-
* and never tears down on tab switches — the assistant's module state
|
|
18
|
-
* scroll position)
|
|
15
|
+
* Hosts the assistant machinery from ai-panel.ts unconditionally: with no project (welcome screen),
|
|
16
|
+
* with a project but no open document, and with a document open. The machinery is mounted once at
|
|
17
|
+
* studio boot and never tears down on tab switches — the assistant's module state (composer draft,
|
|
18
|
+
* scroll position, transcript) persists.
|
|
19
|
+
*
|
|
20
|
+
* **The assistant's SURFACE document is what this module defers.** Mounting `surfaces/ai-chat.ts`
|
|
21
|
+
* costs a traced 100–290 ms of connectedCallback at boot (its island projections; see
|
|
22
|
+
* `scripts/perf/REPORT.md`), paid on every window open although the document is invisible until
|
|
23
|
+
* the tab is picked. Binding is the same seam (`bindAiPanelHost`) and it is now deferred to the
|
|
24
|
+
* first time the Assistant tab is actually shown or revealed: `renderAiPanel` no-ops while the
|
|
25
|
+
* surface is null, and the surface's own watcher projects whatever the machinery wrote before that
|
|
26
|
+
* point, so a prompt seeded into a boot the reader never spends in the tab is not lost — it is one
|
|
27
|
+
* more message waiting when the body first appears.
|
|
19
28
|
*
|
|
20
29
|
* **This module is the SEAM, and it has no markup of its own.** The assistant is a Jx document
|
|
21
30
|
* (`surfaces/ai-chat.json`), and a document CLEARS the host it is given — so it needs a container
|
|
@@ -32,6 +41,7 @@ import { effect, effectScope } from "../reactivity";
|
|
|
32
41
|
import { workspace } from "../workspace/workspace";
|
|
33
42
|
import { consumePendingAgentPrompt, hasPendingAgentPrompt } from "../services/agent-seed";
|
|
34
43
|
import { REGION_ATTR } from "../ui/regions";
|
|
44
|
+
import { inspectorTab } from "./right-panel";
|
|
35
45
|
import {
|
|
36
46
|
bindAiPanelHost,
|
|
37
47
|
mountAiPanel,
|
|
@@ -48,6 +58,23 @@ const ASSISTANT_REGION = "inspector.assistant";
|
|
|
48
58
|
let _host: HTMLElement | null = null;
|
|
49
59
|
let _container: HTMLElement | null = null;
|
|
50
60
|
let _scope: EffectScope | null = null;
|
|
61
|
+
/** Whether the surface document has been bound. One bind per host; a remount resets it. */
|
|
62
|
+
let _surfaceBound = false;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bind the assistant's surface document the first time its tab is shown or revealed.
|
|
66
|
+
*
|
|
67
|
+
* The container, the region attribute and every prompt path are ready before this; the surface is
|
|
68
|
+
* the expensive part and is invisible until the tab is picked, so this is the earliest moment its
|
|
69
|
+
* cost is worth paying. A restore that lands on the Assistant tab binds on the same tick.
|
|
70
|
+
*/
|
|
71
|
+
function ensureAssistantSurface(): void {
|
|
72
|
+
if (_surfaceBound || !_container) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
_surfaceBound = true;
|
|
76
|
+
bindAiPanelHost(_container);
|
|
77
|
+
}
|
|
51
78
|
|
|
52
79
|
/**
|
|
53
80
|
* Mount the assistant into the host the Inspector hands it. Idempotent per host: the persistent
|
|
@@ -68,11 +95,18 @@ export function mount(host: HTMLElement | null) {
|
|
|
68
95
|
host.append(_container);
|
|
69
96
|
|
|
70
97
|
mountAiPanel();
|
|
71
|
-
|
|
98
|
+
_surfaceBound = false;
|
|
72
99
|
|
|
73
100
|
_scope?.stop();
|
|
74
101
|
_scope = effectScope();
|
|
75
102
|
_scope.run(() => {
|
|
103
|
+
effect(() => {
|
|
104
|
+
// The surface's bind follows the TAB: a session restored onto the Assistant is bound by
|
|
105
|
+
// One re-run of this effect, not by anyone remembering to call the seam directly.
|
|
106
|
+
if (inspectorTab() === "assistant") {
|
|
107
|
+
ensureAssistantSurface();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
76
110
|
effect(() => {
|
|
77
111
|
// A pending agent prompt (stored by the New Project flow, possibly from another window) is
|
|
78
112
|
// Keyed by the absolute project root — consume it as soon as this window adopts that root.
|
|
@@ -98,6 +132,7 @@ export function mount(host: HTMLElement | null) {
|
|
|
98
132
|
export function unmount() {
|
|
99
133
|
_scope?.stop();
|
|
100
134
|
_scope = null;
|
|
135
|
+
_surfaceBound = false;
|
|
101
136
|
// `null` unbinds the surface: the document is disposed and the chat-state watcher stopped.
|
|
102
137
|
bindAiPanelHost(null);
|
|
103
138
|
if (_host) {
|
|
@@ -500,10 +500,11 @@ export interface PresetRow {
|
|
|
500
500
|
*
|
|
501
501
|
* The previous round made the row's ANSWER a pure function of the pane and left its ACTION
|
|
502
502
|
* resolving the focus: every one of these three commands takes the focused pane as its subject
|
|
503
|
-
* (`pane.derive`'s `activePane()`/`
|
|
504
|
-
* row read off the secondary pane's menu derived from the
|
|
505
|
-
* by keyboard, which is why it survived a browser pass:
|
|
506
|
-
* POINTERDOWN, and a keyboard activation of a menu item
|
|
503
|
+
* (`pane.derive`'s `activePane()`/`paneBeside(source.id)`, `pane.unsplit`'s
|
|
504
|
+
* `workspace.activePaneId`), so a row read off the secondary pane's menu derived from the
|
|
505
|
+
* primary. Reachable by keyboard and only by keyboard, which is why it survived a browser pass:
|
|
506
|
+
* `panels/pane-grid.ts` focuses a pane on POINTERDOWN, and a keyboard activation of a menu item
|
|
507
|
+
* fires `click` alone.
|
|
507
508
|
*
|
|
508
509
|
* Carried on the row rather than passed as a command argument. A pane id in `args` would make
|
|
509
510
|
* "which pane" a public input of `pane.derive` — a third property on a record the palette already
|