clay-server 5.3.0-beta.4 → 5.3.0-beta.6

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 (144) hide show
  1. package/lib/ask-user-request-identity.js +18 -1
  2. package/lib/clay-product-control-tools.js +2 -2
  3. package/lib/clay-product-control.js +1 -1
  4. package/lib/clay-screen-controls.js +1 -0
  5. package/lib/clay-split-view-awareness.js +5 -2
  6. package/lib/clay-split-view-controls.js +14 -7
  7. package/lib/clay-split-view-input.js +5 -4
  8. package/lib/clay-split-view-render-proof.js +14 -4
  9. package/lib/clay-view-workspace-input.js +71 -0
  10. package/lib/project-delegated-follow-up.js +18 -9
  11. package/lib/project-file-history.js +11 -9
  12. package/lib/project-file-watch.js +26 -7
  13. package/lib/project-filesystem.js +5 -4
  14. package/lib/project-shared-browser.js +7 -3
  15. package/lib/project-split-view-document.js +16 -2
  16. package/lib/project-split-views.js +15 -294
  17. package/lib/project-user-message.js +4 -3
  18. package/lib/project-vendor-login.js +2 -2
  19. package/lib/project-view-input.js +11 -0
  20. package/lib/project-view-pair.js +26 -0
  21. package/lib/project-view-store.js +346 -0
  22. package/lib/project-view-terminal-binding.js +16 -0
  23. package/lib/project-view-workspace.js +126 -0
  24. package/lib/project.js +1 -0
  25. package/lib/public/app.js +2 -0
  26. package/lib/public/css/issues.css +3 -0
  27. package/lib/public/css/pane.css +5 -1
  28. package/lib/public/css/right-workbench.css +25 -3
  29. package/lib/public/css/scheduled-tasks.css +7 -0
  30. package/lib/public/css/shared-browser.css +21 -0
  31. package/lib/public/css/split-view-layout.css +37 -3
  32. package/lib/public/css/sticky-notes.css +3 -2
  33. package/lib/public/css/tool-pane.css +127 -0
  34. package/lib/public/index.html +3 -3
  35. package/lib/public/modules/app-header.js +24 -2
  36. package/lib/public/modules/app-messages.js +1 -1
  37. package/lib/public/modules/clay-file-links.js +2 -1
  38. package/lib/public/modules/clay-session-links.js +1 -1
  39. package/lib/public/modules/clay-split-view-screen.js +6 -3
  40. package/lib/public/modules/clay-tool-pane-screen.js +35 -0
  41. package/lib/public/modules/file-history-snapshot.js +12 -0
  42. package/lib/public/modules/file-pane-entrypoints.js +55 -0
  43. package/lib/public/modules/file-pane-history.js +78 -0
  44. package/lib/public/modules/file-pane-instances.js +140 -0
  45. package/lib/public/modules/file-pane-links.js +41 -0
  46. package/lib/public/modules/file-pane-render.js +48 -0
  47. package/lib/public/modules/file-pane-review.js +26 -0
  48. package/lib/public/modules/file-pane-toolbar.js +54 -0
  49. package/lib/public/modules/file-viewer-source.js +70 -0
  50. package/lib/public/modules/filebrowser.js +44 -83
  51. package/lib/public/modules/git-panel.js +5 -0
  52. package/lib/public/modules/issues.js +3 -4
  53. package/lib/public/modules/linear-panel.js +2 -2
  54. package/lib/public/modules/mate-knowledge-workbench.js +10 -6
  55. package/lib/public/modules/notifications.js +4 -1
  56. package/lib/public/modules/pane-activation.js +5 -0
  57. package/lib/public/modules/pane-binding.js +5 -1
  58. package/lib/public/modules/pane-browser-bridge.js +20 -6
  59. package/lib/public/modules/pane-browser-transport.js +110 -0
  60. package/lib/public/modules/pane-header-runtime.js +18 -0
  61. package/lib/public/modules/plantuml-preview.js +12 -12
  62. package/lib/public/modules/right-workbench.js +116 -67
  63. package/lib/public/modules/scheduled-tasks.js +7 -6
  64. package/lib/public/modules/shared-browser.js +75 -9
  65. package/lib/public/modules/split-pane-renderer.js +7 -8
  66. package/lib/public/modules/split-pane-runtime.js +23 -0
  67. package/lib/public/modules/split-view-drag-preview.js +90 -0
  68. package/lib/public/modules/split-view-layout-client.js +1 -1
  69. package/lib/public/modules/split-view-presentation-drag.js +36 -13
  70. package/lib/public/modules/split-view-presentation-drop.js +21 -18
  71. package/lib/public/modules/split-view-presentation-renderer.js +15 -10
  72. package/lib/public/modules/split-view-presentations.js +25 -33
  73. package/lib/public/modules/split-view.js +17 -16
  74. package/lib/public/modules/sticky-notes-browser.js +2 -2
  75. package/lib/public/modules/sticky-notes.js +8 -10
  76. package/lib/public/modules/terminal.js +45 -13
  77. package/lib/public/modules/theme.js +1 -1
  78. package/lib/public/modules/tool-pane-chrome.js +82 -0
  79. package/lib/public/modules/tool-pane-drag.js +134 -0
  80. package/lib/public/modules/tool-pane-layout.js +95 -0
  81. package/lib/public/modules/tool-pane-legacy.js +60 -0
  82. package/lib/public/modules/tool-pane-tree.js +51 -0
  83. package/lib/public/modules/view-legacy-coordinates.js +16 -0
  84. package/lib/public/modules/view-mutations.js +24 -0
  85. package/lib/public/modules/view-pair-presentation.js +74 -0
  86. package/lib/public/modules/view-pair-validation.js +12 -0
  87. package/lib/public/modules/view-presentation-state.js +37 -0
  88. package/lib/public/modules/view-workspace-controller.js +210 -0
  89. package/lib/public/modules/view-workspace-state.js +103 -0
  90. package/lib/public/modules/view-workspace-validation.js +47 -0
  91. package/lib/public/modules/wireframe-workbench.js +11 -0
  92. package/lib/public/modules/workbench-layout-preference.js +12 -5
  93. package/lib/public/modules/workbench-panels.js +21 -0
  94. package/lib/public/modules/workspace-assignment-card.js +9 -6
  95. package/lib/public/pane-assets/{app-ED2VCGNQ.js → app-7UZ727NA.js} +1 -1
  96. package/lib/public/pane-assets/chunk-2AIWCI6Y.js +1 -0
  97. package/lib/public/pane-assets/chunk-6MIHIQ2W.js +1 -0
  98. package/lib/public/pane-assets/chunk-6NYCNWVF.js +1 -0
  99. package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
  100. package/lib/public/pane-assets/chunk-6UJ2ALOB.js +1 -0
  101. package/lib/public/pane-assets/chunk-6XK2OATZ.js +1 -0
  102. package/lib/public/pane-assets/chunk-BKIDR2ME.js +1 -0
  103. package/lib/public/pane-assets/chunk-BMHTBRMO.js +1 -0
  104. package/lib/public/pane-assets/chunk-GEFCFZEN.js +1 -0
  105. package/lib/public/pane-assets/chunk-GN2NQEVC.js +2 -0
  106. package/lib/public/pane-assets/chunk-IMLMBPT4.js +166 -0
  107. package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
  108. package/lib/public/pane-assets/chunk-K5S6AILM.js +1 -0
  109. package/lib/public/pane-assets/chunk-LDIZLKB2.js +1 -0
  110. package/lib/public/pane-assets/chunk-OWNNLZ36.js +1 -0
  111. package/lib/public/pane-assets/chunk-P2C3LOWL.js +3 -0
  112. package/lib/public/pane-assets/chunk-PV4PCH2V.js +2 -0
  113. package/lib/public/pane-assets/chunk-R4LMPTKU.js +1 -0
  114. package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
  115. package/lib/public/pane-assets/chunk-TMGSDFJI.js +1 -0
  116. package/lib/public/pane-assets/chunk-VKYBAVWT.js +1 -0
  117. package/lib/public/pane-assets/chunk-VNAV23CM.js +1 -0
  118. package/lib/public/pane-assets/chunk-VWTHIQCE.js +1 -0
  119. package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
  120. package/lib/public/pane-assets/chunk-WM3K5AFH.js +1 -0
  121. package/lib/public/pane-assets/chunk-XOB3O327.js +1 -0
  122. package/lib/public/pane-assets/chunk-XOOUCFYT.js +1 -0
  123. package/lib/public/pane-assets/manifest.json +383 -336
  124. package/lib/public/pane-assets/style-ZM7OOU3M.css +1 -0
  125. package/lib/public/style.css +1 -0
  126. package/lib/server-workbench-layout-settings.js +3 -3
  127. package/lib/server.js +1 -0
  128. package/lib/split-view-layout.js +11 -7
  129. package/lib/users-workbench-layout-preferences.js +14 -4
  130. package/lib/workbench-window-layout.js +30 -0
  131. package/lib/workspace-assignment-service.js +56 -11
  132. package/lib/workspace-follow-up-queue.js +92 -0
  133. package/lib/workspace-query-mcp-server.js +1 -1
  134. package/package.json +1 -1
  135. package/lib/public/pane-assets/chunk-6D4DPE5X.js +0 -1
  136. package/lib/public/pane-assets/chunk-6SM2WQJX.js +0 -1
  137. package/lib/public/pane-assets/chunk-7H36SUZA.js +0 -2
  138. package/lib/public/pane-assets/chunk-DGOFGEUK.js +0 -1
  139. package/lib/public/pane-assets/chunk-I57URYSL.js +0 -1
  140. package/lib/public/pane-assets/chunk-IYFILQHZ.js +0 -2
  141. package/lib/public/pane-assets/chunk-IZ2ZANEY.js +0 -167
  142. package/lib/public/pane-assets/chunk-JAEQCN5S.js +0 -1
  143. package/lib/public/pane-assets/chunk-ST2Q62ET.js +0 -1
  144. package/lib/public/pane-assets/style-JVCOCG3W.css +0 -1
@@ -0,0 +1,90 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { livePaneBinding, samePaneBinding } from './pane-binding.js';
4
+ import { layoutScope, currentLayoutScope } from './split-view-layout-scope.js';
5
+ import { presentationDropTree } from './split-view-presentation-drop.js';
6
+ import { rectangles } from './split-view-layout-client.js';
7
+
8
+ function activeView() {
9
+ return (store.get('projectSplitViews') || []).find(function (view) { return view.id === store.get('projectSplitViewActiveId'); });
10
+ }
11
+ function reducedMotion() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
12
+ export function captureDragPreview(drag, host) {
13
+ var settling = store.get('projectSplitViewDragSettling');
14
+ if (settling && settling.timer) clearTimeout(settling.timer);
15
+ store.set({ projectSplitViewDragSettling: null });
16
+ var view = activeView();
17
+ drag.preview = { host: host, socket: getWs(), slug: store.get('currentSlug'), owner: store.get('myUserId'), projectOwner: store.get('currentProjectOwnerId'),
18
+ scope: view && layoutScope(view), width: host.clientWidth, height: host.clientHeight, tree: null,
19
+ panes: Array.from(host.querySelectorAll(':scope > .split-pane:not(.split-pane-cached)')).filter(function (pane) {
20
+ return !pane.hidden && pane.getBoundingClientRect().width > 0;
21
+ }).map(function (pane) {
22
+ var frame = pane.querySelector('.split-pane-frame'), live = frame && frame.clayPane && frame.clayPane.identity();
23
+ return { pane: pane, frame: frame, document: frame && frame.contentDocument, window: frame && frame.contentWindow,
24
+ binding: live && live.binding, rect: pane.getBoundingClientRect() };
25
+ }) };
26
+ }
27
+ export function dragPreviewCurrent(drag) {
28
+ var preview = drag.preview;
29
+ return !!preview && preview.socket === getWs() && preview.slug === store.get('currentSlug') && preview.owner === store.get('myUserId') &&
30
+ preview.projectOwner === store.get('currentProjectOwnerId') && store.get('connected') && !store.get('projectSplitViewDividerDrag') && (!preview.scope || currentLayoutScope(preview.scope)) &&
31
+ preview.width === preview.host.clientWidth && preview.height === preview.host.clientHeight && preview.panes.every(function (entry) {
32
+ var live = entry.frame && entry.frame.clayPane && entry.frame.clayPane.identity();
33
+ return entry.pane.isConnected && entry.frame.contentDocument === entry.document && entry.frame.contentWindow === entry.window &&
34
+ live && samePaneBinding(live.binding, entry.binding) && livePaneBinding(entry.frame);
35
+ });
36
+ }
37
+ // Hit-test the starting layout, never panes that are travelling under the hand.
38
+ export function dragPreviewPaneAt(drag, x, y) {
39
+ var entries = drag && drag.preview && drag.preview.panes || [];
40
+ return entries.find(function (entry) {
41
+ var r = entry.rect; return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
42
+ });
43
+ }
44
+ export function renderDragPreview(drag, target) {
45
+ var preview = drag.preview, view = activeView();
46
+ if (!preview || !view || !dragPreviewCurrent(drag)) return null;
47
+ var sourceView = (store.get('projectSplitViews') || []).find(function (item) { return item.id === drag.sourceViewId; });
48
+ var tree = target && !target.sidebar && target.viewId === view.id && sourceView && sourceView.id === view.id && !store.get('projectSplitViewFocusedOrigin') ?
49
+ presentationDropTree(drag, target, sourceView, view) : null;
50
+ if (JSON.stringify(tree) === JSON.stringify(preview.tree)) return preview.dropRect || null;
51
+ preview.tree = tree;
52
+ preview.host.classList.add('split-view-drag-preview');
53
+ store.set({ projectSplitViewPreviewTree: tree, projectSplitViewPreviewScope: preview.scope });
54
+ preview.dropRect = null;
55
+ if (tree) {
56
+ var style = window.getComputedStyle(preview.host), left = parseFloat(style.paddingLeft) || 0, top = parseFloat(style.paddingTop) || 0;
57
+ var geometry = rectangles(tree, preview.width - left - (parseFloat(style.paddingRight) || 0), preview.height - top - (parseFloat(style.paddingBottom) || 0), 7);
58
+ var rect = geometry.panes[drag.origin];
59
+ if (rect) preview.dropRect = { left: left + rect.x, top: top + rect.y, width: rect.width, height: rect.height };
60
+ }
61
+ return preview.dropRect;
62
+ }
63
+ function settleMotion() {
64
+ var settling = store.get('projectSplitViewDragSettling');
65
+ if (!settling || settling.timer || store.get('projectSplitViewDrag') || store.get('projectSplitViewPending')) return;
66
+ settling.timer = setTimeout(function () {
67
+ if (store.get('projectSplitViewDragSettling') !== settling) return;
68
+ settling.host.classList.remove('split-view-drag-preview');
69
+ store.set({ projectSplitViewDragSettling: null });
70
+ }, reducedMotion() ? 0 : 320);
71
+ }
72
+ export function restoreDragPreview(drag, keep) {
73
+ var preview = drag && drag.preview;
74
+ if (!preview) return;
75
+ if (!keep && preview.tree && store.get('projectSplitViewPreviewTree') === preview.tree) {
76
+ store.set({ projectSplitViewPreviewTree: null, projectSplitViewPreviewScope: null });
77
+ }
78
+ var previous = store.get('projectSplitViewDragSettling');
79
+ if (previous && previous.timer) clearTimeout(previous.timer);
80
+ store.set({ projectSplitViewDragSettling: { host: preview.host, timer: null } });
81
+ settleMotion();
82
+ }
83
+ // Preserve transcript anchors until animated width/height changes have settled.
84
+ export function afterPaneMotion(host, callback) {
85
+ window.requestAnimationFrame(function () {
86
+ if (host.classList.contains('split-view-drag-preview') && !reducedMotion()) setTimeout(callback, 300);
87
+ else callback();
88
+ });
89
+ }
90
+ store.subscribe(function () { settleMotion(); });
@@ -24,7 +24,7 @@ export function validateTree(tree, expectedIds) {
24
24
  !Number.isFinite(node.ratio) || node.ratio < MIN_RATIO || node.ratio > MAX_RATIO) return false;
25
25
  return walk(node.left, depth + 1) && walk(node.right, depth + 1);
26
26
  }
27
- valid = walk(tree, 0) && ids.length >= 2 && ids.length <= MAX_SESSIONS && new Set(ids).size === ids.length;
27
+ valid = (tree === null || walk(tree, 0)) && ids.length >= 0 && ids.length <= MAX_SESSIONS && new Set(ids).size === ids.length;
28
28
  if (valid && expectedIds) {
29
29
  valid = Array.isArray(expectedIds) && expectedIds.length === ids.length &&
30
30
  expectedIds.every(function (id) { return ids.indexOf(id) !== -1; });
@@ -1,6 +1,8 @@
1
1
  // Pointer drag controller for presentation views. Captured gestures are
2
2
  // source-bound by pointerId and always recompute the release target.
3
3
  import { store } from './store.js';
4
+ import { livePaneBinding } from './pane-binding.js';
5
+ import { captureDragPreview, dragPreviewCurrent, dragPreviewPaneAt, renderDragPreview, restoreDragPreview } from './split-view-drag-preview.js';
4
6
  import { getCachedSessions } from './sidebar-sessions.js';
5
7
  import { showToast } from './utils.js';
6
8
  import { currentFolderState, sendFolderOp } from './session-folders.js';
@@ -29,22 +31,28 @@ function validSession(session) {
29
31
  function legacyMember(session) { return !!session && session.sessionRole === "worker"; }
30
32
 
31
33
  function onPointerDown(event) {
32
- if (event.button !== 0 || event.isPrimary === false || store.get('projectSplitViewDrag')) return;
34
+ if (event.button !== 0 || event.isPrimary === false || store.get('projectSplitViewDrag') || store.get('projectSplitViewPending')) return;
33
35
  if (!event.target || !event.target.closest) return;
34
36
  var paneHeader = event.target.closest(".split-pane-header");
35
37
  var row = event.target.closest(".session-item[data-session-id], .session-loop-child[data-session-id]");
36
38
  if (!paneHeader && !row) return;
39
+ var workspace = store.get('toolPaneWindowGeometry');
40
+ if (paneHeader && workspace && workspace.windows.some(function (entry) { return !!entry.tool; })) return;
41
+ if (paneHeader && event.target.closest(".split-pane-title, .split-view-runtime, .split-worker-runtime")) return;
37
42
  var button = event.target.closest("button");
38
43
  if ((paneHeader && button) || (button && button !== row) || event.target.closest("a, input, textarea, select, [contenteditable='true'], .workbench, .split-view-menu, .split-view-arrange, .session-row-actions, .session-close-btn, .session-more-btn")) return;
39
44
  var pane = paneHeader && paneHeader.closest('.split-pane');
45
+ if (pane && !livePaneBinding(pane.querySelector('.split-pane-frame'))) return;
40
46
  var id = pane ? Number(pane.dataset.sessionId) : row ? Number(row.dataset.sessionId) : 0;
41
47
  var source = sessionById(id);
42
48
  if (!validSession(source) || legacyMember(source)) return;
43
49
  var group = row && row.closest('.split-view-group[data-split-view-id]');
44
50
  var view = viewForOrigin(source.sessionOriginId);
45
- store.set({ projectSplitViewDrag: { pointerId: event.pointerId, sessionId: source.id, origin: source.sessionOriginId,
51
+ var drag = { pointerId: event.pointerId, sessionId: source.id, origin: source.sessionOriginId,
46
52
  sourceViewId: group ? group.dataset.splitViewId : view && view.id || null, sourceElement: row || paneHeader, startX: event.clientX, startY: event.clientY,
47
- moved: false, target: null, pointerType: event.pointerType, frames: [] } });
53
+ moved: false, target: null, pointerType: event.pointerType, frames: [] };
54
+ captureDragPreview(drag, host);
55
+ store.set({ projectSplitViewDrag: drag });
48
56
  }
49
57
 
50
58
  function targetAt(x, y) {
@@ -59,6 +67,8 @@ function targetAt(x, y) {
59
67
  }
60
68
  var sidebar = sidebarTargetAt(element);
61
69
  if (sidebar) return sidebar;
70
+ var snapshot = dragPreviewPaneAt(store.get('projectSplitViewDrag'), x, y);
71
+ if (snapshot) return paneTargetAt(snapshot.pane, x, y, snapshot.rect);
62
72
  var pane = element.closest('.split-pane');
63
73
  if (!pane || !host.contains(pane) || !pane.isConnected) return null;
64
74
  return paneTargetAt(pane, x, y);
@@ -81,11 +91,11 @@ function sidebarTargetAt(element) {
81
91
  rect: { left: bounds.left, top: bounds.top, width: bounds.width, height: bounds.height } };
82
92
  }
83
93
 
84
- function paneTargetAt(pane, x, y) {
94
+ function paneTargetAt(pane, x, y, originalBounds) {
85
95
  var id = Number(pane.dataset.sessionId), session = sessionById(id), frame = pane.querySelector('.split-pane-frame');
86
96
  if (!validSession(session) || legacyMember(session) || !frame || frame.dataset.projectSlug !== store.get('currentSlug') ||
87
97
  frame.dataset.sessionId !== String(id) || frame.dataset.sessionOriginId !== session.sessionOriginId) return null;
88
- var bounds = pane.getBoundingClientRect(), dx = (x - bounds.left) / Math.max(1, bounds.width), dy = (y - bounds.top) / Math.max(1, bounds.height);
98
+ var bounds = originalBounds || pane.getBoundingClientRect(), dx = (x - bounds.left) / Math.max(1, bounds.width), dy = (y - bounds.top) / Math.max(1, bounds.height);
89
99
  var edge = Math.min(dx, 1 - dx, dy, 1 - dy), side = "center";
90
100
  if (edge < 0.25) side = edge === dx ? "left" : edge === 1 - dx ? "right" : edge === dy ? "top" : "bottom";
91
101
  var view = viewForOrigin(session.sessionOriginId);
@@ -102,12 +112,13 @@ function fallbackTargetAt(x, y) {
102
112
  var panes = Array.from(host.querySelectorAll(':scope > .split-pane')), nearest = null, nearestDistance = Infinity;
103
113
  panes.forEach(function (pane) {
104
114
  if (pane.hidden) return;
105
- var rect = pane.getBoundingClientRect(), dx = x < rect.left ? rect.left - x : x > rect.right ? x - rect.right : 0;
115
+ var snapshot = store.get('projectSplitViewDrag'), entry = snapshot && snapshot.preview && snapshot.preview.panes.find(function (item) { return item.pane === pane; });
116
+ var rect = entry ? entry.rect : pane.getBoundingClientRect(), dx = x < rect.left ? rect.left - x : x > rect.right ? x - rect.right : 0;
106
117
  var dy = y < rect.top ? rect.top - y : y > rect.bottom ? y - rect.bottom : 0;
107
118
  var distance = dx * dx + dy * dy;
108
119
  if (distance < nearestDistance) { nearest = pane; nearestDistance = distance; }
109
120
  });
110
- if (nearest) return paneTargetAt(nearest, x, y);
121
+ if (nearest) { var entry = store.get('projectSplitViewDrag').preview.panes.find(function (item) { return item.pane === nearest; }); return paneTargetAt(nearest, x, y, entry && entry.rect); }
111
122
  }
112
123
  if (!inApp) return null;
113
124
  var active = sessionById(Number(store.get('activeSessionId')));
@@ -123,9 +134,12 @@ function resolveTarget(x, y) { return targetAt(x, y) || fallbackTargetAt(x, y);
123
134
  function onPointerMove(event) {
124
135
  var drag = store.get('projectSplitViewDrag');
125
136
  if (!drag || drag.pointerId !== event.pointerId) return;
137
+ if (!dragPreviewCurrent(drag)) { cleanup(true, true); return; }
126
138
  if (!drag.moved && Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) < 6) return;
127
139
  if (!drag.moved) {
128
140
  drag.moved = true;
141
+ var source = drag.preview.panes.find(function (entry) { return entry.binding && entry.binding.sessionOriginId === drag.origin; });
142
+ if (source) { drag.sourcePane = source.pane; source.pane.classList.add('split-pane-drag-source'); }
129
143
  try { document.documentElement.setPointerCapture(event.pointerId); } catch (error) {}
130
144
  drag.frames = Array.from(host.querySelectorAll('.split-pane-frame')).map(function (frame) { return { frame: frame, pointerEvents: frame.style.pointerEvents }; });
131
145
  drag.frames.forEach(function (entry) { entry.frame.style.pointerEvents = "none"; });
@@ -138,6 +152,7 @@ function onPointerMove(event) {
138
152
 
139
153
  function renderPreview(drag, x, y) {
140
154
  clearSidebarPreview();
155
+ var plannedRect = renderDragPreview(drag, drag.target);
141
156
  var preview = host.querySelector(':scope > .split-view-drop-preview'), ghost = document.getElementById("split-view-drag-ghost");
142
157
  if (!ghost) { ghost = document.createElement("div"); ghost.id = "split-view-drag-ghost"; ghost.className = "split-view-drag-ghost"; ghost.hidden = true; document.body.appendChild(ghost); }
143
158
  if (!preview) {
@@ -188,9 +203,10 @@ function renderPreview(drag, x, y) {
188
203
  var hostRect = host.getBoundingClientRect();
189
204
  preview.style.left = (box.left - hostRect.left) + "px"; preview.style.top = (box.top - hostRect.top) + "px";
190
205
  preview.style.width = box.width + "px"; preview.style.height = box.height + "px";
206
+ if (plannedRect) { preview.style.left = plannedRect.left + "px"; preview.style.top = plannedRect.top + "px"; preview.style.width = plannedRect.width + "px"; preview.style.height = plannedRect.height + "px"; }
191
207
  preview.dataset.blocked = blocked ? "true" : "false"; preview.hidden = false;
192
208
  var label = preview.querySelector(".drop-label");
193
- if (label) label.textContent = blocked ? "Eight sessions already open" : drag.origin === target.origin ? "Keep this pane" :
209
+ if (label) label.textContent = blocked ? (drag.origin === target.origin ? "Keep this pane" : "Eight sessions already open") : drag.origin === target.origin ? "Keep this pane" :
194
210
  target.side === "center" ? (sourceView && targetView && sourceView.id === targetView.id ? "Swap panes" : "Split right") : "Split " + target.side;
195
211
  }
196
212
 
@@ -199,8 +215,10 @@ function clearSidebarPreview() {
199
215
  document.querySelectorAll('[data-split-view-drop]').forEach(function (element) { delete element.dataset.splitViewDrop; });
200
216
  }
201
217
 
202
- export function restorePresentationDrag(drag) {
218
+ export function restorePresentationDrag(drag, keepPreview) {
203
219
  if (!drag) return;
220
+ if (drag.sourcePane) drag.sourcePane.classList.remove('split-pane-drag-source');
221
+ restoreDragPreview(drag, keepPreview);
204
222
  clearSidebarPreview();
205
223
  drag.frames.forEach(function (entry) { if (entry.frame.isConnected) entry.frame.style.pointerEvents = entry.pointerEvents; });
206
224
  var preview = host.querySelector(':scope > .split-view-drop-preview'); if (preview) preview.hidden = true;
@@ -215,23 +233,24 @@ export function restorePresentationDrag(drag) {
215
233
  }
216
234
  }
217
235
 
218
- function cleanup(release, suppressClick) {
236
+ function cleanup(release, suppressClick, keepPreview) {
219
237
  var drag = store.get('projectSplitViewDrag');
220
238
  if (!drag) return;
221
- restorePresentationDrag(drag);
239
+ restorePresentationDrag(drag, keepPreview);
222
240
  if (suppressClick && drag.moved) {
223
241
  var token = { pointerId: drag.pointerId, sourceElement: drag.sourceElement, sourceSessionId: drag.sessionId,
224
242
  targetOrigin: drag.target && drag.target.origin, until: Date.now() + 250 };
225
243
  store.set({ projectSplitViewSuppressClick: token });
226
244
  setTimeout(function () { if (store.get('projectSplitViewSuppressClick') === token) store.set({ projectSplitViewSuppressClick: null }); }, 500);
227
245
  }
228
- if (release) try { if (document.documentElement.hasPointerCapture(drag.pointerId)) document.documentElement.releasePointerCapture(drag.pointerId); } catch (error) {}
229
246
  store.set({ projectSplitViewDrag: null });
247
+ if (release) try { if (document.documentElement.hasPointerCapture(drag.pointerId)) document.documentElement.releasePointerCapture(drag.pointerId); } catch (error) {}
230
248
  }
231
249
 
232
250
  function onPointerUp(event) {
233
251
  var drag = store.get('projectSplitViewDrag');
234
252
  if (!drag || drag.pointerId !== event.pointerId) return;
253
+ if (!dragPreviewCurrent(drag)) { cleanup(true, true); return; }
235
254
  if (!drag.moved) { cleanup(true, false); return; }
236
255
  var target = resolveTarget(event.clientX, event.clientY);
237
256
  drag.target = target;
@@ -239,6 +258,11 @@ function onPointerUp(event) {
239
258
  (store.get('projectSplitViews') || []).some(function (view) { return view.id === target.viewId && view.sessions.length >= 8; })) {
240
259
  cleanup(true, true); showToast("Eight sessions are already open. Close a pane to add another.", "warn"); return;
241
260
  }
261
+ if (target && !target.folderId && drag.origin !== target.origin && commitDrop) {
262
+ renderDragPreview(drag, target);
263
+ var queued = commitDrop(drag, target);
264
+ cleanup(true, true, queued === true); return;
265
+ }
242
266
  cleanup(true, true);
243
267
  if (target && target.folderId) {
244
268
  var session = sessionById(drag.sessionId);
@@ -249,7 +273,6 @@ function onPointerUp(event) {
249
273
  else sendFolderOp(favorite ? { op: 'set_favorite', sessionId: session.id, favorite: true } : { op: 'place_session', sessionId: session.id, folderId: folderId });
250
274
  return;
251
275
  }
252
- if (target && drag.origin !== target.origin && commitDrop) commitDrop(drag, target);
253
276
  }
254
277
 
255
278
  function onPointerCancel(event) {
@@ -5,13 +5,25 @@ import { checkComposerDeparture } from './session-rich-drafts.js';
5
5
  import { showToast } from './utils.js';
6
6
  import { listIds, place, swap, pairTree } from './split-view-layout-client.js';
7
7
 
8
+ // Preview and persistence must resolve the same tree for every drop region.
9
+ export function presentationDropTree(drag, target, sourceView, targetView) {
10
+ if (!target || target.folderId || drag.origin === target.origin) return null;
11
+ var sameView = sourceView && targetView && sourceView.id === targetView.id;
12
+ if (target.sidebar && sameView) return null;
13
+ var side = target.side === "center" ? (sameView ? "center" : "right") : target.side;
14
+ if (!targetView) return pairTree(target.origin, drag.origin, side);
15
+ var result = side === "center" ? swap(targetView.tree, drag.origin, target.origin) : place(targetView.tree, drag.origin, target.origin, side, 0.5);
16
+ return result.ok ? result.tree : null;
17
+ }
18
+
8
19
  export function applyPresentationDrop(drag, target, mutation, checkMountedPaneDrafts) {
9
20
  var views = store.get('projectSplitViews') || [], sourceView = views.find(function (view) { return view.id === drag.sourceViewId; }), targetView = views.find(function (view) { return view.id === target.viewId; });
10
21
  var sessions = getCachedSessions() || [], sourceSession = exactDriverForOrigin(drag.origin, sessions), targetSession = exactDriverForOrigin(target.origin, sessions);
11
22
  if (!sourceSession || !targetSession || sourceSession.id !== drag.sessionId) return;
12
23
  var sameView = sourceView && targetView && sourceView.id === targetView.id;
13
24
  if (target.sidebar && sameView) return;
14
- var side = target.side === "center" ? (sameView ? "center" : "right") : target.side;
25
+ var plannedTree = presentationDropTree(drag, target, sourceView, targetView);
26
+ if (!plannedTree) return false;
15
27
  function checkDeparture() {
16
28
  try {
17
29
  if (sourceView && sourceView.id === store.get('projectSplitViewActiveId')) checkMountedPaneDrafts();
@@ -22,36 +34,27 @@ export function applyPresentationDrop(drag, target, mutation, checkMountedPaneDr
22
34
  if (!targetView) {
23
35
  if (sourceView) {
24
36
  if (!checkDeparture()) return;
25
- var newTree = pairTree(target.origin, drag.origin, side);
26
- mutation("project_split_view_move", { sourceViewId: sourceView.id, sourceSessionOriginId: drag.origin,
37
+ var newTree = plannedTree;
38
+ return mutation("project_split_view_move", { sourceViewId: sourceView.id, sourceSessionOriginId: drag.origin,
27
39
  targetSessionOriginId: target.origin, targetSessionOrigins: listIds(newTree), tree: newTree, layout: "balanced",
28
40
  activeSessionOriginId: target.origin, name: "Split view" }, { activateCreatedView: true });
29
41
  } else {
30
42
  if (!checkDeparture()) return;
31
- var createdTree = pairTree(target.origin, drag.origin, side);
32
- mutation("project_split_view_create", { name: "Split view", sessionOrigins: listIds(createdTree), tree: createdTree,
43
+ var createdTree = plannedTree;
44
+ return mutation("project_split_view_create", { name: "Split view", sessionOrigins: listIds(createdTree), tree: createdTree,
33
45
  layout: "balanced", activeSessionOriginId: target.origin }, { activateCreatedView: true });
34
46
  }
35
- return;
36
47
  }
37
48
  if (sourceView && targetView.id !== sourceView.id) {
38
49
  if (targetView.sessions.length >= 8) { showToast("Eight sessions are already open. Close a pane to add another.", "warn"); return; }
39
- var movedTree = place(targetView.tree, drag.origin, target.origin, side, 0.5);
40
- if (!movedTree.ok) return;
41
- mutation("project_split_view_move", { sourceViewId: sourceView.id, sourceSessionOriginId: drag.origin,
50
+ var movedTree = { tree: plannedTree };
51
+ return mutation("project_split_view_move", { sourceViewId: sourceView.id, sourceSessionOriginId: drag.origin,
42
52
  targetViewId: targetView.id, targetSessionOrigins: listIds(movedTree.tree), tree: movedTree.tree,
43
53
  layout: targetView.layout, activeSessionOriginId: targetView.activeSessionOriginId }, target.sidebar ? { activateCreatedView: true, createdViewId: targetView.id } : null);
44
- return;
45
- }
46
- var nextTree;
47
- if (side === "center" && sourceView) nextTree = swap(targetView.tree, drag.origin, target.origin).tree;
48
- else {
49
- var placed = place(targetView.tree, drag.origin, target.origin, side, 0.5);
50
- if (!placed.ok) return;
51
- nextTree = placed.tree;
52
54
  }
55
+ var nextTree = plannedTree;
53
56
  var origins = listIds(nextTree);
54
57
  if (origins.length > 8) { showToast("Eight sessions are already open. Close a pane to add another.", "warn"); return; }
55
- mutation("project_split_view_update", { id: targetView.id, sessionOrigins: origins, tree: nextTree,
58
+ return mutation("project_split_view_update", { id: targetView.id, sessionOrigins: origins, tree: nextTree,
56
59
  layout: targetView.layout, activeSessionOriginId: targetView.activeSessionOriginId }, target.sidebar ? { activateCreatedView: true, createdViewId: targetView.id } : null);
57
60
  }
@@ -1,3 +1,6 @@
1
+ import { paintLegacyToolPanes, clearToolPaneWindows } from './tool-pane-legacy.js';
2
+ import { toolPaneLayoutTree, paintToolPaneWindows, bindWorkspaceDivider, dockedToolPanes } from './tool-pane-layout.js';
3
+ import { afterPaneMotion } from './split-view-drag-preview.js';
1
4
  import { layoutScope, currentLayoutScope, presentationTree } from './split-view-layout-scope.js';
2
5
  // Flat, stable pane presentation for persisted binary trees. Geometry is
3
6
  // applied to existing direct-child .split-pane nodes; iframes are never moved
@@ -41,7 +44,6 @@ function relocateStickyStatus(enabled) {
41
44
  });
42
45
  }
43
46
  }
44
-
45
47
  function dispatch(name, detail) {
46
48
  document.dispatchEvent(new CustomEvent(name, { detail: detail }));
47
49
  }
@@ -84,6 +86,7 @@ export function hidePresentationView() {
84
86
  delete host.dataset.positioned;
85
87
  delete host.dataset.singlePane;
86
88
  if (!legacySplit) {
89
+ clearToolPaneWindows();
87
90
  host.classList.remove("visible");
88
91
  if (app) app.classList.remove("split-native-hidden");
89
92
  }
@@ -98,7 +101,7 @@ export function hidePresentationView() {
98
101
  if (legacySplit) {
99
102
  Array.from(host.querySelectorAll(':scope > .split-view-divider, :scope > .split-view-drop-preview')).forEach(function (item) { item.remove(); });
100
103
  Array.from(host.querySelectorAll('.split-view-arrange-trigger')).forEach(function (trigger) { trigger.remove(); });
101
- closePresentationArrange(false);
104
+ closePresentationArrange(false); paintLegacyToolPanes(host);
102
105
  return;
103
106
  }
104
107
  var frames = host.querySelectorAll('.split-pane-frame');
@@ -169,8 +172,9 @@ function currentSingletonGeometry() {
169
172
  }
170
173
 
171
174
  function renderCurrentGeometry() {
175
+ if (paintLegacyToolPanes(host)) return;
172
176
  var view = currentView(), tree = activeTree();
173
- if (view && tree) { applyGeometry(view, tree); return; }
177
+ if (view && (tree || !view.sessions.length)) { applyGeometry(view, tree); return; }
174
178
  var singleton = currentSingletonGeometry();
175
179
  if (singleton) applyGeometry(singleton.view, { id: singleton.origin }, function () {
176
180
  var current = currentSingletonGeometry();
@@ -211,12 +215,14 @@ function applyGeometry(view, tree, stillCurrent) {
211
215
  });
212
216
  var style = window.getComputedStyle(host), left = parseFloat(style.paddingLeft) || 0, top = parseFloat(style.paddingTop) || 0;
213
217
  var right = parseFloat(style.paddingRight) || 0, bottom = parseFloat(style.paddingBottom) || 0;
214
- var geometry = rectangles(tree, Math.max(0, host.clientWidth - left - right), Math.max(0, host.clientHeight - top - bottom), 7);
218
+ var windowTree = toolPaneLayoutTree(view, tree);
219
+ var geometry = rectangles(windowTree, Math.max(0, host.clientWidth - left - right), Math.max(0, host.clientHeight - top - bottom), 7);
220
+ paintToolPaneWindows(host, view, tree, windowTree, geometry, { left: left, top: top });
215
221
  var focused = store.get('projectSplitViewFocusedOrigin');
216
222
  var panes = host.querySelectorAll(':scope > .split-pane');
217
223
  for (var i = 0; i < panes.length; i++) {
218
224
  var frame = panes[i].querySelector('.split-pane-frame'), origin = panes[i].dataset.presentationOrigin || frame && frame.dataset.sessionOriginId;
219
- var rect = host.dataset.singlePane === 'true' && panes.length === 1 ?
225
+ var rect = host.dataset.singlePane === 'true' && panes.length === 1 && !dockedToolPanes().length ?
220
226
  { x: 0, y: 0, width: Math.max(0, host.clientWidth - left - right), height: Math.max(0, host.clientHeight - top - bottom) } : geometry.panes[origin];
221
227
  if (!rect) continue;
222
228
  if (focused && geometry.panes[focused]) {
@@ -241,11 +247,12 @@ function applyGeometry(view, tree, stillCurrent) {
241
247
  divider.setAttribute("aria-label", "Resize Split view panels");
242
248
  divider.style.left = (left + entry.rect.x) + "px"; divider.style.top = (top + entry.rect.y) + "px";
243
249
  divider.style.width = entry.rect.width + "px"; divider.style.height = entry.rect.height + "px";
244
- bindDivider(divider, entry, tree, listIds(tree), scope); host.appendChild(divider);
250
+ if (dockedToolPanes().length) bindWorkspaceDivider(divider, entry, windowTree);
251
+ else bindDivider(divider, entry, tree, listIds(tree), scope); host.appendChild(divider);
245
252
  if (focusedDividerPath && divider.dataset.path === focusedDividerPath) divider.focus();
246
253
  }
247
254
  ensureDropPreview();
248
- window.requestAnimationFrame(function () {
255
+ afterPaneMotion(host, function () {
249
256
  if (resizeToken !== 'split-view-layout-' + geometryToken || (stillCurrent ? !stillCurrent() : !currentLayoutScope(scope))) return;
250
257
  resizeFrames.forEach(function (item) {
251
258
  var live = item.frame.clayPane && item.frame.clayPane.identity();
@@ -254,13 +261,11 @@ function applyGeometry(view, tree, stillCurrent) {
254
261
  });
255
262
  });
256
263
  }
257
-
258
264
  function bindingMatches(left, right) {
259
265
  return !!left && !!right && left.sessionId === right.sessionId && left.sessionOriginId === right.sessionOriginId &&
260
266
  left.generation === right.generation && left.requestId === right.requestId;
261
267
  }
262
-
263
- function ensureArrangeTrigger(header) {
268
+ export function ensureArrangeTrigger(header) {
264
269
  if (!header) return;
265
270
  if (!header.querySelector('[data-action="arrange"]')) {
266
271
  var trigger = document.createElement("button"); trigger.type = "button"; trigger.className = "split-view-arrange-trigger";
@@ -1,3 +1,9 @@
1
+ import { workspaceChanged } from './view-workspace-state.js';
2
+ import { restoreViewPair } from './view-pair-presentation.js';
3
+ import { repaintLegacySplit } from './split-view.js';
4
+ import { validWorkspaceView } from './view-workspace-validation.js';
5
+ import { mutation } from './view-mutations.js';
6
+ import { initSavedWorkspace } from './view-workspace-controller.js';
1
7
  import { preserveReconnectingPresentation } from './split-pane-retention.js';
2
8
  import { layoutScope, currentLayoutScope, presentationTree } from './split-view-layout-scope.js';
3
9
  import { validPresentationClose } from './split-view-presentation-close.js';
@@ -99,7 +105,7 @@ export function handleProjectSplitViews(msg) {
99
105
  var invalidView = false;
100
106
  msg.views.forEach(function (view) {
101
107
  var ids = view && Array.isArray(view.sessions) ? view.sessions.map(function (session) { return session.sessionOriginId; }) : [];
102
- if (ids.length < 2 || ids.length > 8 || viewSessions(view).length !== ids.length || !validateTree(view.tree, ids).ok) invalidView = true;
108
+ if (ids.length > 8 || viewSessions(view).length !== ids.length || !validateTree(view.tree, ids).ok || !validWorkspaceView(view)) invalidView = true;
103
109
  else nextViews.push(view);
104
110
  });
105
111
  if (invalidView) {
@@ -115,7 +121,7 @@ export function handleProjectSplitViews(msg) {
115
121
  if (state.projectSplitViewDrag || state.projectSplitViewDividerDrag) cleanupTransientInteractions(state);
116
122
  var activeId = state.projectSplitViewActiveId;
117
123
  if (!viewExists(nextViews, activeId)) {
118
- var restored = restorablePresentation(state, nextViews, getCachedSessions());
124
+ var restored = nextViews.find(function (view) { return view.id === msg.activeViewId && !view.sessions.length; }) || restorablePresentation(state, nextViews, getCachedSessions());
119
125
  if (restored) activeId = restored.id;
120
126
  }
121
127
  var removedActive = activeId && !viewExists(nextViews, activeId) ? activeId : null;
@@ -126,7 +132,7 @@ export function handleProjectSplitViews(msg) {
126
132
  var survivor = exactDriverForOrigin(completedPending.meta.activeOrigin, getCachedSessions());
127
133
  if (survivor) standaloneTarget = { id: survivor.id, origin: survivor.sessionOriginId, previousSessionId: state.activeSessionId, contextKey: contextKey(state) };
128
134
  }
129
- store.set({ projectSplitViews: nextViews, projectSplitViewRevision: msg.revision, projectSplitViewHydrated: true,
135
+ store.set({ projectSplitViews: nextViews, projectSplitViewRevision: msg.revision, projectSplitViewSavedActiveId: msg.activeViewId || null, projectSplitViewHydrated: true,
130
136
  projectSplitViewError: msg.error || "", projectSplitViewStandaloneTarget: standaloneTarget, projectSplitViewActiveId: viewExists(nextViews, activeId) ? activeId : null,
131
137
  projectSplitViewFocusedOrigin: viewExists(nextViews, activeId) ? state.projectSplitViewFocusedOrigin : null,
132
138
  projectSplitViewPending: readyPending, projectSplitViewPreviewTree: readyPending ? state.projectSplitViewPreviewTree : null,
@@ -182,22 +188,6 @@ export function organizePresentationView(id, revision, folderId, favorite) {
182
188
  if (!view || view.revision !== revision) return false;
183
189
  return mutation('project_split_view_organize', { id: id, expectedViewRevision: revision, projectSlug: store.get('currentSlug'), folderId: folderId, favorite: favorite });
184
190
  }
185
- function mutation(type, body, meta) {
186
- var state = store.snap();
187
- if (!projectReady(state) || !state.projectSplitViewHydrated || state.projectSplitViewError || state.projectSplitViewPending) return false;
188
- var requestId = "split-view-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
189
- var ws = getWs();
190
- var message = Object.assign({ type: type, requestId: requestId, expectedRevision: state.projectSplitViewRevision }, body || {});
191
- var pending = { requestId: requestId, type: type, socket: ws, contextKey: contextKey(state), expectedRevision: state.projectSplitViewRevision, meta: meta || null,
192
- awaitingBroadcast: false, resultRevision: null };
193
- store.set({ projectSplitViewPending: pending, projectSplitViewError: "" });
194
- if (!sendMessage(message)) {
195
- store.set({ projectSplitViewPending: null, projectSplitViewError: "Split view change could not be sent. Refresh before trying again." });
196
- showToast("Split view change could not be sent. Refresh before trying again.", "error");
197
- return false;
198
- }
199
- return true;
200
- }
201
191
  export function setActiveView(id, preferredOrigin) {
202
192
  var state = store.snap(), view = viewExists(state.projectSplitViews, id);
203
193
  if (!view) return false;
@@ -235,6 +225,7 @@ export function switchToStandalone(sessionId) {
235
225
  return true;
236
226
  }
237
227
  function renderActive() {
228
+ if (store.get('splitPanes')) { repaintLegacySplit(); return; }
238
229
  if (!host || !nativeApp || preserveReconnectingPresentation(host)) return;
239
230
  var state = store.snap(), view = viewExists(state.projectSplitViews, state.projectSplitViewActiveId);
240
231
  if (state.splitIdentityBlockedSocket === getWs()) { hidePresentationView(); return; }
@@ -246,9 +237,11 @@ function renderActive() {
246
237
  }
247
238
  var singleton = currentSingletonPresentation(state, getCachedSessions());
248
239
  if (singleton) {
249
- store.set({ projectSplitViewSingletonOrigin: singleton.origin });
250
- if (stickyContainer && mainPanels) mainPanels.appendChild(stickyContainer);
251
- renderPresentationView(singleton.view, { id: singleton.origin }, [singleton.session]);
240
+ if (state.projectSplitViewHydrated && !state.projectSplitViewPending && !state.projectSplitViewError) mutation("project_split_view_create", { name: "View", sessionOrigins: [singleton.origin], tree: { id: singleton.origin }, layout: "balanced", activeSessionOriginId: singleton.origin }, { activateCreatedView: true });
241
+ return;
242
+ }
243
+ if (state.projectSplitViewHydrated && projectReady(state) && !(getCachedSessions() || []).length && !state.projectSplitViewPending && !state.projectSplitViewError) {
244
+ mutation('project_split_view_create', { name: 'View', sessionOrigins: [], tree: null, layout: 'balanced', activeSessionOriginId: null }, { activateCreatedView: true });
252
245
  return;
253
246
  }
254
247
  if (state.projectSplitViewSingletonOrigin) store.set({ projectSplitViewSingletonOrigin: null });
@@ -257,10 +250,11 @@ function renderActive() {
257
250
  return;
258
251
  }
259
252
  if (state.projectSplitViewSingletonOrigin) store.set({ projectSplitViewSingletonOrigin: null });
253
+ if (restoreViewPair(view)) return;
260
254
  var sessions = viewSessions(view);
261
255
  var tree = presentationTree(view);
262
256
  var ids = listIds(tree);
263
- if (sessions.length !== view.sessions.length || ids.length < 2 || ids.length > 8 || !validateTree(tree, ids).ok) return;
257
+ if (sessions.length !== view.sessions.length || ids.length > 8 || !validateTree(tree, ids).ok) return;
264
258
  if (stickyContainer && mainPanels) mainPanels.appendChild(stickyContainer);
265
259
  renderPresentationView(view, tree, sessions);
266
260
  }
@@ -424,14 +418,9 @@ function onClosePane(event) {
424
418
  if (!session || !view.sessions.some(function (item) { return item.sessionOriginId === session.sessionOriginId; })) return;
425
419
  try { checkMountedPaneDrafts(); } catch (error) { showToast(error.message, "warn"); return; }
426
420
  var origins = view.sessions.map(function (item) { return item.sessionOriginId; }).filter(function (origin) { return origin !== session.sessionOriginId; });
427
- if (origins.length < 2) {
428
- var remaining = view.sessions.filter(function (item) { return item.sessionOriginId !== session.sessionOriginId; })[0];
429
- if (remaining) mutation("project_split_view_separate", { id: view.id }, { separateViewId: view.id, activeOrigin: remaining.sessionOriginId });
430
- return;
431
- }
432
421
  var nextTree = remove(view.tree, session.sessionOriginId);
433
- if (!nextTree || !validateTree(nextTree, origins).ok) return;
434
- var nextActive = view.activeSessionOriginId === session.sessionOriginId ? origins[0] : view.activeSessionOriginId;
422
+ if (!validateTree(nextTree, origins).ok) return;
423
+ var nextActive = view.activeSessionOriginId === session.sessionOriginId ? origins[0] || null : view.activeSessionOriginId;
435
424
  mutation("project_split_view_update", { id: view.id, sessionOrigins: origins, tree: nextTree,
436
425
  layout: view.layout, activeSessionOriginId: nextActive }, { closedOrigin: session.sessionOriginId, activeOrigin: nextActive });
437
426
  }
@@ -442,13 +431,15 @@ function onFocus(event) {
442
431
  if (event.detail && event.detail.focused && !source) return;
443
432
  store.set({ projectSplitViewFocusedOrigin: event.detail && event.detail.focused ? source.sessionOriginId : null });
444
433
  renderActive();
434
+ if (!store.get('savedWorkspaceApplying')) workspaceChanged();
445
435
  }
446
436
  function init(hostElement) {
447
437
  host = hostElement; nativeApp = document.getElementById("app"); mainPanels = document.getElementById("main-panels");
448
438
  stickyContainer = document.getElementById("sticky-notes-container");
449
439
  if (stickyContainer) { stickyHome = stickyContainer.parentNode; stickyAnchor = stickyContainer.nextSibling; }
450
440
  initPresentationRenderer(host);
451
- initPresentationDrag(host, function (drag, target) { applyPresentationDrop(drag, target, mutation, checkMountedPaneDrafts); });
441
+ initSavedWorkspace();
442
+ initPresentationDrag(host, function (drag, target) { return applyPresentationDrop(drag, target, mutation, checkMountedPaneDrafts); });
452
443
  initSplitViewCreateMenu();
453
444
  document.addEventListener('clay-split-view-create', function (event) { handleSplitViewCreateEvent(event, mutation, checkMountedPaneDrafts); });
454
445
  document.addEventListener("click", onClick, true);
@@ -476,7 +467,7 @@ function init(hostElement) {
476
467
  state.paneBindingSessions !== previous.paneBindingSessions || state.sessionActivatedProjectSlug !== previous.sessionActivatedProjectSlug) renderActive();
477
468
  if (state.activeSessionId !== previous.activeSessionId && state.projectSplitViewActiveId) {
478
469
  var activeView = viewExists(state.projectSplitViews, state.projectSplitViewActiveId);
479
- if (activeView && !activeView.sessions.some(function (session) { return session.id === state.activeSessionId; })) {
470
+ if (activeView && activeView.sessions.length && !activeView.sessions.some(function (session) { return session.id === state.activeSessionId; })) {
480
471
  store.set({ projectSplitViewActiveId: null, projectSplitViewPreviewTree: null });
481
472
  }
482
473
  }
@@ -493,6 +484,7 @@ export function applyReviewedSplitView(target) {
493
484
  var view = viewExists(store.get('projectSplitViews'), target.view.id);
494
485
  if (!projectReady(store.snap()) || store.get('projectSplitViewPending') || !view || JSON.stringify(view) !== JSON.stringify(target.view)) return false;
495
486
  if (!setActiveView(view.id, view.activeSessionOriginId)) return false;
496
- onFocus({ detail: { focused: !!target.focusOrigin, sessionOriginId: target.focusOrigin } });
487
+ store.set({ savedWorkspaceApplying: true });
488
+ try { onFocus({ detail: { focused: !!target.focusOrigin, sessionOriginId: target.focusOrigin } }); } finally { store.set({ savedWorkspaceApplying: false }); }
497
489
  return true;
498
490
  }