clay-server 3.4.0-beta.12 → 3.4.0-beta.14

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.
@@ -21,6 +21,7 @@ import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette
21
21
  import { handleFindInSessionResults } from './session-search.js';
22
22
  import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
23
23
  import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
24
+ import { handleSessionActionMessage } from './session-actions.js';
24
25
  import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
25
26
  import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent } from './input.js';
26
27
  import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
@@ -601,6 +602,13 @@ export function processMessage(msg) {
601
602
  if (createdPairGroup) openGroup(createdPairGroup);
602
603
  break;
603
604
 
605
+ case "session_handoff_result":
606
+ case "handoff_session_options":
607
+ case "handoff_context":
608
+ case "handoff_created":
609
+ handleSessionActionMessage(msg);
610
+ break;
611
+
604
612
  case "worker_proposal":
605
613
  renderWorkerProposal(msg);
606
614
  break;
@@ -814,9 +814,7 @@ export function updateContextPanel() {
814
814
  hCtxEl = document.createElement("div");
815
815
  hCtxEl.className = "header-context";
816
816
  hCtxEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
817
- var workerBtn = document.getElementById("header-add-worker-btn");
818
- var contextAnchor = workerBtn && workerBtn.parentNode === statusArea ? workerBtn.nextSibling : statusArea.firstChild;
819
- statusArea.insertBefore(hCtxEl, contextAnchor);
817
+ statusArea.insertBefore(hCtxEl, statusArea.firstChild);
820
818
  hCtxEl.addEventListener("mouseenter", function() {
821
819
  if (store.get('richContextUsage')) {
822
820
  showCtxPopover();
@@ -238,8 +238,6 @@ export function renderModelPicker() {
238
238
  return;
239
239
  }
240
240
 
241
- var caps = s.vendorCapabilities || {};
242
- var locked = s.activeSessionMode === "gui" && !!s.sessionHasHistory && !caps.midSessionModelSwitch;
243
241
  for (var i = 0; i < s.currentModels.length; i++) {
244
242
  var item = s.currentModels[i];
245
243
  var value = modelEntryValue(item);
@@ -247,29 +245,18 @@ export function renderModelPicker() {
247
245
  var button = document.createElement("button");
248
246
  button.className = "config-radio-item";
249
247
  if (modelEntryMatches(item, s.currentModel)) button.classList.add("active");
250
- if (locked) button.classList.add("locked");
251
248
  button.dataset.model = value;
252
249
  button.textContent = typeof item === "string" ? item : (item.displayName || value);
253
250
  if (s.modelSelectionPending && s.modelSelectionPending.model === value) {
254
251
  button.classList.add("pending");
255
252
  button.textContent += " · Selecting…";
256
253
  }
257
- button.disabled = locked || !!s.modelSelectionPending;
258
- if (locked) {
259
- button.title = "This vendor binds the model when the session starts. Start a new session to change it.";
260
- } else {
261
- button.addEventListener("click", function() { requestModelSelection(this.dataset.model); });
262
- }
254
+ button.disabled = !!s.modelSelectionPending;
255
+ button.addEventListener("click", function() { requestModelSelection(this.dataset.model); });
263
256
  configModelList.appendChild(button);
264
257
  }
265
258
 
266
259
  if (s.modelSelectionError) {
267
260
  appendState("Model selection failed", s.modelSelectionError, false);
268
261
  }
269
- if (locked) {
270
- var hint = document.createElement("div");
271
- hint.className = "config-model-hint";
272
- hint.textContent = "Locked after first message. Start a new session to change models.";
273
- configModelList.appendChild(hint);
274
- }
275
262
  }
@@ -0,0 +1,294 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { refreshIcons, iconHtml } from './icons.js';
4
+ import { addSystemMessage, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
5
+ import { openPairDialog } from './split-pair-ui.js';
6
+ import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAgentEffort } from './agent-config-selects.js';
7
+ import { showToast } from './utils.js';
8
+
9
+ var menu = null;
10
+ var button = null;
11
+ var outsideHandler = null;
12
+ var escapeHandler = null;
13
+ var handoffDialog = null;
14
+ var handoffEscapeHandler = null;
15
+ var handoffSubmitButton = null;
16
+
17
+ function closeMenu() {
18
+ if (menu) menu.remove();
19
+ menu = null;
20
+ if (button) button.setAttribute("aria-expanded", "false");
21
+ if (outsideHandler) document.removeEventListener("pointerdown", outsideHandler, true);
22
+ if (escapeHandler) document.removeEventListener("keydown", escapeHandler);
23
+ outsideHandler = null;
24
+ escapeHandler = null;
25
+ }
26
+
27
+ function positionMenu() {
28
+ if (!menu || !button) return;
29
+ var rect = button.getBoundingClientRect();
30
+ var width = menu.offsetWidth;
31
+ menu.style.top = Math.round(rect.bottom + 6) + "px";
32
+ menu.style.left = Math.max(8, Math.round(rect.right - width)) + "px";
33
+ }
34
+
35
+ function actionRow(icon, label, description) {
36
+ var row = document.createElement("button");
37
+ row.type = "button";
38
+ row.className = "session-actions-row";
39
+ row.innerHTML = iconHtml(icon, "session-actions-row-icon") +
40
+ '<span class="session-actions-row-copy"><strong></strong><span></span></span>' +
41
+ iconHtml("chevron-right", "session-actions-row-chevron");
42
+ row.querySelector("strong").textContent = label;
43
+ row.querySelector(".session-actions-row-copy > span").textContent = description;
44
+ return row;
45
+ }
46
+
47
+ function menuShell(label) {
48
+ var el = document.createElement("div");
49
+ el.className = "session-actions-menu";
50
+ el.setAttribute("role", "menu");
51
+ el.setAttribute("aria-label", label);
52
+ document.body.appendChild(el);
53
+ menu = el;
54
+ button.setAttribute("aria-expanded", "true");
55
+ return el;
56
+ }
57
+
58
+ function installCloseHandlers() {
59
+ outsideHandler = function (event) {
60
+ if (menu && !menu.contains(event.target) && event.target !== button && !button.contains(event.target)) closeMenu();
61
+ };
62
+ escapeHandler = function (event) {
63
+ if (event.key === "Escape") {
64
+ event.preventDefault();
65
+ closeMenu();
66
+ if (button) button.focus();
67
+ }
68
+ };
69
+ setTimeout(function () {
70
+ document.addEventListener("pointerdown", outsideHandler, true);
71
+ document.addEventListener("keydown", escapeHandler);
72
+ }, 0);
73
+ }
74
+
75
+ function closeHandoffDialog() {
76
+ if (handoffDialog) handoffDialog.remove();
77
+ if (handoffEscapeHandler) document.removeEventListener("keydown", handoffEscapeHandler);
78
+ handoffDialog = null;
79
+ handoffEscapeHandler = null;
80
+ handoffSubmitButton = null;
81
+ }
82
+
83
+ function handoffField(labelText, control) {
84
+ var field = document.createElement("div");
85
+ field.className = "handoff-modal-field";
86
+ var label = document.createElement("label");
87
+ label.className = "wt-modal-label";
88
+ label.textContent = labelText;
89
+ field.appendChild(label);
90
+ field.appendChild(control);
91
+ return field;
92
+ }
93
+
94
+ function requestHandoffDialog() {
95
+ var ws = getWs();
96
+ if (!ws || ws.readyState !== 1) return;
97
+ closeMenu();
98
+ ws.send(JSON.stringify({ type: "handoff_session_options" }));
99
+ }
100
+
101
+ function showHandoffDialog(options) {
102
+ closeHandoffDialog();
103
+ var installed = options.installedVendors || [];
104
+ var vendorInfo = store.get('vendorInfo') || {};
105
+ if (store.get('isOsUsers')) {
106
+ installed = installed.filter(function (vendor) {
107
+ return !vendorInfo[vendor] || vendorInfo[vendor].osUserIsolation !== false;
108
+ });
109
+ }
110
+ if (installed.length < 1) {
111
+ showToast("Install a coding agent before continuing this session.", "error");
112
+ return;
113
+ }
114
+
115
+ var currentVendor = store.get('currentVendor') || "claude";
116
+ var currentModel = store.get('currentModel') || "";
117
+ var currentEffort = store.get('currentEffort') || "";
118
+ var container = document.createElement("div");
119
+ var overlay = document.createElement("div");
120
+ overlay.className = "wt-modal-overlay";
121
+ container.appendChild(overlay);
122
+ var modal = document.createElement("div");
123
+ modal.className = "wt-modal handoff-modal";
124
+ modal.setAttribute("role", "dialog");
125
+ modal.setAttribute("aria-modal", "true");
126
+ modal.setAttribute("aria-labelledby", "handoff-modal-title");
127
+
128
+ var title = document.createElement("div");
129
+ title.id = "handoff-modal-title";
130
+ title.className = "wt-modal-title";
131
+ title.textContent = "Continue in a new agent";
132
+ modal.appendChild(title);
133
+ var desc = document.createElement("div");
134
+ desc.className = "handoff-modal-desc";
135
+ desc.textContent = "Start an independent session with a focused copy of this conversation's context.";
136
+ modal.appendChild(desc);
137
+
138
+ var source = document.createElement("div");
139
+ source.className = "handoff-modal-source";
140
+ var sourceAvatar = document.createElement("img");
141
+ sourceAvatar.src = VENDOR_AVATARS[currentVendor] || VENDOR_AVATARS.claude;
142
+ sourceAvatar.alt = "";
143
+ source.appendChild(sourceAvatar);
144
+ var sourceCopy = document.createElement("span");
145
+ var sourceTitle = document.createElement("strong");
146
+ sourceTitle.textContent = document.getElementById("header-title").textContent || "Current session";
147
+ var sourceMeta = document.createElement("span");
148
+ sourceMeta.textContent = "From " + (VENDOR_NAMES[currentVendor] || currentVendor);
149
+ sourceCopy.appendChild(sourceTitle);
150
+ sourceCopy.appendChild(sourceMeta);
151
+ source.appendChild(sourceCopy);
152
+ modal.appendChild(source);
153
+
154
+ var vendorSelect = buildAgentVendorSelect(installed, currentVendor);
155
+ var modelSelect = document.createElement("select");
156
+ modelSelect.className = "wt-modal-input";
157
+ var effortSelect = buildAgentEffortSelect();
158
+ var vendorField = handoffField("Agent", vendorSelect);
159
+ var modelField = handoffField("Model", modelSelect);
160
+ var effortField = handoffField("Reasoning effort", effortSelect);
161
+ modal.appendChild(vendorField);
162
+ modal.appendChild(modelField);
163
+ modal.appendChild(effortField);
164
+
165
+ function syncEffort(preferred) {
166
+ fillAgentEffort(effortSelect, vendorSelect.value, options, modelSelect.value, preferred);
167
+ var capabilities = (options.capabilitiesByVendor && options.capabilitiesByVendor[vendorSelect.value]) || {};
168
+ effortField.style.display = capabilities.effort === false ? "none" : "";
169
+ }
170
+ fillAgentModels(modelSelect, vendorSelect.value, options, vendorSelect.value === currentVendor ? currentModel : "");
171
+ syncEffort(vendorSelect.value === currentVendor ? currentEffort : "");
172
+ vendorSelect.addEventListener("change", function () {
173
+ fillAgentModels(modelSelect, vendorSelect.value, options, "");
174
+ syncEffort("");
175
+ });
176
+ modelSelect.addEventListener("change", function () { syncEffort(effortSelect.value); });
177
+
178
+ var actions = document.createElement("div");
179
+ actions.className = "wt-modal-actions";
180
+ var cancel = document.createElement("button");
181
+ cancel.type = "button";
182
+ cancel.className = "wt-modal-btn";
183
+ cancel.textContent = "Cancel";
184
+ var submit = document.createElement("button");
185
+ submit.type = "button";
186
+ submit.className = "wt-modal-btn primary";
187
+ submit.textContent = "Continue";
188
+ actions.appendChild(cancel);
189
+ actions.appendChild(submit);
190
+ modal.appendChild(actions);
191
+ container.appendChild(modal);
192
+ document.body.appendChild(container);
193
+ handoffDialog = container;
194
+ handoffSubmitButton = submit;
195
+
196
+ cancel.addEventListener("click", closeHandoffDialog);
197
+ overlay.addEventListener("click", closeHandoffDialog);
198
+ handoffEscapeHandler = function (event) {
199
+ if (event.key === "Escape") closeHandoffDialog();
200
+ };
201
+ document.addEventListener("keydown", handoffEscapeHandler);
202
+ submit.addEventListener("click", function () {
203
+ var ws = getWs();
204
+ if (!ws || ws.readyState !== 1) return;
205
+ ws.send(JSON.stringify({
206
+ type: "handoff_session",
207
+ targetVendor: vendorSelect.value,
208
+ model: modelSelect.value,
209
+ effort: effortSelect.value,
210
+ }));
211
+ submit.disabled = true;
212
+ submit.textContent = "Continuing…";
213
+ });
214
+ vendorSelect.focus();
215
+ }
216
+
217
+ function showMainMenu() {
218
+ closeMenu();
219
+ var el = menuShell("Session actions");
220
+ var addWorker = actionRow("bot", "Add AI Worker", "Open a second agent beside this session.");
221
+ addWorker.addEventListener("click", function () {
222
+ var sessionId = store.get('activeSessionId');
223
+ if (!sessionId) return;
224
+ closeMenu();
225
+ openPairDialog({
226
+ sessionId: sessionId,
227
+ title: document.getElementById("header-title").textContent,
228
+ vendor: store.get('currentVendor') || "claude",
229
+ });
230
+ });
231
+ var handoff = actionRow("forward", "Continue in another agent", "Carry this conversation into a new session.");
232
+ var unavailable = !store.get('sessionHasHistory') || store.get('sessionIsProcessing');
233
+ handoff.disabled = unavailable;
234
+ handoff.title = unavailable ? "Available after the current turn is complete" : "";
235
+ handoff.addEventListener("click", requestHandoffDialog);
236
+ el.appendChild(addWorker);
237
+ el.appendChild(handoff);
238
+ refreshIcons();
239
+ positionMenu();
240
+ installCloseHandlers();
241
+ }
242
+
243
+ function updateVisibility() {
244
+ if (!button) return;
245
+ var state = store.snap();
246
+ var hidden = !state.activeSessionId || state.dmMode || state.paneMode || !!state.splitPanes || state.activeSessionMode !== "gui";
247
+ button.classList.toggle("hidden", hidden);
248
+ if (hidden) closeMenu();
249
+ }
250
+
251
+ export function initSessionActions() {
252
+ button = document.getElementById("header-session-actions-btn");
253
+ if (!button) return;
254
+ button.addEventListener("click", function () {
255
+ if (menu) closeMenu();
256
+ else showMainMenu();
257
+ });
258
+ store.subscribe(function (state, prev) {
259
+ if (state.activeSessionId !== prev.activeSessionId || state.dmMode !== prev.dmMode ||
260
+ state.paneMode !== prev.paneMode || state.splitPanes !== prev.splitPanes ||
261
+ state.activeSessionMode !== prev.activeSessionMode) updateVisibility();
262
+ });
263
+ window.addEventListener("resize", closeMenu);
264
+ updateVisibility();
265
+ }
266
+
267
+ export function handleSessionActionMessage(msg) {
268
+ if (msg.type === "handoff_session_options") {
269
+ showHandoffDialog(msg);
270
+ return true;
271
+ }
272
+ if (msg.type === "session_handoff_result") {
273
+ if (msg.ok) closeHandoffDialog();
274
+ else {
275
+ showToast(msg.error || "Could not continue the session in another agent.", "error");
276
+ if (handoffSubmitButton) {
277
+ handoffSubmitButton.disabled = false;
278
+ handoffSubmitButton.textContent = "Continue";
279
+ }
280
+ }
281
+ return true;
282
+ }
283
+ if (msg.type === "handoff_context") {
284
+ var sourceName = VENDOR_NAMES[msg.sourceVendor] || msg.sourceVendor || "another agent";
285
+ addSystemMessage("Continued from “" + (msg.sourceTitle || "Untitled session") + "” in " + sourceName + ".", false);
286
+ return true;
287
+ }
288
+ if (msg.type === "handoff_created") {
289
+ var targetName = VENDOR_NAMES[msg.targetVendor] || msg.targetVendor || "another agent";
290
+ addSystemMessage("Continued in a new " + targetName + " session.", false);
291
+ return true;
292
+ }
293
+ return false;
294
+ }
@@ -1,7 +1,7 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
- import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, isExperimentalVendor } from './app-rendering.js';
4
- import { effortLevelsFor, effortDisplayName } from './app-panels.js';
3
+ import { VENDOR_AVATARS } from './app-rendering.js';
4
+ import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAgentEffort } from './agent-config-selects.js';
5
5
  import { showToast } from './utils.js';
6
6
 
7
7
  var pairDialog = null;
@@ -9,78 +9,14 @@ var pairDialog = null;
9
9
  // that session becomes the Driver and the dialog only configures the Worker.
10
10
  var pendingDriver = null;
11
11
 
12
- function optionValue(entry) {
13
- if (typeof entry === "string") return entry;
14
- return entry && (entry.value || entry.id) || "";
15
- }
16
-
17
- function optionLabel(entry) {
18
- if (typeof entry === "string") return entry;
19
- return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
20
- }
21
-
22
- function fillModels(select, vendor, options) {
23
- var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
24
- select.innerHTML = "";
25
- var automatic = document.createElement("option");
26
- automatic.value = "";
27
- automatic.textContent = "Automatic";
28
- select.appendChild(automatic);
29
- for (var i = 0; i < models.length; i++) {
30
- var option = document.createElement("option");
31
- option.value = optionValue(models[i]);
32
- option.textContent = optionLabel(models[i]);
33
- select.appendChild(option);
34
- }
35
- }
36
-
37
12
  function closePairDialog() {
38
13
  if (pairDialog) pairDialog.remove();
39
14
  pairDialog = null;
40
15
  }
41
16
 
42
- function buildVendorSelect(installed, preferred) {
43
- var select = document.createElement("select");
44
- select.className = "wt-modal-input";
45
- for (var i = 0; i < VENDOR_ORDER.length; i++) {
46
- if (installed.indexOf(VENDOR_ORDER[i]) === -1) continue;
47
- var option = document.createElement("option");
48
- option.value = VENDOR_ORDER[i];
49
- option.textContent = (isExperimentalVendor(VENDOR_ORDER[i]) ? "🧪 " : "") + (VENDOR_NAMES[VENDOR_ORDER[i]] || VENDOR_ORDER[i]);
50
- if (isExperimentalVendor(VENDOR_ORDER[i])) option.title = "Experimental integration; not yet validated through direct use testing";
51
- select.appendChild(option);
52
- }
53
- if (preferred && installed.indexOf(preferred) !== -1) select.value = preferred;
54
- return select;
55
- }
56
-
57
17
  // Effort levels are vendor- and model-specific (codex: minimal..xhigh,
58
18
  // claude/kiro: low..max, plus per-model supportedEffortLevels overrides), so
59
19
  // options are rebuilt whenever the role's vendor or model changes.
60
- function fillEffortOptions(select, vendor, options, modelValue) {
61
- var previous = select.value;
62
- var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
63
- var levels = effortLevelsFor(vendor, models, modelValue);
64
- select.innerHTML = "";
65
- var automatic = document.createElement("option");
66
- automatic.value = "";
67
- automatic.textContent = "Default";
68
- select.appendChild(automatic);
69
- for (var i = 0; i < levels.length; i++) {
70
- var option = document.createElement("option");
71
- option.value = levels[i];
72
- option.textContent = effortDisplayName(levels[i]);
73
- select.appendChild(option);
74
- }
75
- select.value = levels.indexOf(previous) !== -1 ? previous : "";
76
- }
77
-
78
- function buildEffortSelect() {
79
- var select = document.createElement("select");
80
- select.className = "wt-modal-input";
81
- return select;
82
- }
83
-
84
20
  function fieldLabel(text, control) {
85
21
  var label = document.createElement("label");
86
22
  label.className = "wt-modal-label";
@@ -155,25 +91,25 @@ export function showPairDialog(options) {
155
91
  : "One Driver plans and directs a visible Worker session.";
156
92
  modal.appendChild(desc);
157
93
 
158
- var driverVendor = buildVendorSelect(installed, installed.indexOf("claude") !== -1 ? "claude" : installed[0]);
94
+ var driverVendor = buildAgentVendorSelect(installed, installed.indexOf("claude") !== -1 ? "claude" : installed[0]);
159
95
  var workerDefault = options.lastVendor && options.lastVendor !== driverVendor.value ? options.lastVendor : (installed.indexOf("codex") !== -1 ? "codex" : installed[0]);
160
- var workerVendor = buildVendorSelect(installed, workerDefault);
96
+ var workerVendor = buildAgentVendorSelect(installed, workerDefault);
161
97
  var driverModel = document.createElement("select");
162
98
  driverModel.className = "wt-modal-input";
163
99
  var workerModel = document.createElement("select");
164
100
  workerModel.className = "wt-modal-input";
165
- var driverEffort = buildEffortSelect();
166
- var workerEffort = buildEffortSelect();
167
- function refreshDriverEffort() { fillEffortOptions(driverEffort, driverVendor.value, options, driverModel.value); }
168
- function refreshWorkerEffort() { fillEffortOptions(workerEffort, workerVendor.value, options, workerModel.value); }
169
- fillModels(driverModel, driverVendor.value, options);
170
- fillModels(workerModel, workerVendor.value, options);
101
+ var driverEffort = buildAgentEffortSelect();
102
+ var workerEffort = buildAgentEffortSelect();
103
+ function refreshDriverEffort() { fillAgentEffort(driverEffort, driverVendor.value, options, driverModel.value); }
104
+ function refreshWorkerEffort() { fillAgentEffort(workerEffort, workerVendor.value, options, workerModel.value); }
105
+ fillAgentModels(driverModel, driverVendor.value, options);
106
+ fillAgentModels(workerModel, workerVendor.value, options);
171
107
  refreshDriverEffort();
172
108
  refreshWorkerEffort();
173
109
  workerEffort.value = "medium";
174
110
  if (workerEffort.value !== "medium") workerEffort.value = "";
175
- driverVendor.addEventListener("change", function () { fillModels(driverModel, driverVendor.value, options); refreshDriverEffort(); });
176
- workerVendor.addEventListener("change", function () { fillModels(workerModel, workerVendor.value, options); refreshWorkerEffort(); });
111
+ driverVendor.addEventListener("change", function () { fillAgentModels(driverModel, driverVendor.value, options); refreshDriverEffort(); });
112
+ workerVendor.addEventListener("change", function () { fillAgentModels(workerModel, workerVendor.value, options); refreshWorkerEffort(); });
177
113
  driverModel.addEventListener("change", refreshDriverEffort);
178
114
  workerModel.addEventListener("change", refreshWorkerEffort);
179
115
 
@@ -319,7 +255,8 @@ export function syncPairChrome(host, split) {
319
255
  setBtn.title = "Make this session the Driver; the other pane becomes its Worker";
320
256
  setBtn.addEventListener("click", function () { sendSetPair(group.id, paneSessionId); });
321
257
  var titleEl = header.querySelector(".split-pane-title");
322
- header.insertBefore(setBtn, titleEl ? titleEl.nextSibling : null);
258
+ var accessEl = header.querySelector(".split-pane-full-access");
259
+ header.insertBefore(setBtn, accessEl ? accessEl.nextSibling : (titleEl ? titleEl.nextSibling : null));
323
260
  })(split.panes[ai].sessionId, headers[ai]);
324
261
  }
325
262
  return;
@@ -339,7 +276,8 @@ export function syncPairChrome(host, split) {
339
276
  sendSetPair(group.id, isDriver ? null : sessionId);
340
277
  });
341
278
  var title = header.querySelector(".split-pane-title");
342
- header.insertBefore(badge, title ? title.nextSibling : null);
279
+ var access = header.querySelector(".split-pane-full-access");
280
+ header.insertBefore(badge, access ? access.nextSibling : (title ? title.nextSibling : null));
343
281
  })(split.panes[pi].sessionId, headers[pi]);
344
282
  }
345
283
  var active = (store.get('splitDelegations') || {})[group.id];
@@ -231,6 +231,9 @@ function createPane(pane, index) {
231
231
  }, "Skip Permissions", true);
232
232
  });
233
233
  header.appendChild(fullAccess);
234
+ // Permission state belongs to the session identity on the left. Keep it
235
+ // immediately after the title instead of grouping it with usage and close.
236
+ header.insertBefore(fullAccess, ctxChip);
234
237
 
235
238
  var close = document.createElement("button");
236
239
  close.type = "button";
@@ -3,6 +3,7 @@
3
3
  @import url("css/title-bar.css?v=20260824");
4
4
  @import url("css/pane.css");
5
5
  @import url("css/worker-proposal.css");
6
+ @import url("css/session-actions.css");
6
7
  @import url("css/sidebar.css");
7
8
  @import url("css/overlays.css");
8
9
  @import url("css/menus.css");
@@ -0,0 +1,110 @@
1
+ var spawnSync = require("child_process").spawnSync;
2
+ var yoke = require("./yoke");
3
+
4
+ var MAX_CONTEXT_CHARS = 36000;
5
+ var MAX_TURNS = 12;
6
+ var MAX_USER_CHARS = 5000;
7
+ var MAX_ASSISTANT_CHARS = 9000;
8
+ var MAX_GIT_CHARS = 5000;
9
+ var MAX_TRANSCRIPT_CHARS = 24000;
10
+
11
+ function trimText(value, limit) {
12
+ var text = typeof value === "string" ? value.trim() : "";
13
+ if (text.length <= limit) return text;
14
+ return text.slice(0, limit) + "\n[truncated]";
15
+ }
16
+
17
+ function recentTurns(history) {
18
+ var turns = [];
19
+ var current = null;
20
+ history = Array.isArray(history) ? history : [];
21
+ for (var i = 0; i < history.length; i++) {
22
+ var entry = history[i];
23
+ if (!entry) continue;
24
+ if (entry.type === "user_message" || (entry.type === "handoff_context" && entry.request)) {
25
+ current = { user: trimText(entry.text || entry.request, MAX_USER_CHARS), assistant: "" };
26
+ turns.push(current);
27
+ } else if (entry.type === "delta" && entry.text) {
28
+ if (!current) {
29
+ current = { user: "", assistant: "" };
30
+ turns.push(current);
31
+ }
32
+ current.assistant += entry.text;
33
+ if (current.assistant.length > MAX_ASSISTANT_CHARS) {
34
+ current.assistant = current.assistant.slice(-MAX_ASSISTANT_CHARS);
35
+ }
36
+ }
37
+ }
38
+ return turns.slice(-MAX_TURNS);
39
+ }
40
+
41
+ function latestUserRequest(history) {
42
+ var turns = recentTurns(history);
43
+ for (var i = turns.length - 1; i >= 0; i--) {
44
+ if (turns[i].user) return turns[i].user;
45
+ }
46
+ return "";
47
+ }
48
+
49
+ function gitCommand(cwd, args) {
50
+ var result = spawnSync("git", args, {
51
+ cwd: cwd,
52
+ encoding: "utf8",
53
+ timeout: 5000,
54
+ maxBuffer: 1024 * 1024,
55
+ });
56
+ if (result.error || result.status !== 0) return "";
57
+ return trimText(result.stdout, MAX_GIT_CHARS);
58
+ }
59
+
60
+ function repositoryState(cwd) {
61
+ var status = gitCommand(cwd, ["status", "--short", "--branch"]);
62
+ var lines = [];
63
+ lines.push("Working tree:\n" + (status || "clean"));
64
+ return lines.join("\n\n");
65
+ }
66
+
67
+ function transcriptText(turns) {
68
+ var sections = [];
69
+ for (var i = 0; i < turns.length; i++) {
70
+ var turn = turns[i];
71
+ var parts = ["Turn " + (i + 1)];
72
+ if (turn.user) parts.push("USER:\n" + turn.user);
73
+ if (turn.assistant) parts.push("ASSISTANT:\n" + trimText(turn.assistant, MAX_ASSISTANT_CHARS));
74
+ sections.push(parts.join("\n\n"));
75
+ }
76
+ while (sections.length > 1 && sections.join("\n\n---\n\n").length > MAX_TRANSCRIPT_CHARS) {
77
+ sections.shift();
78
+ }
79
+ return sections.join("\n\n---\n\n");
80
+ }
81
+
82
+ function buildHandoffContext(options) {
83
+ var source = options.source;
84
+ var targetVendor = options.targetVendor;
85
+ var sourceVendor = source.vendor || "claude";
86
+ var sourceName = (yoke.getVendorInfo(sourceVendor) || {}).displayName || sourceVendor;
87
+ var targetName = (yoke.getVendorInfo(targetVendor) || {}).displayName || targetVendor;
88
+ var turns = recentTurns(source.history);
89
+ var latestUser = latestUserRequest(source.history);
90
+ var parts = [
91
+ "[Clay session handoff]",
92
+ "You are continuing work from another Clay coding-agent session. This is a snapshot, not a native conversation resume. Verify the current filesystem state before acting.",
93
+ "Source agent: " + sourceName,
94
+ "Target agent: " + targetName,
95
+ "Source session: " + (source.title || "Untitled session") + " (#" + source.localId + ")",
96
+ ];
97
+ if (latestUser) parts.push("Current user request, verbatim:\n" + latestUser);
98
+ parts.push("Repository state at handoff:\n" + repositoryState(options.cwd));
99
+ parts.push("Recent conversation:\n" + transcriptText(turns));
100
+ parts.push("Continue from the unresolved work above. Preserve the user's decisions and constraints, inspect the actual files before making assumptions, and proceed without asking the user to repeat context.");
101
+ return trimText(parts.join("\n\n"), MAX_CONTEXT_CHARS);
102
+ }
103
+
104
+ module.exports = {
105
+ MAX_CONTEXT_CHARS: MAX_CONTEXT_CHARS,
106
+ buildHandoffContext: buildHandoffContext,
107
+ latestUserRequest: latestUserRequest,
108
+ recentTurns: recentTurns,
109
+ repositoryState: repositoryState,
110
+ };
package/lib/sessions.js CHANGED
@@ -169,6 +169,7 @@ function createSessionManager(opts) {
169
169
  if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
170
170
  if (session.loop) metaObj.loop = session.loop;
171
171
  if (session.spawn) metaObj.spawn = session.spawn;
172
+ if (session.handoff) metaObj.handoff = session.handoff;
172
173
  if (session.debateState) metaObj.debateState = session.debateState;
173
174
  if (session.debateSetupMode) metaObj.debateSetupMode = true;
174
175
  var meta = JSON.stringify(metaObj);
@@ -281,6 +282,7 @@ function createSessionManager(opts) {
281
282
  session.effort = m.effort || null;
282
283
  if (m.loop) session.loop = m.loop;
283
284
  if (m.spawn) session.spawn = m.spawn;
285
+ if (m.handoff) session.handoff = m.handoff;
284
286
  if (m.debateState) session.debateState = m.debateState;
285
287
  if (m.debateSetupMode) session.debateSetupMode = true;
286
288
  if (m.ownerId) session.ownerId = m.ownerId;