pi-weave 0.1.12 → 0.1.13
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/README.md +8 -37
- package/package.json +1 -2
- package/src/core/concurrency.ts +3 -6
- package/src/core/frontmatter.ts +0 -53
- package/src/core/graph/build.ts +6 -7
- package/src/core/graph/current.ts +2 -4
- package/src/core/graph/model.ts +1 -1
- package/src/core/graph/wikilinks.ts +3 -3
- package/src/core/index.ts +26 -27
- package/src/core/paths.ts +0 -7
- package/src/core/vault.ts +16 -681
- package/src/core/view/detail.ts +1 -1
- package/src/core/view/health.ts +1 -1
- package/src/core/view/tree.ts +1 -1
- package/src/pi/index.ts +6 -85
- package/src/pi/summarize.ts +2 -2
- package/src/pi/viewer/tui/bodyStore.ts +4 -7
- package/src/pi/viewer/tui/branding.ts +7 -148
- package/src/pi/viewer/tui/run.ts +3 -17
- package/src/pi/viewer/tui/surface/base.ts +24 -3
- package/src/pi/viewer/tui/surface/explore.ts +41 -6
- package/src/pi/viewer/tui/workspace.ts +23 -351
- package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
- package/src/pi/viewer/web/run.ts +7 -117
- package/src/web/client/api.dom.ts +2 -2
- package/src/web/client/api.ts +14 -223
- package/src/web/client/bootstrap.ts +5 -14
- package/src/web/client/context/context.model.ts +9 -11
- package/src/web/client/dist/app.js +93 -219
- package/src/web/client/graph/dynamics.ts +5 -65
- package/src/web/client/graph/renderer.dom.ts +7 -8
- package/src/web/client/graph/renderer.ts +9 -35
- package/src/web/client/main.tsx +1 -1
- package/src/web/client/note/Note.tsx +21 -63
- package/src/web/client/search/SearchPalette.tsx +45 -36
- package/src/web/client/search/search.model.ts +33 -454
- package/src/web/client/shell/Columns.tsx +13 -83
- package/src/web/client/shell/Header.tsx +2 -10
- package/src/web/client/shell/Shell.tsx +50 -125
- package/src/web/client/shell/StatusBar.tsx +1 -4
- package/src/web/client/shell/icons.model.ts +4 -7
- package/src/web/client/shell/keys.model.ts +5 -42
- package/src/web/client/shell/keys.ts +2 -2
- package/src/web/client/shell/shell.model.ts +10 -133
- package/src/web/client/shell/theme.model.ts +2 -2
- package/src/web/client/shell/theme.ts +33 -157
- package/src/web/client/state.ts +9 -89
- package/src/web/client/tree/Tree.tsx +25 -575
- package/src/web/client/tree/tree.model.ts +8 -162
- package/src/web/client/workspace.ts +72 -242
- package/src/web/server/page.ts +8 -10
- package/src/web/server/routes.ts +30 -563
- package/src/web/server/server.ts +6 -145
- package/src/web/shared/layout.ts +72 -624
- package/src/web/shared/wire.ts +10 -196
- package/src/core/sessions.ts +0 -929
- package/src/pi/sessionScan.ts +0 -104
- package/src/pi/viewer/tui/explorer.ts +0 -586
- package/src/web/client/live.model.ts +0 -275
- package/src/web/client/live.ts +0 -151
- package/src/web/client/note/Editor.tsx +0 -109
- package/src/web/client/note/editor.controller.ts +0 -151
- package/src/web/client/note/editor.model.ts +0 -686
- package/src/web/client/search/search.ts +0 -107
- package/src/web/client/shell/Divider.tsx +0 -44
- package/src/web/client/shell/cssvars.ts +0 -70
- package/src/web/client/shell/drag.model.ts +0 -170
- package/src/web/client/shell/layout.model.ts +0 -500
- package/src/web/client/shell/viewport.ts +0 -29
- package/src/web/server/sse.ts +0 -321
- package/src/web/server/watcher.ts +0 -507
|
@@ -1,381 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* workspace.ts — the pure split-tree workspace model (weave-view-tui-v2 §4).
|
|
3
|
-
*
|
|
4
|
-
* A workspace is a recursive tree of splits (HStack rows / VStack columns)
|
|
5
|
-
* and panes. Each pane is a surface instance with its own selection/scroll/
|
|
6
|
-
* filter state (held by the pane component, not here); this module models the
|
|
7
|
-
* *structure* only: the split tree, pane ids, per-pane surface + bound node,
|
|
8
|
-
* focus (active pane), resize weights, and responsive collapse.
|
|
9
|
-
*
|
|
10
|
-
* This layer is harness-free (imports only core types) so every operation is
|
|
11
|
-
* unit-tested without a terminal — mirroring v1's model.ts posture (§11).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
1
|
import type { GraphModel } from "../../../core/graph/model";
|
|
15
2
|
import { graphRoots } from "./model";
|
|
16
3
|
|
|
17
|
-
/** The four surface kinds a pane can host. */
|
|
18
4
|
export type SurfaceKind = "explore" | "detail" | "focus" | "health";
|
|
19
5
|
|
|
20
6
|
export interface PaneNode {
|
|
21
|
-
type: "pane";
|
|
22
|
-
/** Stable pane id. */
|
|
23
7
|
id: string;
|
|
24
8
|
surface: SurfaceKind;
|
|
25
|
-
/** Node id bound for detail/focus panes; null until opened. */
|
|
26
9
|
nodeId: string | null;
|
|
27
10
|
}
|
|
28
11
|
|
|
29
|
-
/** Split direction. `row` → HStack (side-by-side); `column` → VStack (stacked). */
|
|
30
|
-
export type SplitDir = "row" | "column";
|
|
31
|
-
|
|
32
|
-
export interface SplitNode {
|
|
33
|
-
type: "split";
|
|
34
|
-
direction: SplitDir;
|
|
35
|
-
/** Flex weights per child (drives the pi-tui Stack grow values). */
|
|
36
|
-
sizes: number[];
|
|
37
|
-
children: WorkspaceNode[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type WorkspaceNode = PaneNode | SplitNode;
|
|
41
|
-
|
|
42
12
|
export interface Workspace {
|
|
43
13
|
name: string;
|
|
44
|
-
|
|
45
|
-
/** The single active (focus-owning) pane id. */
|
|
14
|
+
panes: PaneNode[];
|
|
46
15
|
activePaneId: string;
|
|
47
16
|
}
|
|
48
17
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/** Split direction a `\`/`|` command produces. */
|
|
53
|
-
export type SplitRequest = "vertical" | "horizontal";
|
|
54
|
-
|
|
55
|
-
let nextPaneId = 1;
|
|
56
|
-
/** Fresh, collision-free pane id (test seam resets via resetWorkspaceIds). */
|
|
57
|
-
export function newPaneId(): string {
|
|
58
|
-
return `pane${nextPaneId++}`;
|
|
59
|
-
}
|
|
60
|
-
export function resetWorkspaceIds(): void {
|
|
61
|
-
nextPaneId = 1;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function paneNode(surface: SurfaceKind, nodeId: string | null = null): PaneNode {
|
|
65
|
-
return { type: "pane", id: newPaneId(), surface, nodeId };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function splitNode(direction: SplitDir, children: WorkspaceNode[], sizes?: number[]): SplitNode {
|
|
69
|
-
const s = sizes ?? children.map(() => 1);
|
|
70
|
-
return { type: "split", direction, children, sizes: s.length === children.length ? s : children.map(() => 1) };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// Navigation / collection
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
/** All panes in layout (render) order — the focus-cycle order. */
|
|
78
|
-
export function collectPanes(node: WorkspaceNode): PaneNode[] {
|
|
79
|
-
if (node.type === "pane") return [node];
|
|
80
|
-
const out: PaneNode[] = [];
|
|
81
|
-
for (const c of node.children) out.push(...collectPanes(c));
|
|
82
|
-
return out;
|
|
18
|
+
function pane(surface: SurfaceKind, nodeId: string | null = null): PaneNode {
|
|
19
|
+
return { id: surface, surface, nodeId };
|
|
83
20
|
}
|
|
84
21
|
|
|
85
22
|
export function workspacePanes(ws: Workspace): PaneNode[] {
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function findPane(node: WorkspaceNode, id: string): PaneNode | null {
|
|
90
|
-
if (node.type === "pane") return node.id === id ? node : null;
|
|
91
|
-
for (const c of node.children) {
|
|
92
|
-
const found = findPane(c, id);
|
|
93
|
-
if (found) return found;
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
23
|
+
return ws.panes;
|
|
96
24
|
}
|
|
97
25
|
|
|
98
|
-
export function
|
|
99
|
-
return
|
|
26
|
+
export function findPane(panes: PaneNode[], id: string): PaneNode | null {
|
|
27
|
+
return panes.find((item) => item.id === id) ?? null;
|
|
100
28
|
}
|
|
101
29
|
|
|
102
|
-
/** Cycle the active pane in layout order; `dir` is +1 (next) or -1 (prev). */
|
|
103
30
|
export function focusNext(ws: Workspace, dir: 1 | -1): Workspace {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
if (!next) return ws;
|
|
109
|
-
return { ...ws, activePaneId: next.id };
|
|
31
|
+
if (ws.panes.length <= 1) return ws;
|
|
32
|
+
const index = ws.panes.findIndex((item) => item.id === ws.activePaneId);
|
|
33
|
+
const next = ws.panes[(index + dir + ws.panes.length) % ws.panes.length];
|
|
34
|
+
return next ? { ...ws, activePaneId: next.id } : ws;
|
|
110
35
|
}
|
|
111
36
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
* becomes active. Returns the new workspace.
|
|
122
|
-
*/
|
|
123
|
-
export function split(ws: Workspace, id: string, dir: SplitRequest, surface: SurfaceKind = "explore"): Workspace {
|
|
124
|
-
const parent = findParent(ws.root, id);
|
|
125
|
-
const holder = parent?.children ?? [ws.root];
|
|
126
|
-
const idx = holder.findIndex((c) => c.type === "pane" && c.id === id);
|
|
127
|
-
if (idx < 0) return ws;
|
|
128
|
-
const target = holder[idx] as PaneNode;
|
|
129
|
-
|
|
130
|
-
const fresh = paneNode(surface);
|
|
131
|
-
const newSplit = splitNode(
|
|
132
|
-
dir === "vertical" ? "column" : "row",
|
|
133
|
-
[target, fresh],
|
|
134
|
-
[1, 1],
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
if (!parent) {
|
|
138
|
-
return { ...ws, root: newSplit, activePaneId: fresh.id };
|
|
139
|
-
}
|
|
140
|
-
const children = parent.children.slice();
|
|
141
|
-
children[idx] = newSplit;
|
|
142
|
-
const nextRoot = replaceChild(ws.root, parent, children);
|
|
143
|
-
return { ...ws, root: nextRoot, activePaneId: fresh.id };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Swap two panes' surface + bound node within a split (the `move` command). */
|
|
147
|
-
export function movePane(ws: Workspace, id: string, dir: Axis): Workspace {
|
|
148
|
-
const parent = findParent(ws.root, id);
|
|
149
|
-
if (!parent || parent.direction !== dir) return ws;
|
|
150
|
-
const idx = parent.children.findIndex((c) => c.type === "pane" && c.id === id);
|
|
151
|
-
if (idx < 0) return ws;
|
|
152
|
-
// swap with the adjacent pane (right sibling if present, else left)
|
|
153
|
-
const neighborIdx = parent.children[idx + 1] ? idx + 1 : idx - 1;
|
|
154
|
-
const neighbor = neighborIdx !== undefined ? parent.children[neighborIdx] : undefined;
|
|
155
|
-
if (neighbor === undefined || neighbor.type !== "pane") return ws;
|
|
156
|
-
const children = parent.children.slice();
|
|
157
|
-
const a = children[idx] as PaneNode;
|
|
158
|
-
const b = neighbor;
|
|
159
|
-
children[idx] = { ...a, surface: b.surface, nodeId: b.nodeId };
|
|
160
|
-
children[neighborIdx] = { ...b, surface: a.surface, nodeId: a.nodeId };
|
|
161
|
-
return { ...ws, root: replaceChild(ws.root, parent, children) };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Close the active pane. If it was the last pane, keep one empty pane (don't
|
|
166
|
-
* quit). The focus moves to the pane that replaced it (nearest neighbor).
|
|
167
|
-
*/
|
|
168
|
-
export function close(ws: Workspace, id: string): Workspace {
|
|
169
|
-
const parent = findParent(ws.root, id);
|
|
170
|
-
if (!parent) {
|
|
171
|
-
// single-pane root or id not present: keep one empty pane (don't quit)
|
|
172
|
-
if (ws.root.type === "pane") {
|
|
173
|
-
return {
|
|
174
|
-
...ws,
|
|
175
|
-
root: paneNode("explore"),
|
|
176
|
-
activePaneId: newPaneId(),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
return ws; // id not found
|
|
180
|
-
}
|
|
181
|
-
const idx = parent.children.findIndex((c) => c.type === "pane" && c.id === id);
|
|
182
|
-
if (idx < 0) return ws;
|
|
183
|
-
const children = parent.children.slice();
|
|
184
|
-
children.splice(idx, 1);
|
|
185
|
-
const nextRoot = collapseEmptySplits(replaceChild(ws.root, parent, children));
|
|
186
|
-
const panes = collectPanes(nextRoot);
|
|
187
|
-
const nextActive = panes[Math.min(idx, panes.length - 1)]?.id ?? newPaneId();
|
|
188
|
-
return { ...ws, root: nextRoot, activePaneId: nextActive };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Remove a split node that has a single child, unwrapping it (fixes close). */
|
|
192
|
-
export function collapseEmptySplits(node: WorkspaceNode): WorkspaceNode {
|
|
193
|
-
if (node.type === "pane") return node;
|
|
194
|
-
const children = node.children.map(collapseEmptySplits);
|
|
195
|
-
if (children.length === 1) return children[0]!;
|
|
196
|
-
return { ...node, children };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Set a pane's surface in place (the e/d/f/h surface swap). */
|
|
200
|
-
export function setPaneSurface(ws: Workspace, id: string, surface: SurfaceKind): Workspace {
|
|
201
|
-
return { ...ws, root: setPaneNodeSurface(ws.root, id, surface) };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function setPaneNodeSurface(node: WorkspaceNode, id: string, surface: SurfaceKind): WorkspaceNode {
|
|
205
|
-
if (node.type === "pane") return node.id === id ? { ...node, surface } : node;
|
|
206
|
-
return { ...node, children: node.children.map((c) => setPaneNodeSurface(c, id, surface)) };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ---------------------------------------------------------------------------
|
|
210
|
-
// Resize
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Resize the active pane along `axis` by `delta` weight units, adjusting it
|
|
215
|
-
* against its sibling within the nearest split on that axis. Weights are
|
|
216
|
-
* clamped to [1, 100]. Returns the same workspace when there is no sibling on
|
|
217
|
-
* the axis.
|
|
218
|
-
*/
|
|
219
|
-
export function resize(ws: Workspace, id: string, axis: Axis, delta: number): Workspace {
|
|
220
|
-
const parent = findParent(ws.root, id);
|
|
221
|
-
if (!parent || parent.direction !== axis) return ws;
|
|
222
|
-
const idx = parent.children.findIndex((c) => c.type === "pane" && c.id === id);
|
|
223
|
-
if (idx < 0) return ws;
|
|
224
|
-
// neighbor to adjust: shrink/grow against the next sibling if present, else prev
|
|
225
|
-
const neighborIdx: number | undefined = parent.children[idx + 1] ? idx + 1 : parent.children[idx - 1] ? idx - 1 : undefined;
|
|
226
|
-
if (neighborIdx === undefined) return ws;
|
|
227
|
-
const sizes = parent.sizes.slice();
|
|
228
|
-
const cur = sizes[idx] ?? 1;
|
|
229
|
-
const nbr = sizes[neighborIdx] ?? 1;
|
|
230
|
-
const d = Math.sign(delta) * Math.min(3, Math.abs(delta));
|
|
231
|
-
const nextCur = Math.max(1, Math.min(100, cur + d));
|
|
232
|
-
const nextNbr = Math.max(1, Math.min(100, nbr - d));
|
|
233
|
-
sizes[idx] = nextCur;
|
|
234
|
-
sizes[neighborIdx] = nextNbr;
|
|
235
|
-
const next: SplitNode = { ...parent, sizes };
|
|
236
|
-
return { ...ws, root: replaceNode(ws.root, parent, next) };
|
|
37
|
+
/** The only layout: four equal-width read-only surfaces. */
|
|
38
|
+
export function defaultWorkspace(model: GraphModel): Workspace {
|
|
39
|
+
const panes = [
|
|
40
|
+
pane("explore", graphRoots(model)[0] ?? null),
|
|
41
|
+
pane("detail"),
|
|
42
|
+
pane("focus"),
|
|
43
|
+
pane("health"),
|
|
44
|
+
];
|
|
45
|
+
return { name: "Explore", panes, activePaneId: panes[0]!.id };
|
|
237
46
|
}
|
|
238
47
|
|
|
239
|
-
|
|
240
|
-
// Default workspaces (ship 3, decision 6: Explore is the first-run default)
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
|
|
243
|
-
/** The default first-run workspace: Explore 40/60. */
|
|
244
|
-
export function defaultWorkspace(model: GraphModel, name = "Explore"): Workspace {
|
|
245
|
-
const roots = graphRoots(model);
|
|
246
|
-
const explore = paneNode("explore", roots[0] ?? null);
|
|
247
|
-
const detail = paneNode("detail", null);
|
|
248
|
-
const root = splitNode("row", [explore, detail], [40, 60]);
|
|
249
|
-
return { name, root, activePaneId: explore.id };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Triple: Explore 30 / Detail 40 / Focus 30 — walk the graph while reading. */
|
|
253
|
-
export function tripleWorkspace(name = "Triple"): Workspace {
|
|
254
|
-
const explore = paneNode("explore");
|
|
255
|
-
const detail = paneNode("detail");
|
|
256
|
-
const focus = paneNode("focus");
|
|
257
|
-
const root = splitNode("row", [explore, detail, focus], [30, 40, 30]);
|
|
258
|
-
return { name, root, activePaneId: explore.id };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Wide: dashboard on top, reading below. */
|
|
262
|
-
export function wideWorkspace(name = "Wide"): Workspace {
|
|
263
|
-
const explore = paneNode("explore");
|
|
264
|
-
const health = paneNode("health");
|
|
265
|
-
const detail = paneNode("detail");
|
|
266
|
-
const top = splitNode("row", [explore, health], [50, 50]);
|
|
267
|
-
const root = splitNode("column", [top, detail], [40, 60]);
|
|
268
|
-
return { name, root, activePaneId: explore.id };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** All three named defaults. */
|
|
272
|
-
export function defaultWorkspaces(model: GraphModel): Workspace[] {
|
|
273
|
-
return [defaultWorkspace(model), tripleWorkspace(), wideWorkspace()];
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ---------------------------------------------------------------------------
|
|
277
|
-
// Responsive collapse (design §9.2)
|
|
278
|
-
// ---------------------------------------------------------------------------
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Collapse the workspace to fit a terminal width:
|
|
282
|
-
* - ≥ 110: unchanged.
|
|
283
|
-
* - 80–109: at most one column split — collapse any column split nested inside
|
|
284
|
-
* another split down to its first child pane.
|
|
285
|
-
* - < 80: single visible pane (the active one); the component shows the others
|
|
286
|
-
* as a one-line tab bar (decision 5).
|
|
287
|
-
*/
|
|
48
|
+
/** Hide inactive panes in a narrow terminal. */
|
|
288
49
|
export function collapseForWidth(ws: Workspace, width: number): Workspace {
|
|
289
|
-
if (width >=
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const single = active ? { ...active, nodeId: active.nodeId } : paneNode("explore");
|
|
293
|
-
return { ...ws, root: single, activePaneId: single.id };
|
|
294
|
-
}
|
|
295
|
-
return { ...ws, root: collapseNestedColumns(ws.root) };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Flatten a column split nested directly under another column split to its first pane. */
|
|
299
|
-
export function collapseNestedColumns(node: WorkspaceNode, insideColumn = false): WorkspaceNode {
|
|
300
|
-
if (node.type === "pane") return node;
|
|
301
|
-
const wasColumn = node.direction === "column";
|
|
302
|
-
const children = node.children.map((c) => collapseNestedColumns(c, wasColumn));
|
|
303
|
-
if (node.direction === "column" && insideColumn && children.length > 1) {
|
|
304
|
-
return children[0] ?? node;
|
|
305
|
-
}
|
|
306
|
-
return { ...node, children };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// ---------------------------------------------------------------------------
|
|
310
|
-
// Serialize / deserialize (persistence shape; the component adds the vault IO)
|
|
311
|
-
// ---------------------------------------------------------------------------
|
|
312
|
-
|
|
313
|
-
export interface SerializedWorkspace {
|
|
314
|
-
name: string;
|
|
315
|
-
activePaneId: string;
|
|
316
|
-
root: SerializedNode;
|
|
317
|
-
}
|
|
318
|
-
export type SerializedNode = { t: "pane"; id: string; s: SurfaceKind; n: string | null } | { t: "split"; d: SplitDir; sizes: number[]; c: SerializedNode[] };
|
|
319
|
-
|
|
320
|
-
export function serialize(ws: Workspace): SerializedWorkspace {
|
|
321
|
-
return { name: ws.name, activePaneId: ws.activePaneId, root: serializeNode(ws.root) };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function serializeNode(node: WorkspaceNode): SerializedNode {
|
|
325
|
-
if (node.type === "pane") return { t: "pane", id: node.id, s: node.surface, n: node.nodeId };
|
|
326
|
-
return { t: "split", d: node.direction, sizes: node.sizes, c: node.children.map(serializeNode) };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/** Deserialize a serialized workspace; degrades to a fresh Explore on bad shape. */
|
|
330
|
-
export function deserialize(json: SerializedWorkspace | null, fallback: Workspace): Workspace {
|
|
331
|
-
if (!json || typeof json.name !== "string") return fallback;
|
|
332
|
-
const root = deserializeNode(json.root);
|
|
333
|
-
if (!root) return fallback;
|
|
334
|
-
const active = findPane(root, json.activePaneId);
|
|
335
|
-
return {
|
|
336
|
-
name: json.name,
|
|
337
|
-
root,
|
|
338
|
-
activePaneId: active ? active.id : workspacePanes({ name: json.name, root, activePaneId: json.activePaneId })[0]?.id ?? newPaneId(),
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function deserializeNode(node: SerializedNode | undefined): WorkspaceNode | null {
|
|
343
|
-
if (!node) return null;
|
|
344
|
-
if (node.t === "pane") return { type: "pane", id: node.id || newPaneId(), surface: node.s, nodeId: node.n ?? null };
|
|
345
|
-
if (node.t === "split") {
|
|
346
|
-
const children = (node.c ?? []).map(deserializeNode).filter((c): c is WorkspaceNode => c !== null);
|
|
347
|
-
if (children.length === 0) return null;
|
|
348
|
-
return { type: "split", direction: node.d, sizes: node.sizes ?? children.map(() => 1), children };
|
|
349
|
-
}
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ---------------------------------------------------------------------------
|
|
354
|
-
// Helpers
|
|
355
|
-
// ---------------------------------------------------------------------------
|
|
356
|
-
|
|
357
|
-
/** Find the split node directly containing pane `id`, or null if root. */
|
|
358
|
-
function findParent(node: WorkspaceNode, id: string): SplitNode | null {
|
|
359
|
-
if (node.type === "pane") return null;
|
|
360
|
-
for (const c of node.children) {
|
|
361
|
-
if (c.type === "pane" && c.id === id) return node;
|
|
362
|
-
}
|
|
363
|
-
for (const c of node.children) {
|
|
364
|
-
const found = c.type === "split" ? findParent(c, id) : null;
|
|
365
|
-
if (found) return found;
|
|
366
|
-
}
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** Replace `parent` (by reference identity within a node tree) with a new split. */
|
|
371
|
-
function replaceChild(root: WorkspaceNode, parent: SplitNode, children: WorkspaceNode[]): WorkspaceNode {
|
|
372
|
-
const next: SplitNode = { ...parent, children };
|
|
373
|
-
return replaceNode(root, parent, next);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** Swap one node reference for another within a tree (structural copy). */
|
|
377
|
-
function replaceNode(root: WorkspaceNode, target: SplitNode, replacement: SplitNode): WorkspaceNode {
|
|
378
|
-
if (root === target) return replacement;
|
|
379
|
-
if (root.type === "pane") return root;
|
|
380
|
-
return { ...root, children: root.children.map((c) => replaceNode(c, target, replacement)) };
|
|
50
|
+
if (width >= 80) return ws;
|
|
51
|
+
const active = findPane(ws.panes, ws.activePaneId);
|
|
52
|
+
return active ? { ...ws, panes: [active] } : ws;
|
|
381
53
|
}
|