clay-server 3.4.0-beta.2 → 3.4.0-beta.20

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 (80) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +59 -59
  3. package/lib/project-connection.js +35 -7
  4. package/lib/project-debate.js +4 -0
  5. package/lib/project-mate-interaction.js +21 -1
  6. package/lib/project-memory.js +3 -0
  7. package/lib/project-models.js +220 -0
  8. package/lib/project-session-handoff.js +162 -0
  9. package/lib/project-sessions.js +13 -45
  10. package/lib/project-worker-proposal.js +49 -14
  11. package/lib/project.js +38 -80
  12. package/lib/public/antigravity-avatar.png +0 -0
  13. package/lib/public/app.js +29 -0
  14. package/lib/public/copilot-avatar.svg +5 -0
  15. package/lib/public/css/command-palette.css +22 -2
  16. package/lib/public/css/icon-strip.css +29 -13
  17. package/lib/public/css/input.css +56 -0
  18. package/lib/public/css/mates.css +6 -0
  19. package/lib/public/css/menus.css +38 -25
  20. package/lib/public/css/messages.css +9 -0
  21. package/lib/public/css/pane.css +16 -3
  22. package/lib/public/css/pwa-mobile.css +17 -0
  23. package/lib/public/css/session-actions.css +98 -0
  24. package/lib/public/css/title-bar.css +19 -0
  25. package/lib/public/grok-avatar.svg +4 -0
  26. package/lib/public/index.html +29 -7
  27. package/lib/public/junie-avatar.svg +11 -0
  28. package/lib/public/kimi-avatar.svg +4 -0
  29. package/lib/public/modules/agent-config-selects.js +69 -0
  30. package/lib/public/modules/app-header.js +4 -22
  31. package/lib/public/modules/app-messages.js +54 -11
  32. package/lib/public/modules/app-panels.js +36 -83
  33. package/lib/public/modules/app-projects.js +3 -1
  34. package/lib/public/modules/app-rendering.js +19 -4
  35. package/lib/public/modules/background-tasks-ui.js +55 -0
  36. package/lib/public/modules/mate-sidebar.js +11 -3
  37. package/lib/public/modules/model-picker.js +263 -0
  38. package/lib/public/modules/notifications.js +7 -1
  39. package/lib/public/modules/pane-bridge.js +11 -0
  40. package/lib/public/modules/pane-links.js +23 -0
  41. package/lib/public/modules/project-switcher.js +7 -6
  42. package/lib/public/modules/session-actions.js +294 -0
  43. package/lib/public/modules/sidebar-mates.js +1 -1
  44. package/lib/public/modules/sidebar-mobile.js +2 -1
  45. package/lib/public/modules/sidebar-projects.js +34 -43
  46. package/lib/public/modules/sidebar-sessions.js +20 -6
  47. package/lib/public/modules/split-pair-ui.js +16 -77
  48. package/lib/public/modules/split-view.js +3 -0
  49. package/lib/public/modules/tools.js +6 -1
  50. package/lib/public/modules/vendor-priority.js +14 -0
  51. package/lib/public/modules/vendor-selection.js +20 -0
  52. package/lib/public/modules/worker-proposal.js +5 -1
  53. package/lib/public/modules/worktree-location.js +17 -0
  54. package/lib/public/qwen-avatar.svg +4 -0
  55. package/lib/public/style.css +4 -2
  56. package/lib/sdk-bridge.js +99 -56
  57. package/lib/sdk-message-processor.js +26 -4
  58. package/lib/session-handoff-context.js +110 -0
  59. package/lib/session-notes-mcp-server.js +8 -1
  60. package/lib/sessions.js +4 -0
  61. package/lib/worktree.js +9 -4
  62. package/lib/ws-schema.js +9 -1
  63. package/lib/yoke/acp-agent-profiles.js +64 -14
  64. package/lib/yoke/adapters/antigravity.js +417 -0
  65. package/lib/yoke/adapters/claude.js +34 -0
  66. package/lib/yoke/adapters/codex.js +101 -5
  67. package/lib/yoke/adapters/copilot.js +7 -0
  68. package/lib/yoke/adapters/grok.js +7 -0
  69. package/lib/yoke/adapters/junie.js +7 -0
  70. package/lib/yoke/adapters/kimi.js +7 -0
  71. package/lib/yoke/adapters/kiro.js +2 -2
  72. package/lib/yoke/adapters/qwen.js +7 -0
  73. package/lib/yoke/codex-app-server.js +25 -8
  74. package/lib/yoke/index.js +67 -28
  75. package/lib/yoke/instructions.js +3 -0
  76. package/lib/yoke/interface.js +12 -0
  77. package/lib/yoke/vendor-registry.js +61 -6
  78. package/package.json +1 -1
  79. package/lib/public/gemini-avatar.svg +0 -11
  80. package/lib/yoke/adapters/gemini.js +0 -7
@@ -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
+ }
@@ -460,7 +460,7 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
460
460
  var vendorLabels = {
461
461
  claude: "Claude Code",
462
462
  codex: "OpenAI Codex",
463
- gemini: "Gemini CLI",
463
+ antigravity: "Antigravity CLI",
464
464
  opencode: "OpenCode",
465
465
  kiro: "Kiro CLI",
466
466
  };
@@ -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) {
@@ -13,6 +13,7 @@ import { showIconTooltip, hideIconTooltip, closeUserCtxMenu, getCurrentDmUserId
13
13
  import { switchProject, openAddProjectModal, getCachedProjects } from './app-projects.js';
14
14
  import { showHomeHub } from './app-home-hub.js';
15
15
  import { getPendingTuiAttention } from './app-notifications.js';
16
+ import { isExternalWorktree, externalWorktreeTooltip, appendExternalWorktreeBadge } from './worktree-location.js';
16
17
 
17
18
  // --- Project state ---
18
19
  var cachedProjectList = [];
@@ -489,7 +490,7 @@ export function closeProjectCtxMenu() {
489
490
  }
490
491
  }
491
492
 
492
- function showIconCtxMenu(anchorEl, slug, name) {
493
+ function showIconCtxMenu(anchorEl, slug, name, options) {
493
494
  closeProjectCtxMenu();
494
495
  if (closeUserCtxMenu) closeUserCtxMenu();
495
496
  closeEmojiPicker();
@@ -510,17 +511,19 @@ function showIconCtxMenu(anchorEl, slug, name) {
510
511
  menu.appendChild(iconItem);
511
512
 
512
513
  if (isWorktree) {
513
- var removeWtItem = document.createElement("button");
514
- removeWtItem.className = "project-ctx-item project-ctx-delete";
515
- removeWtItem.innerHTML = iconHtml("trash-2") + " <span>Remove Worktree</span>";
516
- removeWtItem.addEventListener("click", function (e) {
517
- e.stopPropagation();
518
- closeProjectCtxMenu();
519
- if (getWs() && store.get('connected')) {
520
- getWs().send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
521
- }
522
- });
523
- menu.appendChild(removeWtItem);
514
+ if (!options || options.canRemoveWorktree !== false) {
515
+ var removeWtItem = document.createElement("button");
516
+ removeWtItem.className = "project-ctx-item project-ctx-delete";
517
+ removeWtItem.innerHTML = iconHtml("trash-2") + " <span>Remove Worktree</span>";
518
+ removeWtItem.addEventListener("click", function (e) {
519
+ e.stopPropagation();
520
+ closeProjectCtxMenu();
521
+ if (getWs() && store.get('connected')) {
522
+ getWs().send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
523
+ }
524
+ });
525
+ menu.appendChild(removeWtItem);
526
+ }
524
527
  } else {
525
528
  var wtItem = document.createElement("button");
526
529
  wtItem.className = "project-ctx-item";
@@ -1253,8 +1256,8 @@ export function renderIconStrip(projects, currentSlug) {
1253
1256
  (function (wt) {
1254
1257
  var wtEl = document.createElement("a");
1255
1258
  var isWtActive = wt.slug === currentSlug && !currentDmUserId;
1256
- var isAccessible = wt.worktreeAccessible !== false;
1257
- wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (!isAccessible ? " wt-disabled" : "");
1259
+ var isExternal = isExternalWorktree(wt);
1260
+ wtEl.className = "icon-strip-wt-item" + (isWtActive ? " active" : "") + (isExternal ? " wt-external" : "");
1258
1261
  wtEl.href = "/p/" + wt.slug + "/";
1259
1262
  wtEl.dataset.slug = wt.slug;
1260
1263
 
@@ -1276,42 +1279,28 @@ export function renderIconStrip(projects, currentSlug) {
1276
1279
  if (wt.isProcessing) wtStatus.classList.add("processing");
1277
1280
  wtEl.appendChild(wtStatus);
1278
1281
 
1279
- var tooltipText = wt.name;
1280
- if (!isAccessible) {
1281
- tooltipText += " (outside project path, cannot be accessed)";
1282
- }
1282
+ if (isExternal) appendExternalWorktreeBadge(wtEl, "worktree-external-badge");
1283
+
1284
+ var tooltipText = isExternal ? externalWorktreeTooltip(wt.name) : wt.name;
1283
1285
  (function (text, elem) {
1284
1286
  elem.addEventListener("mouseenter", function () { showIconTooltip(elem, text); });
1285
1287
  elem.addEventListener("mouseleave", hideIconTooltip);
1286
1288
  })(tooltipText, wtEl);
1287
1289
 
1288
- if (isAccessible) {
1289
- (function (slug) {
1290
- wtEl.addEventListener("click", function (e) {
1291
- e.preventDefault();
1292
- if (switchProject) switchProject(slug);
1293
- });
1294
- })(wt.slug);
1295
- } else {
1290
+ (function (slug) {
1296
1291
  wtEl.addEventListener("click", function (e) {
1297
1292
  e.preventDefault();
1293
+ if (switchProject) switchProject(slug);
1298
1294
  });
1299
- }
1295
+ })(wt.slug);
1300
1296
 
1301
- if (isAccessible) {
1302
- (function (slug, name, elem) {
1303
- elem.addEventListener("contextmenu", function (e) {
1304
- e.preventDefault();
1305
- e.stopPropagation();
1306
- showIconCtxMenu(elem, slug, name);
1307
- });
1308
- })(wt.slug, wt.name, wtEl);
1309
- } else {
1310
- wtEl.addEventListener("contextmenu", function (e) {
1297
+ (function (slug, name, elem, external) {
1298
+ elem.addEventListener("contextmenu", function (e) {
1311
1299
  e.preventDefault();
1312
1300
  e.stopPropagation();
1301
+ showIconCtxMenu(elem, slug, name, { canRemoveWorktree: !external });
1313
1302
  });
1314
- }
1303
+ })(wt.slug, wt.name, wtEl, isExternal);
1315
1304
 
1316
1305
  if (!isWtActive && (wt.pendingPermissions > 0 || getPendingTuiAttention(wt.slug) > 0)) {
1317
1306
  wtEl.classList.add("has-pending-perm");
@@ -1360,7 +1349,10 @@ export function renderIconStrip(projects, currentSlug) {
1360
1349
 
1361
1350
  renderProjectList(projects, currentSlug);
1362
1351
 
1363
- try { lucide.createIcons({ nodes: [container] }); } catch (e) {}
1352
+ var iconNodes = [container];
1353
+ var mobileProjectList = document.getElementById("project-list");
1354
+ if (mobileProjectList) iconNodes.push(mobileProjectList);
1355
+ try { lucide.createIcons({ nodes: iconNodes }); } catch (e) {}
1364
1356
  }
1365
1357
 
1366
1358
  function renderProjectList(projects, currentSlug) {
@@ -1401,11 +1393,10 @@ function renderProjectList(projects, currentSlug) {
1401
1393
  var wtList = document.createElement("div");
1402
1394
  wtList.className = "mobile-folder-items";
1403
1395
  for (var wi = 0; wi < worktrees.length; wi++) {
1404
- var isAccessible = worktrees[wi].worktreeAccessible !== false;
1405
1396
  var wtItem = createMobileProjectItem(worktrees[wi], currentSlug, true);
1406
- if (!isAccessible) wtItem.classList.add("wt-disabled");
1407
- if (!isAccessible) {
1408
- wtItem.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); });
1397
+ if (isExternalWorktree(worktrees[wi])) {
1398
+ wtItem.classList.add("wt-external");
1399
+ appendExternalWorktreeBadge(wtItem, "mobile-worktree-external-badge");
1409
1400
  }
1410
1401
  wtList.appendChild(wtItem);
1411
1402
  }
@@ -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';
@@ -299,13 +299,10 @@ function appendSessionCloseButton(el, session) {
299
299
  el.appendChild(closeBtn);
300
300
  }
301
301
 
302
- // Resolve the vendor the "New session" button should launch: the project's
303
- // last-used vendor when its CLI is still installed, else the first installed
304
- // vendor, else Claude (so the button always has a sane label to render).
302
+ // Resolve the vendor the "New session" button should launch from the stable
303
+ // installed-vendor priority (Claude, then Codex, then the remaining vendors).
305
304
  export function resolveDefaultVendor() {
306
305
  var installed = store.get('installedVendors') || [];
307
- var last = store.get('lastVendor') || "";
308
- if (last && installed.indexOf(last) !== -1) return last;
309
306
  for (var i = 0; i < VENDOR_ORDER.length; i++) {
310
307
  if (installed.indexOf(VENDOR_ORDER[i]) !== -1) return VENDOR_ORDER[i];
311
308
  }
@@ -330,6 +327,7 @@ export function startNewSession(vendor, extra) {
330
327
  currentModel: "",
331
328
  currentModels: [],
332
329
  vendorSelectionLocked: true,
330
+ sessionVendorBound: true,
333
331
  });
334
332
  }
335
333
 
@@ -396,10 +394,12 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
396
394
  item.type = "button";
397
395
  item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
398
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>' : '') +
399
398
  (isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
400
399
  item.title = isInstalled
401
400
  ? "New " + name + " session"
402
401
  : name + " is not installed - open " + VENDOR_HOMEPAGES[vendor];
402
+ if (isExperimentalVendor(vendor)) item.title += " · Experimental integration; not yet validated through direct use testing";
403
403
  item.addEventListener("click", function (e) {
404
404
  e.stopPropagation();
405
405
  closeSessionCtxMenu();
@@ -977,6 +977,9 @@ function renderLoopChild(s) {
977
977
  if (s.isProcessing) {
978
978
  textHtml += '<span class="session-processing"></span>';
979
979
  }
980
+ if (s.backgroundTaskCount) {
981
+ textHtml += '<span class="session-background-task-count">' + s.backgroundTaskCount + '</span>';
982
+ }
980
983
  if (s.loop) {
981
984
  var isRalphChild = s.loop.source === "ralph";
982
985
  var roleName = s.loop.role === "crafting" ? "Crafting" : s.loop.role === "judge" ? "Judge" : (isRalphChild ? "Coder" : "Run");
@@ -1096,6 +1099,9 @@ function renderLoopGroup(loopId, children, groupKey) {
1096
1099
  if (anyProcessing) {
1097
1100
  textHtml += '<span class="session-processing"></span>';
1098
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>';
1099
1105
  var groupIcon = isDebate ? "mic" : (isRalph ? "repeat" : "calendar-clock");
1100
1106
  var iconClass = isDebate ? " debate" : (isRalph ? "" : " scheduled");
1101
1107
  textHtml += '<span class="session-loop-icon' + iconClass + '">' + iconHtml(groupIcon) + '</span>';
@@ -1205,6 +1211,9 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
1205
1211
  if (anyProcessing) {
1206
1212
  textHtml += '<span class="session-processing"></span>';
1207
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>';
1208
1217
  textHtml += '<span class="session-loop-run-time">' + escapeHtml(timeLabel) + '</span>';
1209
1218
  textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + sessions.length + '</span>';
1210
1219
  textSpan.innerHTML = textHtml;
@@ -1254,6 +1263,9 @@ function renderSessionItem(s) {
1254
1263
  if (s.isProcessing) {
1255
1264
  textHtml += '<span class="session-processing"></span>';
1256
1265
  }
1266
+ if (s.backgroundTaskCount) {
1267
+ textHtml += '<span class="session-background-task-count">' + s.backgroundTaskCount + '</span>';
1268
+ }
1257
1269
  if (s.loop && s.loop.source === "debate") {
1258
1270
  textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
1259
1271
  }
@@ -1322,6 +1334,8 @@ function renderSplitGroupItem(group, members) {
1322
1334
  }
1323
1335
  html += "</span>";
1324
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>';
1325
1339
  html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", searchQuery) + "</span>";
1326
1340
  text.innerHTML = html;
1327
1341
  el.appendChild(text);