clay-server 5.2.1-beta.2 → 5.3.0-beta.2

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 (154) hide show
  1. package/lib/clay-control-presentation-binding.js +5 -3
  2. package/lib/clay-product-control-tools.js +3 -3
  3. package/lib/clay-product-control.js +4 -2
  4. package/lib/clay-screen-controls.js +18 -3
  5. package/lib/clay-split-view-awareness.js +35 -0
  6. package/lib/clay-split-view-controls.js +122 -0
  7. package/lib/clay-split-view-input.js +20 -0
  8. package/lib/clay-split-view-render-proof.js +22 -0
  9. package/lib/pane-asset-manifest.js +63 -0
  10. package/lib/project-connection.js +19 -10
  11. package/lib/project-session-actions.js +1 -0
  12. package/lib/project-session-delete.js +2 -2
  13. package/lib/project-session-folders.js +1 -0
  14. package/lib/project-sessions.js +6 -1
  15. package/lib/project-split-view-document.js +33 -0
  16. package/lib/project-split-view-names.js +67 -0
  17. package/lib/project-split-view-organization.js +23 -0
  18. package/lib/project-split-views.js +479 -0
  19. package/lib/project-worker-conversation.js +222 -0
  20. package/lib/project-worker-creation-proposal.js +3 -4
  21. package/lib/project-worker-proposal.js +1 -1
  22. package/lib/project-worker-removal.js +101 -0
  23. package/lib/project.js +28 -1
  24. package/lib/public/app.js +32 -13
  25. package/lib/public/css/base.css +9 -0
  26. package/lib/public/css/filebrowser.css +0 -1
  27. package/lib/public/css/pane-header-controls.css +100 -0
  28. package/lib/public/css/pane.css +24 -41
  29. package/lib/public/css/popup-chat.css +123 -0
  30. package/lib/public/css/right-workbench.css +50 -18
  31. package/lib/public/css/session-folders.css +1 -1
  32. package/lib/public/css/split-view-layout.css +463 -0
  33. package/lib/public/css/split-view-worker-activity.css +97 -0
  34. package/lib/public/css/sticky-notes.css +13 -0
  35. package/lib/public/index.html +3 -3
  36. package/lib/public/modules/app-connection.js +4 -1
  37. package/lib/public/modules/app-favicon.js +1 -1
  38. package/lib/public/modules/app-messages.js +31 -9
  39. package/lib/public/modules/chat-render-runtime.js +54 -0
  40. package/lib/public/modules/clay-control-copy.js +3 -0
  41. package/lib/public/modules/clay-navigation-render.js +1 -1
  42. package/lib/public/modules/clay-owner-navigation.js +15 -5
  43. package/lib/public/modules/clay-split-view-screen.js +74 -0
  44. package/lib/public/modules/clay-tool-activity.js +5 -0
  45. package/lib/public/modules/command-palette.js +30 -28
  46. package/lib/public/modules/contextual-clay-session.js +26 -2
  47. package/lib/public/modules/contextual-clay.js +5 -2
  48. package/lib/public/modules/contextual-mate-sessions.js +2 -0
  49. package/lib/public/modules/filebrowser.js +6 -2
  50. package/lib/public/modules/generation-stop.js +7 -1
  51. package/lib/public/modules/knowledge-workbench-entry.js +43 -0
  52. package/lib/public/modules/markdown-view-mode.js +34 -0
  53. package/lib/public/modules/markdown.js +11 -0
  54. package/lib/public/modules/mate-knowledge-workbench.js +2 -7
  55. package/lib/public/modules/mate-sidebar.js +1 -1
  56. package/lib/public/modules/palette-clay-conversation.js +87 -0
  57. package/lib/public/modules/palette-render-helpers.js +25 -0
  58. package/lib/public/modules/pane-activation.js +46 -7
  59. package/lib/public/modules/pane-binding.js +12 -2
  60. package/lib/public/modules/pane-bridge.js +12 -3
  61. package/lib/public/modules/pane-deferred-bootstrap.js +32 -0
  62. package/lib/public/modules/pane-feature-assets.js +35 -0
  63. package/lib/public/modules/pane-file-bridge.js +8 -3
  64. package/lib/public/modules/pane-search-receipt.js +12 -0
  65. package/lib/public/modules/permission-control.js +6 -6
  66. package/lib/public/modules/popup-chat-rendering.js +244 -0
  67. package/lib/public/modules/project-activation.js +2 -2
  68. package/lib/public/modules/qrcode.js +12 -1
  69. package/lib/public/modules/right-workbench.js +68 -0
  70. package/lib/public/modules/search-hints.js +79 -0
  71. package/lib/public/modules/session-context-menu.js +35 -2
  72. package/lib/public/modules/session-folder-collapse.js +2 -1
  73. package/lib/public/modules/session-folder-context.js +24 -13
  74. package/lib/public/modules/session-folder-delete.js +18 -1
  75. package/lib/public/modules/session-folder-dnd.js +4 -1
  76. package/lib/public/modules/session-folder-layout.js +7 -6
  77. package/lib/public/modules/session-search-pane-state.js +30 -0
  78. package/lib/public/modules/session-search.js +53 -13
  79. package/lib/public/modules/sidebar-mobile.js +3 -3
  80. package/lib/public/modules/sidebar-projects.js +1 -0
  81. package/lib/public/modules/sidebar-session-folders-ui.js +3 -3
  82. package/lib/public/modules/sidebar-sessions.js +57 -24
  83. package/lib/public/modules/sidebar.js +20 -3
  84. package/lib/public/modules/split-group-helpers.js +1 -1
  85. package/lib/public/modules/split-group-restore.js +8 -11
  86. package/lib/public/modules/split-pair-ui.js +18 -0
  87. package/lib/public/modules/split-pane-header-state.js +45 -0
  88. package/lib/public/modules/split-pane-reconciler.js +23 -4
  89. package/lib/public/modules/split-pane-renderer.js +330 -23
  90. package/lib/public/modules/split-pane-retention.js +157 -0
  91. package/lib/public/modules/split-session-boundary.js +21 -0
  92. package/lib/public/modules/split-view-create-action.js +15 -0
  93. package/lib/public/modules/split-view-create-menu.js +83 -0
  94. package/lib/public/modules/split-view-create-request.js +23 -0
  95. package/lib/public/modules/split-view-layout-client.js +198 -0
  96. package/lib/public/modules/split-view-layout-icons.js +37 -0
  97. package/lib/public/modules/split-view-layout-scope.js +38 -0
  98. package/lib/public/modules/split-view-organization.js +73 -0
  99. package/lib/public/modules/split-view-presentation-close.js +35 -0
  100. package/lib/public/modules/split-view-presentation-drag.js +278 -0
  101. package/lib/public/modules/split-view-presentation-drop.js +57 -0
  102. package/lib/public/modules/split-view-presentation-lookup.js +44 -0
  103. package/lib/public/modules/split-view-presentation-renderer.js +494 -0
  104. package/lib/public/modules/split-view-presentations.js +498 -0
  105. package/lib/public/modules/split-view-shell-controls.js +28 -0
  106. package/lib/public/modules/split-view-sidebar-menu.js +42 -0
  107. package/lib/public/modules/split-view-sidebar-renderer.js +30 -0
  108. package/lib/public/modules/split-view-singleton.js +21 -0
  109. package/lib/public/modules/split-view-worker-activity.js +94 -0
  110. package/lib/public/modules/split-view-worker-controls.js +30 -0
  111. package/lib/public/modules/split-view-worker-removal.js +137 -0
  112. package/lib/public/modules/split-view-worker-scroll.js +57 -0
  113. package/lib/public/modules/split-view-worker-transcript.js +46 -0
  114. package/lib/public/modules/split-view-workers.js +418 -0
  115. package/lib/public/modules/split-view.js +61 -22
  116. package/lib/public/modules/sticky-notes.js +9 -2
  117. package/lib/public/modules/tools.js +25 -5
  118. package/lib/public/modules/workbench-layout-preference.js +76 -0
  119. package/lib/public/modules/worker-created-presentation.js +37 -0
  120. package/lib/public/pane-assets/app-2DRBYY3L.js +1 -0
  121. package/lib/public/pane-assets/chunk-4BM7UVNG.js +2 -0
  122. package/lib/public/pane-assets/chunk-6SM2WQJX.js +1 -0
  123. package/lib/public/pane-assets/chunk-7H36SUZA.js +2 -0
  124. package/lib/public/pane-assets/chunk-C4RVVYNT.js +1 -0
  125. package/lib/public/pane-assets/chunk-DGOFGEUK.js +1 -0
  126. package/lib/public/pane-assets/chunk-I57URYSL.js +1 -0
  127. package/lib/public/pane-assets/chunk-O6MQL6MR.js +167 -0
  128. package/lib/public/pane-assets/chunk-PO2JOHHW.js +1 -0
  129. package/lib/public/pane-assets/chunk-UMGHINOT.js +1 -0
  130. package/lib/public/pane-assets/manifest.json +553 -0
  131. package/lib/public/pane-assets/style-63EZCREE.css +1 -0
  132. package/lib/public/style.css +5 -1
  133. package/lib/server-owner-ui.js +6 -2
  134. package/lib/server-owner-view-observation.js +34 -0
  135. package/lib/server-palette.js +1 -0
  136. package/lib/server-pane-bootstrap.js +24 -0
  137. package/lib/server-settings.js +2 -0
  138. package/lib/server-static-assets.js +52 -0
  139. package/lib/server-workbench-layout-settings.js +56 -0
  140. package/lib/server.js +6 -45
  141. package/lib/session-activity-state.js +28 -0
  142. package/lib/session-list-delivery.js +17 -0
  143. package/lib/session-pair-turn-control.js +1 -6
  144. package/lib/session-search.js +3 -0
  145. package/lib/session-split-group-anchors.js +2 -2
  146. package/lib/session-split-group-roles.js +5 -8
  147. package/lib/session-split-group-v2-store.js +0 -3
  148. package/lib/sessions.js +16 -6
  149. package/lib/split-view-layout.js +132 -0
  150. package/lib/users-workbench-layout-preferences.js +33 -0
  151. package/lib/users.js +3 -0
  152. package/lib/workspace-query-service.js +1 -5
  153. package/lib/ws-schema.js +16 -0
  154. package/package.json +6 -3
@@ -39,7 +39,7 @@ export function splitGroupRoles(group) {
39
39
  var driverId = pair.driverId;
40
40
  var workerIds = hasWorkerIds ? (Array.isArray(pair.workerIds) ? pair.workerIds.slice() : []) :
41
41
  (pair.workerId !== undefined ? [pair.workerId] : []);
42
- if (!Number.isInteger(driverId) || workerIds.length < 1 || workerIds.length > 2) return null;
42
+ if (!Number.isInteger(driverId) || workerIds.length < 1) return null;
43
43
  if (group.members.indexOf(driverId) === -1) return null;
44
44
  var ordered = [driverId];
45
45
  for (var i = 0; i < workerIds.length; i++) {
@@ -26,7 +26,7 @@ export function reconcileSplitIdentity(state, previous, host) {
26
26
 
27
27
  // The initial owner response can arrive after history and split metadata.
28
28
  // Wait for it so identity cleanup cannot discard panes we just restored.
29
- export function restoreSplitGroup(openGroup) {
29
+ export function restoreSplitGroup() {
30
30
  var state = store.snap();
31
31
  if (state.authIdentityLoaded === false || state.isMultiUserMode && !state.myUserId) return;
32
32
  if (!isCurrentProjectSessionReady(state, getWs())) return;
@@ -36,6 +36,10 @@ export function restoreSplitGroup(openGroup) {
36
36
  return;
37
37
  }
38
38
  if (state.paneMode) return;
39
+ var activePresentation = (state.projectSplitViews || []).find(function (view) { return view.id === state.projectSplitViewActiveId; });
40
+ if (activePresentation && activePresentation.sessions.some(function (session) {
41
+ return session.id === state.activeSessionId || session.sessionOriginId === state.projectSplitViewFocusedOrigin;
42
+ })) return;
39
43
  if (split) {
40
44
  var reconciliation = reconcileRestoredSplit(split, state.splitGroups);
41
45
  if (reconciliation.action === "clear") {
@@ -55,14 +59,7 @@ export function restoreSplitGroup(openGroup) {
55
59
  }
56
60
  return;
57
61
  }
58
- var activeId = store.get('activeSessionId');
59
- if (!activeId) return;
60
- var groups = store.get('splitGroups') || [];
61
- for (var i = 0; i < groups.length; i++) {
62
- var members = groups[i].members || [];
63
- if (members.indexOf(activeId) !== -1) {
64
- openGroup(groups[i]);
65
- return;
66
- }
67
- }
62
+ // An execution group is not a presentation choice. Restore an explicitly
63
+ // saved legacy split above, but keep an ordinary selected Driver in its
64
+ // single Pane even when it owns Workers. Worker Controls opens the pair.
68
65
  }
@@ -325,3 +325,21 @@ export function syncPairChrome(host, split) {
325
325
  livePane.querySelector(".split-pane-header").appendChild(live);
326
326
  }
327
327
  }
328
+
329
+ // A reconciled pane may move from a legacy Worker pair into a presentation
330
+ // Split view. Remove pair-only role and runtime chrome before refreshing the
331
+ // new session metadata; legacy rendering adds it again immediately afterward.
332
+ export function clearPanePairChrome(paneEl) {
333
+ if (!paneEl) return;
334
+ paneEl.classList.remove("split-pane-role-driver");
335
+ paneEl.classList.remove("split-pane-role-worker");
336
+ if (paneEl.style.removeProperty) {
337
+ paneEl.style.removeProperty("--split-pane-order");
338
+ paneEl.style.removeProperty("--split-worker-row");
339
+ }
340
+ [".split-pair-role", ".split-pair-live", ".split-pair-set-driver", ".split-worker-runtime"].forEach(function (selector) {
341
+ Array.from(paneEl.querySelectorAll(selector)).forEach(function (item) { item.remove(); });
342
+ });
343
+ var header = paneEl.querySelector(".split-pane-header");
344
+ if (header) header.classList.remove("has-worker-runtime");
345
+ }
@@ -0,0 +1,45 @@
1
+ import { store } from './store.js';
2
+ import { getCachedSessions } from './sidebar-sessions.js';
3
+ import { livePaneBinding, paneTarget, samePaneTarget } from './pane-binding.js';
4
+
5
+ function exactReadyPane(paneEl, frame) {
6
+ if (!paneEl || !frame || !frame.clayPane || !store.get('connected') || frame.dataset.requestedSessionId) return false;
7
+ var live = frame.clayPane.identity();
8
+ var sessionId = Number(paneEl.dataset.sessionId);
9
+ var target = live && paneTarget(store.snap(), getCachedSessions(), live.sessionId);
10
+ return !!(live && live.ready && sessionId === live.sessionId && Number(frame.dataset.sessionId) === live.sessionId &&
11
+ live.binding && live.binding.sessionOriginId === frame.dataset.sessionOriginId &&
12
+ samePaneTarget(target, live.binding) && livePaneBinding(frame));
13
+ }
14
+
15
+ export function syncPaneHeaderState(paneEl) {
16
+ var header = paneEl && paneEl.querySelector('.split-pane-header');
17
+ var frame = paneEl && paneEl.querySelector('.split-pane-frame');
18
+ if (!header || !frame) return false;
19
+ var ready = exactReadyPane(paneEl, frame);
20
+ header.setAttribute('aria-busy', ready ? 'false' : 'true');
21
+ header.dataset.paneLoading = ready ? 'false' : 'true';
22
+ header.inert = !ready;
23
+ var actions = header.querySelectorAll('.split-pane-info, .split-pane-search, .split-pane-context, .split-pane-sidebar, .split-view-arrange-trigger');
24
+ for (var i = 0; i < actions.length; i++) {
25
+ var action = actions[i];
26
+ if (!ready) {
27
+ if (action.dataset.panePriorDisabled == null) action.dataset.panePriorDisabled = action.disabled ? 'true' : 'false';
28
+ action.disabled = true;
29
+ } else if (action.dataset.panePriorDisabled != null) {
30
+ action.disabled = action.dataset.panePriorDisabled === 'true';
31
+ delete action.dataset.panePriorDisabled;
32
+ }
33
+ }
34
+ var close = paneEl.querySelector('.split-pane-close');
35
+ if (close) {
36
+ var host = paneEl.parentNode;
37
+ var singletonOnly = !!host && host.id === 'split-host' && host.dataset && host.dataset.singlePane === 'true' &&
38
+ host.querySelectorAll('.split-pane').length === 1;
39
+ close.hidden = singletonOnly;
40
+ close.disabled = !ready || singletonOnly;
41
+ }
42
+ var title = paneEl.querySelector('.split-pane-title');
43
+ if (title) title.setAttribute('aria-disabled', ready ? 'false' : 'true');
44
+ return ready;
45
+ }
@@ -11,9 +11,11 @@ export function splitPaneIdentity(pane) {
11
11
  return String(pane && pane.slug || "") + "#" + String(pane && pane.sessionId || "");
12
12
  }
13
13
 
14
- export function reconcileSplitPanes(host, panes, createPane, updatePane) {
14
+ export function reconcileSplitPanes(host, panes, createPane, updatePane, retention) {
15
15
  var desired = Array.isArray(panes) ? panes : [];
16
- var existing = host.querySelectorAll(".split-pane");
16
+ var existing = host.querySelectorAll(retention ? ".split-pane, .split-pane-cached" : ".split-pane");
17
+ existing = Array.from(existing);
18
+ if (retention) existing.sort(function (a, b) { return retention.age(a) - retention.age(b); });
17
19
  var byKey = new Map();
18
20
  for (var i = 0; i < existing.length; i++) byKey.set(existing[i].dataset.splitPaneKey, existing[i]);
19
21
  var used = new Set();
@@ -24,20 +26,37 @@ export function reconcileSplitPanes(host, panes, createPane, updatePane) {
24
26
  if (!paneEl) {
25
27
  for (var ci = 0; ci < existing.length; ci++) {
26
28
  var candidate = existing[ci], frame = candidate.querySelector('.split-pane-frame');
27
- if (!used.has(candidate) && frame && frame.dataset.projectSlug === pane.slug && frame.clayPane &&
29
+ if (candidate.parentNode === host && !used.has(candidate) && (!retention || retention.canRetarget(candidate)) && frame && frame.dataset.projectSlug === pane.slug && frame.clayPane &&
28
30
  !desired.some(function (item) { return splitPaneIdentity(item) === candidate.dataset.splitPaneKey; })) {
29
31
  paneEl = candidate; break;
30
32
  }
31
33
  }
32
34
  }
35
+ if (!paneEl && retention) {
36
+ while (host.querySelectorAll('.split-pane, .split-pane-cached').length >= retention.limit) {
37
+ var victim = existing.find(function (item) { return item.parentNode === host && !used.has(item) &&
38
+ !desired.some(function (model) { return splitPaneIdentity(model) === item.dataset.splitPaneKey; }); });
39
+ if (!victim) break;
40
+ retention.evict(victim);
41
+ }
42
+ }
33
43
  if (!paneEl) paneEl = createPane(pane, pi);
44
+ if (retention) retention.restore(paneEl);
34
45
  paneEl.dataset.splitPaneKey = key;
35
46
  updatePane(paneEl, pane, pi);
47
+ if (retention) retention.remember(paneEl, pane);
36
48
  if (paneEl.style) paneEl.style.order = String(pi);
37
49
  used.add(paneEl);
38
50
  if (!paneEl.parentNode) host.appendChild(paneEl);
39
51
  }
40
52
  for (var ei = existing.length - 1; ei >= 0; ei--) {
41
- if (!used.has(existing[ei])) existing[ei].remove();
53
+ if (!used.has(existing[ei]) && existing[ei].parentNode === host) {
54
+ if (retention && retention.retain(existing[ei])) retention.park(existing[ei]);
55
+ else if (retention) retention.evict(existing[ei]);
56
+ else {
57
+ if (existing[ei].dispatchEvent) existing[ei].dispatchEvent(new Event('clay-pane-dispose'));
58
+ existing[ei].remove();
59
+ }
60
+ }
42
61
  }
43
62
  }
@@ -1,13 +1,17 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
+ import { copyToClipboard } from './utils.js';
3
4
  import { getCachedSessions } from './sidebar-sessions.js';
4
5
  import { iconHtml } from './icons.js';
5
6
  import { formatTokens } from './app-panels.js';
6
7
  import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
7
8
  import { createPermissionControl, bindPermissionControl, renderPermissionControl } from './permission-control.js';
8
9
  import { splitGroupRoles } from './split-group-helpers.js';
9
- import { paneTarget, livePaneBinding, samePaneTarget } from './pane-binding.js';
10
- import { syncPairChrome } from './split-pair-ui.js';
10
+ import { paneTarget, livePaneBinding, samePaneTarget, samePaneBinding } from './pane-binding.js';
11
+ import { syncPairChrome, clearPanePairChrome } from './split-pair-ui.js';
12
+ import { renderPaneWorkers } from './split-view-workers.js';
13
+ import { toggleSidebarCollapse } from './sidebar.js';
14
+ import { syncPaneHeaderState } from './split-pane-header-state.js';
11
15
 
12
16
  function sessionById(sessionId) {
13
17
  var sessions = getCachedSessions() || [];
@@ -25,19 +29,219 @@ function headerBindingStamp(frame) {
25
29
  live && live.binding && live.binding.requestId, live && live.binding && live.binding.generation]);
26
30
  }
27
31
 
32
+ function capturePaneSearchReceipt(paneEl, frame, binding) {
33
+ var live = frame.clayPane && frame.clayPane.identity(), state = store.snap();
34
+ if (!live || !live.ready || !binding || !samePaneBinding(live.binding, binding) || !livePaneBinding(frame, { paneBinding: binding }) ||
35
+ live.sessionId !== Number(frame.dataset.sessionId) || binding.sessionOriginId !== frame.dataset.sessionOriginId ||
36
+ Number(paneEl.dataset.sessionId) !== live.sessionId || paneEl.hidden || paneEl.inert || frame.closest('.split-pane-cached') ||
37
+ state.paneSuspended || !state.connected || !getWs()) return null;
38
+ return { binding: binding, source: frame.contentWindow, document: frame.contentDocument, socket: getWs(),
39
+ projectSlug: state.currentSlug, ownerId: state.myUserId, sessionId: live.sessionId, sessionOriginId: binding.sessionOriginId };
40
+ }
41
+
42
+ function paneSearchReceiptCurrent(paneEl, frame, receipt) {
43
+ if (!receipt || !paneEl.isConnected || paneEl.hidden || paneEl.inert || frame.closest('.split-pane-cached') ||
44
+ frame.contentWindow !== receipt.source || frame.contentDocument !== receipt.document || getWs() !== receipt.socket) return false;
45
+ var state = store.snap(), live = frame.clayPane && frame.clayPane.identity();
46
+ return state.connected && !state.paneSuspended && state.currentSlug === receipt.projectSlug && state.myUserId === receipt.ownerId &&
47
+ live && live.ready && live.sessionId === receipt.sessionId && live.binding.sessionOriginId === receipt.sessionOriginId &&
48
+ samePaneBinding(live.binding, receipt.binding) && livePaneBinding(frame, { paneBinding: receipt.binding }) &&
49
+ Number(paneEl.dataset.sessionId) === receipt.sessionId && Number(frame.dataset.sessionId) === receipt.sessionId &&
50
+ frame.dataset.sessionOriginId === receipt.sessionOriginId;
51
+ }
52
+
53
+ function sendPaneSearchCommand(paneEl, frame, receipt, action, query) {
54
+ if (!paneSearchReceiptCurrent(paneEl, frame, receipt) || !['open', 'query', 'next', 'previous', 'close'].includes(action)) return false;
55
+ frame.contentWindow.postMessage({ type: "clay-pane-search-command", paneBinding: receipt.binding, action: action, query: query }, window.location.origin);
56
+ return true;
57
+ }
58
+
59
+ var paneSearchDismissalInstalled = false;
60
+
61
+ function createPaneSearchPopover(paneEl, frame, searchButton) {
62
+ var popover = document.createElement("div");
63
+ popover.className = "split-pane-search-popover";
64
+ popover.hidden = true;
65
+ popover.setAttribute("role", "search");
66
+ var input = document.createElement("input");
67
+ input.type = "search"; input.className = "split-pane-search-input";
68
+ input.placeholder = "Search this session"; input.autocomplete = "off"; input.spellcheck = false;
69
+ input.setAttribute("aria-label", "Search this session");
70
+ var count = document.createElement("span"); count.className = "split-pane-search-count"; count.setAttribute("aria-live", "polite");
71
+ var paneHeader = paneEl.querySelector(".split-pane-header");
72
+ var receipt = null;
73
+ var timer = null;
74
+ var disposed = false;
75
+ var actionGesture = null;
76
+ var headerObserver = null;
77
+ var unsubscribe = null;
78
+ function setSearchTop() {
79
+ if (!popover.hidden && paneHeader && paneEl.getBoundingClientRect && paneHeader.getBoundingClientRect && paneEl.style && paneEl.style.setProperty) {
80
+ var paneRect = paneEl.getBoundingClientRect(), headerRect = paneHeader.getBoundingClientRect();
81
+ paneEl.style.setProperty('--split-pane-search-top', Math.max(0, headerRect.bottom - paneRect.top) + 'px');
82
+ }
83
+ }
84
+ function actionButton(label, action, icon) {
85
+ var button = document.createElement("button"); button.type = "button";
86
+ button.className = "split-pane-search-action"; button.title = label; button.setAttribute("aria-label", label);
87
+ button.innerHTML = iconHtml(icon);
88
+ function arm(event) { if (event.type === 'pointerdown' || event.key === 'Enter' || event.key === ' ') actionGesture = { receipt: receipt }; }
89
+ button.addEventListener('pointerdown', arm);
90
+ button.addEventListener('keydown', arm);
91
+ button.addEventListener("click", function () {
92
+ var gesture = actionGesture; actionGesture = null;
93
+ var captured = gesture ? gesture.receipt : receipt;
94
+ if (!disposed && captured === receipt && sendPaneSearchCommand(paneEl, frame, captured, action)) input.focus();
95
+ });
96
+ return button;
97
+ }
98
+ var previous = actionButton("Previous match", "previous", "chevron-up");
99
+ var next = actionButton("Next match", "next", "chevron-down");
100
+ var close = document.createElement("button"); close.type = "button"; close.className = "split-pane-search-action";
101
+ close.title = "Close search"; close.setAttribute("aria-label", "Close search"); close.innerHTML = iconHtml("x");
102
+ input.addEventListener("input", function () {
103
+ clearTimeout(timer);
104
+ var openedReceipt = receipt;
105
+ timer = setTimeout(function () {
106
+ if (!disposed && openedReceipt === receipt) sendPaneSearchCommand(paneEl, frame, openedReceipt, "query", input.value);
107
+ }, 180);
108
+ });
109
+ input.addEventListener("keydown", function (event) {
110
+ if (event.key === "Enter") { event.preventDefault(); sendPaneSearchCommand(paneEl, frame, receipt, event.shiftKey ? "previous" : "next"); }
111
+ if (event.key === "Escape") { event.preventDefault(); closeSearch(); }
112
+ });
113
+ popover.addEventListener("click", function (event) { event.stopPropagation(); });
114
+ function closeSearch(restoreFocus, sendClose, expectedReceipt) {
115
+ if (disposed) return;
116
+ if (expectedReceipt && expectedReceipt !== receipt) return;
117
+ var closingReceipt = receipt;
118
+ clearTimeout(timer);
119
+ if (sendClose !== false) sendPaneSearchCommand(paneEl, frame, closingReceipt, "close");
120
+ receipt = null;
121
+ popover.hidden = true; searchButton.setAttribute("aria-expanded", "false");
122
+ if (restoreFocus !== false) searchButton.focus();
123
+ }
124
+ function armClose(event) { if (event.type === 'pointerdown' || event.key === 'Enter' || event.key === ' ') actionGesture = { receipt: receipt }; }
125
+ close.addEventListener('pointerdown', armClose);
126
+ close.addEventListener('keydown', armClose);
127
+ close.addEventListener("click", function () {
128
+ var gesture = actionGesture; actionGesture = null;
129
+ if (!disposed && (!gesture || gesture.receipt === receipt)) closeSearch();
130
+ });
131
+ popover.closePaneSearch = closeSearch;
132
+ popover.getPaneSearchReceipt = function () { return receipt; };
133
+ popover.openPaneSearch = function (binding) {
134
+ if (disposed) return false;
135
+ var captured = capturePaneSearchReceipt(paneEl, frame, binding);
136
+ if (!captured) return false;
137
+ receipt = captured;
138
+ popover.hidden = false; searchButton.setAttribute("aria-expanded", "true");
139
+ setSearchTop(); sendPaneSearchCommand(paneEl, frame, receipt, "open", input.value); input.focus(); input.select();
140
+ return true;
141
+ };
142
+ if (!paneSearchDismissalInstalled) {
143
+ paneSearchDismissalInstalled = true;
144
+ document.addEventListener("pointerdown", function (event) {
145
+ var openPopovers = document.querySelectorAll(".split-pane-search-popover:not([hidden])");
146
+ for (var i = 0; i < openPopovers.length; i++) {
147
+ var openPopover = openPopovers[i];
148
+ var owner = openPopover.closest(".split-pane");
149
+ var trigger = owner && owner.querySelector(".split-pane-search");
150
+ if (!openPopover.contains(event.target) && event.target !== trigger && openPopover.closePaneSearch) openPopover.closePaneSearch(false);
151
+ }
152
+ });
153
+ }
154
+ searchButton.setAttribute("aria-expanded", "false");
155
+ searchButton.addEventListener("click", function (event) {
156
+ event.stopPropagation();
157
+ if (!syncPaneHeaderState(paneEl)) return;
158
+ var live = frame.clayPane && frame.clayPane.identity();
159
+ if (!live || !live.ready || !livePaneBinding(frame) || live.sessionId !== Number(frame.dataset.sessionId) ||
160
+ live.binding.sessionOriginId !== frame.dataset.sessionOriginId || Number(paneEl.dataset.sessionId) !== live.sessionId) return;
161
+ if (!popover.hidden) { closeSearch(); return; }
162
+ popover.openPaneSearch(live.binding);
163
+ });
164
+ function resetForBindingChange() { closeSearch(false, false); input.value = ""; count.textContent = ""; syncPaneHeaderState(paneEl); }
165
+ function onStoreChange() {
166
+ if (receipt && !paneSearchReceiptCurrent(paneEl, frame, receipt)) resetForBindingChange();
167
+ else syncPaneHeaderState(paneEl);
168
+ }
169
+ function dispose() {
170
+ if (disposed) return;
171
+ closeSearch(false, false);
172
+ disposed = true; clearTimeout(timer); timer = null; actionGesture = null;
173
+ frame.removeEventListener("clay-pane-retarget", resetForBindingChange);
174
+ if (paneHeader) paneHeader.removeEventListener("clay-pane-retarget", resetForBindingChange);
175
+ frame.removeEventListener("clay-pane-activation-failed", resetForBindingChange);
176
+ frame.removeEventListener("clay-pane-suspend", resetForBindingChange);
177
+ if (unsubscribe) { unsubscribe(); unsubscribe = null; }
178
+ window.removeEventListener("resize", setSearchTop);
179
+ if (headerObserver) { headerObserver.disconnect(); headerObserver = null; }
180
+ paneEl.removeEventListener('clay-pane-dispose', dispose);
181
+ }
182
+ paneEl.addEventListener('clay-pane-dispose', dispose);
183
+ paneEl.disposePaneSearchPopover = dispose;
184
+ try {
185
+ frame.addEventListener("clay-pane-retarget", resetForBindingChange);
186
+ if (paneHeader) paneHeader.addEventListener("clay-pane-retarget", resetForBindingChange);
187
+ frame.addEventListener("clay-pane-activation-failed", resetForBindingChange);
188
+ frame.addEventListener("clay-pane-suspend", resetForBindingChange);
189
+ unsubscribe = store.subscribe(onStoreChange);
190
+ window.addEventListener("resize", setSearchTop);
191
+ if (typeof ResizeObserver === 'function') {
192
+ headerObserver = new ResizeObserver(setSearchTop);
193
+ if (paneHeader) headerObserver.observe(paneHeader);
194
+ }
195
+ popover.appendChild(input); popover.appendChild(count); popover.appendChild(previous); popover.appendChild(next); popover.appendChild(close);
196
+ paneEl.appendChild(popover);
197
+ return { popover: popover, input: input, count: count, close: closeSearch, button: searchButton };
198
+ } catch (error) {
199
+ dispose();
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ export function handlePaneSearchMessage(event, host) {
205
+ var frames = host ? host.querySelectorAll(".split-pane-frame") : [];
206
+ for (var i = 0; i < frames.length; i++) {
207
+ var frame = frames[i], live = frame.clayPane && frame.clayPane.identity();
208
+ if (frame.contentWindow !== event.source || !live || !livePaneBinding(frame, event.data) || live.sessionId !== Number(frame.dataset.sessionId)) continue;
209
+ var paneEl = frame.closest(".split-pane"), popover = paneEl && paneEl.querySelector(".split-pane-search-popover");
210
+ if (!popover) return true;
211
+ if (event.data.type === "clay-pane-search-status") {
212
+ var count = popover.querySelector(".split-pane-search-count");
213
+ var input = popover.querySelector(".split-pane-search-input");
214
+ var receipt = popover.getPaneSearchReceipt && popover.getPaneSearchReceipt();
215
+ if (receipt && samePaneBinding(receipt.binding, live.binding) && paneSearchReceiptCurrent(paneEl, frame, receipt)) {
216
+ if (event.data.open === false) popover.closePaneSearch(true, false, receipt);
217
+ else if (!popover.hidden && event.data.query === input.value.trim()) count.textContent = event.data.status || "";
218
+ }
219
+ } else if (event.data.type === "clay-pane-search-open" && popover.hidden) {
220
+ var input = popover.querySelector('.split-pane-search-input');
221
+ popover.openPaneSearch(live.binding);
222
+ }
223
+ return true;
224
+ }
225
+ return false;
226
+ }
227
+
28
228
  function startPaneRename(header, titleEl, pane) {
29
229
  if (header.querySelector(".split-pane-rename-input")) return;
230
+ var section = header.parentNode, frame = section.querySelector('.split-pane-frame');
231
+ var initial = frame && frame.clayPane && frame.clayPane.identity();
232
+ if (!initial || !initial.ready || !livePaneBinding(frame) || initial.sessionId !== pane.sessionId ||
233
+ initial.binding.sessionOriginId !== frame.dataset.sessionOriginId) return;
30
234
  var input = document.createElement("input");
31
235
  input.type = "text"; input.className = "split-pane-rename-input"; input.value = pane.title;
32
236
  titleEl.style.display = "none"; header.insertBefore(input, titleEl); input.focus(); input.select();
33
237
  var done = false;
34
- var section = header.parentNode, frame = section.querySelector('.split-pane-frame');
35
- var origin = frame.dataset.sessionOriginId, binding = frame.clayPane && frame.clayPane.identity().binding, socket = getWs();
238
+ var origin = frame.dataset.sessionOriginId, binding = initial.binding, socket = getWs();
36
239
  function current() {
37
240
  var live = frame.clayPane && frame.clayPane.identity();
38
241
  return header.isConnected && !header.inert && socket === getWs() && store.get('connected') &&
39
242
  frame.dataset.sessionId === String(pane.sessionId) && frame.dataset.sessionOriginId === origin && !frame.dataset.requestedSessionId &&
40
- (!frame.clayPane || livePaneBinding(frame) && live.binding === binding);
243
+ !!live && live.ready && live.sessionId === pane.sessionId && live.binding.sessionOriginId === origin &&
244
+ livePaneBinding(frame) && samePaneBinding(live.binding, binding);
41
245
  }
42
246
  function finish(commit) {
43
247
  if (done) return;
@@ -86,11 +290,11 @@ export function updatePaneFullAccessButton(button, session) {
86
290
  export function createSplitPane(pane, closePane) {
87
291
  var paneEl = document.createElement("section"); paneEl.className = "split-pane";
88
292
  var header = document.createElement("header"); header.className = "split-pane-header"; header.title = pane.title;
89
- var session = sessionById(pane.sessionId); var vendor = session && session.vendor || "claude";
90
- var vendorIcon = document.createElement("img"); vendorIcon.className = "split-pane-vendor";
91
- vendorIcon.src = VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude; vendorIcon.alt = ""; vendorIcon.title = VENDOR_NAMES[vendor] || vendor; header.appendChild(vendorIcon);
293
+ var session = sessionById(pane.sessionId); var vendor = session && session.vendor || "unknown";
294
+ var vendorIcon = document.createElement("img"); vendorIcon.className = "split-pane-vendor"; vendorIcon.hidden = !VENDOR_AVATARS[vendor];
295
+ if (!vendorIcon.hidden) vendorIcon.src = VENDOR_AVATARS[vendor]; vendorIcon.alt = ""; vendorIcon.title = VENDOR_NAMES[vendor] || vendor; header.appendChild(vendorIcon);
92
296
  var title = document.createElement("span"); title.className = "split-pane-title"; title.textContent = pane.title; title.title = "Rename session";
93
- title.addEventListener("click", function () { var current = sessionById(Number(paneEl.dataset.sessionId)); if (current) startPaneRename(header, title, { sessionId: current.id, title: current.title }); }); header.appendChild(title);
297
+ 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);
94
298
  var frame = document.createElement("iframe"); frame.className = "split-pane-frame"; frame.dataset.projectSlug = pane.slug;
95
299
  frame.dataset.sessionId = String(pane.sessionId); frame.src = paneUrl(pane); frame.title = pane.title;
96
300
  frame.dataset.sessionOriginId = session && session.sessionOriginId || '';
@@ -109,12 +313,38 @@ export function createSplitPane(pane, closePane) {
109
313
  });
110
314
  var ctxChip = document.createElement("button"); ctxChip.type = "button"; ctxChip.className = "split-pane-context"; ctxChip.title = "Context usage";
111
315
  ctxChip.innerHTML = '<span class="split-pane-context-bar"><span class="split-pane-context-fill"></span></span><span class="split-pane-context-label"></span>';
112
- ctxChip.addEventListener("click", function () { if (frame.contentWindow && (!frame.clayPane || livePaneBinding(frame))) frame.contentWindow.postMessage({ type: "clay-pane-toggle-context", paneBinding: frame.clayPane && frame.clayPane.identity().binding }, window.location.origin); }); header.appendChild(ctxChip);
316
+ ctxChip.addEventListener("click", function () { if (syncPaneHeaderState(paneEl) && frame.contentWindow && frame.clayPane && livePaneBinding(frame)) frame.contentWindow.postMessage({ type: "clay-pane-toggle-context", paneBinding: frame.clayPane.identity().binding }, window.location.origin); }); header.appendChild(ctxChip);
113
317
  var fullAccess = createPermissionControl("split-pane-full-access hidden", "Driver session permission mode");
114
318
  updatePaneFullAccessButton(fullAccess, session); bindPermissionControl(fullAccess, function () { return permissionState(sessionById(Number(paneEl.dataset.sessionId))); }); header.appendChild(fullAccess); header.insertBefore(fullAccess, ctxChip);
115
319
  var close = document.createElement("button"); close.type = "button"; close.className = "split-pane-close"; close.title = "Close pane"; close.setAttribute("aria-label", "Close pane"); close.innerHTML = iconHtml("x");
116
- close.dataset.sessionId = String(pane.sessionId); close.addEventListener("click", function () { closePane(Number(this.dataset.sessionId)); }); header.appendChild(close);
320
+ if (store.get('projectSplitViewSingletonOrigin') === (session && session.sessionOriginId)) {
321
+ close.hidden = true; close.title = "The only pane cannot be closed"; close.setAttribute("aria-label", "The only pane cannot be closed");
322
+ }
323
+ close.dataset.sessionId = String(pane.sessionId); close.addEventListener("click", function () { if (syncPaneHeaderState(paneEl)) closePane(Number(this.dataset.sessionId)); }); header.appendChild(close);
324
+ var info = document.createElement("button"); info.type = "button"; info.className = "split-pane-info"; info.setAttribute("aria-label", "Session information"); info.title = "Session information"; info.innerHTML = iconHtml("info");
325
+ info.addEventListener("click", function (event) { event.stopPropagation(); if (syncPaneHeaderState(paneEl)) togglePaneInfo(info, frame); }); header.appendChild(info);
326
+ var sidebarButton = document.createElement("button"); sidebarButton.type = "button"; sidebarButton.className = "split-pane-sidebar";
327
+ var sidebarExpanded = !document.getElementById("layout").classList.contains("sidebar-collapsed");
328
+ sidebarButton.setAttribute("aria-label", sidebarExpanded ? "Collapse sidebar" : "Open sidebar");
329
+ sidebarButton.setAttribute("aria-expanded", sidebarExpanded ? "true" : "false");
330
+ sidebarButton.title = sidebarExpanded ? "Collapse sidebar" : "Open sidebar"; sidebarButton.innerHTML = iconHtml("panel-left");
331
+ sidebarButton.addEventListener("click", function (event) {
332
+ event.stopPropagation();
333
+ if (!syncPaneHeaderState(paneEl)) return;
334
+ var live = frame.clayPane && frame.clayPane.identity();
335
+ var activePane = paneEl.dataset.active === "true" || live && store.get('projectSplitViewSingletonOrigin') === live.binding.sessionOriginId;
336
+ if (!live || !live.ready || !livePaneBinding(frame) || live.sessionId !== Number(frame.dataset.sessionId) ||
337
+ live.binding.sessionOriginId !== frame.dataset.sessionOriginId || !activePane) return;
338
+ toggleSidebarCollapse();
339
+ var expanded = !document.getElementById("layout").classList.contains("sidebar-collapsed");
340
+ sidebarButton.setAttribute("aria-expanded", expanded ? "true" : "false");
341
+ sidebarButton.setAttribute("aria-label", expanded ? "Collapse sidebar" : "Open sidebar");
342
+ sidebarButton.title = expanded ? "Collapse sidebar" : "Open sidebar";
343
+ }); header.appendChild(sidebarButton);
344
+ var search = document.createElement("button"); search.type = "button"; search.className = "split-pane-search"; search.setAttribute("aria-label", "Search this session"); search.title = "Search this session"; search.innerHTML = iconHtml("search");
345
+ header.insertBefore(search, close);
117
346
  frame.addEventListener('clay-pane-ready', function () {
347
+ if (paneEl.classList.contains('split-pane-cached')) return;
118
348
  var id = Number(frame.dataset.requestedSessionId || frame.dataset.sessionId), current = sessionById(id);
119
349
  var live = frame.clayPane && frame.clayPane.identity(), target = paneTarget(store.snap(), getCachedSessions(), id);
120
350
  if (!current || !live || !live.ready || !samePaneTarget(target, live.binding) || live.sessionId !== id) return;
@@ -124,24 +354,74 @@ export function createSplitPane(pane, closePane) {
124
354
  syncPairChrome(paneEl.parentNode, store.get('splitPanes'));
125
355
  });
126
356
  frame.addEventListener('clay-pane-activation-failed', function () {
357
+ if (paneEl.classList.contains('split-pane-cached')) return;
358
+ syncPaneHeaderState(paneEl);
127
359
  var id = Number(frame.dataset.requestedSessionId || frame.dataset.sessionId);
128
360
  var fallbackTarget = paneTarget(store.snap(), getCachedSessions(), id);
129
- if (!fallbackTarget) { header.inert = true; return; }
361
+ if (!fallbackTarget) { header.inert = true; syncPaneHeaderState(paneEl); return; }
130
362
  frame.dataset.sessionOriginId = fallbackTarget.sessionOriginId;
131
363
  frame.src = paneUrl({ slug: frame.dataset.projectSlug, sessionId: id });
132
364
  frame.dataset.sessionId = String(id); delete frame.dataset.requestedSessionId;
365
+ syncPaneHeaderState(paneEl);
133
366
  });
134
- paneEl.appendChild(header); paneEl.appendChild(frame); return paneEl;
367
+ paneEl.appendChild(header);
368
+ createPaneSearchPopover(paneEl, frame, search);
369
+ var runtime = document.createElement("div"); runtime.className = "split-view-runtime";
370
+ vendorIcon.classList.add("split-view-runtime-icon"); runtime.appendChild(vendorIcon);
371
+ var runtimeModel = document.createElement("span"); runtimeModel.className = "split-view-model"; runtime.appendChild(runtimeModel);
372
+ header.insertBefore(runtime, title); header.appendChild(close); paneEl.appendChild(frame);
373
+ syncPaneHeaderState(paneEl);
374
+ return paneEl;
375
+ }
376
+
377
+ function togglePaneInfo(anchor, frame) {
378
+ var existing = document.getElementById("split-pane-info-popover");
379
+ if (existing) { existing.remove(); if (existing.dataset.origin === frame.dataset.sessionOriginId) return; }
380
+ var live = frame.clayPane && frame.clayPane.identity();
381
+ if (!live || !live.ready || !livePaneBinding(frame) || live.binding.sessionOriginId !== frame.dataset.sessionOriginId || live.sessionId !== Number(frame.dataset.sessionId)) return;
382
+ var session = sessionById(live.sessionId);
383
+ if (!session || session.sessionOriginId !== live.binding.sessionOriginId) return;
384
+ var binding = live.binding;
385
+ function current() {
386
+ var currentLive = frame.clayPane && frame.clayPane.identity();
387
+ return anchor.isConnected && !anchor.closest('.split-pane-header').inert && frame.dataset.sessionId === String(session.id) &&
388
+ frame.dataset.sessionOriginId === session.sessionOriginId && !frame.dataset.requestedSessionId && currentLive &&
389
+ currentLive.ready && currentLive.sessionId === session.id && samePaneBinding(currentLive.binding, binding) && livePaneBinding(frame);
390
+ }
391
+ var pop = document.createElement("div"); pop.id = "split-pane-info-popover"; pop.className = "session-info-popover"; pop.dataset.origin = session.sessionOriginId;
392
+ var values = [["Local ID", session.id]];
393
+ if (session.cliSessionId) values.push(["Session ID", session.cliSessionId]);
394
+ var vendor = (store.get("vendorInfo") || {})[session.vendor || "claude"] || {};
395
+ if (session.cliSessionId && vendor.terminalResumeCommand) values.push(["Resume", vendor.terminalResumeCommand + " " + session.cliSessionId]);
396
+ values.forEach(function (entry) {
397
+ var row = document.createElement("div"); row.className = "info-row";
398
+ var label = document.createElement("span"); label.className = "info-label"; label.textContent = entry[0];
399
+ var value = document.createElement("span"); value.className = "info-value"; value.textContent = String(entry[1]);
400
+ var copy = document.createElement("button"); copy.type = "button"; copy.className = "info-copy-btn"; copy.title = "Copy " + entry[0]; copy.setAttribute("aria-label", "Copy " + entry[0]); copy.innerHTML = iconHtml("copy");
401
+ copy.addEventListener("click", function () { if (!current()) { pop.remove(); return; } copyToClipboard(String(entry[1])).then(function () { if (pop.isConnected && current()) copy.innerHTML = iconHtml("check"); }); });
402
+ row.append(label, value, copy); pop.appendChild(row);
403
+ });
404
+ document.body.appendChild(pop);
405
+ var rect = anchor.getBoundingClientRect(); pop.style.top = (rect.bottom + 6) + "px"; pop.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - pop.offsetWidth - 8)) + "px";
406
+ function dismiss(event) { if (event.type === "keydown" && event.key !== "Escape") return; if (event.type === "click" && (pop.contains(event.target) || anchor.contains(event.target))) return; pop.remove(); document.removeEventListener("click", dismiss, true); document.removeEventListener("keydown", dismiss, true); frame.removeEventListener('clay-pane-retarget', dismiss); frame.removeEventListener('clay-pane-ready', dismiss); if (event.type === "keydown") anchor.focus(); }
407
+ document.addEventListener("click", dismiss, true); document.addEventListener("keydown", dismiss, true);
408
+ frame.addEventListener('clay-pane-retarget', dismiss); frame.addEventListener('clay-pane-ready', dismiss);
135
409
  }
136
410
 
137
411
  export function updateSplitPaneMetadata(paneEl, pane) {
412
+ clearPanePairChrome(paneEl);
138
413
  var session = sessionById(pane.sessionId); var header = paneEl.querySelector(".split-pane-header");
139
414
  var title = paneEl.querySelector(".split-pane-title"); var frame = paneEl.querySelector(".split-pane-frame"); var vendor = paneEl.querySelector(".split-pane-vendor");
140
415
  var live = frame && frame.clayPane && frame.clayPane.identity();
141
416
  var retarget = frame && (Number(frame.dataset.sessionId) !== pane.sessionId || frame.dataset.sessionOriginId !== (session && session.sessionOriginId || '') || frame.clayPane && (!live || !live.ready));
417
+ if (retarget && frame && frame.clayPane) {
418
+ // Invalidate the old ready binding before header sync exposes any action.
419
+ frame.dataset.requestedSessionId = String(pane.sessionId);
420
+ }
142
421
  if (retarget && header) {
143
422
  header.dispatchEvent(new Event('clay-pane-retarget'));
144
423
  header.inert = true;
424
+ syncPaneHeaderState(paneEl);
145
425
  header.title = 'Loading conversation';
146
426
  if (title) { title.style.display = ''; title.textContent = 'Loading conversation'; }
147
427
  if (vendor) vendor.hidden = true;
@@ -151,24 +431,51 @@ export function updateSplitPaneMetadata(paneEl, pane) {
151
431
  }
152
432
  if (frame && frame.clayPane && retarget) {
153
433
  var target = paneTarget(store.snap(), getCachedSessions(), pane.sessionId);
154
- if (target && frame.clayPane.source === frame.contentWindow && frame.clayPane.document === frame.contentDocument && frame.clayPane.activate(pane.sessionId, false, target)) {
155
- frame.dataset.requestedSessionId = String(pane.sessionId);
156
- if (header) header.inert = true;
157
- return;
434
+ if (target && frame.clayPane.source === frame.contentWindow && frame.clayPane.document === frame.contentDocument) {
435
+ if (frame.clayPane.activate(pane.sessionId, false, target)) {
436
+ if (header) header.inert = true;
437
+ return;
438
+ }
439
+ // A same-target document can expose its controller before its socket and
440
+ // identity are ready. Its bootstrap retries activation; reloading here
441
+ // restarts that bootstrap on every metadata update.
442
+ if ((!live || !live.ready) && Number(frame.dataset.sessionId) === pane.sessionId &&
443
+ frame.dataset.projectSlug === pane.slug && frame.dataset.sessionOriginId === target.sessionOriginId) return;
158
444
  }
445
+ delete frame.dataset.requestedSessionId;
159
446
  frame.src = paneUrl(pane);
160
447
  frame.dataset.sessionId = String(pane.sessionId); frame.dataset.projectSlug = pane.slug;
161
448
  frame.dataset.sessionOriginId = session && session.sessionOriginId || '';
162
449
  }
163
- if (header) header.inert = false;
164
450
  paneEl.dataset.sessionId = String(pane.sessionId); paneEl.dataset.projectSlug = pane.slug;
451
+ paneEl.dataset.sessionOrigin = session && session.sessionOriginId || "";
165
452
  if (header) header.title = pane.title; if (title && title.style.display !== "none") title.textContent = pane.title; if (frame) frame.title = pane.title;
166
- if (vendor) { vendor.hidden = false; var vendorName = session && session.vendor || "claude"; vendor.src = VENDOR_AVATARS[vendorName] || VENDOR_AVATARS.claude; vendor.title = VENDOR_NAMES[vendorName] || vendorName; }
453
+ var sidebarButton = paneEl.querySelector('.split-pane-sidebar');
454
+ if (sidebarButton) sidebarButton.hidden = store.get('projectSplitViewSingletonOrigin') !== (session && session.sessionOriginId) && paneEl.dataset.active !== 'true';
455
+ if (vendor) { var vendorName = session && session.vendor || "unknown"; vendor.hidden = !VENDOR_AVATARS[vendorName]; if (!vendor.hidden) vendor.src = VENDOR_AVATARS[vendorName]; vendor.title = VENDOR_NAMES[vendorName] || vendorName; }
456
+ var runtime = paneEl.querySelector('.split-view-runtime');
457
+ var runtimeName = session && session.vendor || "unknown", runtimeModelName = session && typeof session.model === "string" && session.model.trim() || "Model unavailable";
458
+ var runtimeIcon = runtime.querySelector('.split-view-runtime-icon'), runtimeModel = runtime.querySelector('.split-view-model');
459
+ if (runtimeIcon) { runtimeIcon.hidden = !VENDOR_AVATARS[runtimeName]; if (!runtimeIcon.hidden) runtimeIcon.src = VENDOR_AVATARS[runtimeName]; runtimeIcon.title = VENDOR_NAMES[runtimeName] || runtimeName; runtimeIcon.alt = ""; }
460
+ if (runtimeModel) { runtimeModel.textContent = runtimeModelName; runtimeModel.title = (VENDOR_NAMES[runtimeName] || runtimeName) + " · " + runtimeModelName; }
461
+ runtime.setAttribute("aria-label", (VENDOR_NAMES[runtimeName] || runtimeName) + " · " + runtimeModelName);
462
+ renderPaneWorkers(paneEl, session);
167
463
  updatePaneFullAccessButton(paneEl.querySelector('.split-pane-full-access'), session);
168
- var close = paneEl.querySelector(".split-pane-close");
169
- if (close) {
170
- close.dataset.sessionId = String(pane.sessionId); close.disabled = false; close.title = "Close pane"; close.setAttribute("aria-label", close.title);
171
- }
464
+ syncPaneHeaderState(paneEl);
465
+ updateSplitPaneCloseVisibility(paneEl, pane.sessionId);
466
+ }
467
+
468
+ export function updateSplitPaneCloseVisibility(paneEl, sessionId) {
469
+ var close = paneEl && paneEl.querySelector('.split-pane-close');
470
+ if (!close) return;
471
+ var paneHost = paneEl.parentNode;
472
+ var singletonOnly = !!paneHost && paneHost.id === 'split-host' && paneHost.dataset && paneHost.dataset.singlePane === 'true' &&
473
+ paneHost.querySelectorAll(':scope > .split-pane').length === 1;
474
+ if (sessionId != null) close.dataset.sessionId = String(sessionId);
475
+ close.hidden = singletonOnly;
476
+ close.disabled = singletonOnly;
477
+ close.title = singletonOnly ? "The only pane cannot be closed" : "Close pane";
478
+ close.setAttribute("aria-label", close.title);
172
479
  }
173
480
 
174
481
  function resetPaneContextChip(paneEl) {