clay-server 5.3.0 → 5.3.1-beta.3

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 (70) hide show
  1. package/lib/contextual-screen-snapshot.js +13 -4
  2. package/lib/pane-session-transport.js +3 -0
  3. package/lib/project-shared-browser.js +69 -21
  4. package/lib/project-worker-proposal.js +1 -2
  5. package/lib/project.js +2 -0
  6. package/lib/public/css/issues.css +1 -1
  7. package/lib/public/css/linear.css +7 -24
  8. package/lib/public/css/menus.css +2 -2
  9. package/lib/public/css/notifications-center.css +2 -2
  10. package/lib/public/css/split-view-layout.css +0 -16
  11. package/lib/public/css/tool-pane.css +12 -0
  12. package/lib/public/modules/contextual-clay-session.js +2 -2
  13. package/lib/public/modules/contextual-mate-context.js +7 -3
  14. package/lib/public/modules/contextual-view-metadata.js +23 -0
  15. package/lib/public/modules/linear-panel.js +5 -5
  16. package/lib/public/modules/pane-activation.js +6 -1
  17. package/lib/public/modules/pane-header-tooltips.js +1 -1
  18. package/lib/public/modules/shared-browser-tabs.js +4 -2
  19. package/lib/public/modules/split-pane-renderer.js +5 -0
  20. package/lib/public/modules/split-view-presentations.js +3 -1
  21. package/lib/public/modules/split-view-worker-scroll.js +1 -1
  22. package/lib/public/modules/split-view-workers.js +20 -51
  23. package/lib/public/modules/tool-pane-chrome.js +3 -0
  24. package/lib/public/modules/tool-pane-drag.js +1 -1
  25. package/lib/public/modules/tool-pane-layout.js +4 -3
  26. package/lib/public/modules/workbench-window-controls.js +39 -0
  27. package/lib/public/modules/worker-created-presentation.js +3 -1
  28. package/lib/public/pane-assets/{app-5W6KJXK5.js → app-BDYH6Y7R.js} +1 -1
  29. package/lib/public/pane-assets/{chunk-BQN7B62H.js → chunk-42C2CZEP.js} +1 -1
  30. package/lib/public/pane-assets/chunk-46OOGVHU.js +1 -0
  31. package/lib/public/pane-assets/chunk-4OJW4FF4.js +1 -0
  32. package/lib/public/pane-assets/chunk-E6NRPGVH.js +2 -0
  33. package/lib/public/pane-assets/chunk-F7VIE6FJ.js +1 -0
  34. package/lib/public/pane-assets/chunk-FCJYJLVZ.js +1 -0
  35. package/lib/public/pane-assets/chunk-FEJDEXOW.js +1 -0
  36. package/lib/public/pane-assets/chunk-FQQMXMH7.js +168 -0
  37. package/lib/public/pane-assets/chunk-HEMCIILG.js +1 -0
  38. package/lib/public/pane-assets/chunk-HWLBH6AL.js +1 -0
  39. package/lib/public/pane-assets/{chunk-ENPAHXLB.js → chunk-KB4T5B2R.js} +1 -1
  40. package/lib/public/pane-assets/chunk-KF3LZRJK.js +1 -0
  41. package/lib/public/pane-assets/{chunk-UPXU2IPC.js → chunk-N5SIEIOG.js} +1 -1
  42. package/lib/public/pane-assets/{chunk-EMLN2Y7L.js → chunk-QRVKAVH7.js} +1 -1
  43. package/lib/public/pane-assets/chunk-RV5O66TO.js +1 -0
  44. package/lib/public/pane-assets/{chunk-SLMAFKI2.js → chunk-TMUCEZ7C.js} +1 -1
  45. package/lib/public/pane-assets/{chunk-7OVNC5HT.js → chunk-TT3NECKK.js} +1 -1
  46. package/lib/public/pane-assets/{chunk-FQTNR4ZS.js → chunk-U5GH2UJM.js} +1 -1
  47. package/lib/public/pane-assets/{chunk-SJOT74VW.js → chunk-UMIYQYGM.js} +1 -1
  48. package/lib/public/pane-assets/chunk-VCNLKOVT.js +1 -0
  49. package/lib/public/pane-assets/{chunk-SJTGHIN5.js → chunk-X452YCE5.js} +1 -1
  50. package/lib/public/pane-assets/{chunk-VEBYD7TL.js → chunk-XI2DJM4H.js} +1 -1
  51. package/lib/public/pane-assets/chunk-YE45RR7W.js +1 -0
  52. package/lib/public/pane-assets/manifest.json +381 -379
  53. package/lib/public/pane-assets/style-VDIGFFLR.css +1 -0
  54. package/lib/shared-browser-workspace.js +28 -0
  55. package/lib/workbench-window-layout.js +7 -5
  56. package/package.json +1 -1
  57. package/lib/public/pane-assets/chunk-2IJUTAB6.js +0 -2
  58. package/lib/public/pane-assets/chunk-EPBGGZRT.js +0 -1
  59. package/lib/public/pane-assets/chunk-EYTE56IA.js +0 -1
  60. package/lib/public/pane-assets/chunk-L6LSA5WI.js +0 -1
  61. package/lib/public/pane-assets/chunk-M57UIGZV.js +0 -1
  62. package/lib/public/pane-assets/chunk-MGTFTEJP.js +0 -1
  63. package/lib/public/pane-assets/chunk-MMG6MYCN.js +0 -1
  64. package/lib/public/pane-assets/chunk-MZLMEXMW.js +0 -1
  65. package/lib/public/pane-assets/chunk-OG3E5SVA.js +0 -166
  66. package/lib/public/pane-assets/chunk-RGAFZ3T2.js +0 -1
  67. package/lib/public/pane-assets/chunk-RNDRURMF.js +0 -1
  68. package/lib/public/pane-assets/chunk-U3I5KY3B.js +0 -1
  69. package/lib/public/pane-assets/chunk-XBHYNXXT.js +0 -1
  70. package/lib/public/pane-assets/style-BSKKK54Q.css +0 -1
@@ -111,7 +111,7 @@ export function filterPaneActivation(message) {
111
111
  var pending = store.get('panePendingBinding'), committed = store.get('paneCommittedBinding');
112
112
  if (message.type === 'pane_activation_rejected') { failed(message.requestId); return true; }
113
113
  if (!pending && !committed) return !!message.paneActivation;
114
- if (['session_list', 'split_groups', 'session_presence', 'pong', 'account_auth_required', 'project_removed', 'project_access_revoked'].indexOf(message.type) >= 0 && !message.paneActivation) return false;
114
+ if (['session_list', 'split_groups', 'session_presence', 'mate_list', 'mate_updated', 'mate_deleted', 'pong', 'account_auth_required', 'project_removed', 'project_access_revoked'].indexOf(message.type) >= 0 && !message.paneActivation) return false;
115
115
  var expected = pending || committed;
116
116
  if (!samePaneBinding(expected, message.paneActivation)) return true;
117
117
  if (message.type === 'pane_activation_started') return true;
@@ -139,6 +139,10 @@ export function filterPaneActivation(message) {
139
139
  clearDeadline();
140
140
  store.set({ paneCommittedBinding: expected, panePendingBinding: null, paneReady: true });
141
141
  block(false);
142
+ // Initial owner metadata may have arrived during activation. Read it again
143
+ // after committing; session-bound actions still require their live gesture.
144
+ var socket = getWs();
145
+ if (socket && socket.readyState === 1) socket.send(JSON.stringify({ type: 'mate_list' }));
142
146
  if (typeof performance !== 'undefined' && performance.mark) { performance.clearMarks('clay:pane-ready'); performance.mark('clay:pane-ready'); }
143
147
  var frame = window.frameElement;
144
148
  if (frame) frame.dispatchEvent(new window.parent.CustomEvent('clay-pane-ready'));
@@ -250,6 +254,7 @@ export function initPaneActivation() {
250
254
  if (state.connected && (state.splitGroups !== previous.splitGroups || state.splitGroupsProjectSlug !== previous.splitGroupsProjectSlug || state.paneBindingSessions !== previous.paneBindingSessions || state.authIdentityLoaded !== previous.authIdentityLoaded || state.myUserId !== previous.myUserId || state.activeSessionId !== previous.activeSessionId || state.connected !== previous.connected)) {
251
255
  queueMicrotask(function () {
252
256
  refreshPaneActivation();
257
+ frame.dispatchEvent(new window.parent.CustomEvent('clay-pane-metadata-ready'));
253
258
  if (!store.get('panePendingBinding') && !store.get('paneSuspended')) {
254
259
  if (store.get('panePinPending')) primePaneSession();
255
260
  else if (store.get('activeSessionId') === store.get('paneSessionId')) activatePaneSession(store.get('paneSessionId'));
@@ -4,7 +4,7 @@ import { registerTooltip, pruneTooltip } from './tooltip.js';
4
4
  var HEADERS = '.split-pane-header, .tool-pane-header, .file-viewer-tabbar, .terminal-header, .shared-browser-header, .split-view-workers, #top-bar .top-bar-actions';
5
5
  function sync(header) {
6
6
  header.querySelectorAll('[title], button, [role="button"], a[aria-label]').forEach(function (element) {
7
- var label = element.getAttribute('title') || element.getAttribute('aria-label') || element.getAttribute('data-tip');
7
+ var label = element.getAttribute('title') || element.getAttribute('data-tip') || element.getAttribute('aria-label');
8
8
  if (!label || !label.trim()) return;
9
9
  if (element.matches('button, [role="button"], a') && (!element.hasAttribute('aria-label') || element.dataset.tooltipGeneratedLabel === 'true' && element.hasAttribute('title'))) {
10
10
  element.dataset.tooltipGeneratedLabel = 'true';
@@ -5,7 +5,7 @@ export function renderBrowserTabs(el, select, send) {
5
5
  var bar = el.querySelector('.shared-browser-tabs');
6
6
  var tabs = store.get('sharedBrowsers') || [];
7
7
  var selected = store.get('sharedBrowser');
8
- var key = JSON.stringify([tabs.map(function (tab) { return [tab.id, tab.url, tab.control, tab.phase, tab.incognito, tab.personalCookiesAvailable]; }), selected && selected.id, store.get('connected')]);
8
+ var key = JSON.stringify([tabs.map(function (tab) { return [tab.id, tab.url, tab.control, tab.phase, tab.incognito, tab.personalCookiesAvailable, tab.sourceSessionId, tab.sourceRole, tab.sourceTitle, tab.sourceGeneration]; }), selected && selected.id, store.get('connected')]);
9
9
  if (bar.dataset.key === key) return;
10
10
  var focused = bar.contains(document.activeElement) ? document.activeElement : null;
11
11
  var focusedId = focused && focused.dataset.browserId, focusedAction = focused && focused.dataset.tabAction;
@@ -23,8 +23,10 @@ export function renderBrowserTabs(el, select, send) {
23
23
  button.setAttribute('aria-selected', String(active)); button.tabIndex = active ? 0 : -1;
24
24
  var title = 'New tab';
25
25
  try { if (tab.url) title = new URL(tab.url).hostname || 'New tab'; } catch (e) {}
26
+ var source = tab.sourceRole === 'worker' ? 'Worker #' + tab.sourceSessionId : '';
27
+ if (source) title = source + ' · ' + title;
26
28
  var mode = tab.phase === 'ended' ? 'Ended' : tab.phase === 'starting' ? 'Starting' : tab.control === 'agent' ? 'You and Clay can control this tab' : 'Only you can control this tab. Clay is paused.';
27
- var description = title + ' · ' + (tab.incognito === false ? 'Personal browser' : 'Incognito') + ' · ' + mode;
29
+ var description = title + (source ? ' · ' + tab.sourceTitle + ' · Gen ' + tab.sourceGeneration : '') + ' · ' + (tab.incognito === false ? 'Personal browser' : 'Incognito') + ' · ' + mode;
28
30
  var label = document.createElement('span'); label.className = 'shared-browser-tab-label'; label.textContent = title;
29
31
  button.innerHTML = iconHtml(tab.personalCookiesAvailable && tab.incognito ? 'glasses' : 'globe');
30
32
  button.appendChild(label); button.setAttribute('aria-label', description); button.setAttribute('data-tip', description);
@@ -357,6 +357,11 @@ export function createSplitPane(pane, closePane) {
357
357
  }
358
358
  frame.addEventListener('clay-pane-ready', refreshReadyHeader);
359
359
  frame.addEventListener('clay-pane-config', refreshReadyHeader);
360
+ frame.addEventListener('clay-pane-metadata-ready', function () {
361
+ if (paneEl.hidden || paneEl.inert || paneEl.classList.contains('split-pane-cached') || !frame.dataset.requestedSessionId) return;
362
+ var current = sessionById(Number(frame.dataset.requestedSessionId));
363
+ if (current) updateSplitPaneMetadata(paneEl, { slug: frame.dataset.projectSlug, sessionId: current.id, title: current.title });
364
+ });
360
365
  frame.addEventListener('clay-pane-activation-failed', function () {
361
366
  if (paneEl.classList.contains('split-pane-cached')) return;
362
367
  syncPaneHeaderState(paneEl);
@@ -292,7 +292,9 @@ function onClick(event) {
292
292
  if (store.get('projectSplitViewActiveId') && target.closest('.split-group-item[data-split-group-id]')) {
293
293
  store.set({ projectSplitViewActiveId: null, projectSplitViewFocusedOrigin: null }); return;
294
294
  }
295
- var row = target.closest(".session-item[data-session-id], .session-loop-child[data-session-id]"); if (row && store.get('projectSplitViewActiveId')) {
295
+ var row = target.closest(".session-item[data-session-id], .session-loop-child[data-session-id]");
296
+ if (row && target.closest("button, a, input, textarea, select, [contenteditable='true']")) return;
297
+ if (row && store.get('projectSplitViewActiveId')) {
296
298
  var session = (getCachedSessions() || []).find(function (item) { return item.id === Number(row.dataset.sessionId); });
297
299
  var membershipViewId = session && viewIdForOrigin(session.sessionOriginId); if (membershipViewId) {
298
300
  event.preventDefault(); event.stopImmediatePropagation(); var membershipView = viewExists(store.get('projectSplitViews'), membershipViewId);
@@ -14,7 +14,7 @@ export function shouldLoadWorkerPage(state, body, intentional) {
14
14
  if (!state || !body || state.inFlight || !state.hasMore || state.nextBefore == null || !workerCursorKey(state.nextBefore)) return false;
15
15
  if (state.status === 'error' && !intentional) return false;
16
16
  if (state.status !== 'ready' && state.status !== 'error') return false;
17
- return intentional ? body.scrollTop <= 96 : body.clientHeight > 0 && body.scrollHeight <= body.clientHeight + 1;
17
+ return intentional === true && body.scrollTop <= 96;
18
18
  }
19
19
  export function captureWorkerAnchor(body) {
20
20
  var top = body.getBoundingClientRect().top;
@@ -34,15 +34,6 @@ function workersFor(driver) {
34
34
  (Number(b.workerGeneration) || 0) - (Number(a.workerGeneration) || 0); });
35
35
  return { items: all, currentIds: currentIds };
36
36
  }
37
- function configuredGroup(driver) {
38
- if (!driver) return null;
39
- var groups = store.get('splitGroups') || [];
40
- for (var i = 0; i < groups.length; i++) {
41
- var roles = splitGroupRoles(groups[i]);
42
- if (roles && roles.version === 2 && roles.driverId === driver.id && roles.workerIds.length) return groups[i];
43
- }
44
- return null;
45
- }
46
37
  function presentationSource(container, driver, connectionErrorBinding) {
47
38
  var frame = container && container.querySelector('.split-pane-frame'), live = frame && frame.clayPane && frame.clayPane.identity();
48
39
  // A read-only connection error may outlive the child's committed binding.
@@ -130,7 +121,6 @@ function close(returnFocus) {
130
121
  requestId: state.subscriptionId, parentSessionOriginId: state.driverOriginId,
131
122
  workerSessionOriginId: state.workerOriginId, generation: state.generation })); } catch (_error) {}
132
123
  }
133
- if (state.fillTimer) clearTimeout(state.fillTimer);
134
124
  if (state.requestTimer) clearTimeout(state.requestTimer);
135
125
  store.set({ splitViewWorkerConversation: null });
136
126
  syncWorkerSelection();
@@ -152,7 +142,6 @@ function requestPage(state, before, append, refresh) {
152
142
  if (!state || !current || state.requestId !== current.requestId || current.inFlight || workerCursorKey(before) === null || !ensurePopover()) return;
153
143
  state = store.get('splitViewWorkerConversation');
154
144
  if (append && workerCursorKey(before) !== workerCursorKey(state.nextBefore)) return;
155
- if (state.fillTimer) clearTimeout(state.fillTimer);
156
145
  var ws = getWs();
157
146
  if (!store.get('connected') || !ws || ws.readyState !== 1) {
158
147
  store.set({ splitViewWorkerConversation: Object.assign({}, state, { status: "error", error: "Worker conversation is unavailable while disconnected.", append: false }) });
@@ -161,7 +150,7 @@ function requestPage(state, before, append, refresh) {
161
150
  var requestId = "worker-read-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 9);
162
151
  var subscribe = !state.subscriptionId || !state.subscriptionConfirmed;
163
152
  var pending = Object.assign({}, state, { subscriptionId: subscribe ? requestId : state.subscriptionId, requestId: requestId, socket: ws, projectSlug: store.get('currentSlug'), ownerId: store.get('myUserId') || null,
164
- socketGeneration: store.get('projectSplitViewSocketGeneration') || 0, status: "loading", error: null, inFlight: true, fillTimer: null, requestedBefore: before == null ? null : before, append: append === true, refreshing: refresh === true, refreshPending: false });
153
+ socketGeneration: store.get('projectSplitViewSocketGeneration') || 0, status: "loading", error: null, inFlight: true, requestedBefore: before == null ? null : before, append: append === true, refreshing: refresh === true, refreshPending: false });
165
154
  pending.requestTimer = setTimeout(function () {
166
155
  var current = store.get('splitViewWorkerConversation');
167
156
  if (!current || current.requestId !== requestId || !current.inFlight) return;
@@ -219,8 +208,23 @@ function ensurePopover() {
219
208
  closeButton.addEventListener("click", function () { close(true); }); header.appendChild(closeButton); element.appendChild(header);
220
209
  var source = document.createElement("div"); source.className = "split-view-worker-popover-source"; element.appendChild(source);
221
210
  var body = document.createElement("div"); body.className = "split-view-worker-popover-body readonly-session-transcript"; body.tabIndex = 0; body.setAttribute("aria-label", "Read-only Worker transcript"); element.appendChild(body);
222
- body.addEventListener('scroll', function () { loadEarlier(body, true); }, { passive: true });
223
- body.addEventListener('wheel', function (event) { if (event.deltaY < 0) loadEarlier(body, true); }, { passive: true });
211
+ body.addEventListener('scroll', function () {
212
+ var state = store.get('splitViewWorkerConversation');
213
+ if (!state) return;
214
+ var upward = body.scrollTop < state.scrollTop;
215
+ store.set({ splitViewWorkerConversation: Object.assign({}, state, { scrollTop: body.scrollTop }) });
216
+ if (upward && state.scrollIntentUntil > Date.now()) loadEarlier(body, true);
217
+ }, { passive: true });
218
+ function scrollIntent() {
219
+ var state = store.get('splitViewWorkerConversation');
220
+ if (state) store.set({ splitViewWorkerConversation: Object.assign({}, state, { scrollIntentUntil: Date.now() + 1500 }) });
221
+ }
222
+ body.addEventListener('wheel', function (event) { if (event.deltaY < 0) { scrollIntent(); loadEarlier(body, true); } }, { passive: true });
223
+ body.addEventListener('keydown', function (event) {
224
+ if (['ArrowUp', 'PageUp', 'Home'].indexOf(event.key) >= 0 || event.key === ' ' && event.shiftKey) scrollIntent();
225
+ });
226
+ body.addEventListener('pointerdown', function (event) { if (event.target === body) scrollIntent(); }, { passive: true });
227
+ body.addEventListener('touchstart', scrollIntent, { passive: true });
224
228
  var footer = document.createElement("footer"); footer.className = "split-view-worker-popover-footer"; element.appendChild(footer);
225
229
  }
226
230
  return element;
@@ -249,7 +253,7 @@ function renderContent() {
249
253
  else body.scrollTop = oldTop;
250
254
  if (wasFocused) body.focus({ preventScroll: true });
251
255
  }
252
- if (body) store.set({ splitViewWorkerConversation: Object.assign({}, state, { renderedMessages: state.messages, prepend: false, liveRefresh: false,
256
+ if (body) store.set({ splitViewWorkerConversation: Object.assign({}, state, { renderedMessages: state.messages, scrollTop: body.scrollTop, scrollIntentUntil: 0, prepend: false, liveRefresh: false,
253
257
  initialPage: state.status === 'ready' ? false : state.initialPage }) });
254
258
  if (footer) {
255
259
  footer.textContent = state.status === 'error' ? 'Read-only conversation' : state.refreshing ? 'Updating live conversation…' : state.subscriptionConfirmed ? 'Read-only · Live updates' : 'Read-only conversation';
@@ -265,25 +269,13 @@ function renderContent() {
265
269
  }
266
270
  if (footerFocused) { var nextFocus = footer.querySelector('button') || body; if (nextFocus) nextFocus.focus({ preventScroll: true }); }
267
271
  refreshIcons();
268
- scheduleWorkerFill(body);
272
+
269
273
  }
270
274
  function loadEarlier(body, intentional) {
271
275
  var state = store.get('splitViewWorkerConversation');
272
276
  if (!shouldLoadWorkerPage(state, body, intentional)) return;
273
277
  requestPage(state, state.nextBefore, true);
274
278
  }
275
- function scheduleWorkerFill(body) {
276
- var state = store.get('splitViewWorkerConversation');
277
- if (!shouldLoadWorkerPage(state, body, false) || state.fillTimer) return;
278
- var requestId = state.requestId;
279
- var timer = setTimeout(function () {
280
- var current = store.get('splitViewWorkerConversation');
281
- if (!current || current.requestId !== requestId || current.fillTimer !== timer || !body.isConnected) return;
282
- store.set({ splitViewWorkerConversation: Object.assign({}, current, { fillTimer: null }) });
283
- loadEarlier(body, false);
284
- }, 120);
285
- store.set({ splitViewWorkerConversation: Object.assign({}, state, { fillTimer: timer }) });
286
- }
287
279
  function anchorChip(state) {
288
280
  if (!state) return null;
289
281
  return document.querySelector('.split-pane[data-session-origin="' + CSS.escape(state.driverOriginId) + '"] [data-worker-origin="' + CSS.escape(state.workerOriginId) + '"]');
@@ -397,28 +389,6 @@ export function renderPaneWorkers(container, driver) {
397
389
  button.addEventListener("click", function () { if (presentationSource(container, exactDriver(driver.sessionOriginId))) open(button, driver, worker); }); pill.appendChild(button); root.appendChild(pill);
398
390
  appendWorkerRemoval(pill, button, container, driver, worker);
399
391
  });
400
- var activeView = (store.get('projectSplitViews') || []).find(function (view) { return view.id === store.get('projectSplitViewActiveId'); });
401
- var group = configuredGroup(driver);
402
- if ((activeView && activeView.sessions.some(function (session) { return session.sessionOriginId === driver.sessionOriginId; }) || source.singleton) && group) {
403
- var controls = document.createElement('button'); controls.type = 'button'; controls.className = 'split-view-worker-controls';
404
- controls.textContent = 'Controls'; controls.setAttribute('aria-label', 'Open Worker controls for ' + (driver.title || 'Driver session'));
405
- controls.dataset.driverOrigin = driver.sessionOriginId; controls.dataset.groupId = String(group.id);
406
- controls.addEventListener('click', function (event) {
407
- event.preventDefault(); event.stopPropagation();
408
- var frame = container.querySelector('.split-pane-frame'), live = frame && frame.clayPane && frame.clayPane.identity();
409
- var currentSource = presentationSource(container, exactDriver(driver.sessionOriginId));
410
- if (!frame || !live || !live.ready || !currentSource || !frame.isConnected || frame.dataset.sessionOriginId !== driver.sessionOriginId ||
411
- Number(frame.dataset.sessionId) !== driver.id || live.sessionId !== driver.id || live.binding.sessionOriginId !== driver.sessionOriginId ||
412
- frame.dataset.projectSlug !== store.get('currentSlug') || configuredGroup(exactDriver(driver.sessionOriginId)) !== group) return;
413
- document.dispatchEvent(new CustomEvent('clay-split-view-open-worker-controls', { detail: {
414
- driverOriginId: driver.sessionOriginId, driverId: driver.id, groupId: group.id, projectSlug: store.get('currentSlug'),
415
- ownerId: driver.ownerId || null, workerOrigins: group.members.map(function (id) {
416
- var member = sessions().find(function (session) { return session.id === id; }); return member && member.sessionRole === 'worker' ? member.sessionOriginId : null;
417
- })
418
- } }));
419
- });
420
- root.appendChild(controls);
421
- }
422
392
  introduceWorkerViewer(container, driver);
423
393
  syncWorkerMessagePreviews();
424
394
  var conversation = store.get('splitViewWorkerConversation');
@@ -468,7 +438,6 @@ store.subscribe(function (state, previous) {
468
438
  var conversation = state.splitViewWorkerConversation;
469
439
  if (conversation) {
470
440
  if (conversation.requestTimer) clearTimeout(conversation.requestTimer);
471
- if (conversation.fillTimer) clearTimeout(conversation.fillTimer);
472
441
  store.set({ splitViewWorkerConversation: Object.assign({}, conversation, { status: "error", error: "Worker conversation connection was lost.", inFlight: false, append: false }) });
473
442
  renderContent();
474
443
  }
@@ -1,3 +1,4 @@
1
+ import { ensureWorkbenchWindowControls, syncWorkbenchWindowControls } from './workbench-window-controls.js';
1
2
  import { WORKBENCH_PANELS } from './workbench-panels.js';
2
3
  import { recordToolExpansion } from './view-presentation-state.js';
3
4
  import { store } from './store.js';
@@ -73,6 +74,7 @@ function renderWorkbenchChrome(panel, name) {
73
74
  header.appendChild(actions);
74
75
  }
75
76
  actions.classList.add('workbench-pane-actions');
77
+ ensureWorkbenchWindowControls(panel, name, actions);
76
78
  var title = header.querySelector('strong, [class$="-title"]');
77
79
  if (title) title.classList.add('tool-pane-title');
78
80
  var space = document.createElement('span'); space.className = 'split-pane-drag-space';
@@ -80,6 +82,7 @@ function renderWorkbenchChrome(panel, name) {
80
82
  header.insertBefore(space, actions);
81
83
  var expanded = panel.classList.contains('panel-fullscreen');
82
84
  var observer = new MutationObserver(function () {
85
+ syncWorkbenchWindowControls(panel, actions);
83
86
  var next = panel.classList.contains('panel-fullscreen');
84
87
  if (next !== expanded) { expanded = next; recordToolExpansion(name, next); }
85
88
  });
@@ -122,7 +122,7 @@ export function initToolPaneDrag() {
122
122
  document.addEventListener('pointerdown', pointerDown, true);
123
123
  document.addEventListener('pointermove', move, true); document.addEventListener('pointerup', up, true);
124
124
  document.addEventListener('pointercancel', function () { finish(false); }, true);
125
- document.addEventListener('keydown', function (event) { if (event.key === 'Escape' && store.get('toolPaneDrag')) { event.preventDefault(); finish(false); } }, true);
125
+ document.addEventListener('keydown', function (event) { if (event.key === 'Escape' && store.get('toolPaneDrag')) { event.preventDefault(); event.stopImmediatePropagation(); finish(false); } }, true);
126
126
  window.addEventListener('blur', function () { finish(false); });
127
127
  }
128
128
 
@@ -6,7 +6,7 @@ import { store } from './store.js';
6
6
  import { getWs } from './ws-ref.js';
7
7
  import { TOOL_PANES, toolWindowId, workspaceWindowTree } from './tool-pane-tree.js';
8
8
  import { renderPresentationSplitView } from './split-view-presentations.js';
9
- import { ensureWorkbenchLayoutPreference } from './workbench-layout-preference.js';
9
+ import { ensureWorkbenchLayoutPreference, saveWorkbenchWindowLayout } from './workbench-layout-preference.js';
10
10
  import { showToast } from './utils.js';
11
11
  import { initToolPaneDrag, bindToolPaneDivider } from './tool-pane-drag.js';
12
12
 
@@ -79,8 +79,9 @@ export async function saveToolPaneTree(receipt, tree) {
79
79
  if (!live || live.key !== toolPaneContext() || live.key !== receipt.key || live.socket !== receipt.socket || live.scope !== receipt.scope || live.revision !== receipt.revision || !store.get('connected')) return false;
80
80
  var view = activeSavedView();
81
81
  if (view) { var value = copy(view.workspace); value.tree = fromDisplayTree(value, tree); if (value.linkedPair) value.linkedPair.tree = copy(tree); return persistWorkspace(value); }
82
- showToast('Wait for the saved View before changing pane positions.', 'warn');
83
- return false;
82
+ var saved = await saveWorkbenchWindowLayout(receipt.key, tree, receipt.base);
83
+ if (!saved) showToast((store.get('workbenchLayoutPreference') || {}).error || 'Pane positions could not be saved. Try again.', 'warn');
84
+ return saved;
84
85
  }
85
86
  store.subscribe(function (state, previous) {
86
87
  if (state.paneMode) return;
@@ -0,0 +1,39 @@
1
+ import { store } from './store.js';
2
+ import { iconHtml } from './icons.js';
3
+ import { WORKBENCH_PANELS } from './workbench-panels.js';
4
+
5
+ // Every registered workbench gets the same window actions. Existing controllers
6
+ // retain their handlers; missing actions use the common shell and presentation.
7
+ export function ensureWorkbenchWindowControls(panel, name, actions) {
8
+ var definition = WORKBENCH_PANELS[name];
9
+ if (!definition || !actions) return;
10
+ function ensure(kind, selector, label, icon) {
11
+ var button = actions.querySelector(selector);
12
+ if (!button) {
13
+ button = document.createElement('button'); button.type = 'button';
14
+ button.setAttribute('aria-label', label); button.setAttribute('data-tip', label);
15
+ button.setAttribute('aria-pressed', String(kind === 'wide' && !!((store.get('workbenchWindowStates') || {})[name] || {}).wide)); button.innerHTML = iconHtml(icon);
16
+ var close = actions.querySelector('button[aria-label^="Close"]');
17
+ actions.insertBefore(button, close || null);
18
+ button.addEventListener('click', function () {
19
+ if (kind === 'fullscreen') panel.classList.toggle('panel-fullscreen');
20
+ else {
21
+ var values = store.get('workbenchWindowStates') || {}, previous = values[name] || {};
22
+ var wide = !previous.wide;
23
+ store.set({ workbenchWindowStates: Object.assign({}, values, { [name]: Object.assign({}, previous, { wide: wide }) }) });
24
+ button.setAttribute('aria-pressed', String(wide));
25
+ }
26
+ });
27
+ }
28
+ button.dataset.workbenchWindow = kind;
29
+ return button;
30
+ }
31
+ ensure('wide', 'button[aria-label^="Widen"]', 'Widen ' + definition.label, 'chevrons-left-right');
32
+ ensure('fullscreen', 'button[aria-label*="fullscreen"]', 'Toggle ' + definition.label + ' fullscreen', 'maximize-2');
33
+ syncWorkbenchWindowControls(panel, actions);
34
+ }
35
+ export function syncWorkbenchWindowControls(panel, actions) {
36
+ var full = panel.classList.contains('panel-fullscreen');
37
+ var button = actions.querySelector('[data-workbench-window="fullscreen"]');
38
+ if (button) button.setAttribute('aria-pressed', String(full));
39
+ }
@@ -26,7 +26,9 @@ function reconcileCreatedWorkerPresentation() {
26
26
  if (state.sessionListProjectSlug !== state.currentSlug || !driver || !driver.sessionOriginId || !getWs() || getWs().readyState !== 1) return false;
27
27
  store.set({ createdWorkerPresentation: null });
28
28
  showCreatedWorkerViewer(pending.group);
29
- if (state.activeSessionId === driver.id && !state.splitPanes) return true;
29
+ var currentView = (state.projectSplitViews || []).find(function (item) { return item.id === state.projectSplitViewActiveId; });
30
+ if (currentView && currentView.sessions.some(function (ref) { return ref.id === driver.id && ref.sessionOriginId === driver.sessionOriginId; }) ||
31
+ state.activeSessionId === driver.id && !state.splitPanes) return true;
30
32
  var view = (state.projectSplitViews || []).find(function (item) {
31
33
  return item.sessions.some(function (ref) { return ref.id === driver.id && ref.sessionOriginId === driver.sessionOriginId; });
32
34
  });