clay-server 5.3.0-beta.5 → 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 (129) 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-file-history.js +11 -9
  10. package/lib/project-file-watch.js +26 -7
  11. package/lib/project-filesystem.js +5 -4
  12. package/lib/project-shared-browser.js +7 -3
  13. package/lib/project-split-view-document.js +16 -2
  14. package/lib/project-split-views.js +15 -294
  15. package/lib/project-user-message.js +4 -3
  16. package/lib/project-vendor-login.js +2 -2
  17. package/lib/project-view-input.js +11 -0
  18. package/lib/project-view-pair.js +26 -0
  19. package/lib/project-view-store.js +346 -0
  20. package/lib/project-view-terminal-binding.js +16 -0
  21. package/lib/project-view-workspace.js +126 -0
  22. package/lib/project.js +1 -0
  23. package/lib/public/app.js +2 -0
  24. package/lib/public/css/issues.css +3 -0
  25. package/lib/public/css/pane.css +5 -1
  26. package/lib/public/css/right-workbench.css +0 -1
  27. package/lib/public/css/scheduled-tasks.css +7 -0
  28. package/lib/public/css/split-view-layout.css +37 -3
  29. package/lib/public/css/tool-pane.css +127 -0
  30. package/lib/public/index.html +1 -1
  31. package/lib/public/modules/clay-file-links.js +2 -1
  32. package/lib/public/modules/clay-split-view-screen.js +6 -3
  33. package/lib/public/modules/clay-tool-pane-screen.js +35 -0
  34. package/lib/public/modules/file-history-snapshot.js +12 -0
  35. package/lib/public/modules/file-pane-entrypoints.js +55 -0
  36. package/lib/public/modules/file-pane-history.js +78 -0
  37. package/lib/public/modules/file-pane-instances.js +140 -0
  38. package/lib/public/modules/file-pane-links.js +41 -0
  39. package/lib/public/modules/file-pane-render.js +48 -0
  40. package/lib/public/modules/file-pane-review.js +26 -0
  41. package/lib/public/modules/file-pane-toolbar.js +54 -0
  42. package/lib/public/modules/file-viewer-source.js +70 -0
  43. package/lib/public/modules/filebrowser.js +43 -82
  44. package/lib/public/modules/git-panel.js +5 -0
  45. package/lib/public/modules/issues.js +3 -4
  46. package/lib/public/modules/linear-panel.js +2 -2
  47. package/lib/public/modules/mate-knowledge-workbench.js +10 -6
  48. package/lib/public/modules/notifications.js +4 -1
  49. package/lib/public/modules/pane-activation.js +2 -0
  50. package/lib/public/modules/pane-binding.js +5 -1
  51. package/lib/public/modules/pane-browser-bridge.js +6 -2
  52. package/lib/public/modules/pane-browser-transport.js +110 -0
  53. package/lib/public/modules/plantuml-preview.js +12 -12
  54. package/lib/public/modules/right-workbench.js +34 -15
  55. package/lib/public/modules/scheduled-tasks.js +7 -6
  56. package/lib/public/modules/shared-browser.js +69 -9
  57. package/lib/public/modules/split-pane-renderer.js +1 -0
  58. package/lib/public/modules/split-view-drag-preview.js +90 -0
  59. package/lib/public/modules/split-view-layout-client.js +1 -1
  60. package/lib/public/modules/split-view-presentation-drag.js +36 -13
  61. package/lib/public/modules/split-view-presentation-drop.js +21 -18
  62. package/lib/public/modules/split-view-presentation-renderer.js +15 -10
  63. package/lib/public/modules/split-view-presentations.js +25 -33
  64. package/lib/public/modules/split-view.js +9 -2
  65. package/lib/public/modules/sticky-notes-browser.js +2 -2
  66. package/lib/public/modules/terminal.js +46 -19
  67. package/lib/public/modules/theme.js +1 -6
  68. package/lib/public/modules/tool-pane-chrome.js +82 -0
  69. package/lib/public/modules/tool-pane-drag.js +134 -0
  70. package/lib/public/modules/tool-pane-layout.js +95 -0
  71. package/lib/public/modules/tool-pane-legacy.js +60 -0
  72. package/lib/public/modules/tool-pane-tree.js +51 -0
  73. package/lib/public/modules/view-legacy-coordinates.js +16 -0
  74. package/lib/public/modules/view-mutations.js +24 -0
  75. package/lib/public/modules/view-pair-presentation.js +74 -0
  76. package/lib/public/modules/view-pair-validation.js +12 -0
  77. package/lib/public/modules/view-presentation-state.js +37 -0
  78. package/lib/public/modules/view-workspace-controller.js +210 -0
  79. package/lib/public/modules/view-workspace-state.js +103 -0
  80. package/lib/public/modules/view-workspace-validation.js +47 -0
  81. package/lib/public/modules/wireframe-workbench.js +11 -0
  82. package/lib/public/modules/workbench-layout-preference.js +12 -5
  83. package/lib/public/modules/workbench-panels.js +21 -0
  84. package/lib/public/pane-assets/{app-6H5S2Y3Z.js → app-7UZ727NA.js} +1 -1
  85. package/lib/public/pane-assets/chunk-2AIWCI6Y.js +1 -0
  86. package/lib/public/pane-assets/chunk-6MIHIQ2W.js +1 -0
  87. package/lib/public/pane-assets/chunk-6NYCNWVF.js +1 -0
  88. package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
  89. package/lib/public/pane-assets/chunk-6UJ2ALOB.js +1 -0
  90. package/lib/public/pane-assets/chunk-6XK2OATZ.js +1 -0
  91. package/lib/public/pane-assets/chunk-BKIDR2ME.js +1 -0
  92. package/lib/public/pane-assets/chunk-BMHTBRMO.js +1 -0
  93. package/lib/public/pane-assets/chunk-GEFCFZEN.js +1 -0
  94. package/lib/public/pane-assets/chunk-GN2NQEVC.js +2 -0
  95. package/lib/public/pane-assets/chunk-IMLMBPT4.js +166 -0
  96. package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
  97. package/lib/public/pane-assets/chunk-K5S6AILM.js +1 -0
  98. package/lib/public/pane-assets/chunk-LDIZLKB2.js +1 -0
  99. package/lib/public/pane-assets/chunk-OWNNLZ36.js +1 -0
  100. package/lib/public/pane-assets/chunk-P2C3LOWL.js +3 -0
  101. package/lib/public/pane-assets/chunk-PV4PCH2V.js +2 -0
  102. package/lib/public/pane-assets/chunk-R4LMPTKU.js +1 -0
  103. package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
  104. package/lib/public/pane-assets/chunk-TMGSDFJI.js +1 -0
  105. package/lib/public/pane-assets/chunk-VKYBAVWT.js +1 -0
  106. package/lib/public/pane-assets/chunk-VNAV23CM.js +1 -0
  107. package/lib/public/pane-assets/chunk-VWTHIQCE.js +1 -0
  108. package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
  109. package/lib/public/pane-assets/chunk-WM3K5AFH.js +1 -0
  110. package/lib/public/pane-assets/chunk-XOB3O327.js +1 -0
  111. package/lib/public/pane-assets/chunk-XOOUCFYT.js +1 -0
  112. package/lib/public/pane-assets/manifest.json +426 -381
  113. package/lib/public/pane-assets/style-ZM7OOU3M.css +1 -0
  114. package/lib/public/style.css +1 -0
  115. package/lib/server-workbench-layout-settings.js +3 -3
  116. package/lib/split-view-layout.js +11 -7
  117. package/lib/users-workbench-layout-preferences.js +14 -4
  118. package/lib/workbench-window-layout.js +30 -0
  119. package/package.json +1 -1
  120. package/lib/public/pane-assets/chunk-3QV5SDC4.js +0 -1
  121. package/lib/public/pane-assets/chunk-4FM3U3DW.js +0 -1
  122. package/lib/public/pane-assets/chunk-7GZH3FZY.js +0 -1
  123. package/lib/public/pane-assets/chunk-AGJTF5WQ.js +0 -1
  124. package/lib/public/pane-assets/chunk-DGOFGEUK.js +0 -1
  125. package/lib/public/pane-assets/chunk-I57URYSL.js +0 -1
  126. package/lib/public/pane-assets/chunk-NPKRTUNG.js +0 -2
  127. package/lib/public/pane-assets/chunk-VGA2Y3D2.js +0 -2
  128. package/lib/public/pane-assets/chunk-WKO3T75E.js +0 -167
  129. package/lib/public/pane-assets/style-TSRDU54Z.css +0 -1
@@ -0,0 +1,110 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { livePaneBinding, paneMessageBinding, samePaneBinding, paneTarget } from './pane-binding.js';
4
+
5
+ // Browser-only presentation bridge. Effects still use the source pane's real
6
+ // authenticated socket and the existing server browser service.
7
+ function browserSource() { return store.get('paneBrowserSource') || null; }
8
+ function focusedOrigin() {
9
+ var focused = store.get('projectSplitViewFocusedOrigin') || store.get('projectSplitViewSingletonOrigin');
10
+ if (focused) return focused;
11
+ var view = (store.get('projectSplitViews') || []).find(function (item) { return item.id === store.get('projectSplitViewActiveId'); });
12
+ return view && view.activeSessionOriginId || store.get('sessionOriginId') || null;
13
+ }
14
+ var receive = null;
15
+ var changed = null;
16
+ var fieldsByAction = {
17
+ state_request: [], open: ['url', 'newTab', 'incognito'], view: ['browserId', 'visible'],
18
+ control: ['browserId', 'control'], close: ['browserId'], end: ['browserId'], input: ['browserId', 'epoch', 'event'],
19
+ };
20
+ function frames() {
21
+ return Array.from(document.querySelectorAll('#split-host .split-pane:not(.split-pane-cached) .split-pane-frame'));
22
+ }
23
+ export function hasBrowserPaneHost() { return !store.get('paneMode') && frames().some(function (frame) { return !!frame.clayPane || !!paneTarget(store.snap(), store.get('paneBindingSessions') || [], Number(frame.dataset.sessionId)); }); }
24
+ function current() {
25
+ var source = browserSource();
26
+ if (!source || source.socket !== getWs() || source.owner !== store.get('myUserId') || source.project !== store.get('currentSlug')) return false;
27
+ var frame = source.frame;
28
+ return frames().includes(frame) && frame.contentWindow === source.window && frame.contentDocument === source.document &&
29
+ !frame.dataset.requestedSessionId && livePaneBinding(frame, { paneBinding: source.binding });
30
+ }
31
+ export function browserPaneSessionId() { return current() ? browserSource().binding.sessionId : null; }
32
+ export function browserPaneStamp() { var source = browserSource(); return current() ? source.binding.requestId + ':' + source.binding.generation : null; }
33
+ export function browserPaneBound() { return !!browserSource(); }
34
+ export function selectBrowserPane(frame, binding) {
35
+ var source = browserSource();
36
+ if (!frame || !binding || !frames().includes(frame) || frame.dataset.requestedSessionId || !livePaneBinding(frame, { paneBinding: binding })) return false;
37
+ if (current() && source.frame === frame && samePaneBinding(source.binding, binding)) {
38
+ if (source.focusedOrigin !== focusedOrigin()) store.set({ paneBrowserSource: Object.assign({}, source, { focusedOrigin: focusedOrigin() }) });
39
+ return true;
40
+ }
41
+ var previous = store.get('sharedBrowser');
42
+ if (current() && previous) sendBrowserPaneCommand('view', { browserId: previous.id, visible: false });
43
+ store.set({ paneBrowserSource: { frame: frame, window: frame.contentWindow, document: frame.contentDocument, binding: binding,
44
+ socket: getWs(), owner: store.get('myUserId'), project: store.get('currentSlug'), focusedOrigin: focusedOrigin() }, paneBrowserTarget: binding });
45
+ if (changed) changed(true);
46
+ return true;
47
+ }
48
+ export function ensureBrowserPaneSource() {
49
+ if (current() && browserSource().focusedOrigin === focusedOrigin()) return true;
50
+ var candidates = frames();
51
+ var origin = focusedOrigin();
52
+ var frame = candidates.find(function (item) { return origin && item.dataset.sessionOriginId === origin; }) ||
53
+ candidates.find(function (item) { var pane = item.closest('.split-pane'); return pane && pane.dataset.active === 'true'; }) || candidates[0];
54
+ var live = frame && frame.clayPane && frame.clayPane.identity();
55
+ return !!live && selectBrowserPane(frame, live.binding);
56
+ }
57
+ export function sendBrowserPaneCommand(action, fields) {
58
+ var source = browserSource();
59
+ if (!Object.prototype.hasOwnProperty.call(fieldsByAction, action)) return false;
60
+ if (!source && !hasBrowserPaneHost()) return null;
61
+ if (!current() || !source.frame.clayPane || typeof source.frame.clayPane.browserCommand !== 'function') return false;
62
+ return source.frame.clayPane.browserCommand(action, fields || {}, source.binding);
63
+ }
64
+ export function sendBoundBrowserCommand(action, fields, binding) {
65
+ var allowed = Object.prototype.hasOwnProperty.call(fieldsByAction, action) && fieldsByAction[action], ws = getWs();
66
+ if (!allowed || !ws || ws.readyState !== 1 || !samePaneBinding(binding, paneMessageBinding()) ||
67
+ !store.get('connected') || store.get('panePendingBinding') || store.get('activeSessionId') !== binding.sessionId) return false;
68
+ var message = { type: 'shared_browser_' + action, sessionId: binding.sessionId, paneBinding: binding };
69
+ allowed.forEach(function (key) { if (Object.prototype.hasOwnProperty.call(fields, key)) message[key] = fields[key]; });
70
+ ws.send(JSON.stringify(message));
71
+ return true;
72
+ }
73
+ export function forwardBrowserPaneMessage(message) {
74
+ var frame = window.frameElement;
75
+ if (!store.get('paneMode') || !frame || !frame.clayPane) return false;
76
+ if (['shared_browser_state', 'shared_browser_frame', 'shared_browser_error'].indexOf(message.type) < 0) return false;
77
+ var binding = paneMessageBinding();
78
+ if (binding && samePaneBinding(binding, message.paneActivation)) {
79
+ window.parent.postMessage({ type: 'clay-pane-browser-update', paneBinding: binding, message: message }, location.origin);
80
+ }
81
+ return true;
82
+ }
83
+ export function initBrowserPaneTransport(onMessage, onChanged) {
84
+ if (store.get('paneMode')) return;
85
+ receive = onMessage; changed = onChanged;
86
+ window.addEventListener('message', function (event) {
87
+ var data = event.data;
88
+ if (event.origin !== location.origin || !data || data.type !== 'clay-pane-browser-update' || !data.message) return;
89
+ var frame = frames().find(function (item) { return item.contentWindow === event.source; });
90
+ var message = data.message, source = browserSource();
91
+ if (!frame || !livePaneBinding(frame, data) || !samePaneBinding(data.paneBinding, message.paneActivation) ||
92
+ message.sessionId !== data.paneBinding.sessionId || ['shared_browser_state', 'shared_browser_frame', 'shared_browser_error'].indexOf(message.type) < 0) return;
93
+ if (!current() || source.frame !== frame || !samePaneBinding(source.binding, data.paneBinding)) {
94
+ if (message.type !== 'shared_browser_state' || !message.present || !selectBrowserPane(frame, data.paneBinding)) return;
95
+ }
96
+ receive(message, true);
97
+ });
98
+ document.addEventListener('clay-pane-ready', function () {
99
+ queueMicrotask(function () {
100
+ if (!current() && ensureBrowserPaneSource()) sendBrowserPaneCommand('state_request', {});
101
+ });
102
+ }, true);
103
+ store.subscribe(function () {
104
+ var source = browserSource();
105
+ if (source && !current()) {
106
+ store.set({ paneBrowserSource: null, paneBrowserTarget: null });
107
+ if (changed) changed(false);
108
+ }
109
+ });
110
+ }
@@ -6,17 +6,17 @@ export function isPlantUmlFile(path) {
6
6
  return /\.(puml|plantuml|pu|uml|salt)$/i.test(String(path || ""));
7
7
  }
8
8
 
9
- export function cancelPlantUmlPreview() {
10
- var job = store.get('plantUmlPreviewJob');
9
+ export function cancelPlantUmlPreview(instanceId) {
10
+ var job = store.get(instanceId ? 'plantUmlPreviewJob:' + instanceId : 'plantUmlPreviewJob');
11
11
  if (!job) return;
12
12
  clearTimeout(job.timer);
13
13
  job.observer.disconnect();
14
14
  job.controller.abort();
15
- store.set({ plantUmlPreviewJob: null });
15
+ var cleared = {}; cleared[instanceId ? 'plantUmlPreviewJob:' + instanceId : 'plantUmlPreviewJob'] = null; store.set(cleared);
16
16
  }
17
17
 
18
- export function renderPlantUmlPreview(body, source, path) {
19
- cancelPlantUmlPreview();
18
+ export function renderPlantUmlPreview(body, source, path, instanceId) {
19
+ cancelPlantUmlPreview(instanceId);
20
20
  body.innerHTML = "";
21
21
  var preview = document.createElement("div");
22
22
  preview.className = "file-viewer-plantuml-preview";
@@ -36,22 +36,22 @@ export function renderPlantUmlPreview(body, source, path) {
36
36
 
37
37
  var job = { controller: new AbortController(), observer: null, timer: null };
38
38
  job.observer = new MutationObserver(function () {
39
- if (!preview.isConnected && store.get('plantUmlPreviewJob') === job) cancelPlantUmlPreview();
39
+ if (!preview.isConnected && store.get(instanceId ? 'plantUmlPreviewJob:' + instanceId : 'plantUmlPreviewJob') === job) cancelPlantUmlPreview(instanceId);
40
40
  });
41
41
  job.observer.observe(body, { childList: true });
42
42
  job.timer = setTimeout(function () {
43
- if (store.get('plantUmlPreviewJob') !== job) return;
43
+ if (store.get(instanceId ? 'plantUmlPreviewJob:' + instanceId : 'plantUmlPreviewJob') !== job) return;
44
44
  showError("The diagram preview timed out.");
45
- cancelPlantUmlPreview();
45
+ cancelPlantUmlPreview(instanceId);
46
46
  }, 20000);
47
- store.set({ plantUmlPreviewJob: job });
47
+ var patch = {}; patch[instanceId ? 'plantUmlPreviewJob:' + instanceId : 'plantUmlPreviewJob'] = job; store.set(patch);
48
48
  fetch("api/file/plantuml?path=" + encodeURIComponent(path), { signal: job.controller.signal, cache: "no-store" }).then(function (response) {
49
49
  return response.text().then(function (text) {
50
50
  if (!response.ok) throw new Error(text || "Unable to render this diagram.");
51
51
  return text;
52
52
  });
53
53
  }).then(function (svg) {
54
- if (store.get('plantUmlPreviewJob') !== job || !preview.isConnected) return;
54
+ if (store.get(instanceId ? 'plantUmlPreviewJob:' + instanceId : 'plantUmlPreviewJob') !== job || !preview.isConnected) return;
55
55
  if (svg.length > 8 * 1024 * 1024 || !/<svg[\s>]/i.test(svg)) throw new Error("The renderer returned an invalid diagram.");
56
56
  var image = document.createElement("img");
57
57
  image.alt = "Diagram preview of " + path;
@@ -69,8 +69,8 @@ export function renderPlantUmlPreview(body, source, path) {
69
69
  mountWireframeViewport(viewport, image, toolbar);
70
70
 
71
71
  }).catch(function (error) {
72
- if (store.get('plantUmlPreviewJob') === job && error.name !== "AbortError") showError(error.message);
72
+ if (store.get(instanceId ? 'plantUmlPreviewJob:' + instanceId : 'plantUmlPreviewJob') === job && error.name !== "AbortError") showError(error.message);
73
73
  }).finally(function () {
74
- if (store.get('plantUmlPreviewJob') === job) cancelPlantUmlPreview();
74
+ if (store.get(instanceId ? 'plantUmlPreviewJob:' + instanceId : 'plantUmlPreviewJob') === job) cancelPlantUmlPreview(instanceId);
75
75
  });
76
76
  }
@@ -1,14 +1,15 @@
1
+ import { WORKBENCH_PANELS } from './workbench-panels.js';
2
+ import { activeSavedView, workspaceChanged } from './view-workspace-state.js';
1
3
  // Shared arbitration for docked background panes and one foreground overlay.
2
4
 
5
+ import { renderToolPaneChrome } from './tool-pane-chrome.js';
3
6
  import { store } from './store.js';
4
7
  import { iconHtml, refreshIcons } from './icons.js';
5
8
  import { showToast } from './utils.js';
6
9
 
7
10
  var closers = {};
8
- var panelIds = { terminal: 'terminal-container', browser: 'shared-browser-panel', 'file-viewer': 'file-viewer',
9
- 'project-logs': 'project-logs-panel', you: 'you-panel', issues: 'issues-panel', linear: 'linear-panel',
10
- 'notes-browser': 'notes-browser', 'scheduled-tasks': 'scheduled-tasks-panel', debates: 'debates-workbench',
11
- 'mcp-skills': 'mcp-skills-workbench', 'mate-knowledge': 'mate-knowledge-workbench', wireframe: 'wireframe-panel' };
11
+ var panelIds = { terminal: 'terminal-container', browser: 'shared-browser-panel', 'file-viewer': 'file-viewer' };
12
+ Object.keys(WORKBENCH_PANELS).forEach(function (name) { panelIds[name] = WORKBENCH_PANELS[name].id; });
12
13
 
13
14
  // Pane startup does not need account settings for a closed workbench.
14
15
  function ensureLayoutPreference() {
@@ -34,6 +35,7 @@ export function rightWorkbenchRevisionFor(name) {
34
35
  function publish(owner, docked, revisions) {
35
36
  store.set({ rightWorkbenchOwner: owner, rightWorkbenchDockedOwners: docked,
36
37
  rightWorkbenchPanelRevisions: revisions, rightWorkbenchRevision: (store.get('rightWorkbenchRevision') || 0) + 1 });
38
+ workspaceChanged();
37
39
  }
38
40
  function setDocked(name, docked) {
39
41
  if (!openOwner(name)) return;
@@ -53,16 +55,18 @@ function renderLayoutControls() {
53
55
  var panel = document.getElementById(panelIds[name]);
54
56
  if (!panel) return;
55
57
  var docked = owners.indexOf(name) !== -1;
58
+ renderToolPaneChrome(panel, name);
56
59
  panel.dataset.workbenchPlacement = docked ? 'docked' : 'overlay';
57
60
  panel.dataset.workbenchActive = store.get('rightWorkbenchOwner') === name ? 'true' : 'false';
58
61
  if (!openOwner(name)) return;
59
- var header = panel.querySelector(':scope > header, :scope > .file-viewer-header .file-viewer-tabbar, :scope > .terminal-header');
62
+ var header = panel.querySelector(':scope > .tool-pane-header, :scope > header, :scope > .file-viewer-header .file-viewer-tabbar, :scope > .terminal-header');
60
63
  if (!header) return;
61
64
  var button = panel.querySelector('.workbench-layout-toggle');
62
65
  if (!button) {
63
- var actions = header.querySelector('[class*="window-actions"], .terminal-header-actions, .shared-browser-window, :scope.mcp-skills-header > div') || header;
66
+ var actions = header.querySelector('[class*="window-actions"], .terminal-header-actions, .shared-browser-window, .file-viewer-window-actions, .workbench-pane-actions, :scope.mcp-skills-header > div') || header;
64
67
  button = document.createElement('button'); button.type = 'button'; button.className = 'workbench-layout-toggle';
65
68
  button.addEventListener('click', function () {
69
+ if (activeSavedView()) { setDocked(name, dockedOwners().indexOf(name) < 0); return; }
66
70
  var current = store.get('workbenchLayoutPreference') || {};
67
71
  if (!current.ready || current.loading || current.saving) { ensureLayoutPreference(); return; }
68
72
  var owner = store.get('myUserId'), project = store.get('currentSlug'), revision = rightWorkbenchRevisionFor(name);
@@ -83,6 +87,7 @@ function renderLayoutControls() {
83
87
  });
84
88
  });
85
89
  var anchor = actions === header ? header.querySelector('[data-knowledge-wide]') : actions.firstElementChild;
90
+ if (panel.dataset.fileInstance) { var pin = actions.querySelector('[data-action="pin"]'); anchor = pin && pin.nextSibling; }
86
91
  actions.insertBefore(button, anchor || null);
87
92
  }
88
93
  if (button.dataset.mode !== (docked ? 'docked' : 'overlay')) {
@@ -92,20 +97,26 @@ function renderLayoutControls() {
92
97
  button.setAttribute('aria-label', docked ? 'Float workbench over sessions' : 'Dock workbench beside sessions');
93
98
  button.title = state.error || button.getAttribute('aria-label');
94
99
  button.setAttribute('aria-pressed', docked ? 'true' : 'false');
95
- button.disabled = !!state.loading || !!state.saving;
100
+ // View edits are queued by the workspace controller. Disabling this button
101
+ // for an unrelated in-flight save can swallow a click between down and up.
102
+ button.disabled = activeSavedView() ? false : !!state.loading || !!state.saving;
96
103
  button.setAttribute('aria-busy', state.loading || state.saving ? 'true' : 'false');
97
104
  });
98
105
  }
99
106
 
100
- export function registerRightWorkbench(name, close) {
107
+ export function registerRightWorkbench(name, close, panelId) {
108
+ if (panelId) panelIds[name] = panelId;
101
109
  if (!name || typeof close !== "function") return;
102
110
  closers[name] = close;
103
111
  }
104
112
 
113
+ export function unregisterRightWorkbench(name) {
114
+ delete closers[name];
115
+ if (name.indexOf('file-') === 0 && name !== 'file-viewer') delete panelIds[name];
116
+ }
105
117
  export function claimRightWorkbench(name) {
106
118
  if (typeof document !== 'undefined') ensureLayoutPreference();
107
- var owner = store.get('rightWorkbenchOwner'), docked = dockedOwners().slice(), preference = store.get('workbenchLayoutPreference') || {};
108
- if (!owner && !docked.length && preference.ready && preference.mode === 'docked') docked.push(name);
119
+ var owner = store.get('rightWorkbenchOwner'), docked = dockedOwners().slice();
109
120
  var retained = docked.indexOf(name) !== -1 && owner && docked.indexOf(owner) === -1 ? owner : name;
110
121
  var revision = (store.get('rightWorkbenchRevision') || 0) + 1, revisions = Object.assign({}, store.get('rightWorkbenchPanelRevisions') || {});
111
122
  Object.keys(revisions).forEach(function (key) { if (key !== retained && docked.indexOf(key) === -1) delete revisions[key]; });
@@ -133,10 +144,18 @@ export function resetRightWorkbench() {
133
144
  }
134
145
  store.subscribe(function (state, previous) {
135
146
  if (state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId) { resetRightWorkbench(); return; }
136
- var preference = state.workbenchLayoutPreference || {}, before = previous.workbenchLayoutPreference || {};
137
- if (!before.ready && preference.ready && preference.mode === 'docked' && !dockedOwners().length && state.rightWorkbenchOwner) {
138
- setDocked(state.rightWorkbenchOwner, true); return;
139
- }
140
- if (state.workbenchLayoutPreference !== previous.workbenchLayoutPreference || state.rightWorkbenchOwner !== previous.rightWorkbenchOwner ||
147
+ if (state.projectSplitViewPending !== previous.projectSplitViewPending || state.workbenchLayoutPreference !== previous.workbenchLayoutPreference || state.rightWorkbenchOwner !== previous.rightWorkbenchOwner ||
141
148
  state.rightWorkbenchDockedOwners !== previous.rightWorkbenchDockedOwners) renderLayoutControls();
142
149
  });
150
+
151
+ export function restoreWorkbenchDock(name) { setDocked(name, true); }
152
+
153
+ // Document-level shortcuts belong to the focused pane, then the foreground overlay.
154
+ export function handlesWorkbenchKey(name, event) {
155
+ if (event.defaultPrevented) return false;
156
+ var target = event.target, focused = Object.keys(panelIds).find(function (owner) {
157
+ var panel = document.getElementById(panelIds[owner]);
158
+ return panel && target && panel.contains(target);
159
+ });
160
+ return (focused || store.get('rightWorkbenchOwner')) === name;
161
+ }
@@ -65,18 +65,19 @@ function ensurePanel() {
65
65
  panel.innerHTML =
66
66
  '<header class="scheduled-tasks-topbar">' +
67
67
  '<span class="scheduled-tasks-title">' + iconHtml("calendar-clock") + 'Scheduled</span>' +
68
- '<div class="scheduled-tasks-create-group">' +
69
- '<button class="scheduled-tasks-new" type="button" data-action="new">' + iconHtml("plus") + 'New</button>' +
70
- '<button id="scheduled-tasks-create-menu-trigger" class="scheduled-tasks-new-menu" type="button" data-action="new-menu" aria-label="More task creation options" aria-haspopup="menu" aria-expanded="false" aria-controls="scheduled-tasks-create-menu">' + iconHtml("chevron-down") + '</button>' +
71
- '<div id="scheduled-tasks-create-menu" class="scheduled-tasks-create-menu hidden" role="menu" aria-labelledby="scheduled-tasks-create-menu-trigger"><button type="button" role="menuitem" data-action="manual">Manual creation</button></div>' +
72
- '</div>' +
73
68
  '<div class="scheduled-tasks-window-actions">' +
74
69
  '<button class="scheduled-tasks-icon-btn" type="button" data-action="wide" title="Widen panel" aria-label="Widen Scheduled panel" aria-pressed="false">' + iconHtml("chevrons-left-right") + '</button>' +
75
70
  '<button class="scheduled-tasks-icon-btn" type="button" data-action="fullscreen" title="Toggle fullscreen" aria-label="Toggle Scheduled fullscreen" aria-pressed="false">' + iconHtml("maximize-2") + '</button>' +
76
71
  '<button class="scheduled-tasks-icon-btn" type="button" data-action="close" title="Close" aria-label="Close Scheduled">' + iconHtml("x") + '</button>' +
77
72
  '</div>' +
78
73
  '</header>' +
79
- '<div class="scheduled-task-default-ai"><span>New interviews use your shared Default AI.</span><button type="button" data-action="default-ai">Change default</button></div>' +
74
+ '<div class="scheduled-tasks-toolbar" role="toolbar" aria-label="Scheduled task actions">' +
75
+ '<div class="scheduled-tasks-create-group">' +
76
+ '<button class="scheduled-tasks-new" type="button" data-action="new">' + iconHtml("plus") + 'New</button>' +
77
+ '<button id="scheduled-tasks-create-menu-trigger" class="scheduled-tasks-new-menu" type="button" data-action="new-menu" aria-label="More task creation options" aria-haspopup="menu" aria-expanded="false" aria-controls="scheduled-tasks-create-menu">' + iconHtml("chevron-down") + '</button>' +
78
+ '<div id="scheduled-tasks-create-menu" class="scheduled-tasks-create-menu hidden" role="menu" aria-labelledby="scheduled-tasks-create-menu-trigger"><button type="button" role="menuitem" data-action="manual">Manual creation</button></div>' +
79
+ '</div>' +
80
+ '<div class="scheduled-task-default-ai"><span>New interviews use your shared Default AI.</span><button type="button" data-action="default-ai">Change default</button></div></div>' +
80
81
  '<div class="scheduled-task-draft-slot"></div>' +
81
82
  '<div class="scheduled-tasks-list" role="list"></div>';
82
83
  host.appendChild(panel);
@@ -1,4 +1,6 @@
1
+ import { recordToolExpansion } from './view-presentation-state.js';
1
2
  import { registerStudioPaint } from './clay-studio-render.js';
3
+ import { hasBrowserPaneHost, initBrowserPaneTransport, forwardBrowserPaneMessage, sendBrowserPaneCommand, ensureBrowserPaneSource, browserPaneSessionId, browserPaneStamp, browserPaneBound } from './pane-browser-transport.js';
2
4
  import { browserLinkTarget, initBrowserLinks } from './clay-browser-links.js';
3
5
  import { browserSettingsHtml, bindBrowserSettings, renderBrowserSettings, closeBrowserSettings, handleBrowserSettingsMessage } from './shared-browser-settings.js';
4
6
  import { renderBrowserTabs } from './shared-browser-tabs.js';
@@ -19,9 +21,11 @@ function eligible() { return visibleEntry() && !!store.get('activeSessionId'); }
19
21
  function panel() { return document.getElementById('shared-browser-panel'); }
20
22
  function ui(patch) { store.set({ sharedBrowserUi: Object.assign({}, store.get('sharedBrowserUi'), patch) }); }
21
23
  function send(type, fields) {
24
+ var browser = store.get('sharedBrowser');
25
+ var paneSent = sendBrowserPaneCommand(type, Object.assign({ browserId: browser && browser.id, epoch: browser && browser.epoch }, fields || {}));
26
+ if (paneSent !== null) return paneSent;
22
27
  var ws = getWs();
23
28
  if (!ws || ws.readyState !== 1 || !store.get('activeSessionId')) return false;
24
- var browser = store.get('sharedBrowser');
25
29
  ws.send(JSON.stringify(Object.assign({ type: 'shared_browser_' + type, sessionId: store.get('activeSessionId'),
26
30
  browserId: browser && browser.id, epoch: browser && browser.epoch }, fields || {})));
27
31
  return true;
@@ -65,7 +69,7 @@ function ensurePanel() {
65
69
  root.appendChild(el);
66
70
  el.querySelector('[data-action="hide"]').onclick = hideSharedBrowser;
67
71
  el.querySelector('[data-action="wide"]').onclick = function () { var wide = !(store.get('sharedBrowserUi') || {}).wide; ui({ wide: wide }); render(); };
68
- el.querySelector('[data-action="fullscreen"]').onclick = function () { var full = !(store.get('sharedBrowserUi') || {}).fullscreen; ui({ fullscreen: full }); render(); };
72
+ el.querySelector('[data-action="fullscreen"]').onclick = function () { var full = !(store.get('sharedBrowserUi') || {}).fullscreen; setBrowserExpanded(full); recordToolExpansion('browser', full); };
69
73
  ['back', 'forward', 'reload'].forEach(function (action) { el.querySelector('[data-action="' + action + '"]').onclick = function () { sendBrowserInput({ kind: action }); }; });
70
74
  el.querySelector('form').onsubmit = function (event) { event.preventDefault(); sendBrowserInput({ kind: 'navigate', url: el.querySelector('form input').value }); };
71
75
  el.querySelector('[data-action="control"]').onclick = function () {
@@ -178,7 +182,28 @@ function selectBrowserTab(id) {
178
182
  setBrowserTab(browser);
179
183
  send('view', { visible: desktop() && !document.hidden }); render();
180
184
  }
185
+ function browserOpenContext() {
186
+ return JSON.stringify([store.get('myUserId'), store.get('currentSlug'), store.get('projectSplitViewActiveId'), store.get('sessionOriginId'), store.get('projectSplitViewSocketGeneration')]);
187
+ }
188
+ function sendOrQueueBrowserOpen(fields) {
189
+ store.set({ pendingBrowserOpen: null });
190
+ if (send('open', fields)) return;
191
+ // A false transport result means nothing was sent. Only this fresh human
192
+ // opening can wait for pane readiness; restoration never enters this path.
193
+ store.set({ pendingBrowserOpen: { context: browserOpenContext(), socket: getWs(), fields: fields || {}, expires: Date.now() + 15000 } });
194
+ }
195
+ function resumePendingBrowserOpen() {
196
+ var pending = store.get('pendingBrowserOpen');
197
+ if (!pending) return;
198
+ if (pending.context !== browserOpenContext() || pending.socket !== getWs() || pending.expires < Date.now() || !store.get('connected') || !store.get('sharedBrowserOpen')) {
199
+ store.set({ pendingBrowserOpen: null }); return;
200
+ }
201
+ if (!ensureBrowserPaneSource()) return;
202
+ store.set({ pendingBrowserOpen: null });
203
+ if (!send('open', pending.fields)) store.set({ pendingBrowserOpen: pending });
204
+ }
181
205
  export function openSharedBrowserAt(value) {
206
+ ensureBrowserPaneSource();
182
207
  var url = browserLinkTarget('browser:' + value);
183
208
  if (!url) throw new Error('Enter a valid HTTP or HTTPS browser address.');
184
209
  if (!desktop()) throw new Error('Clay Browser is available on desktop screens.');
@@ -186,19 +211,22 @@ export function openSharedBrowserAt(value) {
186
211
  if (!store.get('connected')) throw new Error('Reconnect before opening this browser link.');
187
212
  ensurePanel(); closeBrowserSettings(false);
188
213
  claimRightWorkbench('browser'); store.set({ sharedBrowserOpen: true });
189
- ui({ error: '' }); send('open', { url: url }); render();
214
+ ui({ error: '' }); sendOrQueueBrowserOpen({ url: url }); render();
190
215
  }
191
216
  export function openSharedBrowser() {
217
+ ensureBrowserPaneSource();
192
218
  if (!eligible() || !store.get('connected')) return;
193
219
  ensurePanel();
194
220
  claimRightWorkbench('browser');
195
221
  store.set({ sharedBrowserOpen: true });
196
222
  var browser = store.get('sharedBrowser');
197
- if (!browser) send('open');
223
+ if (!browser) sendOrQueueBrowserOpen();
198
224
  else send('view', { visible: true });
199
225
  render();
200
226
  }
201
227
  export function hideSharedBrowser() {
228
+ setBrowserExpanded(false); recordToolExpansion('browser', false);
229
+ if (!store.get('savedWorkspaceApplying')) store.set({ pendingBrowserOpen: null });
202
230
  closeViewportMenu();
203
231
  closeBrowserSettings();
204
232
  clearBrowserInput();
@@ -207,9 +235,11 @@ export function hideSharedBrowser() {
207
235
  releaseRightWorkbench('browser');
208
236
  render();
209
237
  }
210
- export function handleSharedBrowserMessage(msg) {
238
+ export function handleSharedBrowserMessage(msg, fromPane) {
211
239
  if (!msg.type || msg.type.indexOf('shared_browser_') !== 0) return false;
212
- if (String(msg.sessionId) !== String(store.get('activeSessionId'))) return true;
240
+ if (forwardBrowserPaneMessage(msg)) return true;
241
+ if (browserPaneBound() && !fromPane) return true;
242
+ if (String(msg.sessionId) !== String(browserPaneSessionId() || store.get('activeSessionId'))) return true;
213
243
  if (handleBrowserSettingsMessage(msg)) return true;
214
244
  if (msg.type === 'shared_browser_state') {
215
245
  var old = store.get('sharedBrowser');
@@ -218,6 +248,7 @@ export function handleSharedBrowserMessage(msg) {
218
248
  var selected = !msg.focus && old && tabs.find(function (tab) { return tab.id === old.id; });
219
249
  selected = selected || msg.browser || tabs[0] || null;
220
250
  setBrowserTab(selected);
251
+ store.set({ savedViewBrowserListReady: true });
221
252
  if (msg.present && eligible()) {
222
253
  ensurePanel(); claimRightWorkbench('browser'); store.set({ sharedBrowserOpen: true });
223
254
  }
@@ -228,23 +259,35 @@ export function handleSharedBrowserMessage(msg) {
228
259
  var current = store.get('sharedBrowser');
229
260
  if (!desktop() || !store.get('sharedBrowserOpen') || !current || current.id !== msg.browserId || (store.get('sharedBrowserUi') || {}).decoding) return true;
230
261
  var sessionId = store.get('activeSessionId');
262
+ var paneStamp = browserPaneStamp();
231
263
  ui({ decoding: true });
232
264
  var image = new Image();
233
265
  image.onload = function () {
234
266
  var latest = store.get('sharedBrowser');
235
- if (latest && latest.id === msg.browserId && store.get('activeSessionId') === sessionId && panel()) {
267
+ if (latest && latest.id === msg.browserId && browserPaneStamp() === paneStamp && store.get('activeSessionId') === sessionId && panel()) {
236
268
  var canvas = panel().querySelector('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight;
237
269
  canvas.getContext('2d').drawImage(image, 0, 0);
238
270
  ui({ decoding: false, hasFrame: true }); render();
239
271
  registerStudioPaint(msg, canvas, null, canvas);
240
272
  }
241
273
  };
242
- image.onerror = function () { if (store.get('activeSessionId') === sessionId) ui({ decoding: false }); };
274
+ image.onerror = function () { if (browserPaneStamp() === paneStamp && store.get('activeSessionId') === sessionId) ui({ decoding: false }); };
243
275
  image.src = 'data:image/jpeg;base64,' + msg.data;
244
276
  }
245
277
  return true;
246
278
  }
247
279
  export function initSharedBrowser() {
280
+ initBrowserPaneTransport(handleSharedBrowserMessage, function (available) {
281
+ var applying = store.get('savedWorkspaceApplying');
282
+ store.set({ savedWorkspaceApplying: true, savedViewBrowserListReady: false });
283
+ try {
284
+ clearBrowserInput(); setBrowserTab(null);
285
+ store.set({ sharedBrowsers: [], sharedBrowserUi: {}, sharedBrowserSettings: {} });
286
+ if (!available) hideSharedBrowser();
287
+ render();
288
+ } finally { store.set({ savedWorkspaceApplying: applying }); }
289
+ });
290
+ document.addEventListener('clay-pane-ready', function () { queueMicrotask(resumePendingBrowserOpen); }, true);
248
291
  initBrowserLinks();
249
292
  var button = document.getElementById('shared-browser-btn');
250
293
  if (button) button.onclick = openSharedBrowser;
@@ -261,14 +304,31 @@ export function initSharedBrowser() {
261
304
  registerRightWorkbench('browser', hideSharedBrowser);
262
305
  store.subscribe(function (state, previous) {
263
306
  if (state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.myUserId !== previous.myUserId) {
307
+ if (browserPaneSessionId() !== null) { render(); return; }
264
308
  clearBrowserInput(); releaseRightWorkbench('browser');
265
- store.set({ sharedBrowser: null, sharedBrowsers: [], sharedBrowserOpen: false, sharedBrowserUi: {}, sharedBrowserSettings: {} });
309
+ store.set({ sharedBrowser: null, sharedBrowsers: [], savedViewBrowserListReady: false, sharedBrowserOpen: false, sharedBrowserUi: {}, sharedBrowserSettings: {} });
266
310
  if (state.connected) send('state_request');
267
311
  render();
268
312
  } else if (state.connected !== previous.connected) {
313
+ store.set({ savedViewBrowserListReady: false });
269
314
  if (state.connected) send('state_request');
270
315
  render();
271
316
  } else if (state.permissions !== previous.permissions || state.dmMode !== previous.dmMode) {
272
317
  if (!eligible()) hideSharedBrowser(); renderEntry();
273
318
  }
274
319
  });
320
+
321
+ export function restoreExistingBrowser(resource) {
322
+ if (hasBrowserPaneHost() && !ensureBrowserPaneSource()) return 'pending';
323
+ if (!store.get('savedViewBrowserListReady')) { send('state_request'); return 'pending'; }
324
+ if (!store.get('connected') || !store.get('controlRuntimeId')) return 'pending';
325
+ ensurePanel();
326
+ var browser = (store.get('sharedBrowsers') || []).find(function (item) { return item.id === resource.id && item.phase !== 'ended'; });
327
+ var exact = browser && resource.runtimeId === store.get('controlRuntimeId') && resource.sessionOriginId === store.get('sessionOriginId');
328
+ claimRightWorkbench('browser'); store.set({ sharedBrowserOpen: true });
329
+ if (exact) { setBrowserTab(browser); send('view', { visible: true }); ui({ error: '' }); render(); return 'restored'; }
330
+ ui({ error: 'This saved browser is unavailable in the current conversation. No replacement browser was opened.' }); render();
331
+ return 'unavailable';
332
+ }
333
+
334
+ export function setBrowserExpanded(enabled) { ui({ fullscreen: !!enabled }); if (document.getElementById('shared-browser-panel')) render(); }
@@ -296,6 +296,7 @@ export function createSplitPane(pane, closePane) {
296
296
  if (!vendorIcon.hidden) vendorIcon.src = VENDOR_AVATARS[vendor]; vendorIcon.alt = ""; vendorIcon.title = VENDOR_NAMES[vendor] || vendor; header.appendChild(vendorIcon);
297
297
  var title = document.createElement("span"); title.className = "split-pane-title"; title.textContent = pane.title; title.title = "Rename session";
298
298
  title.addEventListener("click", function () { if (title.getAttribute('aria-disabled') === 'true' || !syncPaneHeaderState(paneEl)) return; var current = sessionById(Number(paneEl.dataset.sessionId)); if (current) startPaneRename(header, title, { sessionId: current.id, title: current.title }); }); header.appendChild(title);
299
+ var dragSpace = document.createElement("span"); dragSpace.className = "split-pane-drag-space"; dragSpace.title = "Drag to rearrange panes"; dragSpace.setAttribute("aria-hidden", "true"); header.appendChild(dragSpace);
299
300
  var frame = document.createElement("iframe"); frame.className = "split-pane-frame"; frame.dataset.projectSlug = pane.slug;
300
301
  frame.dataset.sessionId = String(pane.sessionId); frame.src = paneUrl(pane); frame.title = pane.title;
301
302
  frame.dataset.sessionOriginId = session && session.sessionOriginId || '';
@@ -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; });