pinokiod 3.86.0 → 3.87.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.
Files changed (67) hide show
  1. package/Dockerfile +61 -0
  2. package/docker-entrypoint.sh +75 -0
  3. package/kernel/api/hf/index.js +1 -1
  4. package/kernel/api/index.js +1 -1
  5. package/kernel/api/shell/index.js +6 -0
  6. package/kernel/api/terminal/index.js +166 -0
  7. package/kernel/bin/conda.js +3 -2
  8. package/kernel/bin/index.js +53 -2
  9. package/kernel/bin/setup.js +32 -0
  10. package/kernel/bin/vs.js +11 -2
  11. package/kernel/index.js +42 -2
  12. package/kernel/info.js +36 -0
  13. package/kernel/peer.js +42 -15
  14. package/kernel/router/index.js +23 -15
  15. package/kernel/router/localhost_static_router.js +0 -3
  16. package/kernel/router/pinokio_domain_router.js +333 -0
  17. package/kernel/shells.js +21 -1
  18. package/kernel/util.js +2 -2
  19. package/package.json +2 -1
  20. package/script/install-mode.js +33 -0
  21. package/script/pinokio.json +7 -0
  22. package/server/index.js +513 -173
  23. package/server/public/Socket.js +48 -0
  24. package/server/public/common.js +1441 -276
  25. package/server/public/fseditor.js +71 -12
  26. package/server/public/install.js +1 -1
  27. package/server/public/layout.js +740 -0
  28. package/server/public/modalinput.js +0 -1
  29. package/server/public/style.css +97 -105
  30. package/server/public/tab-idle-notifier.js +629 -0
  31. package/server/public/terminal_input_tracker.js +63 -0
  32. package/server/public/urldropdown.css +319 -53
  33. package/server/public/urldropdown.js +615 -159
  34. package/server/public/window_storage.js +97 -28
  35. package/server/socket.js +40 -9
  36. package/server/views/500.ejs +2 -2
  37. package/server/views/app.ejs +3136 -1367
  38. package/server/views/bookmarklet.ejs +1 -1
  39. package/server/views/bootstrap.ejs +1 -1
  40. package/server/views/columns.ejs +2 -13
  41. package/server/views/connect.ejs +3 -4
  42. package/server/views/container.ejs +1 -2
  43. package/server/views/d.ejs +223 -53
  44. package/server/views/editor.ejs +1 -1
  45. package/server/views/file_explorer.ejs +1 -1
  46. package/server/views/index.ejs +12 -11
  47. package/server/views/index2.ejs +4 -4
  48. package/server/views/init/index.ejs +4 -5
  49. package/server/views/install.ejs +1 -1
  50. package/server/views/layout.ejs +105 -0
  51. package/server/views/net.ejs +39 -7
  52. package/server/views/network.ejs +20 -6
  53. package/server/views/network2.ejs +1 -1
  54. package/server/views/old_network.ejs +2 -2
  55. package/server/views/partials/dynamic.ejs +3 -5
  56. package/server/views/partials/menu.ejs +3 -5
  57. package/server/views/partials/running.ejs +1 -1
  58. package/server/views/pro.ejs +1 -1
  59. package/server/views/prototype/index.ejs +1 -1
  60. package/server/views/review.ejs +11 -23
  61. package/server/views/rows.ejs +2 -13
  62. package/server/views/screenshots.ejs +293 -138
  63. package/server/views/settings.ejs +3 -4
  64. package/server/views/setup.ejs +1 -2
  65. package/server/views/shell.ejs +277 -26
  66. package/server/views/terminal.ejs +322 -49
  67. package/server/views/tools.ejs +448 -4
@@ -0,0 +1,740 @@
1
+ (() => {
2
+ const configEl = document.getElementById('pinokio-layout-config');
3
+ if (!configEl) {
4
+ console.warn('[PinokioLayout] Missing configuration element.');
5
+ return;
6
+ }
7
+
8
+ let parsedConfig = {};
9
+ try {
10
+ parsedConfig = JSON.parse(configEl.textContent || '{}');
11
+ } catch (error) {
12
+ console.error('[PinokioLayout] Failed to parse configuration JSON.', error);
13
+ parsedConfig = {};
14
+ }
15
+ configEl.remove();
16
+
17
+ const rootEl = document.getElementById('layout-root');
18
+ if (!rootEl) {
19
+ console.warn('[PinokioLayout] Missing layout root container.');
20
+ return;
21
+ }
22
+
23
+ const HOST_ORIGIN = window.location.origin;
24
+ const STORAGE_PREFIX = 'pinokio:layout:';
25
+ const MIN_PANEL_SIZE = 120;
26
+ const GUTTER_SIZE = 6;
27
+
28
+ const state = {
29
+ sessionId: typeof parsedConfig.sessionId === 'string' && parsedConfig.sessionId.trim() ? parsedConfig.sessionId.trim() : null,
30
+ root: null,
31
+ defaultPath: null,
32
+ initialPath: null,
33
+ };
34
+
35
+ const nodeById = new Map();
36
+ const parentById = new Map();
37
+ const leafElements = new Map();
38
+ const gutterElements = new Map();
39
+ const layoutCache = new Map();
40
+
41
+ function normalizeSrc(raw) {
42
+ if (!raw || typeof raw !== 'string') {
43
+ return state.defaultPath || '/home';
44
+ }
45
+ const trimmed = raw.trim();
46
+ if (!trimmed) {
47
+ return state.defaultPath || '/home';
48
+ }
49
+ try {
50
+ const url = new URL(trimmed, HOST_ORIGIN);
51
+ if (url.origin === HOST_ORIGIN) {
52
+ if (url.pathname === '/') {
53
+ url.pathname = '/home';
54
+ }
55
+ if (url.searchParams.has('embed')) {
56
+ url.searchParams.delete('embed');
57
+ }
58
+ return url.pathname + url.search + url.hash;
59
+ }
60
+ return url.href;
61
+ } catch (_) {
62
+ if (trimmed.startsWith('/')) {
63
+ try {
64
+ const url = new URL(trimmed, HOST_ORIGIN);
65
+ if (url.pathname === '/') {
66
+ url.pathname = '/home';
67
+ }
68
+ if (url.searchParams.has('embed')) {
69
+ url.searchParams.delete('embed');
70
+ }
71
+ return url.pathname + url.search + url.hash;
72
+ } catch (err) {
73
+ return trimmed;
74
+ }
75
+ }
76
+ return trimmed;
77
+ }
78
+ }
79
+
80
+ state.defaultPath = normalizeSrc(parsedConfig.defaultPath || '/home');
81
+ state.initialPath = normalizeSrc(parsedConfig.initialPath || state.defaultPath);
82
+
83
+ function storageKey(sessionId) {
84
+ return `${STORAGE_PREFIX}${sessionId}`;
85
+ }
86
+
87
+ function generateId() {
88
+ return 'f_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
89
+ }
90
+
91
+ function sanitizeDirection(direction) {
92
+ return direction === 'rows' ? 'rows' : 'columns';
93
+ }
94
+
95
+ function clampRatio(value) {
96
+ if (!Number.isFinite(value)) {
97
+ return 0.5;
98
+ }
99
+ return Math.min(0.95, Math.max(0.05, value));
100
+ }
101
+
102
+ function createLeaf(src, options = {}) {
103
+ return {
104
+ id: typeof options.id === 'string' ? options.id : generateId(),
105
+ type: 'leaf',
106
+ src: normalizeSrc(src || state.defaultPath),
107
+ };
108
+ }
109
+
110
+ function createSplit(direction, first, second, ratio = 0.5, options = {}) {
111
+ return {
112
+ id: typeof options.id === 'string' ? options.id : generateId(),
113
+ type: 'split',
114
+ direction: sanitizeDirection(direction),
115
+ ratio: clampRatio(ratio),
116
+ children: [first, second],
117
+ };
118
+ }
119
+
120
+ function hydrateNode(raw) {
121
+ if (!raw || typeof raw !== 'object') {
122
+ return null;
123
+ }
124
+ if (raw.type === 'split') {
125
+ if (!Array.isArray(raw.children) || raw.children.length !== 2) {
126
+ return null;
127
+ }
128
+ const first = hydrateNode(raw.children[0]);
129
+ const second = hydrateNode(raw.children[1]);
130
+ if (!first || !second) {
131
+ return null;
132
+ }
133
+ return createSplit(raw.direction, first, second, typeof raw.ratio === 'number' ? raw.ratio : 0.5, { id: typeof raw.id === 'string' ? raw.id : undefined });
134
+ }
135
+ return createLeaf(raw.src, { id: typeof raw.id === 'string' ? raw.id : undefined });
136
+ }
137
+
138
+ function serializeNode(node) {
139
+ if (!node) {
140
+ return null;
141
+ }
142
+ if (node.type === 'split') {
143
+ return {
144
+ id: node.id,
145
+ type: 'split',
146
+ direction: node.direction,
147
+ ratio: node.ratio,
148
+ children: node.children.map(serializeNode),
149
+ };
150
+ }
151
+ return {
152
+ id: node.id,
153
+ type: 'leaf',
154
+ src: node.src,
155
+ };
156
+ }
157
+
158
+ function rebuildNodeIndex() {
159
+ nodeById.clear();
160
+ parentById.clear();
161
+ (function walk(node, parent, index) {
162
+ if (!node) {
163
+ return;
164
+ }
165
+ nodeById.set(node.id, node);
166
+ if (parent) {
167
+ parentById.set(node.id, { parentId: parent.id, index });
168
+ }
169
+ if (node.type === 'split') {
170
+ node.children.forEach((child, idx) => walk(child, node, idx));
171
+ }
172
+ })(state.root, null, 0);
173
+ }
174
+
175
+ function ensureLeafElement(node) {
176
+ if (!node || node.type !== 'leaf') {
177
+ return null;
178
+ }
179
+ let entry = leafElements.get(node.id);
180
+ if (entry) {
181
+ return entry;
182
+ }
183
+ const container = document.createElement('div');
184
+ container.className = 'layout-leaf';
185
+ container.dataset.nodeId = node.id;
186
+
187
+ const iframe = document.createElement('iframe');
188
+ iframe.dataset.nodeId = node.id;
189
+ iframe.name = node.id;
190
+ iframe.src = node.src || state.defaultPath;
191
+
192
+ container.appendChild(iframe);
193
+ rootEl.appendChild(container);
194
+
195
+ const updateNodeLocation = () => {
196
+ if (!nodeById.has(node.id)) {
197
+ return;
198
+ }
199
+ let resolved;
200
+ try {
201
+ if (iframe.contentWindow && iframe.contentWindow.location) {
202
+ resolved = iframe.contentWindow.location.href;
203
+ }
204
+ } catch (_) {
205
+ resolved = iframe.dataset.src || iframe.src;
206
+ }
207
+ if (resolved) {
208
+ node.src = normalizeSrc(resolved);
209
+ iframe.dataset.src = node.src;
210
+ saveStateToStorage();
211
+ }
212
+ };
213
+
214
+ iframe.addEventListener('load', updateNodeLocation);
215
+
216
+ entry = { container, iframe, updateNodeLocation };
217
+ leafElements.set(node.id, entry);
218
+ return entry;
219
+ }
220
+
221
+ function removeLeafElement(nodeId) {
222
+ const entry = leafElements.get(nodeId);
223
+ if (!entry) {
224
+ return;
225
+ }
226
+ if (entry.container.parentNode === rootEl) {
227
+ rootEl.removeChild(entry.container);
228
+ } else {
229
+ entry.container.remove();
230
+ }
231
+ leafElements.delete(nodeId);
232
+ }
233
+
234
+ function ensureGutterElement(node) {
235
+ let gutter = gutterElements.get(node.id);
236
+ if (gutter) {
237
+ return gutter;
238
+ }
239
+ gutter = document.createElement('div');
240
+ gutter.className = `layout-gutter layout-gutter-${node.direction}`;
241
+ gutter.dataset.nodeId = node.id;
242
+ gutter.tabIndex = 0;
243
+ gutter.setAttribute('role', 'separator');
244
+ gutter.setAttribute('aria-orientation', node.direction === 'rows' ? 'horizontal' : 'vertical');
245
+ rootEl.appendChild(gutter);
246
+ gutterElements.set(node.id, gutter);
247
+ return gutter;
248
+ }
249
+
250
+ function removeGutterElement(splitId) {
251
+ const gutter = gutterElements.get(splitId);
252
+ if (!gutter) {
253
+ return;
254
+ }
255
+ gutter.remove();
256
+ gutterElements.delete(splitId);
257
+ }
258
+
259
+ function loadStateFromStorage() {
260
+ if (!state.sessionId) {
261
+ return false;
262
+ }
263
+ try {
264
+ const raw = window.localStorage.getItem(storageKey(state.sessionId));
265
+ if (!raw) {
266
+ return false;
267
+ }
268
+ const parsed = JSON.parse(raw);
269
+ const hydrated = hydrateNode(parsed);
270
+ if (!hydrated) {
271
+ return false;
272
+ }
273
+ state.root = hydrated;
274
+ return true;
275
+ } catch (error) {
276
+ console.warn('[PinokioLayout] Failed to load layout for session', state.sessionId, error);
277
+ return false;
278
+ }
279
+ }
280
+
281
+ function saveStateToStorage() {
282
+ if (!state.sessionId || !state.root) {
283
+ return;
284
+ }
285
+ try {
286
+ const serialized = serializeNode(state.root);
287
+ const payload = JSON.stringify(serialized);
288
+ window.localStorage.setItem(storageKey(state.sessionId), payload);
289
+ } catch (error) {
290
+ console.warn('[PinokioLayout] Failed to persist layout state', error);
291
+ }
292
+ }
293
+
294
+ function ensureSession() {
295
+ if (state.sessionId) {
296
+ return state.sessionId;
297
+ }
298
+ state.sessionId = generateId();
299
+ const url = new URL(window.location.href);
300
+ url.searchParams.set('session', state.sessionId);
301
+ window.history.replaceState(window.history.state, '', url.toString());
302
+ return state.sessionId;
303
+ }
304
+
305
+ function cleanupSessionIfSingleLeaf() {
306
+ if (!state.root || state.root.type !== 'leaf') {
307
+ return;
308
+ }
309
+ if (!state.sessionId) {
310
+ ensureSession();
311
+ }
312
+ saveStateToStorage();
313
+ }
314
+
315
+ function getNodeInfo(nodeId) {
316
+ const node = nodeById.get(nodeId);
317
+ if (!node) {
318
+ return null;
319
+ }
320
+ const parentMeta = parentById.get(nodeId) || null;
321
+ const parent = parentMeta ? nodeById.get(parentMeta.parentId) || null : null;
322
+ const index = parentMeta ? parentMeta.index : null;
323
+ return { node, parent, index };
324
+ }
325
+
326
+ function captureLeafSnapshot(nodeId) {
327
+ const entry = leafElements.get(nodeId);
328
+ if (!entry) {
329
+ return;
330
+ }
331
+ const { iframe } = entry;
332
+ try {
333
+ if (iframe.contentWindow && iframe.contentWindow.location) {
334
+ iframe.dataset.src = iframe.contentWindow.location.href;
335
+ }
336
+ } catch (_) {
337
+ iframe.dataset.src = iframe.src;
338
+ }
339
+ if (iframe.dataset.src) {
340
+ const node = nodeById.get(nodeId);
341
+ if (node && node.type === 'leaf') {
342
+ node.src = normalizeSrc(iframe.dataset.src);
343
+ }
344
+ }
345
+ }
346
+
347
+ function layoutLeaves(node, bounds, activeLeafIds, activeSplitIds) {
348
+ if (!node) {
349
+ return;
350
+ }
351
+ layoutCache.set(node.id, bounds);
352
+ if (node.type === 'leaf') {
353
+ activeLeafIds.add(node.id);
354
+ const entry = ensureLeafElement(node);
355
+ if (entry) {
356
+ entry.container.style.left = `${bounds.x}px`;
357
+ entry.container.style.top = `${bounds.y}px`;
358
+ entry.container.style.width = `${Math.max(0, bounds.width)}px`;
359
+ entry.container.style.height = `${Math.max(0, bounds.height)}px`;
360
+ }
361
+ return;
362
+ }
363
+
364
+ activeSplitIds.add(node.id);
365
+
366
+ const direction = node.direction;
367
+ const ratio = clampRatio(node.ratio);
368
+ let firstBounds;
369
+ let secondBounds;
370
+
371
+ if (direction === 'rows') {
372
+ const total = bounds.height;
373
+ const gutter = Math.min(GUTTER_SIZE, total);
374
+ const firstSize = Math.max(MIN_PANEL_SIZE, Math.min(total - MIN_PANEL_SIZE, total * ratio));
375
+ const secondSize = Math.max(0, total - gutter - firstSize);
376
+ firstBounds = { x: bounds.x, y: bounds.y, width: bounds.width, height: firstSize };
377
+ secondBounds = { x: bounds.x, y: bounds.y + firstSize + gutter, width: bounds.width, height: secondSize };
378
+ node.ratio = clampRatio(firstSize / total);
379
+ } else {
380
+ const total = bounds.width;
381
+ const gutter = Math.min(GUTTER_SIZE, total);
382
+ const firstSize = Math.max(MIN_PANEL_SIZE, Math.min(total - MIN_PANEL_SIZE, total * ratio));
383
+ const secondSize = Math.max(0, total - gutter - firstSize);
384
+ firstBounds = { x: bounds.x, y: bounds.y, width: firstSize, height: bounds.height };
385
+ secondBounds = { x: bounds.x + firstSize + gutter, y: bounds.y, width: secondSize, height: bounds.height };
386
+ node.ratio = clampRatio(firstSize / total);
387
+ }
388
+
389
+ const gutterEl = ensureGutterElement(node);
390
+ if (direction === 'rows') {
391
+ const gutterHeight = Math.min(GUTTER_SIZE, bounds.height);
392
+ gutterEl.style.left = `${bounds.x}px`;
393
+ gutterEl.style.width = `${bounds.width}px`;
394
+ gutterEl.style.top = `${firstBounds.y + firstBounds.height}px`;
395
+ gutterEl.style.height = `${gutterHeight}px`;
396
+ } else {
397
+ const gutterWidth = Math.min(GUTTER_SIZE, bounds.width);
398
+ gutterEl.style.left = `${firstBounds.x + firstBounds.width}px`;
399
+ gutterEl.style.width = `${gutterWidth}px`;
400
+ gutterEl.style.top = `${bounds.y}px`;
401
+ gutterEl.style.height = `${bounds.height}px`;
402
+ }
403
+
404
+ layoutLeaves(node.children[0], firstBounds, activeLeafIds, activeSplitIds);
405
+ layoutLeaves(node.children[1], secondBounds, activeLeafIds, activeSplitIds);
406
+ }
407
+
408
+ function applyLayout() {
409
+ if (!state.root) {
410
+ return;
411
+ }
412
+ const rect = rootEl.getBoundingClientRect();
413
+ const rootBounds = { x: 0, y: 0, width: rect.width, height: rect.height };
414
+ const activeLeafIds = new Set();
415
+ const activeSplitIds = new Set();
416
+
417
+ layoutCache.clear();
418
+ layoutLeaves(state.root, rootBounds, activeLeafIds, activeSplitIds);
419
+
420
+ leafElements.forEach((entry, id) => {
421
+ if (!activeLeafIds.has(id)) {
422
+ removeLeafElement(id);
423
+ }
424
+ });
425
+ gutterElements.forEach((_, id) => {
426
+ if (!activeSplitIds.has(id)) {
427
+ removeGutterElement(id);
428
+ }
429
+ });
430
+ }
431
+
432
+ function broadcastLayoutState(targetWindow = null, frameId = null) {
433
+ const closable = leafElements.size > 1;
434
+ const total = leafElements.size;
435
+ const payload = {
436
+ e: 'layout-state',
437
+ closable,
438
+ total,
439
+ };
440
+ if (frameId) {
441
+ payload.frameId = frameId;
442
+ }
443
+ const targetOrigin = HOST_ORIGIN;
444
+ const sendToWindow = (win) => {
445
+ if (!win) {
446
+ return;
447
+ }
448
+ try {
449
+ win.postMessage(payload, targetOrigin);
450
+ } catch (error) {
451
+ try {
452
+ win.postMessage(payload, '*');
453
+ } catch (_) {}
454
+ }
455
+ };
456
+ if (targetWindow) {
457
+ sendToWindow(targetWindow);
458
+ return;
459
+ }
460
+ leafElements.forEach((entry) => {
461
+ sendToWindow(entry.iframe?.contentWindow || null);
462
+ });
463
+ }
464
+
465
+ let activeResize = null;
466
+
467
+ function beginResize(splitId, pointerEvent) {
468
+ const splitNode = nodeById.get(splitId);
469
+ if (!splitNode || splitNode.type !== 'split') {
470
+ return;
471
+ }
472
+ const firstNode = splitNode.children[0];
473
+ const secondNode = splitNode.children[1];
474
+ const firstBounds = layoutCache.get(firstNode.id);
475
+ const secondBounds = layoutCache.get(secondNode.id);
476
+ const parentBounds = layoutCache.get(splitNode.id);
477
+ if (!firstBounds || !secondBounds || !parentBounds) {
478
+ return;
479
+ }
480
+
481
+ const direction = splitNode.direction;
482
+ const startCoord = direction === 'rows' ? pointerEvent.clientY : pointerEvent.clientX;
483
+ const firstSize = direction === 'rows' ? firstBounds.height : firstBounds.width;
484
+ const secondSize = direction === 'rows' ? secondBounds.height : secondBounds.width;
485
+ const gutterSize = Math.min(GUTTER_SIZE, direction === 'rows' ? parentBounds.height : parentBounds.width);
486
+ const total = firstSize + secondSize + gutterSize;
487
+
488
+ activeResize = {
489
+ splitId,
490
+ direction,
491
+ startCoord,
492
+ firstSize,
493
+ total,
494
+ };
495
+
496
+ document.body.classList.add('layout-resizing');
497
+ document.body.classList.toggle('layout-resize-rows', direction === 'rows');
498
+ document.body.classList.toggle('layout-resize-columns', direction === 'columns');
499
+ window.addEventListener('pointermove', onPointerMove);
500
+ window.addEventListener('pointerup', endResize);
501
+ window.addEventListener('pointercancel', endResize);
502
+ }
503
+
504
+ function onPointerMove(event) {
505
+ if (!activeResize) {
506
+ return;
507
+ }
508
+ const splitNode = nodeById.get(activeResize.splitId);
509
+ if (!splitNode || splitNode.type !== 'split') {
510
+ return;
511
+ }
512
+
513
+ const rootRect = rootEl.getBoundingClientRect();
514
+ const currentCoord = activeResize.direction === 'rows' ? event.clientY : event.clientX;
515
+ const delta = currentCoord - activeResize.startCoord;
516
+ const newPrimary = Math.max(MIN_PANEL_SIZE, Math.min(activeResize.total - MIN_PANEL_SIZE, activeResize.firstSize + delta));
517
+ const ratio = newPrimary / activeResize.total;
518
+ splitNode.ratio = clampRatio(ratio);
519
+ applyLayout();
520
+ saveStateToStorage();
521
+ }
522
+
523
+ function endResize() {
524
+ if (!activeResize) {
525
+ return;
526
+ }
527
+ activeResize = null;
528
+ document.body.classList.remove('layout-resizing', 'layout-resize-rows', 'layout-resize-columns');
529
+ window.removeEventListener('pointermove', onPointerMove);
530
+ window.removeEventListener('pointerup', endResize);
531
+ window.removeEventListener('pointercancel', endResize);
532
+ saveStateToStorage();
533
+ }
534
+
535
+ function attachGutterHandlers() {
536
+ gutterElements.forEach((gutter, splitId) => {
537
+ gutter.onpointerdown = null;
538
+ gutter.onpointerdown = (event) => {
539
+ if (event.button !== 0) {
540
+ return;
541
+ }
542
+ event.preventDefault();
543
+ beginResize(splitId, event);
544
+ };
545
+
546
+ gutter.onkeydown = null;
547
+ gutter.onkeydown = (event) => {
548
+ const splitNode = nodeById.get(splitId);
549
+ if (!splitNode || splitNode.type !== 'split') {
550
+ return;
551
+ }
552
+ const step = event.shiftKey ? 0.1 : 0.02;
553
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
554
+ const next = clampRatio(splitNode.ratio - step);
555
+ splitNode.ratio = next;
556
+ applyLayout();
557
+ attachGutterHandlers();
558
+ saveStateToStorage();
559
+ event.preventDefault();
560
+ } else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
561
+ const next = clampRatio(splitNode.ratio + step);
562
+ splitNode.ratio = next;
563
+ applyLayout();
564
+ attachGutterHandlers();
565
+ saveStateToStorage();
566
+ event.preventDefault();
567
+ }
568
+ };
569
+ });
570
+ }
571
+
572
+ function splitLeaf(frameId, direction, targetUrl) {
573
+ const info = getNodeInfo(frameId);
574
+ if (!info || info.node.type !== 'leaf') {
575
+ console.warn('[PinokioLayout] Unable to locate leaf to split', frameId);
576
+ return false;
577
+ }
578
+
579
+ captureLeafSnapshot(frameId);
580
+
581
+ const existingLeaf = info.node;
582
+ const newLeaf = createLeaf(targetUrl);
583
+ ensureLeafElement(newLeaf);
584
+
585
+ const splitNode = createSplit(direction, existingLeaf, newLeaf, 0.5);
586
+
587
+ if (!info.parent) {
588
+ state.root = splitNode;
589
+ } else {
590
+ info.parent.children[info.index] = splitNode;
591
+ }
592
+
593
+ ensureSession();
594
+ rebuildNodeIndex();
595
+ applyLayout();
596
+ attachGutterHandlers();
597
+ saveStateToStorage();
598
+ broadcastLayoutState();
599
+ return true;
600
+ }
601
+
602
+ function closeLeaf(frameId) {
603
+ const info = getNodeInfo(frameId);
604
+ if (!info || info.node.type !== 'leaf') {
605
+ return false;
606
+ }
607
+
608
+ captureLeafSnapshot(frameId);
609
+
610
+ const parentNode = info.parent;
611
+ if (!parentNode) {
612
+ const leaf = info.node;
613
+ leaf.src = state.defaultPath;
614
+ const entry = leafElements.get(leaf.id);
615
+ if (entry) {
616
+ entry.iframe.src = leaf.src;
617
+ }
618
+ cleanupSessionIfSingleLeaf();
619
+ applyLayout();
620
+ broadcastLayoutState();
621
+ return true;
622
+ }
623
+
624
+ const siblingIndex = info.index === 0 ? 1 : 0;
625
+ const siblingNode = parentNode.children[siblingIndex];
626
+ const grandMeta = parentById.get(parentNode.id) || null;
627
+ const grandParent = grandMeta ? nodeById.get(grandMeta.parentId) || null : null;
628
+
629
+ if (grandParent) {
630
+ grandParent.children[grandMeta.index] = siblingNode;
631
+ } else {
632
+ state.root = siblingNode;
633
+ }
634
+
635
+ removeLeafElement(info.node.id);
636
+ removeGutterElement(parentNode.id);
637
+
638
+ rebuildNodeIndex();
639
+ applyLayout();
640
+ attachGutterHandlers();
641
+ cleanupSessionIfSingleLeaf();
642
+ saveStateToStorage();
643
+ broadcastLayoutState();
644
+ return true;
645
+ }
646
+
647
+ function onMessage(event) {
648
+ if (!event || !event.data || typeof event.data !== 'object') {
649
+ return;
650
+ }
651
+ if (event.data.e === 'layout-state-request') {
652
+ let frameEntry = null;
653
+ let frameId = null;
654
+ for (const entry of leafElements.values()) {
655
+ if (entry.iframe && entry.iframe.contentWindow === event.source) {
656
+ frameEntry = entry;
657
+ frameId = entry.iframe.dataset?.nodeId || null;
658
+ break;
659
+ }
660
+ }
661
+ broadcastLayoutState(event.source, frameId);
662
+ return;
663
+ }
664
+ if (event.data.e === 'close') {
665
+ const frameId = (() => {
666
+ for (const [id, entry] of leafElements.entries()) {
667
+ if (entry.iframe.contentWindow === event.source) {
668
+ return id;
669
+ }
670
+ }
671
+ return null;
672
+ })();
673
+ if (frameId) {
674
+ closeLeaf(frameId);
675
+ }
676
+ }
677
+ }
678
+
679
+ function initLayout() {
680
+ const restored = loadStateFromStorage();
681
+ if (!restored) {
682
+ state.root = createLeaf(state.initialPath || state.defaultPath);
683
+ ensureSession();
684
+ saveStateToStorage();
685
+ }
686
+
687
+ rebuildNodeIndex();
688
+
689
+ nodeById.forEach((node) => {
690
+ if (node.type === 'leaf') {
691
+ const entry = ensureLeafElement(node);
692
+ if (entry && entry.iframe.src !== node.src) {
693
+ entry.iframe.src = node.src;
694
+ }
695
+ }
696
+ });
697
+
698
+ applyLayout();
699
+ attachGutterHandlers();
700
+ broadcastLayoutState();
701
+ }
702
+
703
+ function onResize() {
704
+ applyLayout();
705
+ attachGutterHandlers();
706
+ }
707
+
708
+ initLayout();
709
+ window.addEventListener('message', onMessage);
710
+ window.addEventListener('resize', onResize);
711
+
712
+ const api = {
713
+ split({ frameId, direction, targetUrl }) {
714
+ if (!frameId || !direction || !targetUrl) {
715
+ return false;
716
+ }
717
+ try {
718
+ return splitLeaf(frameId, direction, normalizeSrc(targetUrl));
719
+ } catch (error) {
720
+ console.error('[PinokioLayout] Split failed', error);
721
+ return false;
722
+ }
723
+ },
724
+ close(frameId) {
725
+ try {
726
+ return closeLeaf(frameId);
727
+ } catch (error) {
728
+ console.error('[PinokioLayout] Close failed', error);
729
+ return false;
730
+ }
731
+ },
732
+ ensureSession,
733
+ getSessionId() {
734
+ return state.sessionId;
735
+ },
736
+ save: saveStateToStorage,
737
+ };
738
+
739
+ window.PinokioLayout = api;
740
+ })();