@jxsuite/studio 5.0.0 → 5.2.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-fyf63pmy.js → cf-account-picker-h4dab9bq.js} +10 -10
- package/dist/chunks/{cf-settings-71bv5zn9.js → cf-settings-adg74qsk.js} +4 -4
- package/dist/chunks/components-5y78e05d.js +20 -0
- package/dist/chunks/content-source-sgg1sv6n.js +46 -0
- package/dist/chunks/{convert-to-repeater-d0cgnb6d.js → convert-to-repeater-mf1cxgfn.js} +25 -25
- package/dist/chunks/entry-commands-w9qsxa6q.js +42 -0
- package/dist/chunks/{extension-sections-fy7q4fe9.js → extension-sections-c6zwzbre.js} +27 -27
- package/dist/chunks/file-ops-vrpq1jjk.js +47 -0
- package/dist/chunks/{files-ef8fgs3w.js → files-msaf95y6.js} +33 -32
- package/dist/chunks/{format-host-3c33nwr8.js → format-host-f64fnesp.js} +2 -2
- package/dist/chunks/{fs-events-13tj6sex.js → fs-events-3y6y0nrm.js} +6 -6
- package/dist/chunks/git-panel-bv5e22j0.js +50 -0
- package/dist/chunks/grid-view-1p6hsyjx.css +8406 -0
- package/dist/chunks/grid-view-paj67nr5.js +15322 -0
- package/dist/chunks/{iframe-host-jgesd924.js → iframe-host-dh5np8vk.js} +14 -14
- package/dist/chunks/{layers-1nq5evsh.js → layers-1vcfdbet.js} +7 -6
- package/dist/chunks/layers-panel-fksbdg4q.js +40 -0
- package/dist/chunks/{media-picker-2qbetq73.js → media-picker-p49nhqhs.js} +14 -14
- package/dist/chunks/menu-qht48vnc.js +15 -0
- package/dist/chunks/{monaco-buffer-bhy3hb6m.js → monaco-buffer-pykwbm92.js} +3 -3
- 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-vz23wwmg.js +27 -0
- package/dist/chunks/quick-search-e9rp74m8.js +46 -0
- package/dist/chunks/{references-n553f0e1.js → references-h69xp9zz.js} +4 -4
- package/dist/chunks/right-panel-j97t2yvy.js +45 -0
- package/dist/chunks/{studio-18avnhjp.js → studio-01jeck2y.js} +36 -30
- package/dist/chunks/{studio-9xxz1q69.js → studio-14d9tq4d.js} +9 -9
- package/dist/chunks/{studio-xsykpvsh.js → studio-15f3kxqe.js} +19 -19
- package/dist/chunks/studio-1tbjfqv4.js +146 -0
- package/dist/chunks/{studio-79y5sz51.js → studio-28vs5k2y.js} +2 -2
- package/dist/chunks/{studio-3hxkd92t.js → studio-2mdbbc76.js} +10 -10
- package/dist/chunks/{studio-7r42c6ca.js → studio-3ntvq6cg.js} +5 -5
- package/dist/chunks/{studio-8jn5yhyn.js → studio-4c041r8y.js} +3 -3
- package/dist/chunks/{studio-qahxvnan.js → studio-4r14yzyy.js} +4 -4
- package/dist/chunks/{studio-arfpe389.js → studio-4xmjcrg3.js} +6 -6
- package/dist/chunks/{studio-5523bpzt.js → studio-5nz00bhh.js} +3 -3
- package/dist/chunks/{studio-x3rty6tr.js → studio-5t7ena5z.js} +8 -8
- package/dist/chunks/{studio-5rt7sqa0.js → studio-66arbpc2.js} +13 -19
- package/dist/chunks/{studio-fabxk6r9.js → studio-68fydsrj.js} +15 -15
- package/dist/chunks/{studio-tb3ev01j.js → studio-9j6sfeba.js} +6 -6
- package/dist/chunks/{studio-akbww0j5.js → studio-a1pswps3.js} +38 -38
- package/dist/chunks/{studio-6xa21ej9.js → studio-c04nzbby.js} +2 -2
- package/dist/chunks/{studio-4m0haseg.js → studio-c07s5ckd.js} +4 -4
- package/dist/chunks/{studio-57mf66w2.js → studio-ctm7bhx5.js} +14 -14
- package/dist/chunks/{studio-p1dw8wfw.js → studio-d8z5bajd.js} +14 -10
- package/dist/chunks/{studio-vz68yn9e.js → studio-en82c639.js} +6 -6
- package/dist/chunks/{studio-rkstz940.js → studio-etvx9s2g.js} +21 -12
- package/dist/chunks/{studio-az3zd49t.js → studio-ez11dmdh.js} +127 -37
- package/dist/chunks/{studio-sgdc5pfc.js → studio-hdc2jt3z.js} +4 -4
- package/dist/chunks/{studio-s6z853aq.js → studio-hhskq2mr.js} +24 -23
- package/dist/chunks/{studio-6ewqv390.js → studio-m1p9b6c3.js} +5 -5
- package/dist/chunks/{studio-25hkdrkw.js → studio-mpzwze9x.js} +12 -12
- package/dist/chunks/{studio-8qtrpzr3.js → studio-ncn9e189.js} +7 -7
- package/dist/chunks/{studio-81390n6f.js → studio-nq5yf5mp.js} +1 -1
- package/dist/chunks/{studio-5jy81t3s.js → studio-p6pmwkvh.js} +229 -24
- package/dist/chunks/{studio-3gxv60tn.js → studio-qa7fzxan.js} +4 -4
- package/dist/chunks/{studio-7xptrg0j.js → studio-sar8g54p.js} +8 -8
- package/dist/chunks/studio-t171hxhe.js +23 -0
- package/dist/chunks/{studio-em9nw8eh.js → studio-t3adrr56.js} +20 -17
- package/dist/chunks/{studio-hhvs4sdg.js → studio-vn5hske5.js} +10 -4
- package/dist/chunks/{studio-pm6hgy7r.js → studio-vt0tj32j.js} +29 -42
- package/dist/chunks/{studio-d1ja2x9r.js → studio-wjpgxywa.js} +4 -4
- package/dist/chunks/{studio-q3x06vmj.js → studio-wnsyffg1.js} +2 -2
- package/dist/chunks/{studio-rpyah2ew.js → studio-x10fp4tp.js} +64 -24
- package/dist/chunks/{studio-26d7myd1.js → studio-x72j58vw.js} +4 -4
- package/dist/chunks/{studio-cpvb2ed1.js → studio-y4f9fdge.js} +8 -8
- package/dist/chunks/{studio-erdatzvp.js → studio-ympe0n7a.js} +35 -6
- package/dist/chunks/{studio-y9zedyjn.js → studio-z2smx5ce.js} +2 -2
- package/dist/chunks/studio-zjjde71a.js +148 -0
- package/dist/chunks/{workspace-wrew8zk4.js → workspace-bnw54cha.js} +18 -10
- package/dist/iframe-entry.js +102 -11
- package/dist/studio.css +1 -2
- package/dist/studio.js +1113 -16062
- package/package.json +11 -11
- package/src/canvas/canvas-utils.ts +29 -2
- package/src/commands/command-args.ts +37 -2
- package/src/commands/registry.ts +18 -8
- package/src/commands/run-reported.ts +120 -0
- package/src/editor/shortcuts.ts +11 -23
- package/src/files/components.ts +39 -0
- package/src/files/files.ts +41 -17
- 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/i18n/i18n-commands.ts +10 -0
- package/src/media/media-open.ts +14 -2
- package/src/packages/pull-package-sync.ts +5 -1
- package/src/panels/ai-panel.ts +5 -2
- package/src/panels/block-action-bar.ts +7 -2
- package/src/panels/i18n-panel.ts +8 -15
- package/src/panels/jump-bar.ts +3 -2
- package/src/panels/pane-context.ts +5 -4
- package/src/panels/pane-grid.ts +100 -10
- package/src/panels/problems-panel.ts +6 -2
- package/src/panels/quick-search.ts +6 -12
- package/src/panels/settings-menu.ts +5 -15
- package/src/panels/tab-drop.ts +220 -0
- package/src/panels/tab-strip.ts +238 -33
- package/src/services/ai-command-tools.ts +78 -8
- package/src/services/ai-models.ts +13 -0
- package/src/services/ai-project-tools.ts +8 -2
- package/src/services/ai-system-prompt.ts +4 -8
- package/src/services/ai-tools.ts +8 -69
- package/src/services/document-assistant.ts +9 -6
- package/src/studio.ts +24 -8
- package/src/surfaces/commandbar.json +35 -24
- package/src/surfaces/commandbar.ts +6 -5
- package/src/surfaces/pane-grid.json +35 -3
- package/src/surfaces/pane-grid.ts +26 -4
- package/src/surfaces/statusbar.ts +2 -1
- package/src/surfaces/tab-strip.json +16 -39
- package/src/surfaces/tab-strip.ts +76 -12
- package/src/tabs/project-config.ts +45 -11
- package/src/ui/ai-credentials-form.ts +12 -1
- package/src/ui/layers.ts +4 -1
- package/src/workspace/pane-derive.ts +7 -5
- package/src/workspace/session.ts +3 -3
- package/src/workspace/workspace.ts +220 -43
- package/styles/shell-frame.css +8 -0
- package/styles/shell-frame.json +4 -0
- package/dist/chunks/components-2w1f35qs.js +0 -18
- package/dist/chunks/content-source-3wqeas45.js +0 -46
- package/dist/chunks/entry-commands-d6g50j7s.js +0 -42
- package/dist/chunks/file-ops-3btj6st0.js +0 -47
- package/dist/chunks/git-panel-4wp2rn9b.js +0 -49
- package/dist/chunks/layers-panel-ar2gh90g.js +0 -40
- package/dist/chunks/menu-rf7n72mf.js +0 -14
- package/dist/chunks/publish-panel-98f7sr3g.js +0 -27
- package/dist/chunks/quick-search-srzz83k2.js +0 -45
- package/dist/chunks/right-panel-rrgx2dn3.js +0 -45
- package/dist/chunks/studio-d055c37e.js +0 -62
|
@@ -43,6 +43,7 @@ import { armPreviewOverlay, flushPreviewOverlay } from "../preview/preview-overl
|
|
|
43
43
|
import { documentUrlPattern, dynamicRouteParams } from "../page-params";
|
|
44
44
|
import { getNodeAtPath, nodeLabel, projectState } from "../store";
|
|
45
45
|
import { activeRegistry } from "../commands/active-registry";
|
|
46
|
+
import { runActiveReported, runReported } from "../commands/run-reported";
|
|
46
47
|
import { notify } from "../services/notify";
|
|
47
48
|
import { mountSurface, registerSurface } from "../ui/surface";
|
|
48
49
|
import { rectOf } from "../utils/geometry";
|
|
@@ -521,7 +522,7 @@ export async function saveLayoutPrompt(registry: CommandRegistry): Promise<void>
|
|
|
521
522
|
placeholder: "Layout name",
|
|
522
523
|
});
|
|
523
524
|
if (name) {
|
|
524
|
-
await registry
|
|
525
|
+
await runReported(registry, "view.saveLayout", { name }, "Command Bar");
|
|
525
526
|
}
|
|
526
527
|
}
|
|
527
528
|
|
|
@@ -533,7 +534,7 @@ export async function renameLayoutPrompt(
|
|
|
533
534
|
): Promise<void> {
|
|
534
535
|
const name = await showPromptDialog("Rename layout", { confirmLabel: "Rename", value: current });
|
|
535
536
|
if (name) {
|
|
536
|
-
await registry
|
|
537
|
+
await runReported(registry, "view.renameLayout", { layout, name }, "Command Bar");
|
|
537
538
|
}
|
|
538
539
|
}
|
|
539
540
|
|
|
@@ -707,7 +708,7 @@ function openStudioMenu(opener: HTMLElement): void {
|
|
|
707
708
|
rows: overflowRows(registry),
|
|
708
709
|
run: (id) => {
|
|
709
710
|
if (registry.isEnabled(id)) {
|
|
710
|
-
void registry
|
|
711
|
+
void runReported(registry, id, undefined, "Command Bar");
|
|
711
712
|
}
|
|
712
713
|
},
|
|
713
714
|
});
|
|
@@ -751,7 +752,7 @@ function state(): CommandBarScope {
|
|
|
751
752
|
run: (id) => {
|
|
752
753
|
const registry = activeRegistry();
|
|
753
754
|
if (registry?.isEnabled(id)) {
|
|
754
|
-
void registry
|
|
755
|
+
void runReported(registry, id, undefined, "Command Bar");
|
|
755
756
|
}
|
|
756
757
|
},
|
|
757
758
|
saveLayout: () => {
|
|
@@ -762,7 +763,7 @@ function state(): CommandBarScope {
|
|
|
762
763
|
},
|
|
763
764
|
segments: [],
|
|
764
765
|
setLayout: (id) => {
|
|
765
|
-
void
|
|
766
|
+
void runActiveReported("view.setLayout", { layout: id }, "Command Bar");
|
|
766
767
|
},
|
|
767
768
|
windowControl: (action) => {
|
|
768
769
|
windowControls()?.[action]();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://jxsuite.com/schema/v1",
|
|
3
3
|
"$id": "StudioPaneGrid",
|
|
4
|
-
"$description": "The pane grid (studio.md §18.1): one cell per pane, with a splitter before every cell but the first. Every box here is EMPTY by design — the
|
|
4
|
+
"$description": "The pane grid (studio.md §18.1): one cell per pane, with a splitter before every cell but the first. Every box here is EMPTY by design — the FIVE inside a cell are hosts other modules fill (`panels/tab-strip.ts` the strip, `panels/jump-bar.ts` the jump bar, `panels/pane-context.ts` the chrome layer, `canvas/canvas-render.ts` the stage, `panels/pane-grid.ts` itself the right-edge drop zone), and the stage is an ISLAND in the §9.4 sense: the canvas host mounts a cross-origin `<iframe>` into it, so nothing this document draws may ever go inside it. What the document owns is the STRUCTURE and the STYLE — the cell's own two-row grid, the three row-2 children that share its lower cell, and the splitter — which is `styles/shell.css`'s `.pane*` block and `styles/canvas.css`'s `.pane-stage` rule, carried here and keyed on `part`. Two things are load-bearing and easy to undo. (1) **A row that moves reloads an iframe**, so the keyed `$map` is not an optimisation: `workspace.panes[0]` is always the primary and `MAX_PANES` is 2, the key is the pane id, and the runtime's forward-cursor pass therefore never moves a kept row — gaining or losing the side pane leaves the primary's node, and the frame inside it, untouched. (2) **The splitter must sit BETWEEN two cells and inside neither**, so each row is a `display: contents` wrapper holding its own leading splitter and its cell: the wrapper generates no box, so the splitter and the two cells are the grid items of `#pane-grid` in DOM order, and a pointer landing on the splitter is in no cell at all. Every selector below is a `>` chain from the root, because a pane's stage hosts other surface documents and a bare `[part=\"row\"]` descendant rule would reach into them.",
|
|
5
5
|
"tagName": "div",
|
|
6
6
|
"attributes": {
|
|
7
7
|
"part": "grid",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"zIndex": "10"
|
|
28
28
|
},
|
|
29
29
|
"& > [part=\"row\"] > [part=\"pane\"]": {
|
|
30
|
-
"$description": "ONE PANE, and one COLUMN, declared.
|
|
30
|
+
"$description": "ONE PANE, and one COLUMN, declared. Four children name `grid-row: 2` in order to share the lower cell, and auto-placement answers that by inventing an implicit column each: the jump bar, the chrome layer, the stage and the drop zone lined up side by side, the stage taking a third of the pane and pinning the document to the right edge. `grid-template-columns` is what makes \"share the cell\" mean it, and every row-2 child restates `grid-column: 1` so a future fifth one cannot re-open the same hole. No unit test can see this — happy-dom does no layout, so every rect is zero — which is why the screenshot lane is what caught it.",
|
|
31
31
|
"display": "grid",
|
|
32
32
|
"gridTemplateColumns": "minmax(0, 1fr)",
|
|
33
33
|
"gridTemplateRows": "auto minmax(0, 1fr)",
|
|
@@ -82,11 +82,33 @@
|
|
|
82
82
|
"display": "flex",
|
|
83
83
|
"gap": "var(--jx-space-6)",
|
|
84
84
|
"alignItems": "flex-start"
|
|
85
|
+
},
|
|
86
|
+
"& > [part=\"row\"] > [part=\"pane\"] > [part=\"drop-zone\"]": {
|
|
87
|
+
"$description": "The right-edge target that creates a second pane (§18.1). Sits ABOVE the stage (`z-index: 4` against the stage's own stacking, jump's 3, chrome's 2) so a drag reaches it before it reaches the cross-origin iframe underneath, which would otherwise swallow every drag event with no affordance to say so. `pointer-events: none` when unarmed — a mouse doing anything other than dragging a tab or a file must pass straight through to the stage — and `auto` only while `data-armed` is set, which `panels/pane-grid.ts`'s own arming monitor writes for the DURATION of a drag it recognises, never for a click.",
|
|
88
|
+
"gridRow": "2",
|
|
89
|
+
"gridColumn": "1",
|
|
90
|
+
"position": "relative",
|
|
91
|
+
"zIndex": "4",
|
|
92
|
+
"minWidth": "0",
|
|
93
|
+
"pointerEvents": "none"
|
|
94
|
+
},
|
|
95
|
+
"& > [part=\"row\"] > [part=\"pane\"] > [part=\"drop-zone\"][data-armed]": {
|
|
96
|
+
"pointerEvents": "auto"
|
|
97
|
+
},
|
|
98
|
+
"& > [part=\"row\"] > [part=\"pane\"] > [part=\"drop-zone\"][data-over]::after": {
|
|
99
|
+
"$description": "The right BAND, not the whole cell: the zone only ever ACCEPTS a drop in that band (`panels/pane-grid.ts`'s `edgeData`), so lighting the whole cell would promise a drop this element refuses everywhere else in it.",
|
|
100
|
+
"content": "\"\"",
|
|
101
|
+
"position": "absolute",
|
|
102
|
+
"insetBlock": "0",
|
|
103
|
+
"insetInlineEnd": "0",
|
|
104
|
+
"insetInlineStart": "50%",
|
|
105
|
+
"background": "color-mix(in srgb, var(--accent) 14%, transparent)",
|
|
106
|
+
"borderInlineStart": "2px solid var(--accent)"
|
|
85
107
|
}
|
|
86
108
|
},
|
|
87
109
|
"children": [
|
|
88
110
|
{
|
|
89
|
-
"$description": "Keyed on the PANE ID, and that is the whole of §18.1 rule 1's structural half: the key is what makes the runtime MOVE a kept row rather than rebuild it, and re-parenting is not a move for an `<iframe>` — it reloads, dropping its `iframe-channel` connection, its shadow document and every `ready` panel with it. A row renders complete (its cell, its
|
|
111
|
+
"$description": "Keyed on the PANE ID, and that is the whole of §18.1 rule 1's structural half: the key is what makes the runtime MOVE a kept row rather than rebuild it, and re-parenting is not a move for an `<iframe>` — it reloads, dropping its `iframe-channel` connection, its shadow document and every `ready` panel with it. A row renders complete (its cell, its five hosts and its splitter) inside the row's own effect scope before it is inserted, so there is no frame in which a cell exists with no stage inside it.",
|
|
90
112
|
"$prototype": "Array",
|
|
91
113
|
"items": { "$ref": "#/state/rows" },
|
|
92
114
|
"key": { "$ref": "$map/item/id" },
|
|
@@ -178,6 +200,16 @@
|
|
|
178
200
|
"part": "pane-stage",
|
|
179
201
|
"data-jx-region": "${$map.item.region}"
|
|
180
202
|
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"$description": "The right-edge drop target (§18.1). No `data-jx-region`: `pane.<id>` uniqueness names the stage, and a second element answering to it would be a silently widened crop the moment a shot cropped this pane.",
|
|
206
|
+
"tagName": "div",
|
|
207
|
+
"attributes": {
|
|
208
|
+
"part": "drop-zone",
|
|
209
|
+
"role": "none",
|
|
210
|
+
"data-armed": "${state.armed ? '' : null}",
|
|
211
|
+
"data-over": "${state.edgePane === $map.item.id ? '' : null}"
|
|
212
|
+
}
|
|
181
213
|
}
|
|
182
214
|
]
|
|
183
215
|
}
|
|
@@ -63,8 +63,8 @@ export const PANE_SELECTOR = `[part="${PANE_PART}"]`;
|
|
|
63
63
|
*/
|
|
64
64
|
export const STAGE_SELECTOR = `[part="${STAGE_PART}"]`;
|
|
65
65
|
|
|
66
|
-
/** The
|
|
67
|
-
export type PaneCellPart = "pane" | "strip" | "jump" | "chrome" | "stage";
|
|
66
|
+
/** The six boxes a cell is made of, in the order the document draws them. */
|
|
67
|
+
export type PaneCellPart = "pane" | "strip" | "jump" | "chrome" | "stage" | "dropZone";
|
|
68
68
|
|
|
69
69
|
const CELL_PARTS: ReadonlyMap<string, PaneCellPart> = new Map([
|
|
70
70
|
["pane", "pane"],
|
|
@@ -72,6 +72,7 @@ const CELL_PARTS: ReadonlyMap<string, PaneCellPart> = new Map([
|
|
|
72
72
|
["jump", "jump"],
|
|
73
73
|
["chrome", "chrome"],
|
|
74
74
|
["pane-stage", "stage"],
|
|
75
|
+
["drop-zone", "dropZone"],
|
|
75
76
|
]);
|
|
76
77
|
|
|
77
78
|
/**
|
|
@@ -134,6 +135,14 @@ export interface PaneGridActions {
|
|
|
134
135
|
settle: () => void;
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
/** Whether the right-edge zones are armed, and which pane's is under the pointer. */
|
|
139
|
+
export interface PaneGridDragState {
|
|
140
|
+
/** Whether ANY tab-drop-eligible drag is live — the zones' shared `pointer-events` gate. */
|
|
141
|
+
armed: boolean;
|
|
142
|
+
/** The pane whose zone is under the pointer, or `""` for none. */
|
|
143
|
+
edgePane: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
137
146
|
export interface PaneGridSurface {
|
|
138
147
|
/** Resolves once the first projection is in the document. */
|
|
139
148
|
ready: Promise<void>;
|
|
@@ -146,12 +155,18 @@ export interface PaneGridSurface {
|
|
|
146
155
|
* gains or loses a pane, which is the only moment anything else could have moved it.
|
|
147
156
|
*/
|
|
148
157
|
update: (rows: PaneGridRow[], split: number) => void;
|
|
158
|
+
/**
|
|
159
|
+
* Write the right-edge indicators, independently of {@link update} — the same reason
|
|
160
|
+
* `TabStripSurface.drag` is separate from its own `update`: a re-project the drag itself did not
|
|
161
|
+
* cause must not clear an indicator the arming monitor just lit.
|
|
162
|
+
*/
|
|
163
|
+
drag: (state: PaneGridDragState) => void;
|
|
149
164
|
/** Take the document down and give `#pane-grid` back empty. Idempotent. */
|
|
150
165
|
dispose: () => void;
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
/** The scope `pane-grid.json` reads. */
|
|
154
|
-
interface PaneGridScope extends Record<string, unknown
|
|
169
|
+
interface PaneGridScope extends Record<string, unknown>, PaneGridDragState {
|
|
155
170
|
rows: PaneGridRow[];
|
|
156
171
|
/** `jx-split`'s `value`. */
|
|
157
172
|
paneSplit: number;
|
|
@@ -202,6 +217,8 @@ export function mountPaneGridSurface(
|
|
|
202
217
|
): PaneGridSurface {
|
|
203
218
|
host.replaceChildren();
|
|
204
219
|
const scope = reactive<PaneGridScope>({
|
|
220
|
+
armed: false,
|
|
221
|
+
edgePane: "",
|
|
205
222
|
move: actions.move,
|
|
206
223
|
paneSplit: split.value,
|
|
207
224
|
rows,
|
|
@@ -217,7 +234,7 @@ export function mountPaneGridSurface(
|
|
|
217
234
|
const ready = mountSurface("pane-grid", scope, host, {
|
|
218
235
|
/* No `instanceof` narrowing and no "is there a pane" guard, and both absences are structural.
|
|
219
236
|
`pane-grid.json` draws no text and no `textContent`, so every node the runtime reports here
|
|
220
|
-
is an element; and the
|
|
237
|
+
is an element; and the six cell parts exist only inside the repeater's rows, so a `$map`
|
|
221
238
|
scope is always there to name the pane. A guard on either would be a branch the document
|
|
222
239
|
cannot reach — which is a line no test could ever cover, which is a claim nothing checks. */
|
|
223
240
|
onNodeCreated: (element, _path, def, state) => {
|
|
@@ -241,6 +258,11 @@ export function mountPaneGridSurface(
|
|
|
241
258
|
mounted = null;
|
|
242
259
|
host.replaceChildren();
|
|
243
260
|
},
|
|
261
|
+
drag(state) {
|
|
262
|
+
// Independent of `update`'s field list — see {@link PaneGridSurface.drag}.
|
|
263
|
+
scope.armed = state.armed;
|
|
264
|
+
scope.edgePane = state.edgePane;
|
|
265
|
+
},
|
|
244
266
|
ready,
|
|
245
267
|
update(next, value) {
|
|
246
268
|
scope.rows = next;
|
|
@@ -53,6 +53,7 @@ import { effect, effectScope, reactive } from "../reactivity";
|
|
|
53
53
|
import { activeTab } from "../workspace/workspace";
|
|
54
54
|
import { EDITOR_KIND_LABELS } from "../commands/context";
|
|
55
55
|
import { activeRegistry } from "../commands/active-registry";
|
|
56
|
+
import { runReported } from "../commands/run-reported";
|
|
56
57
|
import { deployStatusItem } from "../publish/deploy-checklist";
|
|
57
58
|
import { collabState } from "../collab/collab-state";
|
|
58
59
|
import { problemCount, problems } from "../services/notify";
|
|
@@ -378,7 +379,7 @@ function state(): StatusbarScope {
|
|
|
378
379
|
const item = _state?.fields.flatMap((field) => field.items).find((i) => i.key === key);
|
|
379
380
|
const registry = activeRegistry();
|
|
380
381
|
if (item?.command && registry) {
|
|
381
|
-
void registry
|
|
382
|
+
void runReported(registry, item.command, item.args as never, "Status Bar");
|
|
382
383
|
}
|
|
383
384
|
},
|
|
384
385
|
}) as StatusbarScope;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://jxsuite.com/schema/v1",
|
|
3
3
|
"$id": "StudioTabStrip",
|
|
4
|
-
"$description": "One pane's tab strip, as a Jx document over the kit: a real `tablist` of `jx-tab`s, or — for a pane that owns no document — the derivation chip naming what it is a projection of, and in both cases one trailing button. `panels/tab-strip.ts` keeps every decision: which pane a host is drawing, what each tab is called (the shortest unique suffix, and a page's ROUTE), what its tooltip says, whether closing it needs a prompt, what the overflow chevron lists and what the context menu offers. This document owns the markup, the ARIA, the keyboard and the style, and it holds THREE things the old chip drew by hand and the kit now draws: the tab's own selected state (`aria-selected`, not an `.active` class), its close button, and its dirty dot. The three that are Studio's alone go in `jx-tab`'s slots — the drill-in marker in `icon`, the draft pill in `status`, the pin toggle in `actions` (ui.md §5.4) — which is the gap that kept this strip out of the tab pattern until now.",
|
|
4
|
+
"$description": "One pane's tab strip, as a Jx document over the kit: a real `tablist` of `jx-tab`s, or — for a pane that owns no document — the derivation chip naming what it is a projection of, and in both cases one trailing button. `panels/tab-strip.ts` keeps every decision: which pane a host is drawing, what each tab is called (the shortest unique suffix, and a page's ROUTE), what its tooltip says, whether closing it needs a prompt, what the overflow chevron lists and what the context menu offers. This document owns the markup, the ARIA, the keyboard and the style, and it holds THREE things the old chip drew by hand and the kit now draws: the tab's own selected state (`aria-selected`, not an `.active` class), its close button, and its dirty dot. The three that are Studio's alone go in `jx-tab`'s slots — the drill-in marker in `icon`, the draft pill in `status`, the pin toggle in `actions` (ui.md §5.4) — which is the gap that kept this strip out of the tab pattern until now. **Drag is `@atlaskit/pragmatic-drag-and-drop`, not native HTML5 DnD**: a chip carries no `draggable` attribute and no `ondrag*` handler, because `panels/tab-strip.ts` registers every chip and every strip as a pragmatic source/target through `onNodeCreated` and resolves a completed drag in `panels/tab-drop.ts`. What is left here is three SCOPE comparisons, exactly as `files-panel.json` marks a row under drag: `data-dragging` on the chip being carried, `data-drop` on the chip a drop would land beside, `data-drop-tail` on the strip when the drop is its empty tail.",
|
|
5
5
|
"tagName": "div",
|
|
6
6
|
"attributes": {
|
|
7
7
|
"part": "strip-row",
|
|
@@ -44,6 +44,13 @@
|
|
|
44
44
|
"display": "none"
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
+
"& [part=\"tabs\"][data-drop-tail]::after": {
|
|
48
|
+
"$description": "The strip's own tail target: a drop onto the empty space after the last chip, or onto a pane whose strip carries no chips at all — an empty pane made a target during a drag by `panels/tab-strip.ts`'s `project`. A fixed-width bar rather than a background tint, so it reads the same as a chip's own `data-drop` edge instead of a second visual language for the same gesture.",
|
|
49
|
+
"content": "\"\"",
|
|
50
|
+
"flex": "0 0 2px",
|
|
51
|
+
"alignSelf": "stretch",
|
|
52
|
+
"background": "var(--accent)"
|
|
53
|
+
},
|
|
47
54
|
"& [part=\"tab\"]": {
|
|
48
55
|
"$description": "`.tab-strip-tab`. The kit's tab caps itself at 220px and reserves a control's height; a chrome row is tighter than that and its labels are the shortest unique suffix, so both are re-declared. `flex-shrink: 0` is what makes the strip scroll rather than squeeze fifteen documents into one width.",
|
|
49
56
|
"gap": "var(--jx-space-1)",
|
|
@@ -64,6 +71,10 @@
|
|
|
64
71
|
},
|
|
65
72
|
"&[data-dragging]": {
|
|
66
73
|
"opacity": "0.4"
|
|
74
|
+
},
|
|
75
|
+
"&[data-drop]": {
|
|
76
|
+
"$description": "The chip a drop would land BESIDE. An inset shadow rather than a border, so the indicator draws no layout shift mid-drag — the same reason `files-panel.json`'s row indicators are backgrounds rather than borders.",
|
|
77
|
+
"boxShadow": "inset 2px 0 0 var(--accent)"
|
|
67
78
|
}
|
|
68
79
|
},
|
|
69
80
|
"& [part=\"tab\"] [part=\"label\"]": {
|
|
@@ -206,7 +217,8 @@
|
|
|
206
217
|
"attributes": {
|
|
207
218
|
"part": "tabs",
|
|
208
219
|
"label": "${state.stripLabel}",
|
|
209
|
-
"selected": "${state.active}"
|
|
220
|
+
"selected": "${state.active}",
|
|
221
|
+
"data-drop-tail": "${state.dropTail ? '' : null}"
|
|
210
222
|
},
|
|
211
223
|
"onchange": {
|
|
212
224
|
"$prototype": "Function",
|
|
@@ -245,11 +257,11 @@
|
|
|
245
257
|
"label": "${$map.item.label}",
|
|
246
258
|
"title": "${$map.item.title}",
|
|
247
259
|
"closable": "",
|
|
248
|
-
"draggable": "true",
|
|
249
260
|
"dirty": "${$map.item.dirty ? '' : null}",
|
|
250
261
|
"data-pinned": "${$map.item.pinned ? '' : null}",
|
|
251
262
|
"data-preview": "${$map.item.preview ? '' : null}",
|
|
252
|
-
"data-dragging": "${$map.item.
|
|
263
|
+
"data-dragging": "${state.dragKey === $map.item.key ? '' : null}",
|
|
264
|
+
"data-drop": "${state.dropIndex === $map.item.index ? '' : null}"
|
|
253
265
|
},
|
|
254
266
|
"ondblclick": {
|
|
255
267
|
"$prototype": "Function",
|
|
@@ -297,41 +309,6 @@
|
|
|
297
309
|
}
|
|
298
310
|
]
|
|
299
311
|
},
|
|
300
|
-
"ondragstart": {
|
|
301
|
-
"$prototype": "Function",
|
|
302
|
-
"body": [
|
|
303
|
-
{
|
|
304
|
-
"operator": "call",
|
|
305
|
-
"target": { "$ref": "#/state/dragStart" },
|
|
306
|
-
"value": [{ "$ref": "$map/item/key" }, { "$ref": "event#/dataTransfer" }]
|
|
307
|
-
}
|
|
308
|
-
]
|
|
309
|
-
},
|
|
310
|
-
"ondragend": {
|
|
311
|
-
"$prototype": "Function",
|
|
312
|
-
"body": [
|
|
313
|
-
{
|
|
314
|
-
"operator": "call",
|
|
315
|
-
"target": { "$ref": "#/state/dragEnd" },
|
|
316
|
-
"value": []
|
|
317
|
-
}
|
|
318
|
-
]
|
|
319
|
-
},
|
|
320
|
-
"ondragover": {
|
|
321
|
-
"$prototype": "Function",
|
|
322
|
-
"body": [{ "preventDefault": true }]
|
|
323
|
-
},
|
|
324
|
-
"ondrop": {
|
|
325
|
-
"$prototype": "Function",
|
|
326
|
-
"body": [
|
|
327
|
-
{ "preventDefault": true },
|
|
328
|
-
{
|
|
329
|
-
"operator": "call",
|
|
330
|
-
"target": { "$ref": "#/state/drop" },
|
|
331
|
-
"value": [{ "$ref": "$map/item/index" }, { "$ref": "event#/dataTransfer" }]
|
|
332
|
-
}
|
|
333
|
-
]
|
|
334
|
-
},
|
|
335
312
|
"children": [
|
|
336
313
|
{
|
|
337
314
|
"$description": "The drill-in marker: this document was opened FROM another one. `jx-tab`'s icon slot draws it before the label, which is where it has always been.",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
import { reactive } from "../reactivity";
|
|
32
32
|
import { mountSurface, registerSurface } from "../ui/surface";
|
|
33
33
|
import tabStripDoc from "./tab-strip.json";
|
|
34
|
-
import type { JxDocument } from "@jxsuite/schema/types";
|
|
34
|
+
import type { JxDocument, JxElement } from "@jxsuite/schema/types";
|
|
35
|
+
import type { JxScope } from "@jxsuite/runtime/types";
|
|
35
36
|
import type { SurfaceHandle } from "../ui/surface";
|
|
36
37
|
|
|
37
38
|
registerSurface("tab-strip", tabStripDoc as unknown as JxDocument);
|
|
@@ -50,7 +51,6 @@ export interface TabChipView extends Record<string, unknown> {
|
|
|
50
51
|
dirty: boolean;
|
|
51
52
|
pinned: boolean;
|
|
52
53
|
preview: boolean;
|
|
53
|
-
dragging: boolean;
|
|
54
54
|
/** Whether to draw the drill-in marker. */
|
|
55
55
|
origin: boolean;
|
|
56
56
|
/** "Pin" or "Unpin" — the pin button's whole accessible name. */
|
|
@@ -106,18 +106,39 @@ export interface TabStripActions {
|
|
|
106
106
|
context: (id: string, x: number, y: number) => void;
|
|
107
107
|
/** The chevron, or a derived pane's ✕. The anchor is the button that was pressed. */
|
|
108
108
|
openTrailing: (anchor: HTMLElement | null) => void;
|
|
109
|
-
/** A drag left this chip. The `DataTransfer` is the platform's and may be absent. */
|
|
110
|
-
dragStart: (id: string, transfer: DataTransfer | null) => void;
|
|
111
|
-
dragEnd: () => void;
|
|
112
|
-
/** A drop landed on the chip at `index`. */
|
|
113
|
-
drop: (index: number, transfer: DataTransfer | null) => void;
|
|
114
109
|
/** A wheel over the strip. A host function in handler position, so it takes the event. */
|
|
115
110
|
wheel: (scope: unknown, event: WheelEvent) => void;
|
|
111
|
+
/** A chip element exists. `panels/tab-strip.ts` makes it a pragmatic drag source and drop target. */
|
|
112
|
+
chipHost: (element: HTMLElement, id: string) => void;
|
|
113
|
+
/**
|
|
114
|
+
* The `jx-tabs` element exists. `panels/tab-strip.ts` makes it a drop target of its own — the
|
|
115
|
+
* strip's empty tail, and the whole of an empty pane's strip while a drag is live.
|
|
116
|
+
*/
|
|
117
|
+
stripHost: (element: HTMLElement) => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* What one strip shows about the drag in progress — written independently of {@link TabStripValues}
|
|
122
|
+
* so the indicators survive an ordinary re-project mid-drag.
|
|
123
|
+
*/
|
|
124
|
+
export interface TabStripDragState {
|
|
125
|
+
/** The tab id being carried, or `""` when nothing from this strip is. */
|
|
126
|
+
dragKey: string;
|
|
127
|
+
/** The chip a drop would land beside, by slot index, or `-1` for none. */
|
|
128
|
+
dropIndex: number;
|
|
129
|
+
/** Whether a drop would land in the strip's empty tail. */
|
|
130
|
+
dropTail: boolean;
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
export interface TabStripSurface {
|
|
119
134
|
/** Bring the standing document up to date. An assignment; the mount is never rebuilt. */
|
|
120
135
|
update: (values: TabStripValues) => void;
|
|
136
|
+
/**
|
|
137
|
+
* Write the drag indicators, independently of {@link update}. The one writer of `dragKey`,
|
|
138
|
+
* `dropIndex` and `dropTail` — `update` never touches them, which is what keeps an indicator lit
|
|
139
|
+
* across a repaint the drag itself did not cause.
|
|
140
|
+
*/
|
|
141
|
+
drag: (state: TabStripDragState) => void;
|
|
121
142
|
/** The `jx-tabs` element, once it exists — the one thing a measurement needs. */
|
|
122
143
|
strip: () => HTMLElement | null;
|
|
123
144
|
/** Take the document down and give the host back empty. Idempotent. */
|
|
@@ -125,7 +146,21 @@ export interface TabStripSurface {
|
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
/** The scope `tab-strip.json` reads. */
|
|
128
|
-
interface TabStripScope
|
|
149
|
+
interface TabStripScope
|
|
150
|
+
extends Record<string, unknown>, TabStripValues, TabStripActions, TabStripDragState {}
|
|
151
|
+
|
|
152
|
+
/** The `part` a node's definition carries, or `""` for a text node or an unmarked element. */
|
|
153
|
+
function partOf(def: JxElement | string): string {
|
|
154
|
+
const part = typeof def === "string" ? undefined : def.attributes?.["part"];
|
|
155
|
+
return typeof part === "string" ? part : "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The tab id a mapped `jx-tab` node was rendered for, read from the row scope, or `""`. */
|
|
159
|
+
function mappedChipKey(state: JxScope | undefined): string {
|
|
160
|
+
const map = state?.["$map"] as { item?: { key?: unknown } } | undefined;
|
|
161
|
+
const key = map?.item?.key;
|
|
162
|
+
return typeof key === "string" ? key : "";
|
|
163
|
+
}
|
|
129
164
|
|
|
130
165
|
/**
|
|
131
166
|
* Mount one strip into `host`.
|
|
@@ -143,17 +178,38 @@ export function mountTabStripSurface(
|
|
|
143
178
|
values: TabStripValues,
|
|
144
179
|
actions: TabStripActions,
|
|
145
180
|
): TabStripSurface {
|
|
146
|
-
const scope = reactive<TabStripScope>({
|
|
181
|
+
const scope = reactive<TabStripScope>({
|
|
182
|
+
...values,
|
|
183
|
+
...actions,
|
|
184
|
+
dragKey: "",
|
|
185
|
+
dropIndex: -1,
|
|
186
|
+
dropTail: false,
|
|
187
|
+
}) as TabStripScope;
|
|
147
188
|
|
|
148
189
|
host.replaceChildren();
|
|
149
190
|
let mounted: SurfaceHandle | null = null;
|
|
150
191
|
let disposed = false;
|
|
151
192
|
let strip: HTMLElement | null = null;
|
|
152
193
|
void mountSurface("tab-strip", scope, host, {
|
|
153
|
-
onNodeCreated: (element, _path, def) => {
|
|
154
|
-
|
|
155
|
-
|
|
194
|
+
onNodeCreated: (element, _path, def, state) => {
|
|
195
|
+
/* `disposed` first, for the reason `files-panel.ts`'s `mountFilesPanelSurface` guards it: the
|
|
196
|
+
mount is asynchronous, and a strip taken down inside the same turn — the pane whose host
|
|
197
|
+
this is has gone away — still walks this callback for every node its document creates.
|
|
198
|
+
Adopting one then would register a drag source nothing is left to release. */
|
|
199
|
+
if (disposed || !(element instanceof HTMLElement)) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const part = partOf(def);
|
|
203
|
+
if (part === "tabs") {
|
|
156
204
|
strip = element;
|
|
205
|
+
actions.stripHost(element);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (part === "tab") {
|
|
209
|
+
const id = mappedChipKey(state);
|
|
210
|
+
if (id) {
|
|
211
|
+
actions.chipHost(element, id);
|
|
212
|
+
}
|
|
157
213
|
}
|
|
158
214
|
},
|
|
159
215
|
}).then((surface) => {
|
|
@@ -172,6 +228,14 @@ export function mountTabStripSurface(
|
|
|
172
228
|
strip = null;
|
|
173
229
|
host.replaceChildren();
|
|
174
230
|
},
|
|
231
|
+
drag(state) {
|
|
232
|
+
/* Independent of {@link update}'s field list, which is the whole point: these three are
|
|
233
|
+
written by the drag monitor on every pointer move, and `update`'s ordinary re-project must
|
|
234
|
+
not clear them out from under a drag it did not cause. */
|
|
235
|
+
scope.dragKey = state.dragKey;
|
|
236
|
+
scope.dropIndex = state.dropIndex;
|
|
237
|
+
scope.dropTail = state.dropTail;
|
|
238
|
+
},
|
|
175
239
|
strip: () => strip,
|
|
176
240
|
update(next) {
|
|
177
241
|
/* Field by field, so an unchanged one is inert. A whole-object assignment would re-run every
|
|
@@ -48,12 +48,15 @@
|
|
|
48
48
|
* write, and therefore with an empty diff.
|
|
49
49
|
*
|
|
50
50
|
* **A real edit is a one-line diff.** The file's layout — which objects sit on one line, where a
|
|
51
|
-
* blank line falls — is read once per binding ({@link seedCommitted})
|
|
52
|
-
*
|
|
51
|
+
* blank line falls — is read once per binding ({@link seedCommitted}), replaced by the record of
|
|
52
|
+
* whatever bytes reached the file another way ({@link adoptProjectConfig}), and every serialisation
|
|
53
|
+
* here honours it, so what changes on disk is the field that changed.
|
|
53
54
|
*
|
|
54
55
|
* **Collaboration.** `project.json` is out of collab replication; the gate is in
|
|
55
56
|
* `collab/collab-session.ts`'s `ensureCollab`, which is what would otherwise register the Yjs
|
|
56
57
|
* history delegate over this tab.
|
|
58
|
+
*
|
|
59
|
+
* @docs studio/projects/settings
|
|
57
60
|
*/
|
|
58
61
|
|
|
59
62
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
@@ -64,7 +67,7 @@ import { requireProjectState, setProjectState } from "../state";
|
|
|
64
67
|
import { setWorkspaceProject, workspace } from "../workspace/workspace";
|
|
65
68
|
import { PROJECT_CONFIG_PATH, createTab, disposeTab } from "./tab";
|
|
66
69
|
import { transactDoc } from "./transact";
|
|
67
|
-
import { parseJsonDocument, serializeJson } from "../files/json-layout";
|
|
70
|
+
import { deriveJsonLayout, parseJsonDocument, serializeJson } from "../files/json-layout";
|
|
68
71
|
import type { JsonLayout } from "../files/json-layout";
|
|
69
72
|
|
|
70
73
|
import type { Tab } from "./tab";
|
|
@@ -114,8 +117,9 @@ let _detached: Tab | null = null;
|
|
|
114
117
|
let _committed: string | null = null;
|
|
115
118
|
|
|
116
119
|
/**
|
|
117
|
-
* The layout the file was read in ({@link seedCommitted})
|
|
118
|
-
* it. Reset with the binding: a
|
|
120
|
+
* The layout the file was read in ({@link seedCommitted}) or last written in ({@link
|
|
121
|
+
* adoptProjectConfig}), for a binding with no open tab to carry it. Reset with the binding: a
|
|
122
|
+
* different project is a different file.
|
|
119
123
|
*/
|
|
120
124
|
let _layout: JsonLayout | null = null;
|
|
121
125
|
|
|
@@ -241,16 +245,28 @@ function liveConfig(): ProjectConfig | null {
|
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
/**
|
|
244
|
-
* Make `target` hold exactly `source`'s keys
|
|
248
|
+
* Make `target` hold exactly `source`'s keys, in `source`'s ORDER.
|
|
249
|
+
*
|
|
250
|
+
* Every key goes, the ones `source` keeps included, before `source` is assigned over the empty
|
|
251
|
+
* object. Assignment alone would leave the removals behind; deleting only the removals would leave
|
|
252
|
+
* the ORDER behind, because a key that survives keeps the position it had and only a new one takes
|
|
253
|
+
* `source`'s. Key order is what the serializer writes the file in, so a document holding the
|
|
254
|
+
* assistant's configuration in the previous file's order re-lays the file the assistant just wrote
|
|
255
|
+
* on the next commit — even a no-op one, which then disagrees with {@link _committed} (issue 331).
|
|
256
|
+
* The rival handover in {@link commitProjectConfig} is the same case from the other side: the
|
|
257
|
+
* object of record is the source, and its order is the file's.
|
|
258
|
+
*
|
|
259
|
+
* The transient — an observer of a single key would see it gone and back within the transaction —
|
|
260
|
+
* is one the removals already had, and nothing observes the configuration document key by key: the
|
|
261
|
+
* settings surfaces render from `projectState` and mutate it in place, and every reader of the
|
|
262
|
+
* document follows the root reference {@link transactDoc} replaces when the transaction ends.
|
|
245
263
|
*
|
|
246
264
|
* @param {Record<string, unknown>} target
|
|
247
265
|
* @param {ProjectConfig} source
|
|
248
266
|
*/
|
|
249
267
|
function replaceContents(target: Record<string, unknown>, source: ProjectConfig): void {
|
|
250
268
|
for (const key of Object.keys(target)) {
|
|
251
|
-
|
|
252
|
-
delete target[key];
|
|
253
|
-
}
|
|
269
|
+
delete target[key];
|
|
254
270
|
}
|
|
255
271
|
Object.assign(target, source);
|
|
256
272
|
}
|
|
@@ -444,14 +460,26 @@ export async function commitProjectConfig(
|
|
|
444
460
|
* assistant's write back out of existence. Putting the bytes into the document instead keeps the
|
|
445
461
|
* one-object rule true, and the state sync follows from the bind effect like every other change.
|
|
446
462
|
*
|
|
463
|
+
* **The layout record travels with the write.** The assistant authored the whole file, and the text
|
|
464
|
+
* it wrote is the file now — so the record the next commit honours is derived from THAT text
|
|
465
|
+
* (`json-layout.ts`'s `deriveJsonLayout`), exactly as a reload from disk would derive it, and it
|
|
466
|
+
* replaces both the seed's record and the open tab's. Keeping the record the chokepoint read BEFORE
|
|
467
|
+
* the write is what turned the settings edit after an assistant rewrite into a whole-file diff: the
|
|
468
|
+
* file was on disk in the assistant's layout and the commit re-laid it in the previous one (issue
|
|
469
|
+
* 331). The key order travels the same way: {@link replaceContents} installs `config`'s order on
|
|
470
|
+
* the document, so a rewrite that moved a top-level key stays moved. Nothing here re-reads the file
|
|
471
|
+
* to learn any of this: the caller just produced the bytes, and a second parse of `project.json` is
|
|
472
|
+
* the rival object this door exists to prevent.
|
|
473
|
+
*
|
|
447
474
|
* `skipHistory`, because this is a RELOAD and not an edit: the file already says this, and
|
|
448
475
|
* `write_file` tells the model in as many words that its disk writes are not undoable. A history
|
|
449
476
|
* entry here would offer ⌘Z a change it could not take back from the file.
|
|
450
477
|
*
|
|
451
478
|
* @param {ProjectConfig} config The configuration as written — already past the schema gate.
|
|
479
|
+
* @param {string} text The bytes that reached the file, which `config` is the parse of.
|
|
452
480
|
* @returns {Promise<void>}
|
|
453
481
|
*/
|
|
454
|
-
export async function adoptProjectConfig(config: ProjectConfig): Promise<void> {
|
|
482
|
+
export async function adoptProjectConfig(config: ProjectConfig, text: string): Promise<void> {
|
|
455
483
|
if (!requireProjectState()) {
|
|
456
484
|
// No project state is no configuration document either ({@link projectConfigDocument} reads
|
|
457
485
|
// One). The workspace copy is then the only thing there is to keep true.
|
|
@@ -466,7 +494,13 @@ export async function adoptProjectConfig(config: ProjectConfig): Promise<void> {
|
|
|
466
494
|
);
|
|
467
495
|
tab.doc.dirty = false;
|
|
468
496
|
// Settle a read already in flight before saying what the file holds, or it would resolve after
|
|
469
|
-
//
|
|
497
|
+
// These lines with the PREVIOUS bytes and record, and make the next real commit look like a
|
|
498
|
+
// No-op — or re-lay the file the assistant just formatted.
|
|
470
499
|
await seedCommitted();
|
|
500
|
+
// The record of the bytes just written, on the tab as well as on the seed: `configLayout`
|
|
501
|
+
// Prefers the open tab's, and the tab's is the record of the file it opened, which the write has
|
|
502
|
+
// Just replaced. The tab holds the config now; it holds the config's layout too.
|
|
503
|
+
_layout = deriveJsonLayout(text);
|
|
504
|
+
tab.doc.layout = _layout;
|
|
471
505
|
_committed = serializeProjectConfig(config);
|
|
472
506
|
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @license MIT
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { fetchAvailableModels } from "../services/ai-models";
|
|
26
|
+
import { fetchAvailableModels, proxyModelsErrorMessage } from "../services/ai-models";
|
|
27
27
|
import {
|
|
28
28
|
getBaseUrl,
|
|
29
29
|
getOpenAiKey,
|
|
@@ -165,6 +165,17 @@ export function createAiCredentialsForm(opts: AiCredentialsFormOptions): AiCrede
|
|
|
165
165
|
},
|
|
166
166
|
force: true,
|
|
167
167
|
});
|
|
168
|
+
/*
|
|
169
|
+
* A 200 from the proxy does not mean the UPSTREAM listing worked — handleModels answers with
|
|
170
|
+
* defaults and `configured: true` even when the provider's /models route failed, so the list
|
|
171
|
+
* fetch never throws for that case. Cloudflare Workers AI's OpenAI-compatible surface has no
|
|
172
|
+
* /models route at all, so this is the only way that failure ever reaches the form; without
|
|
173
|
+
* it, the field silently filled with OpenAI's default ids instead of the user's own models.
|
|
174
|
+
*/
|
|
175
|
+
const upstreamMessage = proxyModelsErrorMessage();
|
|
176
|
+
if (upstreamMessage) {
|
|
177
|
+
modelsError = `${upstreamMessage} — type the model ID directly instead.`;
|
|
178
|
+
}
|
|
168
179
|
} catch (error: unknown) {
|
|
169
180
|
modelsError = (error as Error).message || "Failed to fetch models";
|
|
170
181
|
} finally {
|
package/src/ui/layers.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { mountSurface, registerSurface } from "./surface";
|
|
|
19
19
|
import toastsDoc from "../surfaces/toasts.json";
|
|
20
20
|
import { dismiss, toasts } from "../services/notify";
|
|
21
21
|
import { activeRegistry } from "../commands/active-registry";
|
|
22
|
+
import { runReported } from "../commands/run-reported";
|
|
22
23
|
import { effect, effectScope, reactive } from "../reactivity";
|
|
23
24
|
import type { DialogChoiceOption, DialogSurfaceOptions } from "../surfaces/dialog";
|
|
24
25
|
import type { Notification, Severity } from "../services/notify";
|
|
@@ -602,7 +603,9 @@ function toastScope(): ToastScope {
|
|
|
602
603
|
return;
|
|
603
604
|
}
|
|
604
605
|
retireToast(id);
|
|
605
|
-
|
|
606
|
+
// The toast is gone by the time the action refuses, so the refusal has to land somewhere the
|
|
607
|
+
// Person can still read it: Problems, through `commands/run-reported.ts`.
|
|
608
|
+
void runReported(registry, record.action, record.actionArgs, "Notifications");
|
|
606
609
|
},
|
|
607
610
|
toasts: [],
|
|
608
611
|
}) as ToastScope;
|