@jxsuite/studio 5.1.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-qwkm9zrf.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-vj72gqtq.js → components-5y78e05d.js} +4 -4
- package/dist/chunks/content-source-sgg1sv6n.js +46 -0
- package/dist/chunks/{convert-to-repeater-n64h8znc.js → convert-to-repeater-mf1cxgfn.js} +22 -22
- package/dist/chunks/entry-commands-w9qsxa6q.js +42 -0
- package/dist/chunks/{extension-sections-3hpcambp.js → extension-sections-c6zwzbre.js} +26 -26
- package/dist/chunks/{file-ops-8yhd83xr.js → file-ops-vrpq1jjk.js} +22 -22
- package/dist/chunks/{files-hnxqxtsg.js → files-msaf95y6.js} +31 -30
- package/dist/chunks/{format-host-779sfp2n.js → format-host-f64fnesp.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-bv5e22j0.js} +23 -22
- package/dist/chunks/grid-view-1p6hsyjx.css +8406 -0
- package/dist/chunks/grid-view-paj67nr5.js +15322 -0
- package/dist/chunks/{iframe-host-syr0f9sb.js → iframe-host-dh5np8vk.js} +13 -13
- package/dist/chunks/{layers-ff81wthn.js → layers-1vcfdbet.js} +5 -5
- package/dist/chunks/layers-panel-fksbdg4q.js +40 -0
- package/dist/chunks/{media-picker-cqhhxhhe.js → media-picker-p49nhqhs.js} +13 -13
- 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-qvvxej7h.js → references-h69xp9zz.js} +2 -2
- package/dist/chunks/right-panel-j97t2yvy.js +45 -0
- package/dist/chunks/{studio-b0k5tqgp.js → studio-01jeck2y.js} +31 -27
- package/dist/chunks/{studio-wqz6dggm.js → studio-14d9tq4d.js} +9 -9
- package/dist/chunks/{studio-try13h8c.js → studio-15f3kxqe.js} +18 -18
- package/dist/chunks/studio-1tbjfqv4.js +146 -0
- package/dist/chunks/{studio-vw7mzp6z.js → studio-28vs5k2y.js} +2 -2
- package/dist/chunks/{studio-93zafjcm.js → studio-2mdbbc76.js} +10 -10
- 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-3773086z.js → studio-4r14yzyy.js} +4 -4
- package/dist/chunks/{studio-3rz5md06.js → studio-4xmjcrg3.js} +6 -6
- package/dist/chunks/{studio-syg2tadj.js → studio-5nz00bhh.js} +3 -3
- package/dist/chunks/{studio-1knfqgy7.js → studio-5t7ena5z.js} +8 -8
- package/dist/chunks/{studio-a79k90n9.js → studio-66arbpc2.js} +8 -8
- package/dist/chunks/{studio-b4s9eykd.js → studio-68fydsrj.js} +15 -15
- package/dist/chunks/{studio-mhhyamxf.js → studio-9j6sfeba.js} +6 -6
- package/dist/chunks/{studio-7xnmgv66.js → studio-a1pswps3.js} +37 -37
- package/dist/chunks/{studio-6xa21ej9.js → studio-c04nzbby.js} +2 -2
- package/dist/chunks/{studio-nzzpvzx9.js → studio-c07s5ckd.js} +4 -4
- package/dist/chunks/{studio-js8nv53n.js → studio-ctm7bhx5.js} +14 -14
- package/dist/chunks/{studio-p1dw8wfw.js → studio-d8z5bajd.js} +14 -10
- package/dist/chunks/{studio-jxp12rnj.js → studio-en82c639.js} +6 -6
- package/dist/chunks/{studio-1z1kcjv4.js → studio-etvx9s2g.js} +11 -11
- package/dist/chunks/{studio-k75n7vhp.js → studio-ez11dmdh.js} +74 -36
- package/dist/chunks/{studio-qhhm6hbt.js → studio-hdc2jt3z.js} +4 -4
- package/dist/chunks/{studio-r6nvacqs.js → studio-hhskq2mr.js} +19 -19
- package/dist/chunks/{studio-6ewqv390.js → studio-m1p9b6c3.js} +5 -5
- package/dist/chunks/{studio-qk90fgqm.js → studio-mpzwze9x.js} +7 -7
- package/dist/chunks/{studio-e1pgga3k.js → studio-ncn9e189.js} +7 -7
- package/dist/chunks/{studio-kkqkcp3d.js → studio-p6pmwkvh.js} +10 -10
- package/dist/chunks/{studio-aj37eer3.js → studio-qa7fzxan.js} +4 -4
- package/dist/chunks/{studio-sbjyc8bt.js → studio-sar8g54p.js} +8 -8
- package/dist/chunks/studio-t171hxhe.js +23 -0
- package/dist/chunks/{studio-pxbgypmg.js → studio-t3adrr56.js} +17 -14
- package/dist/chunks/{studio-hhvs4sdg.js → studio-vn5hske5.js} +10 -4
- package/dist/chunks/{studio-5wt9nvhd.js → studio-vt0tj32j.js} +29 -42
- package/dist/chunks/{studio-qk4bfsq2.js → studio-wjpgxywa.js} +4 -4
- package/dist/chunks/{studio-q3x06vmj.js → studio-wnsyffg1.js} +2 -2
- package/dist/chunks/{studio-gb13095a.js → studio-x10fp4tp.js} +59 -20
- package/dist/chunks/{studio-8scdew0e.js → studio-x72j58vw.js} +4 -4
- package/dist/chunks/{studio-tqgryxwg.js → studio-y4f9fdge.js} +7 -7
- package/dist/chunks/{studio-vfq8qpjt.js → studio-ympe0n7a.js} +3 -3
- package/dist/chunks/{studio-y9zedyjn.js → studio-z2smx5ce.js} +2 -2
- package/dist/chunks/{studio-zdn2g6dt.js → studio-zjjde71a.js} +2 -2
- package/dist/chunks/{workspace-t55jdjbx.js → workspace-bnw54cha.js} +18 -10
- package/dist/iframe-entry.js +7 -3
- package/dist/studio.css +1 -2
- package/dist/studio.js +866 -15698
- package/package.json +6 -6
- 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/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/studio.ts +10 -6
- package/src/surfaces/commandbar.json +26 -16
- 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/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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a completed pragmatic drag over a tab strip or a pane's right edge into a model write.
|
|
4
|
+
*
|
|
5
|
+
* `panels/tab-strip.ts` registers every chip as a source and every strip and chip as a target,
|
|
6
|
+
* `panels/pane-grid.ts` registers the right-edge zone, and both hand their monitor's `onDrop` here
|
|
7
|
+
* with nothing decided yet — the DATA a target's `getData` produced, and nothing else. This module
|
|
8
|
+
* is the one place that DATA becomes a write, which is what keeps the two registration sites (a
|
|
9
|
+
* strip, a grid cell) from having to agree twice about what a drop means.
|
|
10
|
+
*
|
|
11
|
+
* No import of `tab-strip.ts` or `pane-grid.ts`: this module reaches the model through
|
|
12
|
+
* `workspace/workspace.ts` and `workspace/pane-derive.ts`, and the one opener through
|
|
13
|
+
* `files/files.ts` — the same three a keyboard command would use, because a drag and `⌘\` / "Open
|
|
14
|
+
* to the Side" are the same act by a different gesture (studio-ui-guidelines.md §8.2).
|
|
15
|
+
*
|
|
16
|
+
* @docs studio/interface/tabs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
20
|
+
import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index";
|
|
21
|
+
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
22
|
+
import {
|
|
23
|
+
activateTab,
|
|
24
|
+
closePane,
|
|
25
|
+
moveTab,
|
|
26
|
+
moveTabToPane,
|
|
27
|
+
paneById,
|
|
28
|
+
paneIsEmpty,
|
|
29
|
+
promoteTab,
|
|
30
|
+
receivingPane,
|
|
31
|
+
splitRight,
|
|
32
|
+
workspace,
|
|
33
|
+
} from "../workspace/workspace";
|
|
34
|
+
import { clearPaneDerivation } from "../workspace/pane-derive";
|
|
35
|
+
import { openFileInTab } from "../files/files";
|
|
36
|
+
|
|
37
|
+
/** A tab chip dragged off its own strip. */
|
|
38
|
+
export interface TabDragData extends Record<string, unknown> {
|
|
39
|
+
type: "tab";
|
|
40
|
+
tabId: string;
|
|
41
|
+
/**
|
|
42
|
+
* The pane the chip is being dragged FROM, read at drag start — a host can change which pane it
|
|
43
|
+
* draws, so this is never captured earlier than the gesture itself.
|
|
44
|
+
*/
|
|
45
|
+
paneId?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A file row dragged off the Files tree — `files.ts`'s own source shape, read structurally. */
|
|
49
|
+
export interface FileDragData extends Record<string, unknown> {
|
|
50
|
+
type: "file-tree";
|
|
51
|
+
entryType: string;
|
|
52
|
+
path: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type TabDropSourceData = TabDragData | FileDragData;
|
|
56
|
+
|
|
57
|
+
/** A slot in a strip — a chip's edge, or the strip's own empty tail when `tabId` is absent. */
|
|
58
|
+
export interface TabSlotTarget extends Record<string, unknown> {
|
|
59
|
+
type: "tab-slot";
|
|
60
|
+
paneId: string;
|
|
61
|
+
tabId?: string;
|
|
62
|
+
/** The target's own slot in `paneId`'s order at `getData` time — the tail's length, otherwise. */
|
|
63
|
+
index: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TabSlotRefusedTarget extends Record<string, unknown> {
|
|
67
|
+
type: "tab-slot-refused";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The right band of the last pane's stage, armed only while the grid is under {@link MAX_PANES}. */
|
|
71
|
+
export interface PaneEdgeTarget extends Record<string, unknown> {
|
|
72
|
+
type: "pane-edge";
|
|
73
|
+
paneId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PaneEdgeRefusedTarget extends Record<string, unknown> {
|
|
77
|
+
type: "pane-edge-refused";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type TabDropTarget =
|
|
81
|
+
| TabSlotTarget
|
|
82
|
+
| TabSlotRefusedTarget
|
|
83
|
+
| PaneEdgeTarget
|
|
84
|
+
| PaneEdgeRefusedTarget;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether `data` — a drag source's own data, exactly as `getInitialData` produced it — is a source
|
|
88
|
+
* this module knows how to land. A directory row is refused HERE, in the data, rather than by
|
|
89
|
+
* omitting a `dropTargetForElements` registration: pragmatic-dnd bubbles a refusal to the next
|
|
90
|
+
* target OUT, and the next target out of a tab strip is the pane cell, which is not a target at all
|
|
91
|
+
* — so an unhandled refusal would not merely do nothing, it would do nothing silently in a way
|
|
92
|
+
* indistinguishable from a bug. Every caller answers `canDrop` with this, so a directory row (or
|
|
93
|
+
* anything else this module has no branch for) is refused at the SOURCE and never reaches a target
|
|
94
|
+
* closure at all.
|
|
95
|
+
*/
|
|
96
|
+
export function isTabDropSource(data: Record<string, unknown>): data is TabDropSourceData {
|
|
97
|
+
return data.type === "tab" || (data.type === "file-tree" && data.entryType === "file");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Land a tab at a specific slot of `paneId`, releasing any derivation the pane is borrowing FIRST —
|
|
102
|
+
* the same order `receivingPane` uses, and for the same reason: inserting into a `tabOrder` while
|
|
103
|
+
* `pane.derived` is still set is the one-frame D2 violation its own docstring describes, observable
|
|
104
|
+
* to anything that reads the pane between the two writes.
|
|
105
|
+
*/
|
|
106
|
+
function placeTab(tabId: string, paneId: string, index: number): void {
|
|
107
|
+
const pane = paneById(paneId);
|
|
108
|
+
if (pane?.derived) {
|
|
109
|
+
clearPaneDerivation(paneId);
|
|
110
|
+
}
|
|
111
|
+
const landed = moveTabToPane(tabId, paneId, index);
|
|
112
|
+
if (!landed) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// A drag is a commitment, exactly as a double-click promoting a preview tab is one.
|
|
116
|
+
promoteTab(tabId);
|
|
117
|
+
activateTab(tabId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** {@link placeTab}'s file half: land the file's tab at `index`, opening it first if it is not. */
|
|
121
|
+
async function placeFile(path: string, paneId: string, index: number): Promise<void> {
|
|
122
|
+
const existing = [...workspace.tabs.values()].find((tab) => tab.documentPath === path);
|
|
123
|
+
if (existing) {
|
|
124
|
+
placeTab(existing.id, paneId, index);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
await openFileInTab(path, { focus: true, paneId });
|
|
128
|
+
// Idempotent by construction: `openFileInTab` may already have landed the tab at its default
|
|
129
|
+
// Slot, and this re-finds it by path and moves it to the slot the drop actually asked for.
|
|
130
|
+
const opened = [...workspace.tabs.values()].find((tab) => tab.documentPath === path);
|
|
131
|
+
if (opened) {
|
|
132
|
+
placeTab(opened.id, paneId, index);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The landing index a chip-edge target names, or the tail target's own index when there is none. */
|
|
137
|
+
function edgeIndex(target: TabSlotTarget, edge: Edge | null): number {
|
|
138
|
+
if (target.tabId === undefined) {
|
|
139
|
+
return target.index;
|
|
140
|
+
}
|
|
141
|
+
return edge === "left" ? target.index : target.index + 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function resolveTabSlotDrop(source: TabDropSourceData, target: TabSlotTarget): Promise<void> {
|
|
145
|
+
const edge = extractClosestEdge(target as unknown as Record<string | symbol, unknown>);
|
|
146
|
+
if (source.type === "tab") {
|
|
147
|
+
if (source.paneId === target.paneId) {
|
|
148
|
+
const pane = paneById(target.paneId);
|
|
149
|
+
if (!pane) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const startIndex = pane.tabOrder.indexOf(source.tabId);
|
|
153
|
+
if (startIndex === -1) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const to =
|
|
157
|
+
target.tabId === undefined
|
|
158
|
+
? pane.tabOrder.length - 1
|
|
159
|
+
: getReorderDestinationIndex({
|
|
160
|
+
axis: "horizontal",
|
|
161
|
+
closestEdgeOfTarget: edge,
|
|
162
|
+
indexOfTarget: target.index,
|
|
163
|
+
startIndex,
|
|
164
|
+
});
|
|
165
|
+
if (to !== startIndex) {
|
|
166
|
+
moveTab(source.tabId, to);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
placeTab(source.tabId, target.paneId, edgeIndex(target, edge));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await placeFile(source.path, target.paneId, edgeIndex(target, edge));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function resolvePaneEdgeDrop(
|
|
177
|
+
source: TabDropSourceData,
|
|
178
|
+
target: PaneEdgeTarget,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
if (source.type === "tab") {
|
|
181
|
+
// Ignores `target.paneId`: the split moves the tab beside ITS OWN pane, which is the edge zone's
|
|
182
|
+
// Pane by construction — the zone is only armed below `MAX_PANES`, so there is exactly one pane
|
|
183
|
+
// For a dragged tab to have come from.
|
|
184
|
+
const landed = splitRight(source.tabId);
|
|
185
|
+
if (landed) {
|
|
186
|
+
promoteTab(source.tabId);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const pane = receivingPane(target.paneId);
|
|
191
|
+
await openFileInTab(source.path, { paneId: pane.id });
|
|
192
|
+
const landed = [...workspace.tabs.values()].some((tab) => tab.documentPath === source.path);
|
|
193
|
+
if (!landed && paneIsEmpty(pane)) {
|
|
194
|
+
// A refusal (a missing file, no editor for the format) must not leave a pane this drop minted
|
|
195
|
+
// Standing empty — the same rule `document.openToSide` applies to its own refusal.
|
|
196
|
+
closePane(pane.id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Turn a completed drag into a write. `target` is the drop's own INNERMOST target data, or
|
|
202
|
+
* `undefined` for a drag cancelled off every target — both are legal and both mean nothing landed.
|
|
203
|
+
*/
|
|
204
|
+
export async function resolveTabDrop(
|
|
205
|
+
source: { data: Record<string, unknown> },
|
|
206
|
+
target: { data: Record<string, unknown> } | undefined,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
if (!target || !isTabDropSource(source.data)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const sourceData = source.data;
|
|
212
|
+
if (target.data.type === "tab-slot") {
|
|
213
|
+
await resolveTabSlotDrop(sourceData, target.data as TabSlotTarget);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (target.data.type === "pane-edge") {
|
|
217
|
+
await resolvePaneEdgeDrop(sourceData, target.data as PaneEdgeTarget);
|
|
218
|
+
}
|
|
219
|
+
// `tab-slot-refused` / `pane-edge-refused` / anything else: nothing.
|
|
220
|
+
}
|
package/src/panels/tab-strip.ts
CHANGED
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
closeTab,
|
|
49
49
|
focusPane,
|
|
50
50
|
isPaneFocused,
|
|
51
|
-
moveTab,
|
|
52
51
|
promoteDirtyPreviewTabs,
|
|
53
52
|
promoteTab,
|
|
54
53
|
setTabPinned,
|
|
@@ -80,9 +79,17 @@ import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
|
|
|
80
79
|
import { saveFile } from "../files/file-ops";
|
|
81
80
|
import { collabReadOnly } from "../collab/collab-session";
|
|
82
81
|
import { collabState } from "../collab/collab-state";
|
|
83
|
-
import { rectOf } from "../utils/geometry";
|
|
82
|
+
import { elementAtPoint, rectOf } from "../utils/geometry";
|
|
84
83
|
import { resolveRegion } from "../ui/regions";
|
|
85
84
|
import { commitTabBuffers, tabBufferUnsaved } from "../services/monaco-buffer";
|
|
85
|
+
import {
|
|
86
|
+
draggable,
|
|
87
|
+
dropTargetForElements,
|
|
88
|
+
monitorForElements,
|
|
89
|
+
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
90
|
+
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
91
|
+
import { attachClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
92
|
+
import { isTabDropSource, resolveTabDrop } from "./tab-drop";
|
|
86
93
|
import type { EffectScope } from "@vue/reactivity";
|
|
87
94
|
|
|
88
95
|
/**
|
|
@@ -119,8 +126,24 @@ const _overflowing = new Map<string, boolean>();
|
|
|
119
126
|
|
|
120
127
|
let _overflowHandle: MenuHandle | null = null;
|
|
121
128
|
|
|
122
|
-
/**
|
|
123
|
-
let
|
|
129
|
+
/** What is being dragged right now, or null between drags — a tab off some strip, or a file row. */
|
|
130
|
+
let _drag: { kind: "tab"; tabId: string } | { kind: "file"; path: string } | null = null;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Where the pointer is hovering, in the vocabulary a strip draws: which pane, which chip slot (`-1`
|
|
134
|
+
* for the tail), and whether that slot IS the tail. Null off every target this module answers for —
|
|
135
|
+
* a `pane-edge` hover included, which is `panels/pane-grid.ts`'s own indicator to draw.
|
|
136
|
+
*/
|
|
137
|
+
let _over: { paneId: string; index: number; tail: boolean } | null = null;
|
|
138
|
+
|
|
139
|
+
/** Every chip's pragmatic registration, per host, per tab id — released before a re-registration. */
|
|
140
|
+
const _chipDnd = new Map<HTMLElement, Map<string, () => void>>();
|
|
141
|
+
|
|
142
|
+
/** Each host's OWN registration — the strip's empty-tail target. */
|
|
143
|
+
const _stripDnd = new Map<HTMLElement, () => void>();
|
|
144
|
+
|
|
145
|
+
/** The one drag monitor, armed in {@link mount} and released in {@link unmount}. */
|
|
146
|
+
let _dropMonitor: (() => void) | null = null;
|
|
124
147
|
|
|
125
148
|
/** Region id of a pane's tab strip. `ui/regions.ts` owns the grammar; this is one call site. */
|
|
126
149
|
export function paneStripRegion(paneId: string): string {
|
|
@@ -153,6 +176,51 @@ function hostFor(pane: Pane): HTMLElement | null {
|
|
|
153
176
|
export function mount(host: HTMLElement) {
|
|
154
177
|
unmount();
|
|
155
178
|
_primaryHost = host;
|
|
179
|
+
_dropMonitor = monitorForElements({
|
|
180
|
+
onDragStart({ source }) {
|
|
181
|
+
if (!isTabDropSource(source.data)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
_drag =
|
|
185
|
+
source.data.type === "tab"
|
|
186
|
+
? { kind: "tab", tabId: source.data.tabId as string }
|
|
187
|
+
: { kind: "file", path: source.data.path as string };
|
|
188
|
+
// The tail target does not exist until a pane with no tabs is asked to grow one (§7).
|
|
189
|
+
render();
|
|
190
|
+
// `render()` alone does not write `dragKey` — only `syncDrag()` does — so without this the
|
|
191
|
+
// Source chip's `data-dragging` mark would not appear until the pointer's first move.
|
|
192
|
+
syncDrag();
|
|
193
|
+
},
|
|
194
|
+
onDrag({ location }) {
|
|
195
|
+
if (!_drag) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const [target] = location.current.dropTargets;
|
|
199
|
+
const data = target?.data;
|
|
200
|
+
_over =
|
|
201
|
+
data?.type === "tab-slot"
|
|
202
|
+
? {
|
|
203
|
+
index: data.tabId === undefined ? -1 : (data.index as number),
|
|
204
|
+
paneId: data.paneId as string,
|
|
205
|
+
tail: data.tabId === undefined,
|
|
206
|
+
}
|
|
207
|
+
: null;
|
|
208
|
+
syncDrag();
|
|
209
|
+
},
|
|
210
|
+
onDrop({ location, source }) {
|
|
211
|
+
if (!isTabDropSource(source.data)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const [target] = location.current.dropTargets;
|
|
215
|
+
_drag = null;
|
|
216
|
+
_over = null;
|
|
217
|
+
syncDrag();
|
|
218
|
+
render();
|
|
219
|
+
// A cancelled drag (dropped off every target) arrives with `dropTargets: []`; the resolver
|
|
220
|
+
// Reads `target === undefined` as "nothing landed", the same answer a refused target gives.
|
|
221
|
+
void resolveTabDrop(source, target);
|
|
222
|
+
},
|
|
223
|
+
});
|
|
156
224
|
_scope = effectScope();
|
|
157
225
|
_scope.run(() => {
|
|
158
226
|
effect(() => {
|
|
@@ -195,18 +263,37 @@ export function unmount() {
|
|
|
195
263
|
dismissTabContextMenu();
|
|
196
264
|
_scope?.stop();
|
|
197
265
|
_scope = null;
|
|
266
|
+
_dropMonitor?.();
|
|
267
|
+
_dropMonitor = null;
|
|
268
|
+
for (const host of _strips.keys()) {
|
|
269
|
+
releaseHostDnD(host);
|
|
270
|
+
}
|
|
198
271
|
for (const strip of _strips.values()) {
|
|
199
272
|
strip.dispose();
|
|
200
273
|
}
|
|
201
274
|
_strips.clear();
|
|
202
275
|
_drawing.clear();
|
|
203
276
|
_primaryHost = null;
|
|
204
|
-
|
|
277
|
+
_drag = null;
|
|
278
|
+
_over = null;
|
|
205
279
|
_lastActive.clear();
|
|
206
280
|
_overflowing.clear();
|
|
207
281
|
_hosts.clear();
|
|
208
282
|
}
|
|
209
283
|
|
|
284
|
+
/** Take back every pragmatic registration a host holds — its chips and its own tail target. */
|
|
285
|
+
function releaseHostDnD(host: HTMLElement): void {
|
|
286
|
+
const chips = _chipDnd.get(host);
|
|
287
|
+
if (chips) {
|
|
288
|
+
for (const release of chips.values()) {
|
|
289
|
+
release();
|
|
290
|
+
}
|
|
291
|
+
_chipDnd.delete(host);
|
|
292
|
+
}
|
|
293
|
+
_stripDnd.get(host)?.();
|
|
294
|
+
_stripDnd.delete(host);
|
|
295
|
+
}
|
|
296
|
+
|
|
210
297
|
function render() {
|
|
211
298
|
/* One host draws ONE pane. While the shell has a single strip, both panes resolve to the same
|
|
212
299
|
div — and the one whose tabs are on screen is the focused one, so it wins the tie. Without
|
|
@@ -237,6 +324,7 @@ function render() {
|
|
|
237
324
|
_lastActive.delete(paneId);
|
|
238
325
|
_overflowing.delete(paneId);
|
|
239
326
|
if (!claims.has(host)) {
|
|
327
|
+
releaseHostDnD(host);
|
|
240
328
|
_strips.get(host)?.dispose();
|
|
241
329
|
_strips.delete(host);
|
|
242
330
|
_drawing.delete(host);
|
|
@@ -261,6 +349,7 @@ function paint(pane: Pane, host: HTMLElement): void {
|
|
|
261
349
|
} else {
|
|
262
350
|
_strips.set(host, mountTabStripSurface(host, values, actionsFor(host)));
|
|
263
351
|
}
|
|
352
|
+
reconcileStripDnD(host, values);
|
|
264
353
|
|
|
265
354
|
/* The strip is HELD — `surfaces/tab-strip.ts` takes it as the element is created — so this is an
|
|
266
355
|
imperative USE of a node this module owns rather than a re-find of one it renders
|
|
@@ -306,6 +395,9 @@ function actionsFor(host: HTMLElement): TabStripActions {
|
|
|
306
395
|
activate: (id) => {
|
|
307
396
|
activateTab(id);
|
|
308
397
|
},
|
|
398
|
+
chipHost: (element, id) => {
|
|
399
|
+
adoptChip(host, element, id);
|
|
400
|
+
},
|
|
309
401
|
closeTab: (id) => {
|
|
310
402
|
void requestClose(id);
|
|
311
403
|
},
|
|
@@ -315,11 +407,6 @@ function actionsFor(host: HTMLElement): TabStripActions {
|
|
|
315
407
|
openTabContextMenu(tab, x, y);
|
|
316
408
|
}
|
|
317
409
|
},
|
|
318
|
-
dragEnd: onDragEnd,
|
|
319
|
-
dragStart: onDragStart,
|
|
320
|
-
drop: (index, transfer) => {
|
|
321
|
-
onDrop(host, index, transfer);
|
|
322
|
-
},
|
|
323
410
|
focusPane: () => {
|
|
324
411
|
focusPane(drawnPane(host)?.id ?? PRIMARY_PANE);
|
|
325
412
|
},
|
|
@@ -329,6 +416,9 @@ function actionsFor(host: HTMLElement): TabStripActions {
|
|
|
329
416
|
promote: (id) => {
|
|
330
417
|
promoteTab(id);
|
|
331
418
|
},
|
|
419
|
+
stripHost: (element) => {
|
|
420
|
+
adoptStrip(host, element);
|
|
421
|
+
},
|
|
332
422
|
togglePin: (id) => {
|
|
333
423
|
const tab = workspace.tabs.get(id);
|
|
334
424
|
if (tab) {
|
|
@@ -439,6 +529,26 @@ function project(pane: Pane): TabStripValues {
|
|
|
439
529
|
return derivationValues(pane, derived);
|
|
440
530
|
}
|
|
441
531
|
if (pane.tabOrder.length === 0) {
|
|
532
|
+
/* An empty, non-derived pane WHILE A DRAG IS LIVE draws a `tabs` row with no chips instead of
|
|
533
|
+
going blank: `blankValues` hides the row outright, and a hidden row has no `jx-tabs` element
|
|
534
|
+
for `adoptStrip` to make a target of — so a tab dragged toward the empty primary beside a
|
|
535
|
+
derived secondary (the state `detachTab` allows) had nowhere to land. `_drag` gates it rather
|
|
536
|
+
than `pane.tabOrder.length === 0` alone, so the pane stays hidden the rest of the time. */
|
|
537
|
+
if (_drag) {
|
|
538
|
+
return {
|
|
539
|
+
active: "",
|
|
540
|
+
chipTitle: "",
|
|
541
|
+
focused: isPaneFocused(pane.id),
|
|
542
|
+
mode: "tabs",
|
|
543
|
+
preset: "",
|
|
544
|
+
stripLabel: stripLabel(pane),
|
|
545
|
+
subject: "",
|
|
546
|
+
tabs: [],
|
|
547
|
+
trailing: false,
|
|
548
|
+
trailingGlyph: "⌄",
|
|
549
|
+
trailingTitle: "Show hidden tabs",
|
|
550
|
+
};
|
|
551
|
+
}
|
|
442
552
|
_lastActive.set(pane.id, null);
|
|
443
553
|
_overflowing.set(pane.id, false);
|
|
444
554
|
return blankValues(pane);
|
|
@@ -492,7 +602,6 @@ function chipView(
|
|
|
492
602
|
{
|
|
493
603
|
dirty: tab.doc.dirty,
|
|
494
604
|
draft: pill?.draft ?? false,
|
|
495
|
-
dragging: _dragging === id,
|
|
496
605
|
index,
|
|
497
606
|
key: id,
|
|
498
607
|
label: labels.get(id) ?? "Untitled",
|
|
@@ -511,35 +620,131 @@ function chipView(
|
|
|
511
620
|
];
|
|
512
621
|
}
|
|
513
622
|
|
|
514
|
-
// ─── Drag
|
|
623
|
+
// ─── Drag and drop ────────────────────────────────────────────────────────────
|
|
515
624
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Make one chip a pragmatic drag source and drop target. Called from `chipHost`, as the chip is
|
|
627
|
+
* CREATED (`onNodeCreated` fires once per chip; a repaint that keeps a chip alive fires it never
|
|
628
|
+
* again, and one that replaces it — a pane handover, §18.3 — fires it fresh).
|
|
629
|
+
*
|
|
630
|
+
* Released first, in case a chip element is ever handed a second registration for the same id: the
|
|
631
|
+
* SOURCE of truth for "is this id registered on this host" is the map, not whether pragmatic still
|
|
632
|
+
* remembers the old element, and releasing unconditionally is cheaper than asking.
|
|
633
|
+
*/
|
|
634
|
+
function adoptChip(host: HTMLElement, element: HTMLElement, id: string): void {
|
|
635
|
+
let chips = _chipDnd.get(host);
|
|
636
|
+
if (!chips) {
|
|
637
|
+
chips = new Map();
|
|
638
|
+
_chipDnd.set(host, chips);
|
|
521
639
|
}
|
|
522
|
-
|
|
523
|
-
|
|
640
|
+
chips.get(id)?.();
|
|
641
|
+
|
|
642
|
+
const cleanup = combine(
|
|
643
|
+
draggable({
|
|
644
|
+
element,
|
|
645
|
+
// A drag starting on the pin toggle or the kit's own close button is a click on THAT control,
|
|
646
|
+
// Not a request to move the chip — the same guard `panels/dnd.ts`'s Outline rows use.
|
|
647
|
+
canDrag({ input }) {
|
|
648
|
+
const target = elementAtPoint(input.clientX, input.clientY);
|
|
649
|
+
return !(target instanceof Element && target.closest('[part="pin"], [part="close"]'));
|
|
650
|
+
},
|
|
651
|
+
getInitialData() {
|
|
652
|
+
// Read at DRAG TIME, not captured at registration: the pane a host draws can change under
|
|
653
|
+
// An already-mounted chip (a handover, §18.3), and `drawnPane` always answers about now.
|
|
654
|
+
return { paneId: drawnPane(host)?.id, tabId: id, type: "tab" };
|
|
655
|
+
},
|
|
656
|
+
}),
|
|
657
|
+
dropTargetForElements({
|
|
658
|
+
canDrop({ source }) {
|
|
659
|
+
return isTabDropSource(source.data);
|
|
660
|
+
},
|
|
661
|
+
element,
|
|
662
|
+
getData({ element: el, input }) {
|
|
663
|
+
const pane = drawnPane(host);
|
|
664
|
+
if (!pane) {
|
|
665
|
+
return { type: "tab-slot-refused" };
|
|
666
|
+
}
|
|
667
|
+
// Computed HERE, not at chip creation: the target's own slot moves every time a tab is
|
|
668
|
+
// Reordered, and `resolveTabDrop` reads this fresh on every hover and on the drop itself.
|
|
669
|
+
return attachClosestEdge(
|
|
670
|
+
{ index: pane.tabOrder.indexOf(id), paneId: pane.id, tabId: id, type: "tab-slot" },
|
|
671
|
+
{ allowedEdges: ["left", "right"], element: el, input },
|
|
672
|
+
);
|
|
673
|
+
},
|
|
674
|
+
}),
|
|
675
|
+
);
|
|
676
|
+
chips.set(id, cleanup);
|
|
524
677
|
}
|
|
525
678
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
679
|
+
/**
|
|
680
|
+
* Make a strip's OWN element a drop target — the empty tail after the last chip, and, for an empty
|
|
681
|
+
* pane growing one during a drag (§7), the whole of it. Innermost wins, so a chip under the pointer
|
|
682
|
+
* answers first; this only ever resolves a drop that landed on the strip itself.
|
|
683
|
+
*/
|
|
684
|
+
function adoptStrip(host: HTMLElement, element: HTMLElement): void {
|
|
685
|
+
_stripDnd.get(host)?.();
|
|
686
|
+
const cleanup = dropTargetForElements({
|
|
687
|
+
canDrop({ source }) {
|
|
688
|
+
return isTabDropSource(source.data);
|
|
689
|
+
},
|
|
690
|
+
element,
|
|
691
|
+
getData() {
|
|
692
|
+
const pane = drawnPane(host);
|
|
693
|
+
return pane
|
|
694
|
+
? { index: pane.tabOrder.length, paneId: pane.id, type: "tab-slot" }
|
|
695
|
+
: { type: "tab-slot-refused" };
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
_stripDnd.set(host, cleanup);
|
|
529
699
|
}
|
|
530
700
|
|
|
531
701
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
702
|
+
* Release a host's chip registrations for ids that no longer appear — a tab closed, or a pane
|
|
703
|
+
* handover replaced every chip with another pane's — and release the whole host when it is no
|
|
704
|
+
* longer drawing tabs at all. Run at the tail of every {@link paint}, which is what makes a handover
|
|
705
|
+
* self-correcting: the SAME call that draws the new pane's chips (through `onNodeCreated`) is the
|
|
706
|
+
* call that notices the old pane's ids are no longer live.
|
|
534
707
|
*/
|
|
535
|
-
function
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
708
|
+
function reconcileStripDnD(host: HTMLElement, values: TabStripValues): void {
|
|
709
|
+
if (values.mode !== "tabs") {
|
|
710
|
+
releaseHostDnD(host);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const live = new Set(values.tabs.map((chip) => chip.key));
|
|
714
|
+
const chips = _chipDnd.get(host);
|
|
715
|
+
if (!chips) {
|
|
540
716
|
return;
|
|
541
717
|
}
|
|
542
|
-
|
|
718
|
+
for (const [id, release] of chips) {
|
|
719
|
+
if (!live.has(id)) {
|
|
720
|
+
release();
|
|
721
|
+
chips.delete(id);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Write the drag indicators for every standing strip, from {@link _over} and {@link _drag}.
|
|
728
|
+
*
|
|
729
|
+
* `dragKey` is broadcast to every host unconditionally — a tab id only matches a chip on the ONE
|
|
730
|
+
* host that draws its pane, so the comparison in `tab-strip.json` is what scopes it, not this
|
|
731
|
+
* function. `dropIndex` / `dropTail` ARE scoped here, to the host currently drawing `_over.paneId`,
|
|
732
|
+
* because an index is meaningless on a strip it was not measured against.
|
|
733
|
+
*/
|
|
734
|
+
function syncDrag(): void {
|
|
735
|
+
const dragKey = _drag?.kind === "tab" ? _drag.tabId : "";
|
|
736
|
+
for (const [host, drawing] of _drawing) {
|
|
737
|
+
const surface = _strips.get(host);
|
|
738
|
+
if (!surface) {
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const hovering = _over !== null && _over.paneId === drawing.paneId;
|
|
742
|
+
surface.drag({
|
|
743
|
+
dragKey,
|
|
744
|
+
dropIndex: hovering && _over ? _over.index : -1,
|
|
745
|
+
dropTail: hovering && _over ? _over.tail : false,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
543
748
|
}
|
|
544
749
|
|
|
545
750
|
/**
|
|
@@ -75,6 +75,7 @@ let proxyConfigured = false;
|
|
|
75
75
|
let proxyManaged = false;
|
|
76
76
|
let proxyDefaultModel = "";
|
|
77
77
|
let proxyCode: AiModelsResponse["code"];
|
|
78
|
+
let proxyModelsError = "";
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
81
|
* One-shot capability probe shared by every credentials gate, plus the hosts to repaint when it
|
|
@@ -116,6 +117,7 @@ export function resetModelCache() {
|
|
|
116
117
|
proxyManaged = false;
|
|
117
118
|
proxyDefaultModel = "";
|
|
118
119
|
proxyCode = undefined;
|
|
120
|
+
proxyModelsError = "";
|
|
119
121
|
/* The probe's result IS the flags above. Clearing them while keeping the settled promise
|
|
120
122
|
would strand every gate on a permanent "unconfigured, unmanaged" reading — ensureProxyProbe
|
|
121
123
|
would no-op forever and the managed option would vanish until a full reload. */
|
|
@@ -266,6 +268,16 @@ export function proxyStateCode(): AiModelsResponse["code"] {
|
|
|
266
268
|
return proxyCode;
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
/**
|
|
272
|
+
* The upstream's own error message from the last fetch, when it reported one (e.g. Cloudflare's "No
|
|
273
|
+
* route for that URI" for a base URL that doesn't serve `/models`). Empty when the last fetch had
|
|
274
|
+
* no upstream error to report — a stale message from an earlier failure must not survive a fetch
|
|
275
|
+
* that succeeded.
|
|
276
|
+
*/
|
|
277
|
+
export function proxyModelsErrorMessage(): string {
|
|
278
|
+
return proxyModelsError;
|
|
279
|
+
}
|
|
280
|
+
|
|
269
281
|
/** The proxy's preferred model id ("" when it does not declare one). */
|
|
270
282
|
export function getProxyDefaultModel(): string {
|
|
271
283
|
return proxyDefaultModel;
|
|
@@ -356,6 +368,7 @@ export async function fetchAvailableModels(
|
|
|
356
368
|
proxyManaged = data.managed === true;
|
|
357
369
|
proxyDefaultModel = data.defaultModel ?? "";
|
|
358
370
|
proxyCode = data.code;
|
|
371
|
+
proxyModelsError = data.upstreamError !== undefined ? (data.upstreamMessage ?? "") : "";
|
|
359
372
|
/* Capabilities are kept, not dropped. The backend has reported `toolSupport` all along and the
|
|
360
373
|
ingest mapped `{id, name}` only, so a Workers AI model that cannot call tools looked exactly
|
|
361
374
|
like one that can — the agent loop ran, called nothing, and answered as if that were normal.
|
package/src/studio.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
29
|
PRIMARY_PANE,
|
|
30
|
+
activePane,
|
|
30
31
|
activeTab,
|
|
31
32
|
closeAllTabs,
|
|
32
33
|
openTab,
|
|
@@ -384,9 +385,11 @@ function setCanvasMode(tab: Tab | null, mode: string) {
|
|
|
384
385
|
* 1. **To the side.** §8.2 has promised this since P3 and it never shipped — the chain ran
|
|
385
386
|
* `openFileInTab` → `openTab` → `activePane()`, so "open the layout that wraps this page" opened
|
|
386
387
|
* it ON TOP of the page it was teaching about.
|
|
387
|
-
* 2. **
|
|
388
|
-
*
|
|
389
|
-
* pane
|
|
388
|
+
* 2. **The focus FOLLOWS.** The open is a gesture, not a read: the author asked to edit the component,
|
|
389
|
+
* so the pane it lands in takes the keyboard. The failure it replaces is the opposite one — an
|
|
390
|
+
* assistant pane that takes the keyboard means the author's next keystroke edits the definition
|
|
391
|
+
* instead of the document they are looking at, which is why the READS (`pane.compareWith`, the
|
|
392
|
+
* derivation follow, session restore) browse with `focus: false`.
|
|
390
393
|
* 3. **A PREVIEW tab**, because drilling in is browsing: the second drill-in takes the same slot
|
|
391
394
|
* instead of littering the side strip, and an edit promotes it (`promoteDirtyPreviewTabs`).
|
|
392
395
|
* 4. **An ordinary tab, not a derivation.** "Edit definition" is a commitment to edit one component; a
|
|
@@ -396,16 +399,17 @@ function setCanvasMode(tab: Tab | null, mode: string) {
|
|
|
396
399
|
* `openedFrom` is unchanged — §14.2's relationship, which nothing pops and nothing restores from.
|
|
397
400
|
*
|
|
398
401
|
* @param {string} componentPath
|
|
402
|
+
* @docs studio/interface/tabs
|
|
399
403
|
*/
|
|
400
404
|
async function navigateToComponent(componentPath: string) {
|
|
401
405
|
const from = activeTab.value;
|
|
402
|
-
/* {@link receivingPane}, not `
|
|
406
|
+
/* {@link receivingPane}, not `paneBeside`: the pane beside this one may be a LENS, which owns no
|
|
403
407
|
tab. The open then landed in a `tabOrder` `tabOfPane` hops straight past, so the read below got
|
|
404
408
|
the SOURCE tab back, `opened.documentPath !== componentPath`, and the one relationship this
|
|
405
409
|
function exists to record (§14.2) was skipped without a sound. */
|
|
406
|
-
const target = receivingPane();
|
|
410
|
+
const target = receivingPane(activePane().id);
|
|
407
411
|
const alreadyOpen = [...workspace.tabs.values()].some((t) => t.documentPath === componentPath);
|
|
408
|
-
await openFileInTab(componentPath, {
|
|
412
|
+
await openFileInTab(componentPath, { paneId: target.id, preview: true });
|
|
409
413
|
const opened = tabOfPane(target.id);
|
|
410
414
|
if (!alreadyOpen && from && opened && opened.documentPath === componentPath) {
|
|
411
415
|
opened.session.openedFrom = { documentPath: from.documentPath, tabId: from.id };
|