clay-server 5.3.0-beta.5 → 5.3.0-beta.7

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 (185) hide show
  1. package/lib/clay-product-control-tools.js +2 -2
  2. package/lib/clay-product-control.js +1 -1
  3. package/lib/clay-screen-controls.js +1 -0
  4. package/lib/clay-split-view-awareness.js +5 -2
  5. package/lib/clay-split-view-controls.js +14 -7
  6. package/lib/clay-split-view-input.js +5 -4
  7. package/lib/clay-split-view-render-proof.js +14 -4
  8. package/lib/clay-view-workspace-input.js +71 -0
  9. package/lib/project-admin-overview.js +22 -11
  10. package/lib/project-background-tasks.js +76 -0
  11. package/lib/project-connection.js +1 -1
  12. package/lib/project-file-history.js +11 -9
  13. package/lib/project-file-watch.js +26 -7
  14. package/lib/project-filesystem.js +5 -4
  15. package/lib/project-sessions.js +12 -5
  16. package/lib/project-shared-browser.js +7 -3
  17. package/lib/project-split-view-document.js +16 -2
  18. package/lib/project-split-views.js +27 -297
  19. package/lib/project-user-message.js +4 -3
  20. package/lib/project-vendor-login.js +2 -2
  21. package/lib/project-view-input.js +11 -0
  22. package/lib/project-view-pair.js +26 -0
  23. package/lib/project-view-store.js +348 -0
  24. package/lib/project-view-terminal-binding.js +16 -0
  25. package/lib/project-view-workspace.js +126 -0
  26. package/lib/project-worker-conversation.js +5 -0
  27. package/lib/project.js +1 -0
  28. package/lib/public/app.js +9 -0
  29. package/lib/public/css/contextual-clay.css +2 -1
  30. package/lib/public/css/issues.css +3 -0
  31. package/lib/public/css/mate-conversation.css +1 -1
  32. package/lib/public/css/pane.css +5 -1
  33. package/lib/public/css/pending-message-queue.css +24 -19
  34. package/lib/public/css/project-admin-overview.css +42 -13
  35. package/lib/public/css/right-workbench.css +0 -1
  36. package/lib/public/css/scheduled-tasks.css +7 -0
  37. package/lib/public/css/shared-browser.css +31 -8
  38. package/lib/public/css/split-view-layout.css +129 -10
  39. package/lib/public/css/tool-pane.css +124 -0
  40. package/lib/public/index.html +6 -6
  41. package/lib/public/modules/agent-message-bubbles.js +96 -0
  42. package/lib/public/modules/app-header.js +6 -0
  43. package/lib/public/modules/app-messages.js +26 -9
  44. package/lib/public/modules/background-task-previews.js +8 -0
  45. package/lib/public/modules/background-task-viewer.js +97 -0
  46. package/lib/public/modules/background-tasks-ui.js +34 -125
  47. package/lib/public/modules/chat-bubble-renderer.js +11 -5
  48. package/lib/public/modules/clay-file-links.js +2 -1
  49. package/lib/public/modules/clay-split-view-screen.js +6 -3
  50. package/lib/public/modules/clay-tool-pane-screen.js +35 -0
  51. package/lib/public/modules/contextual-clay-geometry.js +7 -3
  52. package/lib/public/modules/file-history-snapshot.js +12 -0
  53. package/lib/public/modules/file-pane-entrypoints.js +55 -0
  54. package/lib/public/modules/file-pane-history.js +78 -0
  55. package/lib/public/modules/file-pane-instances.js +140 -0
  56. package/lib/public/modules/file-pane-links.js +41 -0
  57. package/lib/public/modules/file-pane-render.js +48 -0
  58. package/lib/public/modules/file-pane-review.js +26 -0
  59. package/lib/public/modules/file-pane-toolbar.js +54 -0
  60. package/lib/public/modules/file-viewer-source.js +70 -0
  61. package/lib/public/modules/filebrowser.js +43 -82
  62. package/lib/public/modules/git-panel.js +5 -0
  63. package/lib/public/modules/issues.js +3 -4
  64. package/lib/public/modules/linear-panel.js +2 -2
  65. package/lib/public/modules/mate-knowledge-workbench.js +10 -6
  66. package/lib/public/modules/notifications.js +4 -1
  67. package/lib/public/modules/pane-activation.js +7 -3
  68. package/lib/public/modules/pane-binding.js +5 -1
  69. package/lib/public/modules/pane-browser-bridge.js +6 -2
  70. package/lib/public/modules/pane-browser-transport.js +110 -0
  71. package/lib/public/modules/pane-header-tooltips.js +32 -0
  72. package/lib/public/modules/pending-message-queue-drag.js +69 -0
  73. package/lib/public/modules/pending-message-queue.js +3 -33
  74. package/lib/public/modules/permission-control.js +6 -5
  75. package/lib/public/modules/plantuml-preview.js +12 -12
  76. package/lib/public/modules/project-admin-overview.js +125 -10
  77. package/lib/public/modules/project-grouping.js +9 -0
  78. package/lib/public/modules/right-workbench.js +34 -15
  79. package/lib/public/modules/scheduled-tasks.js +7 -6
  80. package/lib/public/modules/server-settings-controls.js +33 -4
  81. package/lib/public/modules/session-folder-toolbar.js +1 -1
  82. package/lib/public/modules/session-tool-item.js +86 -0
  83. package/lib/public/modules/settings-select.js +2 -1
  84. package/lib/public/modules/shared-browser-tabs.js +37 -21
  85. package/lib/public/modules/shared-browser.js +69 -9
  86. package/lib/public/modules/sidebar-projects.js +1 -10
  87. package/lib/public/modules/split-pane-renderer.js +1 -0
  88. package/lib/public/modules/split-session-empty-pane.js +47 -0
  89. package/lib/public/modules/split-view-create-menu.js +74 -65
  90. package/lib/public/modules/split-view-drag-preview.js +90 -0
  91. package/lib/public/modules/split-view-layout-client.js +1 -1
  92. package/lib/public/modules/split-view-layout-scope.js +4 -0
  93. package/lib/public/modules/split-view-picker-actions.js +71 -0
  94. package/lib/public/modules/split-view-picker-render.js +81 -0
  95. package/lib/public/modules/split-view-presentation-drag.js +36 -13
  96. package/lib/public/modules/split-view-presentation-drop.js +21 -18
  97. package/lib/public/modules/split-view-presentation-renderer.js +22 -17
  98. package/lib/public/modules/split-view-presentations.js +25 -33
  99. package/lib/public/modules/split-view-worker-activity.js +2 -2
  100. package/lib/public/modules/split-view-worker-transcript.js +70 -33
  101. package/lib/public/modules/split-view-workers.js +6 -4
  102. package/lib/public/modules/split-view.js +9 -2
  103. package/lib/public/modules/sticky-notes-browser.js +2 -2
  104. package/lib/public/modules/terminal-viewport.js +14 -0
  105. package/lib/public/modules/terminal.js +49 -23
  106. package/lib/public/modules/theme-palette.js +177 -0
  107. package/lib/public/modules/theme.js +2 -180
  108. package/lib/public/modules/tool-pane-chrome.js +88 -0
  109. package/lib/public/modules/tool-pane-drag.js +134 -0
  110. package/lib/public/modules/tool-pane-layout.js +95 -0
  111. package/lib/public/modules/tool-pane-legacy.js +60 -0
  112. package/lib/public/modules/tool-pane-tree.js +51 -0
  113. package/lib/public/modules/tools.js +5 -89
  114. package/lib/public/modules/tooltip.js +32 -6
  115. package/lib/public/modules/view-legacy-coordinates.js +16 -0
  116. package/lib/public/modules/view-mutations.js +24 -0
  117. package/lib/public/modules/view-pair-presentation.js +74 -0
  118. package/lib/public/modules/view-pair-validation.js +12 -0
  119. package/lib/public/modules/view-presentation-state.js +37 -0
  120. package/lib/public/modules/view-workspace-controller.js +210 -0
  121. package/lib/public/modules/view-workspace-state.js +103 -0
  122. package/lib/public/modules/view-workspace-validation.js +47 -0
  123. package/lib/public/modules/wireframe-workbench.js +13 -2
  124. package/lib/public/modules/workbench-layout-preference.js +12 -5
  125. package/lib/public/modules/workbench-panels.js +21 -0
  126. package/lib/public/modules/worker-message-previews.js +39 -0
  127. package/lib/public/pane-assets/app-5W6KJXK5.js +1 -0
  128. package/lib/public/pane-assets/chunk-2IJUTAB6.js +2 -0
  129. package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
  130. package/lib/public/pane-assets/chunk-7OVNC5HT.js +1 -0
  131. package/lib/public/pane-assets/chunk-BQN7B62H.js +3 -0
  132. package/lib/public/pane-assets/chunk-EMLN2Y7L.js +1 -0
  133. package/lib/public/pane-assets/chunk-ENPAHXLB.js +1 -0
  134. package/lib/public/pane-assets/chunk-EPBGGZRT.js +1 -0
  135. package/lib/public/pane-assets/chunk-EYTE56IA.js +1 -0
  136. package/lib/public/pane-assets/chunk-FQTNR4ZS.js +1 -0
  137. package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
  138. package/lib/public/pane-assets/chunk-L6LSA5WI.js +1 -0
  139. package/lib/public/pane-assets/chunk-M57UIGZV.js +1 -0
  140. package/lib/public/pane-assets/chunk-MGTFTEJP.js +1 -0
  141. package/lib/public/pane-assets/chunk-MMG6MYCN.js +1 -0
  142. package/lib/public/pane-assets/chunk-MZLMEXMW.js +1 -0
  143. package/lib/public/pane-assets/chunk-OG3E5SVA.js +166 -0
  144. package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
  145. package/lib/public/pane-assets/chunk-RGAFZ3T2.js +1 -0
  146. package/lib/public/pane-assets/chunk-RNDRURMF.js +1 -0
  147. package/lib/public/pane-assets/chunk-SJOT74VW.js +2 -0
  148. package/lib/public/pane-assets/chunk-SJTGHIN5.js +1 -0
  149. package/lib/public/pane-assets/chunk-SLMAFKI2.js +1 -0
  150. package/lib/public/pane-assets/chunk-U3I5KY3B.js +1 -0
  151. package/lib/public/pane-assets/chunk-UPXU2IPC.js +1 -0
  152. package/lib/public/pane-assets/chunk-VEBYD7TL.js +1 -0
  153. package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
  154. package/lib/public/pane-assets/chunk-XBHYNXXT.js +1 -0
  155. package/lib/public/pane-assets/manifest.json +438 -381
  156. package/lib/public/pane-assets/style-BSKKK54Q.css +1 -0
  157. package/lib/public/style.css +1 -0
  158. package/lib/sdk-message-processor.js +5 -2
  159. package/lib/server-storage.js +27 -3
  160. package/lib/server-workbench-layout-settings.js +3 -3
  161. package/lib/server.js +4 -2
  162. package/lib/sessions.js +1 -1
  163. package/lib/split-view-layout.js +11 -7
  164. package/lib/storage-usage.js +29 -7
  165. package/lib/terminal-manager.js +2 -2
  166. package/lib/users-workbench-layout-preferences.js +14 -4
  167. package/lib/workbench-window-layout.js +30 -0
  168. package/lib/worker-message-previews.js +50 -0
  169. package/lib/ws-schema.js +1 -1
  170. package/lib/yoke/adapters/claude-worker.js +8 -0
  171. package/lib/yoke/adapters/claude.js +17 -2
  172. package/lib/yoke/claude-task-reader.js +94 -0
  173. package/lib/yoke/interface.js +1 -0
  174. package/package.json +1 -1
  175. package/lib/public/pane-assets/app-6H5S2Y3Z.js +0 -1
  176. package/lib/public/pane-assets/chunk-3QV5SDC4.js +0 -1
  177. package/lib/public/pane-assets/chunk-4FM3U3DW.js +0 -1
  178. package/lib/public/pane-assets/chunk-7GZH3FZY.js +0 -1
  179. package/lib/public/pane-assets/chunk-AGJTF5WQ.js +0 -1
  180. package/lib/public/pane-assets/chunk-DGOFGEUK.js +0 -1
  181. package/lib/public/pane-assets/chunk-I57URYSL.js +0 -1
  182. package/lib/public/pane-assets/chunk-NPKRTUNG.js +0 -2
  183. package/lib/public/pane-assets/chunk-VGA2Y3D2.js +0 -2
  184. package/lib/public/pane-assets/chunk-WKO3T75E.js +0 -167
  185. package/lib/public/pane-assets/style-TSRDU54Z.css +0 -1
@@ -1,83 +1,92 @@
1
- // Pane-header picker for explicitly choosing real Driver sessions for a Split view.
1
+ // Direction menu, then a centered session picker inside the reserved empty pane.
2
2
  import { store } from './store.js';
3
- import { getCachedSessions } from './sidebar-sessions.js';
4
- import { viewForOrigin } from './split-view-presentation-lookup.js';
5
- import { iconHtml } from './icons.js';
3
+ import { retryOptions } from './session-create.js';
4
+ import { pickerReceipt, pickerCurrent, insertPickerSession, createPickerSession } from './split-view-picker-actions.js';
5
+ import { renderPicker, renderPickerResults } from './split-view-picker-render.js';
6
+ import { showToast } from './utils.js';
6
7
 
7
8
  function menuElement() { return document.getElementById('split-view-create-menu'); }
8
9
  function close(returnFocus) {
9
10
  var menu = menuElement(); if (!menu) return;
10
- menu.hidden = true; store.set({ projectSplitViewCreateSelection: [] });
11
- if (returnFocus && menu.clayCreateAnchor && menu.clayCreateAnchor.isConnected) menu.clayCreateAnchor.focus();
12
- menu.clayCreateAnchor = null;
11
+ var state = store.get('splitSessionPicker');
12
+ var update = { splitSessionPicker: null, projectSplitViewCreateSelection: [] };
13
+ if (returnFocus && state && pickerCurrent(state.receipt) && !store.get('projectSplitViewPending')) update.projectSplitViewFocusedOrigin = state.priorFocus || null;
14
+ var anchor = menu.clayCreateAnchor; menu.clayCreateAnchor = null;
15
+ menu.hidden = true; store.set(update);
16
+ if (menu.contains(document.activeElement)) document.activeElement.blur();
17
+ if (menu.parentNode !== document.body) document.body.appendChild(menu);
18
+ if (anchor) anchor.setAttribute('aria-expanded', 'false');
19
+ document.dispatchEvent(new CustomEvent('clay-split-picker-layout'));
20
+ if (returnFocus && anchor && anchor.isConnected) anchor.focus();
13
21
  }
14
- function refreshCreateButton(menu) {
15
- var selected = store.get('projectSplitViewCreateSelection') || [], action = menu.querySelector('[data-create-split-view]');
16
- action.disabled = selected.length < 2 || selected.length > 8;
17
- action.textContent = 'Create Split view (' + selected.length + ')';
22
+ function position() {
23
+ var menu = menuElement(), anchor = menu && menu.clayCreateAnchor;
24
+ if (!menu || menu.hidden) return;
25
+ if (!anchor || !anchor.isConnected) { close(false); return; }
26
+ var state = store.get('splitSessionPicker');
27
+ if (state && state.side) { menu.style.left = ''; menu.style.top = ''; return; }
28
+ var rect = anchor.getBoundingClientRect(), width = menu.offsetWidth;
29
+ menu.style.left = Math.max(8, Math.min(rect.right - width, innerWidth - width - 8)) + 'px';
30
+ menu.style.top = Math.max(8, Math.min(rect.bottom + 5, innerHeight - menu.offsetHeight - 8)) + 'px';
18
31
  }
19
32
  function open(anchor) {
33
+ var receipt = pickerReceipt(anchor);
34
+ if (!receipt || !pickerCurrent(receipt)) { showToast('Wait for this View to finish loading, then try again.', 'warn'); return; }
20
35
  var menu = menuElement();
36
+ if (menu && !menu.hidden && menu.clayCreateAnchor === anchor) { close(true); return; }
37
+ if (menu && !menu.hidden) close(false);
21
38
  if (!menu) {
22
- menu = document.createElement('section'); menu.id = 'split-view-create-menu'; menu.className = 'split-view-arrange';
23
- menu.hidden = true; menu.setAttribute('role', 'dialog'); menu.setAttribute('aria-label', 'Arrange sessions');
24
- var header = document.createElement('div'); header.className = 'split-view-arrange-header';
25
- var title = document.createElement('strong'); title.textContent = 'Arrange sessions'; header.appendChild(title);
26
- var closeButton = document.createElement('button'); closeButton.type = 'button'; closeButton.setAttribute('aria-label', 'Close arrangement picker'); closeButton.innerHTML = iconHtml('x'); header.appendChild(closeButton); menu.appendChild(header);
27
- var list = document.createElement('div'); list.className = 'split-view-arrange-grid'; list.setAttribute('role', 'group'); list.setAttribute('aria-label', 'Choose sessions');
28
- list.style.cssText = 'display:flex;flex-direction:column;max-height:230px;overflow:auto'; menu.appendChild(list);
29
- var create = document.createElement('button'); create.type = 'button'; create.dataset.createSplitView = 'true'; menu.appendChild(create);
30
- document.body.appendChild(menu);
39
+ menu = document.createElement('section'); menu.id = 'split-view-create-menu'; menu.className = 'split-view-arrange split-session-picker'; menu.hidden = true;
40
+ menu.setAttribute('role', 'dialog'); menu.setAttribute('aria-label', 'Add a session split'); document.body.appendChild(menu);
31
41
  }
32
- menu.clayCreateAnchor = anchor;
33
- var list = menu.querySelector('[aria-label="Choose sessions"]'); list.textContent = '';
34
- var activeView = (store.get('projectSplitViews') || []).find(function (view) { return view.id === store.get('projectSplitViewActiveId'); });
35
- var currentId = Number(store.get('activeSessionId'));
36
- var current = (getCachedSessions() || []).find(function (session) { return session.id === currentId; });
37
- var selected = activeView ? activeView.sessions.map(function (item) { return item.sessionOriginId; }) :
38
- current && current.sessionRole !== 'worker' && !current.isMate && !current.isTui ? [current.sessionOriginId] : [];
39
- var candidates = (getCachedSessions() || []).filter(function (session) {
40
- if (!session.sessionOriginId || session.sessionRole === 'worker' || session.isMate || session.isTui) return false;
41
- var owner = viewForOrigin(session.sessionOriginId); return !owner || activeView && owner.id === activeView.id;
42
- });
43
- candidates.forEach(function (session) {
44
- var choice = document.createElement('button'); choice.type = 'button'; choice.dataset.createSessionOrigin = session.sessionOriginId;
45
- choice.setAttribute('aria-pressed', selected.indexOf(session.sessionOriginId) >= 0 ? 'true' : 'false');
46
- choice.textContent = session.title || 'Session'; list.appendChild(choice);
47
- });
48
- store.set({ projectSplitViewCreateSelection: selected }); refreshCreateButton(menu); menu.hidden = false;
49
- var rect = anchor.getBoundingClientRect(), width = Math.min(280, window.innerWidth - 16);
50
- menu.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)) + 'px';
51
- menu.style.top = Math.max(8, Math.min(rect.bottom + 4, window.innerHeight - menu.offsetHeight - 8)) + 'px';
52
- var first = menu.querySelector('[data-create-session-origin]'); if (first) first.focus();
42
+ receipt.reservationId = 'split-picker-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
43
+ menu.clayCreateAnchor = anchor; anchor.setAttribute('aria-expanded', 'true');
44
+ store.set({ splitSessionPicker: { receipt: receipt, side: null, query: '', error: '', priorFocus: store.get('projectSplitViewFocusedOrigin') } });
45
+ renderPicker(menu); menu.hidden = false; position(); menu.querySelector('[data-split-side]').focus();
53
46
  }
54
47
  function onClick(event) {
55
- var target = event.target;
56
- var menu = menuElement(); if (!menu || menu.hidden) return;
57
- if (menu.contains(target)) {
58
- var closeButton = target.closest('[aria-label="Close arrangement picker"]');
59
- if (closeButton) { event.preventDefault(); close(true); return; }
60
- var choice = target.closest('[data-create-session-origin]');
61
- if (choice) {
62
- event.preventDefault(); var selected = (store.get('projectSplitViewCreateSelection') || []).slice(), origin = choice.dataset.createSessionOrigin, index = selected.indexOf(origin);
63
- if (index === -1 && selected.length >= 8) return;
64
- if (index === -1) selected.push(origin); else selected.splice(index, 1);
65
- store.set({ projectSplitViewCreateSelection: selected }); choice.setAttribute('aria-pressed', selected.indexOf(origin) >= 0 ? 'true' : 'false'); refreshCreateButton(menu); return;
66
- }
67
- if (target.closest('[data-create-split-view]') && !target.closest('[data-create-split-view]').disabled) {
68
- event.preventDefault(); document.dispatchEvent(new CustomEvent('clay-split-view-create', { detail: { origins: (store.get('projectSplitViewCreateSelection') || []).slice() } })); close(true);
69
- }
70
- return;
48
+ var menu = menuElement(), state = store.get('splitSessionPicker'); if (!menu || menu.hidden || !state) return;
49
+ var button = event.target.closest && event.target.closest('button');
50
+ if (!menu.contains(event.target)) { if (!state.side && (!event.target.closest || !event.target.closest('.split-view-arrange-trigger'))) close(false); return; }
51
+ if (!button || button.disabled) return;
52
+ event.preventDefault();
53
+ if (button.dataset.splitClose) { close(true); return; }
54
+ if (!pickerCurrent(state.receipt)) { close(true); return; }
55
+ if (button.dataset.splitSide) {
56
+ store.set({ splitSessionPicker: Object.assign({}, state, { side: button.dataset.splitSide, query: '' }), projectSplitViewFocusedOrigin: null }); renderPicker(menu); document.dispatchEvent(new CustomEvent('clay-split-picker-layout')); position(); menu.querySelector('input').focus(); retryOptions(); return;
71
57
  }
72
- if (target.closest && target.closest('.split-view-arrange-trigger')) return;
73
- close(false);
58
+ if (button.dataset.splitRetry) { retryOptions(); return; }
59
+ var accepted = button.dataset.splitSession ? insertPickerSession(state.receipt, state.side, Number(button.dataset.splitSession)) : button.dataset.splitVendor ? createPickerSession(state.receipt, state.side, button.dataset.splitVendor) : false;
60
+ if (accepted) renderPickerResults(menu);
61
+ else { store.set({ splitSessionPicker: Object.assign({}, state, { error: 'The split could not be added. Check the View and try again.' }) }); renderPickerResults(menu); }
62
+ }
63
+ function onKey(event) {
64
+ var menu = menuElement(), state = store.get('splitSessionPicker'); if (!menu || menu.hidden || !state) return;
65
+ if (event.key === 'Escape') { event.preventDefault(); event.stopImmediatePropagation(); close(true); return; }
66
+ if (!menu.contains(event.target)) return;
67
+ var keys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
68
+ if (keys.indexOf(event.key) < 0 && !(event.key === 'Enter' && event.target.tagName === 'INPUT')) return;
69
+ var items = Array.from(menu.querySelectorAll('[data-split-side], [data-split-session], [data-split-vendor]')).filter(function (button) { return !button.disabled; });
70
+ if (!items.length) return;
71
+ if (event.target.tagName === 'INPUT' && (event.key === 'Home' || event.key === 'End')) return;
72
+ event.preventDefault();
73
+ var index = items.indexOf(document.activeElement);
74
+ if (event.key === 'Enter') { items[0].click(); return; }
75
+ var next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : index < 0 ? event.key === 'ArrowUp' ? items.length - 1 : 0 : (index + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
76
+ items[next].focus();
74
77
  }
75
78
  export function initSplitViewCreateMenu() {
76
79
  document.addEventListener('clay-split-view-open-create', function (event) { if (event.detail && event.detail.anchor) open(event.detail.anchor); });
77
- document.addEventListener('click', onClick, true);
78
- document.addEventListener('keydown', function (event) {
79
- if (event.key !== 'Escape' || !menuElement() || menuElement().hidden) return;
80
- event.preventDefault(); event.stopImmediatePropagation(); close(true);
81
- }, true);
82
- window.addEventListener('resize', function () { if (menuElement() && !menuElement().hidden) close(false); });
80
+ document.addEventListener('click', onClick, true); document.addEventListener('keydown', onKey, true);
81
+ document.addEventListener('input', function (event) {
82
+ var state = store.get('splitSessionPicker'); if (!state || !event.target.matches('#split-view-create-menu input')) return;
83
+ store.set({ splitSessionPicker: Object.assign({}, state, { query: event.target.value }) });
84
+ var label = menuElement().querySelector('.split-session-section'); label.textContent = event.target.value ? 'Search results' : 'Recent sessions'; renderPickerResults(menuElement()); position();
85
+ });
86
+ window.addEventListener('resize', position);
87
+ store.subscribe(function (state, previous) {
88
+ if (!state.splitSessionPicker) return;
89
+ if (!pickerCurrent(state.splitSessionPicker.receipt)) { close(false); return; }
90
+ if (state.sessionCreateOptions !== previous.sessionCreateOptions || state.projectSplitViewPending !== previous.projectSplitViewPending || state.paneBindingSessions !== previous.paneBindingSessions || state.splitSessionCreation !== previous.splitSessionCreation) { renderPickerResults(menuElement()); position(); }
91
+ });
83
92
  }
@@ -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; });
@@ -36,3 +36,7 @@ export function presentationTree(view) {
36
36
  JSON.stringify(receipt) === JSON.stringify(layoutScope(view)) ?
37
37
  store.get('projectSplitViewPreviewTree') : view && view.tree;
38
38
  }
39
+ export function bindingMatches(left, right) {
40
+ return !!left && !!right && left.sessionId === right.sessionId && left.sessionOriginId === right.sessionOriginId &&
41
+ left.generation === right.generation && left.requestId === right.requestId;
42
+ }
@@ -0,0 +1,71 @@
1
+ // Exact-context session insertion through the existing View and creation services.
2
+ import { store } from './store.js';
3
+ import { getWs } from './ws-ref.js';
4
+ import { getCachedSessions } from './sidebar-sessions.js';
5
+ import { activeSavedView } from './view-workspace-state.js';
6
+ import { layoutScope, currentLayoutScope } from './split-view-layout-scope.js';
7
+ import { viewForOrigin } from './split-view-presentation-lookup.js';
8
+ import { applyPresentationDrop } from './split-view-presentation-drop.js';
9
+ import { mutation } from './view-mutations.js';
10
+ import { checkComposerDeparture } from './session-rich-drafts.js';
11
+ import { createOptions } from './session-create.js';
12
+ import { showToast } from './utils.js';
13
+
14
+ export function pickerReceipt(anchor) {
15
+ var view = activeSavedView(), pane = anchor && anchor.closest('.split-pane'), frame = pane && pane.querySelector('.split-pane-frame');
16
+ var origin = pane && pane.dataset.presentationOrigin || frame && frame.dataset.sessionOriginId || store.get('sessionOriginId');
17
+ return view && view.sessions.some(function (session) { return session.sessionOriginId === origin; }) ? { scope: layoutScope(view), origin: origin } : null;
18
+ }
19
+ export function pickerCurrent(receipt) {
20
+ var picker = store.get('splitSessionPicker');
21
+ return receipt && (!receipt.reservationId || picker && picker.receipt.reservationId === receipt.reservationId) && !store.get('paneMode') && currentLayoutScope(receipt.scope) && activeSavedView().sessions.some(function (session) { return session.sessionOriginId === receipt.origin; });
22
+ }
23
+ export function pickerCapacity() {
24
+ var view = activeSavedView();
25
+ return !!view && view.sessions.length + (view.workspace.linkedPair ? view.workspace.linkedPair.workerOrigins.length : 0) < 8;
26
+ }
27
+ function checkDrafts() {
28
+ checkComposerDeparture();
29
+ document.querySelectorAll('#split-host .split-pane:not(.split-pane-cached) .split-pane-frame').forEach(function (frame) { if (frame.clayPane) frame.clayPane.checkDraft(); });
30
+ }
31
+ export function insertPickerSession(receipt, side, id) {
32
+ if (['left', 'right', 'top', 'bottom'].indexOf(side) < 0 || !pickerCurrent(receipt) || !pickerCapacity() || store.get('projectSplitViewPending') || store.get('projectSplitViewError')) return false;
33
+ var view = activeSavedView(), session = (getCachedSessions() || []).find(function (item) { return item.id === id; });
34
+ if (!session || session.sessionRole === 'worker' || session.isMate || session.isTui || (session.ownerId || null) !== (store.get('myUserId') || null) || view.sessions.some(function (ref) { return ref.sessionOriginId === session.sessionOriginId; })) return false;
35
+ var source = viewForOrigin(session.sessionOriginId);
36
+ return applyPresentationDrop({ origin: session.sessionOriginId, sessionId: id, sourceViewId: source && source.id },
37
+ { origin: receipt.origin, viewId: view.id, side: side }, mutation, checkDrafts) === true;
38
+ }
39
+ export function createPickerSession(receipt, side, vendor) {
40
+ if (['left', 'right', 'top', 'bottom'].indexOf(side) < 0 || !pickerCurrent(receipt) || !pickerCapacity() || store.get('splitSessionCreation') || store.get('projectSplitViewPending')) return false;
41
+ if (!createOptions().vendors.some(function (item) { return item.id === vendor && item.installed; })) return false;
42
+ try { checkDrafts(); } catch (error) { showToast(error.message, 'warn'); return false; }
43
+ var id = 'split-create-' + (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : Date.now().toString(36) + '-' + Math.random().toString(36).slice(2));
44
+ store.set({ splitSessionCreation: { receipt: receipt, side: side, requestId: id, createdId: null } });
45
+ try { getWs().send(JSON.stringify({ type: 'new_session', slug: store.get('currentSlug'), requestId: id, vendor: vendor, forceNew: true, activate: false })); }
46
+ catch (error) { store.set({ splitSessionCreation: null }); return false; }
47
+ setTimeout(function () {
48
+ var pending = store.get('splitSessionCreation');
49
+ if (pending && pending.requestId === id) {
50
+ store.set({ splitSessionCreation: null });
51
+ showToast('Creation was not confirmed. Check recent sessions before trying again.', 'warn');
52
+ }
53
+ }, 30000);
54
+ return true;
55
+ }
56
+ export function handleSplitSessionCreated(message) {
57
+ var pending = store.get('splitSessionCreation');
58
+ if (message.type !== 'new_session_result' || !pending || message.requestId !== pending.requestId) return false;
59
+ if (!message.ok) { store.set({ splitSessionCreation: null }); showToast(message.error || 'The session could not be created.', 'error'); return true; }
60
+ store.set({ splitSessionCreation: Object.assign({}, pending, { createdId: message.sessionId }) });
61
+ finishPickerCreation(); return true;
62
+ }
63
+ export function finishPickerCreation() {
64
+ var pending = store.get('splitSessionCreation');
65
+ if (!pending || !pending.createdId || !getCachedSessions().some(function (session) { return session.id === pending.createdId; })) return;
66
+ store.set({ splitSessionCreation: null });
67
+ if (!insertPickerSession(pending.receipt, pending.side, pending.createdId)) showToast('The session was created in Unfiled. The View changed, so it was not added to the split.', 'warn');
68
+ }
69
+ store.subscribe(function (state, previous) {
70
+ if (state.splitSessionCreation && (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration || !state.connected)) store.set({ splitSessionCreation: null });
71
+ });
@@ -0,0 +1,81 @@
1
+ import { store } from './store.js';
2
+ import { getCachedSessions } from './sidebar-sessions.js';
3
+ import { activeSavedView } from './view-workspace-state.js';
4
+ import { viewForOrigin } from './split-view-presentation-lookup.js';
5
+ import { createOptions } from './session-create.js';
6
+ import { sortVendorEntries } from './vendor-priority.js';
7
+ import { VENDOR_AVATARS, VENDOR_NAMES } from './chat-render-runtime.js';
8
+ import { iconHtml, refreshIcons } from './icons.js';
9
+ import { pickerCapacity } from './split-view-picker-actions.js';
10
+
11
+ export var SPLIT_SIDES = [['right', 'Right', 'panel-right-open'], ['left', 'Left', 'panel-left-open'], ['top', 'Top', 'panel-top-open'], ['bottom', 'Bottom', 'panel-bottom-open']];
12
+ function button(label, action, value) {
13
+ var el = document.createElement('button'); el.type = 'button'; el.dataset[action] = value; el.setAttribute('aria-label', label); el.title = label; return el;
14
+ }
15
+ function vendorIcon(vendor) {
16
+ var image = document.createElement('img'); image.src = VENDOR_AVATARS[vendor] || '/icon-192.png'; image.alt = ''; return image;
17
+ }
18
+ function age(time) {
19
+ var minutes = Math.max(0, Math.floor((Date.now() - (time || 0)) / 60000));
20
+ return minutes < 1 ? 'Now' : minutes < 60 ? minutes + 'm' : minutes < 1440 ? Math.floor(minutes / 60) + 'h' : Math.floor(minutes / 1440) + 'd';
21
+ }
22
+ export function pickerCandidates(query) {
23
+ var view = activeSavedView(), text = (query || '').trim().toLocaleLowerCase();
24
+ return (getCachedSessions() || []).filter(function (session) {
25
+ return view && session.sessionOriginId && !session.hidden && session.sessionRole !== 'worker' && !session.isMate && !session.isTui &&
26
+ (session.ownerId || null) === (store.get('myUserId') || null) && !view.sessions.some(function (ref) { return ref.sessionOriginId === session.sessionOriginId; }) &&
27
+ (!text || (session.title || 'New session').toLocaleLowerCase().indexOf(text) >= 0);
28
+ }).sort(function (a, b) { return (b.lastActivity || b.createdAt || 0) - (a.lastActivity || a.createdAt || 0); }).slice(0, text ? 40 : 6);
29
+ }
30
+ export function renderPickerResults(menu) {
31
+ var state = store.get('splitSessionPicker'); if (!state) return;
32
+ var list = menu.querySelector('.split-session-results'); if (!list) return;
33
+ var focused = menu.contains(document.activeElement) && document.activeElement.dataset, focusKey = focused && (focused.splitSession ? ['splitSession', focused.splitSession] : focused.splitVendor ? ['splitVendor', focused.splitVendor] : null);
34
+ list.replaceChildren();
35
+ var candidates = pickerCandidates(state.query), blocked = !pickerCapacity() || !!store.get('projectSplitViewPending') || !!store.get('splitSessionCreation');
36
+ candidates.forEach(function (session) {
37
+ var title = session.title || 'New session', owner = viewForOrigin(session.sessionOriginId);
38
+ var choice = button('Add ' + title, 'splitSession', String(session.id)); choice.className = 'split-session-result'; choice.disabled = blocked;
39
+ choice.appendChild(vendorIcon(session.vendor));
40
+ var content = document.createElement('span'); content.className = 'split-session-result-title'; content.textContent = title; choice.appendChild(content);
41
+ var time = document.createElement('small'); time.textContent = age(session.lastActivity || session.createdAt); choice.appendChild(time);
42
+ choice.title = title + (owner ? ' · Move from ' + owner.name : '');
43
+ if (owner && owner.sessions.length > 1) { var note = document.createElement('small'); note.className = 'split-session-move'; note.textContent = 'Move from ' + owner.name; content.appendChild(note); }
44
+ list.appendChild(choice);
45
+ });
46
+ if (!candidates.length) { var empty = document.createElement('p'); empty.className = 'split-session-empty'; empty.textContent = state.query ? 'No matching sessions. Start a new one below.' : 'No other sessions yet. Start a new one below.'; list.appendChild(empty); }
47
+ var vendors = menu.querySelector('.split-session-vendors'); vendors.replaceChildren();
48
+ var options = createOptions(), installed = sortVendorEntries(options.vendors.filter(function (vendor) { return vendor.installed; }));
49
+ installed.forEach(function (vendor) {
50
+ var label = 'New ' + (VENDOR_NAMES[vendor.id] || vendor.displayName || vendor.id) + ' session';
51
+ var control = button(label, 'splitVendor', vendor.id); control.disabled = blocked; control.appendChild(vendorIcon(vendor.id)); vendors.appendChild(control);
52
+ });
53
+ if (options.error || !installed.length) {
54
+ var hint = document.createElement('span'); hint.className = 'split-session-empty'; hint.textContent = options.error || (options.loaded ? 'No installed vendors available.' : 'Loading vendors…'); vendors.appendChild(hint);
55
+ if (options.error) { var retry = button('Retry loading vendors', 'splitRetry', 'true'); retry.textContent = 'Retry'; vendors.appendChild(retry); }
56
+ }
57
+ var status = menu.querySelector('[role="status"]');
58
+ if (focusKey) { var restored = Array.from(menu.querySelectorAll('button')).find(function (item) { return item.dataset[focusKey[0]] === focusKey[1] && !item.disabled; }); if (restored) restored.focus({ preventScroll: true }); }
59
+ status.textContent = !pickerCapacity() ? 'This View already has eight session panes.' : store.get('projectSplitViewPending') ? 'Saving the View…' : store.get('splitSessionCreation') ? 'Creating session…' : state.error || '';
60
+ }
61
+ export function renderPicker(menu) {
62
+ var state = store.get('splitSessionPicker'); if (!state) return;
63
+ menu.replaceChildren(); menu.dataset.stage = state.side ? 'sessions' : 'direction';
64
+ if (!state.side) {
65
+ var header = document.createElement('div'); header.className = 'split-view-arrange-header';
66
+ var title = document.createElement('strong'); title.textContent = 'Add split'; header.appendChild(title);
67
+ var close = button('Close split picker', 'splitClose', 'true'); close.innerHTML = iconHtml('x'); header.appendChild(close); menu.appendChild(header);
68
+ var directions = document.createElement('div'); directions.className = 'split-session-directions';
69
+ SPLIT_SIDES.forEach(function (side) { var choice = button('Add ' + side[1] + ' Split', 'splitSide', side[0]); choice.disabled = !pickerCapacity() || !!store.get('projectSplitViewPending'); choice.innerHTML = iconHtml(side[2]); var label = document.createElement('span'); label.textContent = 'Add ' + side[1] + ' Split'; choice.appendChild(label); directions.appendChild(choice); });
70
+ menu.appendChild(directions); refreshIcons(); return;
71
+ }
72
+ var search = document.createElement('label'); search.className = 'split-session-search'; search.innerHTML = iconHtml('search');
73
+ var input = document.createElement('input'); input.type = 'search'; input.placeholder = 'Search sessions…'; input.setAttribute('aria-label', 'Search sessions'); input.value = state.query || ''; search.appendChild(input); menu.appendChild(search);
74
+ var recent = document.createElement('div'); recent.className = 'split-session-section'; recent.textContent = state.query ? 'Search results' : 'Recent sessions'; menu.appendChild(recent);
75
+ var list = document.createElement('div'); list.className = 'split-session-results'; list.setAttribute('aria-label', 'Available sessions'); menu.appendChild(list);
76
+ var heading = document.createElement('div'); heading.className = 'split-session-section split-session-new'; heading.textContent = 'New session'; menu.appendChild(heading);
77
+ var vendors = document.createElement('div'); vendors.className = 'split-session-vendors'; vendors.setAttribute('aria-label', 'Installed vendors'); menu.appendChild(vendors);
78
+ var status = document.createElement('p'); status.className = 'split-session-status'; status.setAttribute('role', 'status'); menu.appendChild(status);
79
+ var footer = document.createElement('small'); footer.className = 'split-session-hint'; footer.textContent = '↑↓ Choose · Enter Open · Esc Cancel'; menu.appendChild(footer);
80
+ renderPickerResults(menu); refreshIcons();
81
+ }
@@ -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) {