clay-server 5.3.0-beta.3 → 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 (42) 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-workers.js +58 -2
  19. package/lib/public/modules/split-view.js +8 -14
  20. package/lib/public/modules/sticky-notes.js +8 -10
  21. package/lib/public/modules/terminal.js +7 -2
  22. package/lib/public/modules/theme.js +5 -0
  23. package/lib/public/modules/worker-created-presentation.js +2 -0
  24. package/lib/public/modules/workspace-assignment-card.js +9 -6
  25. package/lib/public/pane-assets/{app-K4LGSKX5.js → app-6H5S2Y3Z.js} +1 -1
  26. package/lib/public/pane-assets/chunk-3QV5SDC4.js +1 -0
  27. package/lib/public/pane-assets/{chunk-AILOLDZA.js → chunk-4FM3U3DW.js} +1 -1
  28. package/lib/public/pane-assets/{chunk-6SM2WQJX.js → chunk-7GZH3FZY.js} +1 -1
  29. package/lib/public/pane-assets/chunk-AGJTF5WQ.js +1 -0
  30. package/lib/public/pane-assets/chunk-NPKRTUNG.js +2 -0
  31. package/lib/public/pane-assets/{chunk-BYSBUZDZ.js → chunk-VGA2Y3D2.js} +1 -1
  32. package/lib/public/pane-assets/{chunk-W34XSARI.js → chunk-WKO3T75E.js} +93 -93
  33. package/lib/public/pane-assets/manifest.json +347 -345
  34. package/lib/public/pane-assets/{style-JVCOCG3W.css → style-TSRDU54Z.css} +1 -1
  35. package/lib/server.js +1 -0
  36. package/lib/workspace-assignment-service.js +56 -11
  37. package/lib/workspace-follow-up-queue.js +92 -0
  38. package/lib/workspace-query-mcp-server.js +1 -1
  39. package/package.json +1 -1
  40. package/lib/public/pane-assets/chunk-6AJEGTWL.js +0 -1
  41. package/lib/public/pane-assets/chunk-7H36SUZA.js +0 -2
  42. package/lib/public/pane-assets/chunk-QJPBAN5H.js +0 -1
@@ -0,0 +1,23 @@
1
+ import { store } from './store.js';
2
+ import { getCachedSessions } from './sidebar-sessions.js';
3
+ import { livePaneBinding, samePaneBinding, samePaneTarget, paneTarget } from './pane-binding.js';
4
+ import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
5
+ import { modelEntryValue } from './model-picker.js';
6
+
7
+ export function renderPaneRuntime(paneEl, session) {
8
+ var runtime = paneEl.querySelector('.split-view-runtime');
9
+ if (!runtime) return;
10
+ var vendor = session && session.vendor || 'unknown', model = modelEntryValue(session && session.model);
11
+ var frame = paneEl.querySelector('.split-pane-frame'), controller = frame && frame.clayPane;
12
+ var live = controller && controller.identity(), target = session && paneTarget(store.snap(), getCachedSessions(), session.id);
13
+ if (session && live && live.ready && live.sessionId === session.id && !frame.dataset.requestedSessionId &&
14
+ samePaneTarget(target, live.binding) && livePaneBinding(frame) && typeof controller.headerRuntime === 'function') {
15
+ var observed = controller.headerRuntime();
16
+ if (observed && observed.vendor === vendor && samePaneBinding(live.binding, observed.binding)) model = modelEntryValue(observed.model) || model;
17
+ }
18
+ var label = model || (vendor === 'unknown' ? 'Model unavailable' : 'Provider default');
19
+ var icon = runtime.querySelector('.split-view-runtime-icon'), text = runtime.querySelector('.split-view-model');
20
+ if (icon) { icon.hidden = !VENDOR_AVATARS[vendor]; if (!icon.hidden) icon.src = VENDOR_AVATARS[vendor]; icon.title = VENDOR_NAMES[vendor] || vendor; icon.alt = ''; }
21
+ if (text) { text.textContent = label; text.title = (VENDOR_NAMES[vendor] || vendor) + ' · ' + label; }
22
+ runtime.setAttribute('aria-label', (VENDOR_NAMES[vendor] || vendor) + ' · ' + label);
23
+ }
@@ -71,6 +71,54 @@ export function workerRemovalTarget(container, driverOrigin, workerOrigin, gener
71
71
  bindingGeneration: source.binding.generation, bindingRequestId: source.binding.requestId,
72
72
  viewRevision: store.get('projectSplitViewRevision'), socketGeneration: store.get('projectSplitViewSocketGeneration') };
73
73
  }
74
+ // One introduction per Driver in this page, independent of transcript refreshes.
75
+ export function showCreatedWorkerViewer(group) {
76
+ var state = store.snap(), roles = splitGroupRoles(group);
77
+ if (!roles || state.paneMode || state.dmMode || state.replayingHistory || !state.connected ||
78
+ state.activeSessionId !== roles.driverId || document.hidden) return;
79
+ var workerId = roles.workerIds[roles.workerIds.length - 1];
80
+ var worker = sessions().find(function (item) { return item.id === workerId; });
81
+ var anchors = group.pairAnchors, anchor = anchors && (anchors.workers || [anchors.worker])[roles.workerIds.length - 1];
82
+ var origin = worker && worker.sessionOriginId || anchor && anchor.origin;
83
+ if (!origin) return;
84
+ var driverIdentity = sessions().find(function (item) { return item.id === roles.driverId; });
85
+ var driverOrigin = driverIdentity && driverIdentity.sessionOriginId || anchors && anchors.driver && anchors.driver.origin;
86
+ if (!driverOrigin) return;
87
+ var key = JSON.stringify([state.myUserId, state.currentSlug, driverOrigin]);
88
+ var seen = state.introducedWorkerDrivers || [];
89
+ if (seen.indexOf(key) >= 0) return;
90
+ store.set({ introducedWorkerDrivers: seen.concat(key).slice(-64) });
91
+ if (state.createdWorkerViewer || state.splitViewWorkerConversation) return;
92
+ store.set({ createdWorkerViewer: { driverId: roles.driverId, driverOrigin: driverOrigin, workerId: workerId, workerOrigin: origin,
93
+ generation: worker ? worker.workerGeneration : null,
94
+ ownerId: state.myUserId, slug: state.currentSlug, socketGeneration: state.projectSplitViewSocketGeneration } });
95
+ var driver = sessions().find(function (item) { return item.id === roles.driverId; });
96
+ var pane = driver && document.querySelector('.split-pane[data-session-origin="' + CSS.escape(driver.sessionOriginId) + '"]');
97
+ if (pane) renderPaneWorkers(pane, driver);
98
+ }
99
+ function introduceWorkerViewer(container, driver) {
100
+ var pending = store.get('createdWorkerViewer'), state = store.snap();
101
+ if (!pending || pending.driverId !== driver.id) return;
102
+ var worker = sessions().find(function (item) { return item.id === pending.workerId; });
103
+ if (!worker) return;
104
+ store.set({ createdWorkerViewer: null });
105
+ var group = (state.splitGroups || []).find(function (item) {
106
+ var roles = splitGroupRoles(item);
107
+ return roles && roles.driverId === driver.id && roles.workerIds.indexOf(worker.id) >= 0;
108
+ });
109
+ if (!group || driver.sessionOriginId !== pending.driverOrigin || worker.sessionOriginId !== pending.workerOrigin || pending.generation !== null && worker.workerGeneration !== pending.generation ||
110
+ state.splitViewWorkerConversation || document.hidden || !container.isConnected || container.hidden || container.inert ||
111
+ driver.hidden || worker.hidden || exactWorker(driver, worker.sessionOriginId, worker.workerGeneration) !== worker) return;
112
+ var chip = Array.from(container.querySelectorAll('.split-view-worker-chip')).find(function (item) { return item.dataset.workerOrigin === worker.sessionOriginId; });
113
+ if (chip) open(chip, driver, worker, false);
114
+ }
115
+ function syncWorkerSelection() {
116
+ var state = store.get('splitViewWorkerConversation');
117
+ document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
118
+ chip.setAttribute('aria-expanded', String(!!state && chip.dataset.workerOrigin === state.workerOriginId &&
119
+ chip.dataset.workerGeneration === String(state.generation)));
120
+ });
121
+ }
74
122
  function popover() { return document.getElementById("split-view-worker-popover"); }
75
123
  function close(returnFocus) {
76
124
  var state = store.get('splitViewWorkerConversation');
@@ -84,6 +132,7 @@ function close(returnFocus) {
84
132
  if (state.fillTimer) clearTimeout(state.fillTimer);
85
133
  if (state.requestTimer) clearTimeout(state.requestTimer);
86
134
  store.set({ splitViewWorkerConversation: null });
135
+ syncWorkerSelection();
87
136
  var element = popover(); if (element) element.remove();
88
137
  if (returnFocus) {
89
138
  var chip = document.querySelector('.split-pane[data-session-origin="' + CSS.escape(state.driverOriginId) + '"] [data-worker-origin="' + CSS.escape(state.workerOriginId) + '"]');
@@ -135,7 +184,7 @@ function requestPage(state, before, append, refresh) {
135
184
  }
136
185
  }
137
186
  }
138
- function open(chip, driver, worker) {
187
+ function open(chip, driver, worker, takeFocus) {
139
188
  var existing = store.get('splitViewWorkerConversation');
140
189
  if (existing && existing.driverOriginId === driver.sessionOriginId && existing.workerOriginId === worker.sessionOriginId && existing.generation === worker.workerGeneration) { close(true); return; }
141
190
  if (existing) close(false);
@@ -146,7 +195,8 @@ function open(chip, driver, worker) {
146
195
  store.set({ splitViewWorkerConversation: state }); ensurePopover(); renderContent();
147
196
  var element = popover(); position(element, chip);
148
197
  requestPage(state, undefined, false);
149
- var closeButton = element && element.querySelector('[aria-label="Close Worker conversation"]'); if (closeButton) closeButton.focus();
198
+ syncWorkerSelection();
199
+ var closeButton = element && element.querySelector('[aria-label="Close Worker conversation"]'); if (closeButton && takeFocus !== false) closeButton.focus();
150
200
  }
151
201
  function ensurePopover() {
152
202
  var state = store.get('splitViewWorkerConversation');
@@ -368,6 +418,7 @@ export function renderPaneWorkers(container, driver) {
368
418
  });
369
419
  root.appendChild(controls);
370
420
  }
421
+ introduceWorkerViewer(container, driver);
371
422
  var conversation = store.get('splitViewWorkerConversation');
372
423
  if (conversation) ensurePopover();
373
424
  }
@@ -386,6 +437,11 @@ window.addEventListener("scroll", function (event) {
386
437
  reanchorPopover();
387
438
  }, true);
388
439
  store.subscribe(function (state, previous) {
440
+ var introduction = state.createdWorkerViewer;
441
+ if (introduction && (!state.connected || introduction.slug !== state.currentSlug || introduction.ownerId !== state.myUserId ||
442
+ introduction.socketGeneration !== state.projectSplitViewSocketGeneration || introduction.driverId !== state.activeSessionId)) {
443
+ store.set({ createdWorkerViewer: null });
444
+ }
389
445
  if (state.paneBindingSessions !== previous.paneBindingSessions || state.connected !== previous.connected) {
390
446
  document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
391
447
  var worker = sessions().find(function (item) { return item.sessionOriginId === chip.dataset.workerOrigin; });
@@ -131,21 +131,15 @@ function closePane(sessionId) {
131
131
  // Called on session_list so pane headers follow renames made elsewhere
132
132
  // (sidebar, in-pane app). Panes being renamed inline are left alone.
133
133
  export function syncPaneTitles() {
134
- var split = store.get('splitPanes');
135
- if (!split || !split.panes || !host) return;
136
- var titleEls = host.querySelectorAll(".split-pane-title");
137
- var paneEls = host.querySelectorAll('.split-pane');
138
- for (var i = 0; i < split.panes.length && i < titleEls.length; i++) {
139
- var session = sessionById(paneEls[i] && Number(paneEls[i].dataset.sessionId) || split.panes[i].sessionId);
140
- if (!session || !session.title) continue;
141
- if (titleEls[i].parentNode) titleEls[i].parentNode.title = session.title;
142
- if (titleEls[i].style.display !== "none") titleEls[i].textContent = session.title;
143
- }
144
- var accessBtns = host.querySelectorAll(".split-pane-full-access");
145
- for (var ai = 0; ai < split.panes.length && ai < accessBtns.length; ai++) {
146
- updatePaneFullAccessButton(accessBtns[ai], sessionById(paneEls[ai] && Number(paneEls[ai].dataset.sessionId) || split.panes[ai].sessionId));
134
+ if (!host) return;
135
+ var panes = host.querySelectorAll('.split-pane:not(.split-pane-cached)');
136
+ for (var i = 0; i < panes.length; i++) {
137
+ var frame = panes[i].querySelector('.split-pane-frame');
138
+ if (frame && frame.dataset.requestedSessionId) continue;
139
+ var session = sessionById(Number(panes[i].dataset.sessionId));
140
+ if (session) updateSplitPaneMetadata(panes[i], { sessionId: session.id, slug: panes[i].dataset.projectSlug, title: session.title });
147
141
  }
148
- syncPairChrome(host, split);
142
+ syncPairChrome(host, store.get('splitPanes'));
149
143
  }
150
144
 
151
145
  function handlePaneMessage(event) {
@@ -104,11 +104,17 @@ function recomputeFlightGeometry() {
104
104
  function syncFlightControls() {
105
105
  var buttons = document.querySelectorAll("#sticky-notes-toggle-btn");
106
106
  var hidden = !!store.get("notesTemporarilyHidden");
107
- var label = hidden ? "Show sticky notes" : "Hide sticky notes";
107
+ var count = openCount();
108
+ var label = hidden ? "Show sticky notes (" + count + ")" : "Hide sticky notes";
108
109
  for (var i = 0; i < buttons.length; i++) {
109
110
  buttons[i].setAttribute("aria-label", label);
110
111
  buttons[i].setAttribute("aria-pressed", hidden ? "true" : "false");
111
112
  buttons[i].title = label;
113
+ var badge = buttons[i].querySelector('.sticky-notes-count');
114
+ if (badge) {
115
+ badge.textContent = String(count);
116
+ badge.classList.toggle('hidden', !hidden || count === 0);
117
+ }
112
118
  }
113
119
  refreshIcons();
114
120
  }
@@ -246,15 +252,7 @@ export function createNote() {
246
252
 
247
253
  function updateBadge() {
248
254
  syncToggleAvailability();
249
- var badge = document.querySelector(".sticky-notes-count");
250
- if (!badge) return;
251
- var count = openCount();
252
- if (count > 0) {
253
- badge.textContent = count;
254
- badge.classList.remove("hidden");
255
- } else {
256
- badge.classList.add("hidden");
257
- }
255
+ syncFlightControls();
258
256
  }
259
257
 
260
258
  // --- WS message handlers ---
@@ -3,7 +3,7 @@ import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { closeSidebar } from './sidebar.js';
4
4
  import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
5
5
  import { copyToClipboard } from './utils.js';
6
- import { getTerminalTheme } from './theme.js';
6
+ import { getTerminalTheme, getDockedTerminalTheme } from './theme.js';
7
7
  import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
8
8
  import { createKeyToolbar } from './terminal-toolbar.js';
9
9
 
@@ -358,7 +358,7 @@ function createXtermForTab(tab) {
358
358
  cursorBlink: true,
359
359
  fontSize: getTerminalFontSize(),
360
360
  fontFamily: getTerminalFontFamily(),
361
- theme: getTerminalTheme(),
361
+ theme: (store.get('rightWorkbenchDockedOwners') || []).indexOf('terminal') !== -1 ? getDockedTerminalTheme() : getTerminalTheme(),
362
362
  });
363
363
 
364
364
  var fitAddon = null;
@@ -843,6 +843,7 @@ export function sendTerminalCommand(command) {
843
843
  }
844
844
 
845
845
  export function setTerminalTheme(xtermTheme) {
846
+ if ((store.get('rightWorkbenchDockedOwners') || []).indexOf('terminal') !== -1) xtermTheme = getDockedTerminalTheme();
846
847
  for (var tab of tabs.values()) {
847
848
  if (tab.xterm) {
848
849
  tab.xterm.options.theme = xtermTheme;
@@ -850,6 +851,10 @@ export function setTerminalTheme(xtermTheme) {
850
851
  }
851
852
  }
852
853
 
854
+ store.subscribe(function (state, previous) {
855
+ if ((state.rightWorkbenchDockedOwners || []).includes('terminal') !== (previous.rightWorkbenchDockedOwners || []).includes('terminal')) setTerminalTheme(getTerminalTheme());
856
+ });
857
+
853
858
  // Live font update: applies family/size to every open shell terminal
854
859
  // and refits each (cell dimensions change with font size, so cols/rows
855
860
  // shift too). Hooked via onTerminalFontChange below.
@@ -267,6 +267,11 @@ export function getTerminalTheme() {
267
267
  return computeTerminalTheme(getTheme(DEFAULT_DARK_THEME_ID) || defaultDarkFallback);
268
268
  }
269
269
 
270
+ export function getDockedTerminalTheme() {
271
+ var theme = getCurrentTheme();
272
+ return Object.assign({}, computeTerminalTheme(theme), { background: '#' + theme.base00 });
273
+ }
274
+
270
275
  export function getMermaidThemeVars() {
271
276
  return computeMermaidVars(getCurrentTheme());
272
277
  }
@@ -1,4 +1,5 @@
1
1
  // New execution pairs keep the Driver's current presentation; Workers live in pills.
2
+ import { showCreatedWorkerViewer } from './split-view-workers.js';
2
3
  import { store } from './store.js';
3
4
  import { getWs } from './ws-ref.js';
4
5
  import { getCachedSessions } from './sidebar-sessions.js';
@@ -24,6 +25,7 @@ function reconcileCreatedWorkerPresentation() {
24
25
  (session.ownerId || null) === (state.myUserId || null); });
25
26
  if (state.sessionListProjectSlug !== state.currentSlug || !driver || !driver.sessionOriginId || !getWs() || getWs().readyState !== 1) return false;
26
27
  store.set({ createdWorkerPresentation: null });
28
+ showCreatedWorkerViewer(pending.group);
27
29
  if (state.activeSessionId === driver.id && !state.splitPanes) return true;
28
30
  var view = (state.projectSplitViews || []).find(function (item) {
29
31
  return item.sessions.some(function (ref) { return ref.id === driver.id && ref.sessionOriginId === driver.sessionOriginId; });
@@ -11,6 +11,7 @@ function textElement(tag, className, text) {
11
11
  }
12
12
 
13
13
  function statusLabel(status) {
14
+ if (status === "queued") return "Queued for Driver";
14
15
  if (status === "starting") return "Starting";
15
16
  if (status === "running") return "In progress";
16
17
  if (status === "completed") return "Completed";
@@ -22,7 +23,7 @@ function statusLabel(status) {
22
23
 
23
24
  function sendDecision(assignment, action, surface, context, card, opener) {
24
25
  var ws = getWs();
25
- if (!ws || ws.readyState !== 1 || !assignment || assignment.status !== "proposed") return false;
26
+ if (!ws || ws.readyState !== 1 || !assignment || (assignment.status !== "proposed" && !(assignment.status === "queued" && action === "cancel"))) return false;
26
27
  pendingFocus[assignment.assignmentId] = opener || null;
27
28
  card.setAttribute("aria-busy", "true");
28
29
  var buttons = card.querySelectorAll("button");
@@ -79,10 +80,12 @@ export function createWorkspaceAssignmentCard(assignment, options) {
79
80
  card.appendChild(header);
80
81
 
81
82
  var targetText = followUp
82
- ? "Continue “" + (assignment.targetSessionTitle || "Existing session") + "” in " + (assignment.projectTitle || assignment.projectSlug || "project")
83
+ ? "Send instructions to “" + (assignment.targetSessionTitle || "Existing session") + "” in " + (assignment.projectTitle || assignment.projectSlug || "project")
83
84
  : "New private session in " + (assignment.projectTitle || assignment.projectSlug || "project");
84
85
  var target = textElement("p", "workspace-assignment-target", targetText);
85
86
  card.appendChild(target);
87
+ if (assignment.status === 'queued' && assignment.deliveryState === 'queued') card.appendChild(textElement('p', 'workspace-assignment-result', 'Approved. Waiting for the Driver to finish its current turn.'));
88
+ else if (assignment.deliveryState === 'accepted' && assignment.status === 'running') card.appendChild(textElement('p', 'workspace-assignment-result', 'Accepted by the Driver runtime. Completion is still pending.'));
86
89
  card.appendChild(textElement("p", "workspace-assignment-task", assignment.task || ""));
87
90
 
88
91
  if (assignment.error) {
@@ -94,17 +97,17 @@ export function createWorkspaceAssignmentCard(assignment, options) {
94
97
 
95
98
  var actions = document.createElement("div");
96
99
  actions.className = "workspace-assignment-actions";
97
- if (assignment.status === "proposed") {
98
- var cancel = textElement("button", "workspace-assignment-action secondary", "Cancel");
100
+ if (assignment.status === "proposed" || assignment.status === "queued") {
101
+ var cancel = textElement("button", "workspace-assignment-action secondary", assignment.status === "queued" ? "Cancel queued update" : "Cancel");
99
102
  cancel.type = "button";
100
- cancel.setAttribute("aria-label", "Cancel project assignment");
103
+ cancel.setAttribute("aria-label", assignment.status === "queued" ? "Cancel queued update" : "Cancel project assignment");
101
104
  cancel.addEventListener("click", function () { sendDecision(assignment, "cancel", config.home ? "home" : "project", config.context, card, cancel); });
102
105
  var approve = textElement("button", "workspace-assignment-action primary", "Approve");
103
106
  approve.type = "button";
104
107
  approve.setAttribute("aria-label", followUp ? "Approve follow-up in " + (assignment.targetSessionTitle || "the target session") : "Approve new session in " + (assignment.projectTitle || assignment.projectSlug || "the target project"));
105
108
  approve.addEventListener("click", function () { sendDecision(assignment, "approve", config.home ? "home" : "project", config.context, card, approve); });
106
109
  actions.appendChild(cancel);
107
- actions.appendChild(approve);
110
+ if (assignment.status === "proposed") actions.appendChild(approve);
108
111
  } else {
109
112
  var resolved = textElement("span", "workspace-assignment-resolved", statusLabel(assignment.status));
110
113
  resolved.setAttribute("role", "status");