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

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 (40) hide show
  1. package/lib/ask-user-request-identity.js +18 -1
  2. package/lib/project-delegated-follow-up.js +18 -9
  3. package/lib/public/css/right-workbench.css +26 -3
  4. package/lib/public/css/shared-browser.css +21 -0
  5. package/lib/public/css/sticky-notes.css +3 -2
  6. package/lib/public/index.html +2 -2
  7. package/lib/public/modules/app-header.js +24 -2
  8. package/lib/public/modules/app-messages.js +1 -1
  9. package/lib/public/modules/clay-session-links.js +1 -1
  10. package/lib/public/modules/filebrowser.js +2 -2
  11. package/lib/public/modules/pane-activation.js +3 -0
  12. package/lib/public/modules/pane-browser-bridge.js +15 -5
  13. package/lib/public/modules/pane-header-runtime.js +18 -0
  14. package/lib/public/modules/right-workbench.js +92 -62
  15. package/lib/public/modules/shared-browser.js +6 -0
  16. package/lib/public/modules/split-pane-renderer.js +6 -8
  17. package/lib/public/modules/split-pane-runtime.js +23 -0
  18. package/lib/public/modules/split-view.js +8 -14
  19. package/lib/public/modules/sticky-notes.js +8 -10
  20. package/lib/public/modules/terminal.js +7 -2
  21. package/lib/public/modules/theme.js +5 -0
  22. package/lib/public/modules/workspace-assignment-card.js +9 -6
  23. package/lib/public/pane-assets/{app-ED2VCGNQ.js → app-6H5S2Y3Z.js} +1 -1
  24. package/lib/public/pane-assets/chunk-3QV5SDC4.js +1 -0
  25. package/lib/public/pane-assets/{chunk-JAEQCN5S.js → chunk-4FM3U3DW.js} +1 -1
  26. package/lib/public/pane-assets/{chunk-6SM2WQJX.js → chunk-7GZH3FZY.js} +1 -1
  27. package/lib/public/pane-assets/chunk-AGJTF5WQ.js +1 -0
  28. package/lib/public/pane-assets/chunk-NPKRTUNG.js +2 -0
  29. package/lib/public/pane-assets/{chunk-IYFILQHZ.js → chunk-VGA2Y3D2.js} +1 -1
  30. package/lib/public/pane-assets/{chunk-IZ2ZANEY.js → chunk-WKO3T75E.js} +89 -89
  31. package/lib/public/pane-assets/manifest.json +361 -359
  32. package/lib/public/pane-assets/{style-JVCOCG3W.css → style-TSRDU54Z.css} +1 -1
  33. package/lib/server.js +1 -0
  34. package/lib/workspace-assignment-service.js +56 -11
  35. package/lib/workspace-follow-up-queue.js +92 -0
  36. package/lib/workspace-query-mcp-server.js +1 -1
  37. package/package.json +1 -1
  38. package/lib/public/pane-assets/chunk-6D4DPE5X.js +0 -1
  39. package/lib/public/pane-assets/chunk-7H36SUZA.js +0 -2
  40. package/lib/public/pane-assets/chunk-ST2Q62ET.js +0 -1
@@ -1,3 +1,5 @@
1
+ var normalizeQuestions = require('./yoke/user-input').normalizeQuestions;
2
+
1
3
  function canonicalize(value) {
2
4
  if (Array.isArray(value)) return value.map(canonicalize);
3
5
  if (!value || typeof value !== "object") return value;
@@ -7,9 +9,24 @@ function canonicalize(value) {
7
9
  return result;
8
10
  }
9
11
 
12
+ // Compare SDK input with the canonical display payload without dropping custom
13
+ // review fields or merging independently identified requests.
14
+ function displayInput(input) {
15
+ input = input || {};
16
+ try {
17
+ var questions = normalizeQuestions(input);
18
+ return Object.assign({}, input, { questions: questions.map(function (question, index) {
19
+ var original = input.questions[index];
20
+ return Object.assign({}, original, question, { options: question.options.map(function (option, optionIndex) {
21
+ return Object.assign({}, original.options && original.options[optionIndex], option);
22
+ }) });
23
+ }) });
24
+ } catch (_error) { return input; }
25
+ }
26
+
10
27
  function sameAskUserRequest(event, id, input) {
11
28
  return !!(event && event.type === "tool_executing" && event.name === "AskUserQuestion"
12
- && event.id === id && JSON.stringify(canonicalize(event.input || {})) === JSON.stringify(canonicalize(input || {})));
29
+ && event.id === id && JSON.stringify(canonicalize(displayInput(event.input))) === JSON.stringify(canonicalize(displayInput(input))));
13
30
  }
14
31
 
15
32
  function hasRecordedAskUserRequest(history, id, input) {
@@ -1,25 +1,29 @@
1
+ var isWorker = require("./session-provenance").isWorker;
2
+
1
3
  function hasEntries(value) {
2
4
  return !!(value && Object.keys(value).length > 0);
3
5
  }
4
6
 
5
- function unsafeReason(session, isPaired) {
7
+ function unsafeReason(session, isPaired, options) {
8
+ var opts = options || {}, update = opts.assignmentUpdate === true;
6
9
  if (!session || session.hidden) return "Target session is unavailable.";
10
+ if (isWorker(session) || session.sessionRole === "worker") return "Project follow-ups must target the Driver, not a Worker.";
7
11
  if (!session.cliSessionId) return "Target session must have a durable session reference.";
8
12
  if (session.sessionVisibility !== "private") return "Target session must be private.";
9
- if (session.isProcessing || session._queryStarting || session._awaitingTurnResult) return "Target session must be idle.";
13
+ if (!opts.allowBusy && (session.isProcessing || session._queryStarting || session._awaitingTurnResult)) return "Target session must be idle.";
10
14
  if (hasEntries(session.pendingPermissions) || hasEntries(session.pendingAskUser) || hasEntries(session.pendingElicitations) || hasEntries(session.pendingUserDialogs)) return "Target session is waiting for user input.";
11
15
  if ((session.mode && session.mode !== "gui") || (session.runtimeMode && session.runtimeMode !== "gui") || session.terminalId != null || session.runtimeTerminalId != null || session.tuiSuspended) return "Terminal sessions cannot receive project follow-ups.";
12
16
  if (session.homeDebatePlanning || session.homeDebatePhase || session.debateSetupMode || session.debateState || session._debate) return "Debate sessions cannot receive project follow-ups.";
13
- if (session.loop || session.spawn || session.handoff || session.assignment || session.singleTurn || session._pairDelegation || isPaired) return "Orchestrated sessions cannot receive project follow-ups.";
17
+ if (session.loop || session.spawn || session.handoff || (!update && (session.assignment || session.singleTurn)) || session._pairDelegation || (isPaired && !(opts.allowBusy && !isWorker(session)))) return "Orchestrated sessions cannot receive project follow-ups.";
14
18
  if (session.dangerouslySkipPermissions || session.permissionMode === "bypassPermissions") return "Full-access sessions cannot receive project follow-ups.";
15
- if (session.destroying || session.scheduledMessage || session.rateLimitAutoContinuePending || session.taskStopRequested || session._pendingMessageDrainPaused || session._runtimeRefreshRequested || session.pendingPush && session.pendingPush.length) return "Target session is not safely resumable right now.";
19
+ if (session.destroying || (session.abortController && session.abortController.signal.aborted) || session.scheduledMessage || session.rateLimitAutoContinuePending || session.taskStopRequested || session._lastTurnInterrupted || session._pendingMessageDrainPaused || session._runtimeRefreshRequested || session.pendingPush && session.pendingPush.length) return "Target session is not safely resumable right now.";
16
20
  return "";
17
21
  }
18
22
 
19
23
  function attachDelegatedFollowUp(ctx) {
20
24
  var sm = ctx.sm;
21
25
 
22
- function inspect(principal, targetSessionRef) {
26
+ function inspect(principal, targetSessionRef, options) {
23
27
  if (!principal || typeof targetSessionRef !== "string" || targetSessionRef.indexOf("session:") !== 0) {
24
28
  throw new Error("A durable opaque targetSessionRef is required.");
25
29
  }
@@ -34,15 +38,20 @@ function attachDelegatedFollowUp(ctx) {
34
38
  throw new Error("Target session access denied.");
35
39
  }
36
40
  var paired = !!(ctx.isPairedSession && ctx.isPairedSession(session));
37
- var reason = unsafeReason(session, paired);
41
+ var assignment = session.assignment, opts = options || {};
42
+ var assignmentUpdate = !!(assignment && opts.parentAssignmentId && assignment.assignmentId === opts.parentAssignmentId &&
43
+ assignment.sourceMateId === principal.sourceMateId && assignment.sourceProjectSlug === principal.sourceProjectSlug &&
44
+ assignment.sourceSessionRef === principal.sourceSessionRef);
45
+ var reason = unsafeReason(session, paired, { allowBusy: opts.allowBusy === true, assignmentUpdate: assignmentUpdate });
38
46
  if (reason) throw new Error(reason);
39
47
  if (ctx.isPendingQueuePaused && ctx.isPendingQueuePaused(session)) throw new Error("The Driver queue is stopped; explicit owner resume is required.");
40
48
  if (!ctx.canResumeVendor(session.vendor)) throw new Error("Target session provider cannot safely resume this conversation.");
41
- return { session: session, sessionRef: targetSessionRef, title: session.title || "Existing session" };
49
+ return { session: session, sessionRef: targetSessionRef, title: session.title || "Existing session", linuxUser: ctx.getLinuxUserForSession(session) || null };
42
50
  }
43
51
 
44
52
  async function dispatch(principal, targetSessionRef, task, metadata, onReady) {
45
- var resolved = inspect(principal, targetSessionRef);
53
+ var resolved = inspect(principal, targetSessionRef, { allowBusy: true, parentAssignmentId: metadata.parentAssignmentId });
54
+ if (resolved.session.isProcessing || resolved.session._queryStarting || resolved.session._awaitingTurnResult) throw new Error("Driver is still busy; the reviewed follow-up must remain queued.");
46
55
  var session = resolved.session;
47
56
  var sdk = ctx.getSdk();
48
57
  if (!sdk || typeof sdk.pushMessage !== "function" || typeof sdk.startQuery !== "function") {
@@ -64,7 +73,7 @@ function attachDelegatedFollowUp(ctx) {
64
73
  sm.broadcastSessionList();
65
74
  try {
66
75
  var delivered = sdk.pushMessage(session, task);
67
- if (!delivered) await sdk.startQuery(session, task, undefined, ctx.getLinuxUserForSession(session));
76
+ if (!delivered && await sdk.startQuery(session, task, undefined, resolved.linuxUser) === false) throw new Error("Driver runtime did not accept the follow-up.");
68
77
  } catch (error) {
69
78
  session.isProcessing = false;
70
79
  sm.sendAndRecord(session, {
@@ -49,15 +49,38 @@
49
49
  pointer-events: none;
50
50
  clip-path: inset(100%);
51
51
  }
52
- #main-panels[data-workbench-mode="docked"] > :is(#file-viewer, #terminal-container, #project-logs-panel, #you-panel, #issues-panel, #linear-panel, #notes-browser, #scheduled-tasks-panel, #debates-workbench, #shared-browser-panel, #mcp-skills-workbench, #mate-knowledge-workbench, #wireframe-panel):not(.panel-fullscreen) {
52
+ #main-panels > :is(#file-viewer, #terminal-container, #project-logs-panel, #you-panel, #issues-panel, #linear-panel, #notes-browser, #scheduled-tasks-panel, #debates-workbench, #shared-browser-panel, #mcp-skills-workbench, #mate-knowledge-workbench, #wireframe-panel)[data-workbench-placement="docked"]:not(.panel-fullscreen) {
53
53
  position: relative;
54
54
  inset: auto;
55
- margin: 8px 8px 8px 0;
56
- box-shadow: 0 2px 5px rgba(var(--shadow-rgb), .08);
55
+ flex: 1 1 0;
56
+ width: 0;
57
+ min-width: 240px;
58
+ max-width: none;
59
+ height: 100%;
60
+ max-height: 100%;
61
+ align-self: stretch;
62
+ margin: 0;
63
+ z-index: 0;
64
+ border: 0;
65
+ border-left: 1px solid var(--border-subtle);
66
+ border-radius: 0;
67
+ background: var(--bg);
68
+ box-shadow: none;
57
69
  animation: none;
58
70
  }
59
71
  }
60
72
 
73
+ @media (min-width: 1024px) {
74
+ #main-panels > :is(#file-viewer, #shared-browser-panel)[data-workbench-placement="docked"]:is(.file-viewer-wide, .shared-browser-wide):not(.panel-fullscreen) { flex-grow: 2; }
75
+ #main-panels > [data-workbench-placement="docked"] :is(.terminal-header, .file-viewer-tabbar, .shared-browser-viewport) {
76
+ background: var(--bg);
77
+ }
78
+ #main-panels > #terminal-container[data-workbench-placement="docked"] #terminal-body { background: var(--bg); }
79
+ }
80
+ @media (max-width: 1023px) {
81
+ #main-panels > [data-workbench-placement="docked"][data-workbench-active="false"] { display: none !important; }
82
+ }
83
+
61
84
  #main-panels .workbench-layout-toggle {
62
85
  display: inline-flex;
63
86
  align-items: center;
@@ -1,5 +1,26 @@
1
1
  @import url("./shared-browser-skins.css");
2
2
 
3
+ #shared-browser-toggle-btn { position: relative; }
4
+ .shared-browser-count {
5
+ position: absolute;
6
+ top: -4px;
7
+ right: -4px;
8
+ background: var(--brand-blue);
9
+ color: var(--brand-ink);
10
+ font-size: 9px;
11
+ font-weight: 700;
12
+ min-width: 14px;
13
+ height: 14px;
14
+ border-radius: 7px;
15
+ display: inline-flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ padding: 0 3px;
19
+ font-family: "Roboto Mono", monospace;
20
+ pointer-events: none;
21
+ }
22
+ .shared-browser-count.hidden { display: none; }
23
+
3
24
  /* Shared browser uses the existing mutually exclusive right workbench. */
4
25
  .shared-browser-panel { width: 50%; max-width: 780px; min-width: 360px; height: calc(100% - 16px); align-self: center; margin: 8px 8px 8px 10px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); display: flex; flex-direction: column; overflow: hidden; flex-shrink: 0; box-shadow: 4px 8px 18px rgba(var(--shadow-rgb), .14); }
5
26
  .shared-browser-panel.hidden, #shared-browser-btn.hidden, #shared-browser-toggle-btn.hidden { display: none; }
@@ -59,8 +59,8 @@
59
59
  position: absolute;
60
60
  top: -4px;
61
61
  right: -4px;
62
- background: var(--accent);
63
- color: #fff;
62
+ background: var(--brand-gold);
63
+ color: var(--brand-ink);
64
64
  font-size: 9px;
65
65
  font-weight: 700;
66
66
  min-width: 14px;
@@ -71,6 +71,7 @@
71
71
  justify-content: center;
72
72
  padding: 0 3px;
73
73
  font-family: "Roboto Mono", monospace;
74
+ pointer-events: none;
74
75
  }
75
76
 
76
77
  .sticky-notes-count.hidden { display: none; }
@@ -78,9 +78,9 @@ body { background: transparent; color: inherit; }
78
78
  <div id="skip-perms-pill" class="top-bar-pill pill-error hidden"><i data-lucide="shield-off"></i> <span>Skip perms</span></div>
79
79
  <div id="client-count" class="top-bar-pill pill-accent hidden"><i data-lucide="users"></i> <span id="client-count-text"></span></div>
80
80
  <!-- Right workbench controls: same nodes and handlers as before; app JS returns them to the title bar on mobile. -->
81
- <button type="button" id="shared-browser-toggle-btn" class="hidden" title="Browser" aria-label="Browser" aria-controls="shared-browser-panel" aria-expanded="false" disabled><i data-lucide="globe"></i></button>
81
+ <button type="button" id="shared-browser-toggle-btn" class="hidden" title="Browser" aria-label="Browser" aria-controls="shared-browser-panel" aria-expanded="false" disabled><i data-lucide="globe"></i><span class="shared-browser-count hidden" aria-hidden="true"></span></button>
82
82
  <button id="terminal-toggle-btn" title="Terminal"><i data-lucide="square-terminal"></i><span id="terminal-count" class="hidden"></span></button>
83
- <button type="button" id="sticky-notes-toggle-btn" aria-label="Hide sticky notes" aria-pressed="false" title="Hide sticky notes" hidden><i data-lucide="sticky-note"></i></button>
83
+ <button type="button" id="sticky-notes-toggle-btn" aria-label="Hide sticky notes" aria-pressed="false" title="Hide sticky notes" hidden><i data-lucide="sticky-note"></i><span class="sticky-notes-count hidden" aria-hidden="true"></span></button>
84
84
  <button id="notif-center-btn" title="Notifications"><i data-lucide="bell"></i><span id="notif-center-badge" class="notif-badge hidden">0</span></button>
85
85
  <button id="server-settings-btn" title="Settings" aria-label="Settings" aria-expanded="false"><i data-lucide="settings"></i></button>
86
86
  </div>
@@ -83,6 +83,21 @@ function createHeaderWorkerHistoryControl(root, context) {
83
83
  }
84
84
 
85
85
  export function initHeader() {
86
+ store.subscribe(function (state, prev) {
87
+ if (!state.connected && prev.connected || state.panePendingBinding && state.panePendingBinding !== prev.panePendingBinding) {
88
+ store.set({ loadingMore: false });
89
+ }
90
+ if (state.paneReady && !prev.paneReady || !state.replayingHistory && prev.replayingHistory) {
91
+ // Replay can expose the sentinel before the pane accepts requests.
92
+ // Observe it again once the committed conversation is interactive.
93
+ var messages = getMessagesEl();
94
+ var sentinel = messages && messages.querySelector('.history-sentinel');
95
+ if (sentinel && historySentinelObserver) {
96
+ historySentinelObserver.unobserve(sentinel);
97
+ historySentinelObserver.observe(sentinel);
98
+ }
99
+ }
100
+ });
86
101
  var headerRenameBtn = document.getElementById("header-rename-btn");
87
102
  var headerTitleEl = document.getElementById("header-title");
88
103
  var headerInfoBtn = document.getElementById("header-info-btn");
@@ -339,12 +354,19 @@ export function updateHistorySentinel() {
339
354
  export function requestMoreHistory() {
340
355
  var ws = getWs();
341
356
  var s = store.snap();
342
- if (s.loadingMore || s.historyFrom <= 0 || !ws || !s.connected) return;
357
+ if (s.loadingMore || s.historyFrom <= 0 || !ws || ws.readyState !== 1 || !s.connected ||
358
+ s.replayingHistory || s.historyReplayBatch || s.panePendingBinding || s.paneSuspended ||
359
+ s.paneCommittedBinding && !s.paneReady || s.deferredSessionSwitch) return;
343
360
  store.set({ loadingMore: true });
344
361
  var messagesEl = getMessagesEl();
345
362
  var btn = messagesEl.querySelector(".load-more-btn");
346
363
  if (btn) btn.classList.add("loading");
347
- ws.send(JSON.stringify({ type: "load_more_history", before: s.historyFrom }));
364
+ try {
365
+ ws.send(JSON.stringify({ type: "load_more_history", before: s.historyFrom, paneBinding: s.paneCommittedBinding || undefined }));
366
+ } catch (error) {
367
+ store.set({ loadingMore: false });
368
+ if (btn) btn.classList.remove("loading");
369
+ }
348
370
  }
349
371
 
350
372
  export function prependOlderHistory(items, meta) {
@@ -267,7 +267,7 @@ export function processMessage(msg) {
267
267
  break;
268
268
 
269
269
  case "history_meta":
270
- store.set({ historyFrom: msg.from, historyTotal: msg.total, replayingHistory: true });
270
+ store.set({ historyFrom: msg.from, historyTotal: msg.total, loadingMore: false, replayingHistory: true });
271
271
  updateHistorySentinel();
272
272
  break;
273
273
 
@@ -1,7 +1,7 @@
1
1
  import { parseClaySessionReference, renderClaySessionLink } from './clay-log-links.js';
2
2
 
3
3
  // The prefix is a display compatibility label, never a navigation target.
4
- var citationPattern = /\[([A-Za-z0-9][A-Za-z0-9_-]{0,79})\/(session:[A-Za-z0-9_-]{24}(?:#entry-[0-9]+)?)(?:\s+[\u2014-]\s+([^\]\n]{1,80}))?\]/g;
4
+ var citationPattern = /\[([A-Za-z0-9][A-Za-z0-9_-]{0,79})\/(session:[A-Za-z0-9_-]{24}(?:#entry-[0-9]+)?)(?:\s+[\u2014\u00b7-]\s+([^\]\n]{1,80}))?\]/g;
5
5
  function matches(text) {
6
6
  var pattern = new RegExp(citationPattern.source, 'g'), results = [], match;
7
7
  while ((match = pattern.exec(text))) if (parseClaySessionReference(match[2])) results.push({ start: match.index, end: pattern.lastIndex, prefix: match[1], ref: match[2], label: match[3] || '' });
@@ -4,7 +4,7 @@ import { iconHtml, refreshIcons } from './icons.js';
4
4
  import { escapeHtml, copyToClipboard } from './utils.js';
5
5
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, exportMarkdownAsPdf } from './markdown.js';
6
6
  import { closeSidebar } from './sidebar.js';
7
- import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench, isRightWorkbenchCurrent } from './right-workbench.js';
7
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench, isRightWorkbenchCurrent, rightWorkbenchRevisionFor } from './right-workbench.js';
8
8
  import { renderUnifiedDiff, renderSplitDiff } from './diff.js';
9
9
  import { initFileIcons, getFileIconSvg, getFolderIconSvg } from './fileicons.js';
10
10
  import { copyMarkdownFormatting } from './rich-clipboard.js';
@@ -739,7 +739,7 @@ function requestFileContent(filePath) {
739
739
  if (refreshPresentedDocument(filePath)) return;
740
740
  var requestId = "file-read-" + Date.now() + "-" + Math.random().toString(36).slice(2);
741
741
  var state = store.snap();
742
- store.set({ fileReadRequest: { requestId: requestId, path: filePath, projectSlug: state.currentSlug || null, sessionId: state.activeSessionId != null ? String(state.activeSessionId) : null, accountId: state.myUserId || null, workbenchRevision: store.get('rightWorkbenchRevision') || 0 } });
742
+ store.set({ fileReadRequest: { requestId: requestId, path: filePath, projectSlug: state.currentSlug || null, sessionId: state.activeSessionId != null ? String(state.activeSessionId) : null, accountId: state.myUserId || null, workbenchRevision: rightWorkbenchRevisionFor('file-viewer') } });
743
743
  var pending = store.get('pendingFileNavigation');
744
744
  if (pending && pending.path === filePath) store.set({ pendingFileNavigation: Object.assign({}, pending, { requestId: requestId }) });
745
745
  if (ctx.ws && ctx.connected) {
@@ -1,3 +1,4 @@
1
+ import { paneHeaderRuntime, initPaneHeaderRuntime } from './pane-header-runtime.js';
1
2
  import { disarmStickyBottom } from './chat-render-runtime.js';
2
3
  import { store } from './store.js';
3
4
  import { getWs } from './ws-ref.js';
@@ -204,10 +205,12 @@ export function initPaneActivation() {
204
205
  return binding && { ownerId: binding.viewerId, projectSlug: binding.projectSlug, sessionId: binding.sessionId,
205
206
  binding: binding, ready: store.get('connected') && store.get('paneReady') && !store.get('paneSuspended') };
206
207
  },
208
+ headerRuntime: paneHeaderRuntime,
207
209
  activate: activatePaneSession,
208
210
  checkDraft: function () { if (!store.get('panePendingBinding')) { var draft = takeComposerDraft(); restoreComposerDraft(draft); } },
209
211
  suspend: suspendPane,
210
212
  };
213
+ initPaneHeaderRuntime(frame);
211
214
  // A pointer that began on the previous conversation cannot become a Stop or
212
215
  // approval click on the replacement conversation after hydration.
213
216
  document.addEventListener('pointerdown', function () { store.set({ panePointerBinding: store.get('paneCommittedBinding') }); }, true);
@@ -9,14 +9,24 @@ export function forwardPaneBrowserLink(url) {
9
9
  }
10
10
  export function handlePaneBrowserMessage(event, host) {
11
11
  if (!event || event.origin !== window.location.origin || !host || !event.data || event.data.type !== 'clay-pane-open-browser') return false;
12
- var msg = event.data, s = store.snap();
12
+ var msg = event.data, s = Object.assign({}, store.snap());
13
13
  var frame = Array.from(host.querySelectorAll('.split-pane-frame')).find(function (item) { return item.contentWindow === event.source; });
14
- var pane = frame && s.splitPanes && s.splitPanes.panes && s.splitPanes.panes.find(function (item) { return String(item.slug) === frame.dataset.projectSlug && String(item.sessionId) === frame.dataset.sessionId; });
15
- if (!pane || msg.projectSlug !== s.currentSlug || msg.projectSlug !== frame.dataset.projectSlug || String(msg.sessionId) !== frame.dataset.sessionId) return true;
14
+ var pane = frame && frame.clayPane ? { slug: frame.dataset.projectSlug, sessionId: frame.dataset.sessionId } : frame && s.splitPanes && s.splitPanes.panes && s.splitPanes.panes.find(function (item) { return String(item.slug) === frame.dataset.projectSlug && String(item.sessionId) === frame.dataset.sessionId; });
15
+ if (!s.connected || !pane || msg.projectSlug !== s.currentSlug || msg.projectSlug !== frame.dataset.projectSlug || String(msg.sessionId) !== frame.dataset.sessionId) return true;
16
16
  var expected = new URL('/p/' + encodeURIComponent(pane.slug) + '/?pane=1&session=' + encodeURIComponent(pane.sessionId), window.location.href).href;
17
17
  if (frame.clayPane ? !livePaneBinding(frame, msg) : frame.src !== expected) return true;
18
18
  // The authenticated host owns the browser; a live same-project pane supplies only a URL.
19
- var binding = frame.clayPane && frame.clayPane.identity().binding;
20
- import('./shared-browser.js').then(function (module) { if (binding && (!livePaneBinding(frame, msg) || frame.clayPane.identity().binding !== binding)) return; module.openSharedBrowserAt(msg.url); }).catch(function (error) { showToast(error.message || 'Could not open the browser.', 'warn'); });
19
+ var binding = frame.clayPane && frame.clayPane.identity().binding, document = frame.contentDocument;
20
+ function current() {
21
+ var now = store.snap();
22
+ if (!now.connected || now.currentSlug !== s.currentSlug || now.myUserId !== s.myUserId ||
23
+ now.currentProjectOwnerId !== s.currentProjectOwnerId || now.projectSplitViewSocketGeneration !== s.projectSplitViewSocketGeneration ||
24
+ frame.contentWindow !== event.source || frame.contentDocument !== document ||
25
+ frame.dataset.projectSlug !== msg.projectSlug || String(frame.dataset.sessionId) !== String(msg.sessionId) ||
26
+ !Array.from(host.querySelectorAll('.split-pane-frame')).includes(frame)) return false;
27
+ if (binding) return !!frame.clayPane && livePaneBinding(frame, msg) && frame.clayPane.identity().binding === binding;
28
+ return frame.src === expected && !!(now.splitPanes && now.splitPanes.panes.some(function (item) { return String(item.slug) === pane.slug && String(item.sessionId) === String(pane.sessionId); }));
29
+ }
30
+ import('./shared-browser.js').then(function (module) { if (current()) module.openSharedBrowserAt(msg.url); }).catch(function (error) { showToast(error.message || 'Could not open the browser.', 'warn'); });
21
31
  return true;
22
32
  }
@@ -0,0 +1,18 @@
1
+ import { store } from './store.js';
2
+
3
+ // Read-only presentation metadata from this pane's committed authenticated view.
4
+ export function paneHeaderRuntime() {
5
+ var state = store.snap(), binding = state.paneCommittedBinding;
6
+ if (!binding || !state.connected || !state.paneReady || state.paneSuspended || state.panePendingBinding ||
7
+ state.activeSessionId !== binding.sessionId || state.sessionOriginId !== binding.sessionOriginId ||
8
+ state.currentSlug !== binding.projectSlug || (state.myUserId || null) !== (binding.viewerId || null)) return null;
9
+ return { binding: binding, vendor: state.currentVendor, model: state.currentModel || '' };
10
+ }
11
+
12
+ export function initPaneHeaderRuntime(frame) {
13
+ store.subscribe(function (state, previous) {
14
+ if (state.currentModel === previous.currentModel && state.currentVendor === previous.currentVendor) return;
15
+ if (!paneHeaderRuntime() || frame.contentWindow !== window || frame.contentDocument !== document) return;
16
+ frame.dispatchEvent(new window.parent.CustomEvent('clay-pane-config'));
17
+ });
18
+ }
@@ -1,4 +1,4 @@
1
- // Single arbitration point for the mutually exclusive right-side workbench.
1
+ // Shared arbitration for docked background panes and one foreground overlay.
2
2
 
3
3
  import { store } from './store.js';
4
4
  import { iconHtml, refreshIcons } from './icons.js';
@@ -25,48 +25,76 @@ function ensureLayoutPreference() {
25
25
  });
26
26
  }
27
27
 
28
+ function dockedOwners() { return store.get('rightWorkbenchDockedOwners') || []; }
29
+ function openOwner(name) { return store.get('rightWorkbenchOwner') === name || dockedOwners().indexOf(name) !== -1; }
30
+ export function rightWorkbenchRevisionFor(name) {
31
+ var revisions = store.get('rightWorkbenchPanelRevisions') || {};
32
+ return revisions[name] == null ? store.get('rightWorkbenchRevision') || 0 : revisions[name];
33
+ }
34
+ function publish(owner, docked, revisions) {
35
+ store.set({ rightWorkbenchOwner: owner, rightWorkbenchDockedOwners: docked,
36
+ rightWorkbenchPanelRevisions: revisions, rightWorkbenchRevision: (store.get('rightWorkbenchRevision') || 0) + 1 });
37
+ }
38
+ function setDocked(name, docked) {
39
+ if (!openOwner(name)) return;
40
+ var owners = dockedOwners().filter(function (item) { return item !== name; }), previous = store.get('rightWorkbenchOwner');
41
+ if (docked) owners.push(name);
42
+ publish(docked ? previous : name, owners, store.get('rightWorkbenchPanelRevisions') || {});
43
+ if (!docked && previous && previous !== name && owners.indexOf(previous) === -1 && closers[previous]) closers[previous]();
44
+ }
28
45
  function renderLayoutControls() {
29
46
  if (typeof document === 'undefined') return;
30
47
  var main = document.getElementById('main-panels');
31
48
  if (!main) return;
32
- var state = store.get('workbenchLayoutPreference') || {}, docked = state.mode === 'docked';
33
- main.dataset.workbenchMode = docked ? 'docked' : 'overlay';
34
- var panel = document.getElementById(panelIds[store.get('rightWorkbenchOwner')]);
35
- if (!panel) return;
36
- var header = panel.querySelector(':scope > header, :scope > .file-viewer-header .file-viewer-tabbar, :scope > .terminal-header');
37
- if (!header) return;
38
- var button = panel.querySelector('.workbench-layout-toggle');
39
- if (!button) {
40
- var actions = header.querySelector('[class*="window-actions"], .terminal-header-actions, .shared-browser-window, :scope.mcp-skills-header > div') || header;
41
- button = document.createElement('button'); button.type = 'button'; button.className = 'workbench-layout-toggle';
42
- button.addEventListener('click', function () {
43
- var current = store.get('workbenchLayoutPreference') || {};
44
- if (!current.ready || current.loading || current.saving) { ensureLayoutPreference(); return; }
45
- var owner = store.get('myUserId'), project = store.get('currentSlug');
46
- function currentTarget() { return button.isConnected && owner === store.get('myUserId') && project === store.get('currentSlug'); }
47
- import('./workbench-layout-preference.js').then(function (api) {
48
- var fresh = store.get('workbenchLayoutPreference') || {};
49
- if (!currentTarget() || fresh.mode !== current.mode || fresh.revision !== current.revision || fresh.loading || fresh.saving || !fresh.ready) return false;
50
- return api.saveWorkbenchLayoutMode(current.mode === 'docked' ? 'overlay' : 'docked');
51
- }).then(function (saved) {
52
- if (saved === false && currentTarget()) showToast((store.get('workbenchLayoutPreference') || {}).error || 'Workbench layout could not be saved. Try again.', 'error');
53
- }).catch(function (error) {
54
- if (currentTarget()) showToast(error.message || 'Workbench layout could not be saved. Try again.', 'error');
49
+ var owners = dockedOwners(), state = store.get('workbenchLayoutPreference') || {};
50
+ main.dataset.workbenchMode = owners.length ? 'docked' : 'overlay';
51
+ main.dataset.workbenchDocks = String(owners.length);
52
+ Object.keys(panelIds).forEach(function (name) {
53
+ var panel = document.getElementById(panelIds[name]);
54
+ if (!panel) return;
55
+ var docked = owners.indexOf(name) !== -1;
56
+ panel.dataset.workbenchPlacement = docked ? 'docked' : 'overlay';
57
+ panel.dataset.workbenchActive = store.get('rightWorkbenchOwner') === name ? 'true' : 'false';
58
+ if (!openOwner(name)) return;
59
+ var header = panel.querySelector(':scope > header, :scope > .file-viewer-header .file-viewer-tabbar, :scope > .terminal-header');
60
+ if (!header) return;
61
+ var button = panel.querySelector('.workbench-layout-toggle');
62
+ if (!button) {
63
+ var actions = header.querySelector('[class*="window-actions"], .terminal-header-actions, .shared-browser-window, :scope.mcp-skills-header > div') || header;
64
+ button = document.createElement('button'); button.type = 'button'; button.className = 'workbench-layout-toggle';
65
+ button.addEventListener('click', function () {
66
+ var current = store.get('workbenchLayoutPreference') || {};
67
+ if (!current.ready || current.loading || current.saving) { ensureLayoutPreference(); return; }
68
+ var owner = store.get('myUserId'), project = store.get('currentSlug'), revision = rightWorkbenchRevisionFor(name);
69
+ var wasDocked = dockedOwners().indexOf(name) !== -1;
70
+ function currentTarget() { return button.isConnected && openOwner(name) && owner === store.get('myUserId') && project === store.get('currentSlug') &&
71
+ revision === rightWorkbenchRevisionFor(name) && wasDocked === (dockedOwners().indexOf(name) !== -1); }
72
+ import('./workbench-layout-preference.js').then(function (api) {
73
+ var fresh = store.get('workbenchLayoutPreference') || {};
74
+ if (!currentTarget() || fresh.mode !== current.mode || fresh.revision !== current.revision || fresh.loading || fresh.saving || !fresh.ready) return false;
75
+ return api.saveWorkbenchLayoutMode(wasDocked ? 'overlay' : 'docked');
76
+ }).then(function (saved) {
77
+ if (!currentTarget()) return;
78
+ var fresh = store.get('workbenchLayoutPreference') || {};
79
+ if (saved === true && fresh.ready && fresh.mode === (wasDocked ? 'overlay' : 'docked')) setDocked(name, !wasDocked);
80
+ else if (saved === false) showToast(fresh.error || 'Workbench layout could not be saved. Try again.', 'error');
81
+ }).catch(function (error) {
82
+ if (currentTarget()) showToast(error.message || 'Workbench layout could not be saved. Try again.', 'error');
83
+ });
55
84
  });
56
- });
57
- var anchor = actions === header ? header.querySelector('[data-knowledge-wide]') : actions.firstElementChild;
58
- actions.insertBefore(button, anchor || null);
59
- }
60
- if (button.dataset.mode !== (docked ? 'docked' : 'overlay')) {
61
- button.dataset.mode = docked ? 'docked' : 'overlay';
62
- button.innerHTML = iconHtml(docked ? 'layers-2' : 'panel-right');
63
- refreshIcons();
64
- }
65
- button.setAttribute('aria-label', docked ? 'Float workbench over sessions' : 'Dock workbench beside sessions');
66
- button.title = state.error || button.getAttribute('aria-label');
67
- button.setAttribute('aria-pressed', docked ? 'true' : 'false');
68
- button.disabled = !!state.loading || !!state.saving;
69
- button.setAttribute('aria-busy', state.loading || state.saving ? 'true' : 'false');
85
+ var anchor = actions === header ? header.querySelector('[data-knowledge-wide]') : actions.firstElementChild;
86
+ actions.insertBefore(button, anchor || null);
87
+ }
88
+ if (button.dataset.mode !== (docked ? 'docked' : 'overlay')) {
89
+ button.dataset.mode = docked ? 'docked' : 'overlay';
90
+ button.innerHTML = iconHtml(docked ? 'layers-2' : 'panel-right'); refreshIcons();
91
+ }
92
+ button.setAttribute('aria-label', docked ? 'Float workbench over sessions' : 'Dock workbench beside sessions');
93
+ button.title = state.error || button.getAttribute('aria-label');
94
+ button.setAttribute('aria-pressed', docked ? 'true' : 'false');
95
+ button.disabled = !!state.loading || !!state.saving;
96
+ button.setAttribute('aria-busy', state.loading || state.saving ? 'true' : 'false');
97
+ });
70
98
  }
71
99
 
72
100
  export function registerRightWorkbench(name, close) {
@@ -76,37 +104,39 @@ export function registerRightWorkbench(name, close) {
76
104
 
77
105
  export function claimRightWorkbench(name) {
78
106
  if (typeof document !== 'undefined') ensureLayoutPreference();
79
- var previousRevision = store.get("rightWorkbenchRevision") || 0;
80
- var nextRevision = previousRevision + 1;
81
- storeSet(name, nextRevision);
82
- var keys = Object.keys(closers);
83
- for (var i = 0; i < keys.length; i++) {
84
- if (keys[i] === name) continue;
85
- try { closers[keys[i]](); } catch (error) { /* one panel must not block another */ }
86
- }
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);
109
+ var retained = docked.indexOf(name) !== -1 && owner && docked.indexOf(owner) === -1 ? owner : name;
110
+ var revision = (store.get('rightWorkbenchRevision') || 0) + 1, revisions = Object.assign({}, store.get('rightWorkbenchPanelRevisions') || {});
111
+ Object.keys(revisions).forEach(function (key) { if (key !== retained && docked.indexOf(key) === -1) delete revisions[key]; });
112
+ revisions[name] = revision;
113
+ publish(retained, docked, revisions);
114
+ Object.keys(closers).forEach(function (key) {
115
+ if (key === name || key === retained || docked.indexOf(key) !== -1) return;
116
+ try { closers[key](); } catch (error) { /* one panel must not block another */ }
117
+ });
87
118
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(renderLayoutControls);
88
- return nextRevision;
119
+ return revision;
89
120
  }
90
-
91
- function storeSet(name, value) {
92
- store.set({ rightWorkbenchOwner: name, rightWorkbenchRevision: value });
93
- }
94
-
95
121
  export function releaseRightWorkbench(name) {
96
- if (store.get("rightWorkbenchOwner") !== name) return;
97
- storeSet(null, (store.get("rightWorkbenchRevision") || 0) + 1);
122
+ if (!openOwner(name)) return;
123
+ var docked = dockedOwners().filter(function (item) { return item !== name; }), revisions = Object.assign({}, store.get('rightWorkbenchPanelRevisions') || {});
124
+ delete revisions[name];
125
+ publish(store.get('rightWorkbenchOwner') === name ? docked[docked.length - 1] || null : store.get('rightWorkbenchOwner'), docked, revisions);
98
126
  }
99
-
100
127
  export function isRightWorkbenchCurrent(name, expectedRevision) {
101
- return store.get("rightWorkbenchOwner") === name && store.get("rightWorkbenchRevision") === expectedRevision;
128
+ return openOwner(name) && rightWorkbenchRevisionFor(name) === expectedRevision;
102
129
  }
103
-
104
130
  export function resetRightWorkbench() {
105
- storeSet(null, (store.get("rightWorkbenchRevision") || 0) + 1);
131
+ publish(null, [], {});
132
+ Object.keys(closers).forEach(function (name) { try { closers[name](); } catch (error) { /* Clear other panes after a context change. */ } });
106
133
  }
107
-
108
134
  store.subscribe(function (state, previous) {
109
- if (state.workbenchLayoutPreference !== previous.workbenchLayoutPreference || state.rightWorkbenchOwner !== previous.rightWorkbenchOwner) renderLayoutControls();
110
- if (state.currentSlug === previous.currentSlug && state.myUserId === previous.myUserId) return;
111
- resetRightWorkbench();
135
+ 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 ||
141
+ state.rightWorkbenchDockedOwners !== previous.rightWorkbenchDockedOwners) renderLayoutControls();
112
142
  });
@@ -82,12 +82,18 @@ function ensurePanel() {
82
82
  }
83
83
  function renderEntry() {
84
84
  var browser = store.get('sharedBrowser');
85
+ var count = (store.get('sharedBrowsers') || []).length;
85
86
  var ready = !!store.get('connected') && !!store.get('activeSessionId');
86
87
  var label = !store.get('connected') ? 'Browser · Connecting…' : !store.get('activeSessionId') ? 'Browser · Loading session…' : browser ? 'View browser' : 'Open browser';
87
88
  ['shared-browser-btn', 'shared-browser-toggle-btn'].forEach(function (id) {
88
89
  var button = document.getElementById(id);
89
90
  if (!button) return;
90
91
  var title = id === 'shared-browser-toggle-btn' && ready && store.get('sharedBrowserOpen') ? 'Hide browser' : label;
92
+ if (id === 'shared-browser-toggle-btn') {
93
+ var badge = button.querySelector('.shared-browser-count');
94
+ if (badge) { badge.textContent = String(count); badge.classList.toggle('hidden', count === 0); }
95
+ if (count) title += ' · ' + count + (count === 1 ? ' tab' : ' tabs');
96
+ }
91
97
  button.classList.toggle('hidden', !visibleEntry());
92
98
  button.title = ready && browser ? title + ' · ' + browser.phase : title;
93
99
  button.setAttribute('aria-label', title);
@@ -1,3 +1,4 @@
1
+ import { renderPaneRuntime } from './split-pane-runtime.js';
1
2
  import { store } from './store.js';
2
3
  import { getWs } from './ws-ref.js';
3
4
  import { copyToClipboard } from './utils.js';
@@ -343,7 +344,7 @@ export function createSplitPane(pane, closePane) {
343
344
  }); header.appendChild(sidebarButton);
344
345
  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
346
  header.insertBefore(search, close);
346
- frame.addEventListener('clay-pane-ready', function () {
347
+ function refreshReadyHeader() {
347
348
  if (paneEl.classList.contains('split-pane-cached')) return;
348
349
  var id = Number(frame.dataset.requestedSessionId || frame.dataset.sessionId), current = sessionById(id);
349
350
  var live = frame.clayPane && frame.clayPane.identity(), target = paneTarget(store.snap(), getCachedSessions(), id);
@@ -352,7 +353,9 @@ export function createSplitPane(pane, closePane) {
352
353
  frame.dataset.sessionOriginId = live.binding.sessionOriginId;
353
354
  updateSplitPaneMetadata(paneEl, { slug: frame.dataset.projectSlug, sessionId: id, title: current.title });
354
355
  syncPairChrome(paneEl.parentNode, store.get('splitPanes'));
355
- });
356
+ }
357
+ frame.addEventListener('clay-pane-ready', refreshReadyHeader);
358
+ frame.addEventListener('clay-pane-config', refreshReadyHeader);
356
359
  frame.addEventListener('clay-pane-activation-failed', function () {
357
360
  if (paneEl.classList.contains('split-pane-cached')) return;
358
361
  syncPaneHeaderState(paneEl);
@@ -453,12 +456,7 @@ export function updateSplitPaneMetadata(paneEl, pane) {
453
456
  var sidebarButton = paneEl.querySelector('.split-pane-sidebar');
454
457
  if (sidebarButton) sidebarButton.hidden = store.get('projectSplitViewSingletonOrigin') !== (session && session.sessionOriginId) && paneEl.dataset.active !== 'true';
455
458
  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);
459
+ renderPaneRuntime(paneEl, session);
462
460
  renderPaneWorkers(paneEl, session);
463
461
  updatePaneFullAccessButton(paneEl.querySelector('.split-pane-full-access'), session);
464
462
  syncPaneHeaderState(paneEl);