clay-server 3.4.0-beta.9 → 3.4.0

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 (66) hide show
  1. package/lib/daemon.js +58 -58
  2. package/lib/project-connection.js +19 -6
  3. package/lib/project-models.js +220 -0
  4. package/lib/project-session-handoff.js +162 -0
  5. package/lib/project-session-pair.js +14 -7
  6. package/lib/project-session-spawn.js +1 -1
  7. package/lib/project-sessions.js +13 -45
  8. package/lib/project-worker-proposal.js +51 -14
  9. package/lib/project.js +33 -77
  10. package/lib/public/app.js +11 -0
  11. package/lib/public/copilot-avatar.svg +5 -0
  12. package/lib/public/css/filebrowser.css +6 -1
  13. package/lib/public/css/input.css +56 -0
  14. package/lib/public/css/mates.css +6 -0
  15. package/lib/public/css/menus.css +35 -56
  16. package/lib/public/css/pane.css +18 -3
  17. package/lib/public/css/session-actions.css +98 -0
  18. package/lib/public/grok-avatar.svg +4 -0
  19. package/lib/public/index.html +26 -9
  20. package/lib/public/junie-avatar.svg +11 -0
  21. package/lib/public/kimi-avatar.svg +4 -0
  22. package/lib/public/modules/agent-config-selects.js +69 -0
  23. package/lib/public/modules/app-connection.js +6 -0
  24. package/lib/public/modules/app-header.js +0 -22
  25. package/lib/public/modules/app-messages.js +48 -9
  26. package/lib/public/modules/app-panels.js +33 -82
  27. package/lib/public/modules/app-projects.js +2 -0
  28. package/lib/public/modules/app-rendering.js +16 -1
  29. package/lib/public/modules/background-tasks-ui.js +55 -0
  30. package/lib/public/modules/filebrowser-tabs.js +65 -0
  31. package/lib/public/modules/filebrowser.js +23 -3
  32. package/lib/public/modules/mate-sidebar.js +10 -2
  33. package/lib/public/modules/model-picker.js +263 -0
  34. package/lib/public/modules/pane-bridge.js +6 -0
  35. package/lib/public/modules/session-actions.js +294 -0
  36. package/lib/public/modules/sidebar-mobile.js +2 -1
  37. package/lib/public/modules/sidebar-sessions.js +17 -1
  38. package/lib/public/modules/split-pair-ui.js +16 -77
  39. package/lib/public/modules/split-view.js +11 -1
  40. package/lib/public/modules/tools.js +5 -0
  41. package/lib/public/modules/vendor-priority.js +6 -1
  42. package/lib/public/modules/worker-proposal.js +6 -1
  43. package/lib/public/qwen-avatar.svg +4 -0
  44. package/lib/public/style.css +1 -0
  45. package/lib/sdk-bridge.js +57 -34
  46. package/lib/sdk-message-processor.js +26 -4
  47. package/lib/session-handoff-context.js +110 -0
  48. package/lib/session-notes-mcp-server.js +1 -0
  49. package/lib/sessions.js +4 -0
  50. package/lib/ws-schema.js +9 -1
  51. package/lib/yoke/acp-agent-profiles.js +64 -0
  52. package/lib/yoke/adapters/acp.js +2 -1
  53. package/lib/yoke/adapters/claude.js +34 -0
  54. package/lib/yoke/adapters/codex.js +38 -3
  55. package/lib/yoke/adapters/copilot.js +7 -0
  56. package/lib/yoke/adapters/grok.js +7 -0
  57. package/lib/yoke/adapters/junie.js +7 -0
  58. package/lib/yoke/adapters/kimi.js +7 -0
  59. package/lib/yoke/adapters/kiro.js +4 -3
  60. package/lib/yoke/adapters/qwen.js +7 -0
  61. package/lib/yoke/codex-background-tasks.js +84 -0
  62. package/lib/yoke/index.js +43 -14
  63. package/lib/yoke/instructions.js +3 -0
  64. package/lib/yoke/interface.js +21 -0
  65. package/lib/yoke/vendor-registry.js +55 -0
  66. package/package.json +1 -1
@@ -0,0 +1,263 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+
4
+ var configModelList = null;
5
+ var configPopover = null;
6
+ var configChip = null;
7
+ var requestCounter = 0;
8
+ var selectionCounter = 0;
9
+ var requestTimer = null;
10
+
11
+ export function modelEntryValue(entry) {
12
+ if (!entry) return "";
13
+ if (typeof entry === "string") return entry;
14
+ return entry.value || entry.id || "";
15
+ }
16
+
17
+ export function modelEntryMatches(entry, value) {
18
+ if (!entry || !value) return false;
19
+ if (typeof entry === "string") return entry === value;
20
+ return entry.value === value || entry.id === value || entry.resolvedModel === value;
21
+ }
22
+
23
+ export function modelDisplayName(value, models) {
24
+ if (!value) return "";
25
+ var list = models || [];
26
+ for (var i = 0; i < list.length; i++) {
27
+ if (modelEntryMatches(list[i], value)) {
28
+ return typeof list[i] === "string" ? list[i] : (list[i].displayName || modelEntryValue(list[i]));
29
+ }
30
+ }
31
+ return value;
32
+ }
33
+
34
+ function vendorDisplayName(vendor) {
35
+ var info = store.get('vendorInfo') || {};
36
+ return info[vendor] && info[vendor].displayName ? info[vendor].displayName : vendor;
37
+ }
38
+
39
+ function clearRequestTimer() {
40
+ if (!requestTimer) return;
41
+ clearTimeout(requestTimer);
42
+ requestTimer = null;
43
+ }
44
+
45
+ export function setupModelPicker() {
46
+ configModelList = document.getElementById("config-model-list");
47
+ configPopover = document.getElementById("config-popover");
48
+ configChip = document.getElementById("config-chip");
49
+ }
50
+
51
+ export function resetModelPickerState() {
52
+ clearRequestTimer();
53
+ store.set({
54
+ modelListVendor: "",
55
+ modelListStatus: "idle",
56
+ modelListError: "",
57
+ modelRequestId: "",
58
+ modelSelectionPending: null,
59
+ modelSelectionError: "",
60
+ });
61
+ }
62
+
63
+ export function requestVendorModels(vendor, force) {
64
+ if (!vendor) return null;
65
+ var s = store.snap();
66
+ if (!force && s.modelListVendor === vendor) {
67
+ if (s.modelListStatus === "loading") return s.modelRequestId || null;
68
+ if (s.modelListStatus === "ready" && s.currentModels && s.currentModels.length > 0) return null;
69
+ }
70
+
71
+ var ws = getWs();
72
+ if (!ws || ws.readyState !== 1) {
73
+ store.set({
74
+ modelListVendor: vendor,
75
+ modelListStatus: "error",
76
+ modelListError: "The connection is not ready. Reconnect and retry.",
77
+ });
78
+ return null;
79
+ }
80
+
81
+ requestCounter++;
82
+ var requestId = "models-" + Date.now().toString(36) + "-" + requestCounter;
83
+ clearRequestTimer();
84
+ store.set({
85
+ modelListVendor: vendor,
86
+ modelListStatus: "loading",
87
+ modelListError: "",
88
+ modelRequestId: requestId,
89
+ });
90
+ ws.send(JSON.stringify({ type: "get_vendor_models", vendor: vendor, requestId: requestId }));
91
+ requestTimer = setTimeout(function() {
92
+ var current = store.snap();
93
+ if (current.modelRequestId !== requestId || current.modelListStatus !== "loading") return;
94
+ store.set({
95
+ modelListStatus: "error",
96
+ modelListError: vendorDisplayName(vendor) + " model loading timed out. Retry to try again.",
97
+ });
98
+ }, 35000);
99
+ return requestId;
100
+ }
101
+
102
+ export function prepareModelPickerOpen() {
103
+ var s = store.snap();
104
+ var vendor = s.currentVendor || "claude";
105
+ var needsModels = !s.currentModels || s.currentModels.length === 0;
106
+ var wrongVendor = s.modelListVendor !== vendor;
107
+ var retryable = s.modelListStatus === "error" || s.modelListStatus === "empty" || s.modelListStatus === "idle";
108
+ requestVendorModels(vendor, needsModels || wrongVendor || retryable);
109
+ }
110
+
111
+ export function getModelInfoUpdate(msg) {
112
+ var vendor = msg.vendor || store.get('currentVendor') || "claude";
113
+ var currentVendor = store.get('currentVendor');
114
+ if (currentVendor && vendor !== currentVendor) return null;
115
+ if (msg.sessionId != null && msg.sessionId !== store.get('activeSessionId')) return null;
116
+
117
+ var s = store.snap();
118
+ if (msg.requestId && s.modelRequestId && msg.requestId !== s.modelRequestId) return null;
119
+
120
+ var models = Array.isArray(msg.models) ? msg.models : [];
121
+ var unsolicitedEmptyWhileLoading = s.modelListStatus === "loading"
122
+ && s.modelListVendor === vendor
123
+ && !msg.requestId
124
+ && !msg.modelStatus
125
+ && !msg.error
126
+ && models.length === 0;
127
+ if (unsolicitedEmptyWhileLoading) {
128
+ return {
129
+ modelListVendor: vendor,
130
+ modelListStatus: "loading",
131
+ modelListError: "",
132
+ };
133
+ }
134
+
135
+ clearRequestTimer();
136
+ var status = msg.modelStatus || (msg.error ? "error" : (models.length > 0 ? "ready" : "empty"));
137
+ return {
138
+ modelListVendor: vendor,
139
+ modelListStatus: status,
140
+ modelListError: msg.error || "",
141
+ modelRequestId: "",
142
+ };
143
+ }
144
+
145
+ export function requestModelSelection(model) {
146
+ var ws = getWs();
147
+ if (!ws || ws.readyState !== 1) {
148
+ store.set({ modelSelectionError: "The connection is not ready. Reconnect before selecting a model." });
149
+ return;
150
+ }
151
+ var s = store.snap();
152
+ selectionCounter++;
153
+ var requestId = "model-select-" + Date.now().toString(36) + "-" + selectionCounter;
154
+ store.set({
155
+ currentModel: model,
156
+ modelSelectionPending: { requestId: requestId, model: model, previousModel: s.currentModel || "" },
157
+ modelSelectionError: "",
158
+ });
159
+ ws.send(JSON.stringify({
160
+ type: "set_model",
161
+ model: model,
162
+ vendor: s.currentVendor || "claude",
163
+ requestId: requestId,
164
+ }));
165
+ }
166
+
167
+ export function handleModelSelectionResult(msg) {
168
+ var pending = store.get('modelSelectionPending');
169
+ if (!pending || (msg.requestId && msg.requestId !== pending.requestId)) return false;
170
+ if (msg.ok) {
171
+ store.set({
172
+ currentModel: msg.model || pending.model,
173
+ modelSelectionPending: null,
174
+ modelSelectionError: "",
175
+ });
176
+ if (configPopover) configPopover.classList.add("hidden");
177
+ if (configChip) configChip.classList.remove("active");
178
+ } else {
179
+ store.set({
180
+ currentModel: pending.previousModel,
181
+ modelSelectionPending: null,
182
+ modelSelectionError: msg.error || "The model could not be selected.",
183
+ });
184
+ }
185
+ return true;
186
+ }
187
+
188
+ function appendState(title, detail, retry) {
189
+ var stateEl = document.createElement("div");
190
+ stateEl.className = "config-model-state";
191
+ if (title === "Loading models…") {
192
+ var spinner = document.createElement("span");
193
+ spinner.className = "config-model-spinner";
194
+ spinner.setAttribute("aria-hidden", "true");
195
+ stateEl.appendChild(spinner);
196
+ }
197
+ var textWrap = document.createElement("div");
198
+ var titleEl = document.createElement("div");
199
+ titleEl.className = "config-model-state-title";
200
+ titleEl.textContent = title;
201
+ textWrap.appendChild(titleEl);
202
+ if (detail) {
203
+ var detailEl = document.createElement("div");
204
+ detailEl.className = "config-model-state-detail";
205
+ detailEl.textContent = detail;
206
+ textWrap.appendChild(detailEl);
207
+ }
208
+ stateEl.appendChild(textWrap);
209
+ if (retry) {
210
+ var retryBtn = document.createElement("button");
211
+ retryBtn.type = "button";
212
+ retryBtn.className = "config-model-retry";
213
+ retryBtn.textContent = "Retry";
214
+ retryBtn.addEventListener("click", function() {
215
+ requestVendorModels(store.get('currentVendor') || "claude", true);
216
+ });
217
+ stateEl.appendChild(retryBtn);
218
+ }
219
+ configModelList.appendChild(stateEl);
220
+ }
221
+
222
+ export function renderModelPicker() {
223
+ if (!configModelList) return;
224
+ var s = store.snap();
225
+ var vendor = s.currentVendor || "claude";
226
+ var status = s.modelListVendor === vendor ? s.modelListStatus : "idle";
227
+ configModelList.innerHTML = "";
228
+
229
+ if (status === "loading") {
230
+ appendState("Loading models…", "Starting " + vendorDisplayName(vendor) + " and requesting its model catalog.", false);
231
+ return;
232
+ }
233
+ if (status === "error") {
234
+ appendState("Couldn’t load models", s.modelListError || "An unexpected error occurred.", true);
235
+ return;
236
+ }
237
+ if (status === "empty" || !s.currentModels || s.currentModels.length === 0) {
238
+ appendState("No models available", s.modelListError || "The vendor returned an empty model catalog.", true);
239
+ return;
240
+ }
241
+
242
+ for (var i = 0; i < s.currentModels.length; i++) {
243
+ var item = s.currentModels[i];
244
+ var value = modelEntryValue(item);
245
+ if (!value) continue;
246
+ var button = document.createElement("button");
247
+ button.className = "config-radio-item";
248
+ if (modelEntryMatches(item, s.currentModel)) button.classList.add("active");
249
+ button.dataset.model = value;
250
+ button.textContent = typeof item === "string" ? item : (item.displayName || value);
251
+ if (s.modelSelectionPending && s.modelSelectionPending.model === value) {
252
+ button.classList.add("pending");
253
+ button.textContent += " · Selecting…";
254
+ }
255
+ button.disabled = !!s.modelSelectionPending;
256
+ button.addEventListener("click", function() { requestModelSelection(this.dataset.model); });
257
+ configModelList.appendChild(button);
258
+ }
259
+
260
+ if (s.modelSelectionError) {
261
+ appendState("Model selection failed", s.modelSelectionError, false);
262
+ }
263
+ }
@@ -23,6 +23,12 @@ export function reportPaneContext(data) {
23
23
  }, window.location.origin);
24
24
  }
25
25
 
26
+ export function forwardPaneMarkdownPresentation(message) {
27
+ if (!store.get('paneMode') || window.parent === window) return false;
28
+ window.parent.postMessage({ type: "clay-pane-present-markdown", message: message }, window.location.origin);
29
+ return true;
30
+ }
31
+
26
32
  // Toggle without setContextView: panes share localStorage with the main app,
27
33
  // so persisting here would silently rewrite the user's main-view preference.
28
34
  function togglePaneContextPanel() {
@@ -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
+ }
@@ -15,7 +15,7 @@ import {
15
15
  resolveDefaultVendor,
16
16
  startNewSession
17
17
  } from './sidebar-sessions.js';
18
- import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
18
+ import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
19
19
  import {
20
20
  getCachedProjectList,
21
21
  getCachedCurrentSlug,
@@ -754,6 +754,7 @@ function renderMobileSessionsInto(container) {
754
754
  if (vendor === mobileDefaultVendor) vBtn.classList.add("active");
755
755
  vBtn.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="mobile-session-new-icon" alt="">' +
756
756
  '<span>' + name + '</span>' +
757
+ (isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
757
758
  (isInstalled ? "" : '<span class="mobile-vendor-note">Not installed</span>');
758
759
  vBtn.addEventListener("click", function () {
759
760
  if (!isInstalled) {
@@ -12,7 +12,7 @@ import { dismissOverlayPanels, closeSidebar, updatePageTitle, spawnDustParticles
12
12
  import { showConfirm } from './app-misc.js';
13
13
  import { getUpcomingSchedules } from './scheduler.js';
14
14
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
15
- import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
15
+ import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
16
16
  import { openGroup, separateGroup } from './split-view.js';
17
17
  import { groupedSessionIds } from './split-group-helpers.js';
18
18
  import { openPairDialog } from './split-pair-ui.js';
@@ -394,10 +394,12 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
394
394
  item.type = "button";
395
395
  item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
396
396
  '<span class="session-new-vendor-name">' + name + '</span>' +
397
+ (isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
397
398
  (isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
398
399
  item.title = isInstalled
399
400
  ? "New " + name + " session"
400
401
  : name + " is not installed - open " + VENDOR_HOMEPAGES[vendor];
402
+ if (isExperimentalVendor(vendor)) item.title += " · Experimental integration; not yet validated through direct use testing";
401
403
  item.addEventListener("click", function (e) {
402
404
  e.stopPropagation();
403
405
  closeSessionCtxMenu();
@@ -975,6 +977,9 @@ function renderLoopChild(s) {
975
977
  if (s.isProcessing) {
976
978
  textHtml += '<span class="session-processing"></span>';
977
979
  }
980
+ if (s.backgroundTaskCount) {
981
+ textHtml += '<span class="session-background-task-count">' + s.backgroundTaskCount + '</span>';
982
+ }
978
983
  if (s.loop) {
979
984
  var isRalphChild = s.loop.source === "ralph";
980
985
  var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
@@ -1094,6 +1099,9 @@ function renderLoopGroup(loopId, children, groupKey) {
1094
1099
  if (anyProcessing) {
1095
1100
  textHtml += '<span class="session-processing"></span>';
1096
1101
  }
1102
+ var groupBackgroundTaskCount = 0;
1103
+ for (var bi = 0; bi < children.length; bi++) groupBackgroundTaskCount += children[bi].backgroundTaskCount || 0;
1104
+ if (groupBackgroundTaskCount) textHtml += '<span class="session-background-task-count">' + groupBackgroundTaskCount + '</span>';
1097
1105
  var groupIcon = isDebate ? "mic" : (isRalph ? "repeat" : "calendar-clock");
1098
1106
  var iconClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
1099
1107
  textHtml += '<span class="session-loop-icon' + iconClass + '">' + iconHtml(groupIcon) + '</span>';
@@ -1203,6 +1211,9 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
1203
1211
  if (anyProcessing) {
1204
1212
  textHtml += '<span class="session-processing"></span>';
1205
1213
  }
1214
+ var runBackgroundTaskCount = 0;
1215
+ for (var bi = 0; bi < sessions.length; bi++) runBackgroundTaskCount += sessions[bi].backgroundTaskCount || 0;
1216
+ if (runBackgroundTaskCount) textHtml += '<span class="session-background-task-count">' + runBackgroundTaskCount + '</span>';
1206
1217
  textHtml += '<span class="session-loop-run-time">' + escapeHtml(timeLabel) + '</span>';
1207
1218
  textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + sessions.length + '</span>';
1208
1219
  textSpan.innerHTML = textHtml;
@@ -1252,6 +1263,9 @@ function renderSessionItem(s) {
1252
1263
  if (s.isProcessing) {
1253
1264
  textHtml += '<span class="session-processing"></span>';
1254
1265
  }
1266
+ if (s.backgroundTaskCount) {
1267
+ textHtml += '<span class="session-background-task-count">' + s.backgroundTaskCount + '</span>';
1268
+ }
1255
1269
  if (s.loop && s.loop.source === "debate") {
1256
1270
  textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
1257
1271
  }
@@ -1320,6 +1334,8 @@ function renderSplitGroupItem(group, members) {
1320
1334
  }
1321
1335
  html += "</span>";
1322
1336
  if (members[0].isProcessing || members[1].isProcessing) html += '<span class="session-processing"></span>';
1337
+ var splitBackgroundTaskCount = (members[0].backgroundTaskCount || 0) + (members[1].backgroundTaskCount || 0);
1338
+ if (splitBackgroundTaskCount) html += '<span class="session-background-task-count">' + splitBackgroundTaskCount + '</span>';
1323
1339
  html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", searchQuery) + "</span>";
1324
1340
  text.innerHTML = html;
1325
1341
  el.appendChild(text);