clay-server 3.4.0-beta.8 → 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 (73) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +59 -59
  3. package/lib/project-connection.js +19 -6
  4. package/lib/project-models.js +220 -0
  5. package/lib/project-session-handoff.js +162 -0
  6. package/lib/project-session-pair.js +14 -7
  7. package/lib/project-session-spawn.js +1 -1
  8. package/lib/project-sessions.js +13 -45
  9. package/lib/project-worker-proposal.js +51 -14
  10. package/lib/project.js +35 -79
  11. package/lib/public/app.js +11 -0
  12. package/lib/public/copilot-avatar.svg +5 -0
  13. package/lib/public/css/command-palette.css +22 -2
  14. package/lib/public/css/filebrowser.css +6 -1
  15. package/lib/public/css/icon-strip.css +29 -13
  16. package/lib/public/css/input.css +56 -0
  17. package/lib/public/css/mates.css +6 -0
  18. package/lib/public/css/menus.css +35 -56
  19. package/lib/public/css/pane.css +18 -3
  20. package/lib/public/css/session-actions.css +98 -0
  21. package/lib/public/grok-avatar.svg +4 -0
  22. package/lib/public/index.html +26 -9
  23. package/lib/public/junie-avatar.svg +11 -0
  24. package/lib/public/kimi-avatar.svg +4 -0
  25. package/lib/public/modules/agent-config-selects.js +69 -0
  26. package/lib/public/modules/app-connection.js +6 -0
  27. package/lib/public/modules/app-header.js +0 -22
  28. package/lib/public/modules/app-messages.js +48 -9
  29. package/lib/public/modules/app-panels.js +33 -82
  30. package/lib/public/modules/app-projects.js +3 -1
  31. package/lib/public/modules/app-rendering.js +16 -1
  32. package/lib/public/modules/background-tasks-ui.js +55 -0
  33. package/lib/public/modules/filebrowser-tabs.js +65 -0
  34. package/lib/public/modules/filebrowser.js +23 -3
  35. package/lib/public/modules/mate-sidebar.js +10 -2
  36. package/lib/public/modules/model-picker.js +263 -0
  37. package/lib/public/modules/pane-bridge.js +6 -0
  38. package/lib/public/modules/project-switcher.js +7 -6
  39. package/lib/public/modules/session-actions.js +294 -0
  40. package/lib/public/modules/sidebar-mobile.js +2 -1
  41. package/lib/public/modules/sidebar-projects.js +34 -43
  42. package/lib/public/modules/sidebar-sessions.js +17 -1
  43. package/lib/public/modules/split-pair-ui.js +16 -77
  44. package/lib/public/modules/split-view.js +11 -1
  45. package/lib/public/modules/tools.js +5 -0
  46. package/lib/public/modules/vendor-priority.js +6 -1
  47. package/lib/public/modules/worker-proposal.js +6 -1
  48. package/lib/public/modules/worktree-location.js +17 -0
  49. package/lib/public/qwen-avatar.svg +4 -0
  50. package/lib/public/style.css +1 -0
  51. package/lib/sdk-bridge.js +57 -34
  52. package/lib/sdk-message-processor.js +26 -4
  53. package/lib/session-handoff-context.js +110 -0
  54. package/lib/session-notes-mcp-server.js +1 -0
  55. package/lib/sessions.js +4 -0
  56. package/lib/worktree.js +9 -4
  57. package/lib/ws-schema.js +9 -1
  58. package/lib/yoke/acp-agent-profiles.js +64 -0
  59. package/lib/yoke/adapters/acp.js +2 -1
  60. package/lib/yoke/adapters/claude.js +34 -0
  61. package/lib/yoke/adapters/codex.js +38 -3
  62. package/lib/yoke/adapters/copilot.js +7 -0
  63. package/lib/yoke/adapters/grok.js +7 -0
  64. package/lib/yoke/adapters/junie.js +7 -0
  65. package/lib/yoke/adapters/kimi.js +7 -0
  66. package/lib/yoke/adapters/kiro.js +4 -3
  67. package/lib/yoke/adapters/qwen.js +7 -0
  68. package/lib/yoke/codex-background-tasks.js +84 -0
  69. package/lib/yoke/index.js +43 -14
  70. package/lib/yoke/instructions.js +3 -0
  71. package/lib/yoke/interface.js +21 -0
  72. package/lib/yoke/vendor-registry.js +55 -0
  73. 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() {
@@ -19,6 +19,7 @@ import { getCachedProjects, switchProject } from './app-projects.js';
19
19
  import { openDm } from './app-dm.js';
20
20
  import { mateAvatarUrl } from './avatar.js';
21
21
  import { parseEmojis } from './markdown.js';
22
+ import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
22
23
 
23
24
  var _projectMru = []; // project slugs, most recent first
24
25
  var _mateMru = []; // mate ids (mate_XXX), most recent first
@@ -257,18 +258,16 @@ function buildProjectEntries() {
257
258
  // Keep natural (server-provided) order so the list looks the same
258
259
  // every time the switcher opens. MRU only drives initial highlight.
259
260
  return projects.map(function (p) {
260
- // Worktrees can end up "outside project path" (parent workspace
261
- // unmounted or moved); those are effectively unreachable from
262
- // this session and shouldn't be switchable.
263
- var unreachable = p.isWorktree && p.worktreeAccessible === false;
261
+ var external = isExternalWorktree(p);
264
262
  return {
265
263
  key: p.slug,
266
264
  title: p.title || p.project || p.slug,
267
265
  icon: p.icon || null,
268
266
  fallbackIcon: 'folder',
269
267
  isCurrent: p.slug === store.get('currentSlug'),
270
- disabled: unreachable,
271
- disabledReason: unreachable ? 'Outside project path' : null,
268
+ external: external,
269
+ disabled: false,
270
+ disabledReason: null,
272
271
  };
273
272
  });
274
273
  }
@@ -336,6 +335,7 @@ function buildEntryNode(entry, index) {
336
335
  item.className = 'cmd-palette-item project-switcher-item';
337
336
  if (entry.disabled) item.classList.add('disabled');
338
337
  if (entry.disabled && entry.disabledReason) item.title = entry.disabledReason;
338
+ if (entry.external) item.title = externalWorktreeTooltip(entry.title);
339
339
  item.dataset.index = String(index);
340
340
 
341
341
  var iconWrap = document.createElement('div');
@@ -353,6 +353,7 @@ function buildEntryNode(entry, index) {
353
353
  fallback.setAttribute('data-lucide', entry.fallbackIcon || 'folder');
354
354
  iconWrap.appendChild(fallback);
355
355
  }
356
+ if (entry.external) appendExternalWorktreeBadge(iconWrap, 'project-switcher-external-badge');
356
357
  item.appendChild(iconWrap);
357
358
 
358
359
  var body = document.createElement('div');
@@ -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) {