clay-server 3.4.0-beta.1 → 3.4.0-beta.11

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 (68) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +1 -1
  3. package/lib/project-connection.js +20 -5
  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-shell-command.js +160 -0
  8. package/lib/project-user-message.js +10 -0
  9. package/lib/project.js +19 -4
  10. package/lib/public/antigravity-avatar.png +0 -0
  11. package/lib/public/app.js +28 -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/icon-strip.css +29 -13
  15. package/lib/public/css/input.css +75 -0
  16. package/lib/public/css/mates.css +6 -0
  17. package/lib/public/css/menus.css +48 -14
  18. package/lib/public/css/messages.css +9 -0
  19. package/lib/public/css/pane.css +4 -0
  20. package/lib/public/css/pwa-mobile.css +17 -0
  21. package/lib/public/css/title-bar.css +19 -0
  22. package/lib/public/grok-avatar.svg +4 -0
  23. package/lib/public/index.html +38 -7
  24. package/lib/public/junie-avatar.svg +11 -0
  25. package/lib/public/kimi-avatar.svg +4 -0
  26. package/lib/public/modules/app-header.js +4 -0
  27. package/lib/public/modules/app-messages.js +28 -5
  28. package/lib/public/modules/app-panels.js +28 -8
  29. package/lib/public/modules/app-projects.js +3 -1
  30. package/lib/public/modules/app-rendering.js +19 -4
  31. package/lib/public/modules/input.js +58 -28
  32. package/lib/public/modules/mate-sidebar.js +11 -3
  33. package/lib/public/modules/notifications.js +7 -1
  34. package/lib/public/modules/pane-bridge.js +11 -0
  35. package/lib/public/modules/pane-links.js +23 -0
  36. package/lib/public/modules/project-switcher.js +7 -6
  37. package/lib/public/modules/shell-command.js +148 -0
  38. package/lib/public/modules/sidebar-mates.js +1 -1
  39. package/lib/public/modules/sidebar-mobile.js +2 -1
  40. package/lib/public/modules/sidebar-projects.js +34 -43
  41. package/lib/public/modules/sidebar-sessions.js +6 -6
  42. package/lib/public/modules/split-pair-ui.js +3 -2
  43. package/lib/public/modules/tools.js +6 -1
  44. package/lib/public/modules/vendor-priority.js +14 -0
  45. package/lib/public/modules/vendor-selection.js +20 -0
  46. package/lib/public/modules/worktree-location.js +17 -0
  47. package/lib/public/qwen-avatar.svg +4 -0
  48. package/lib/public/style.css +3 -2
  49. package/lib/sdk-bridge.js +49 -25
  50. package/lib/sdk-message-processor.js +10 -1
  51. package/lib/session-notes-mcp-server.js +7 -1
  52. package/lib/worktree.js +9 -4
  53. package/lib/ws-schema.js +3 -0
  54. package/lib/yoke/acp-agent-profiles.js +64 -14
  55. package/lib/yoke/adapters/antigravity.js +417 -0
  56. package/lib/yoke/adapters/codex.js +98 -3
  57. package/lib/yoke/adapters/copilot.js +7 -0
  58. package/lib/yoke/adapters/grok.js +7 -0
  59. package/lib/yoke/adapters/junie.js +7 -0
  60. package/lib/yoke/adapters/kimi.js +7 -0
  61. package/lib/yoke/adapters/qwen.js +7 -0
  62. package/lib/yoke/codex-app-server.js +25 -8
  63. package/lib/yoke/index.js +67 -28
  64. package/lib/yoke/instructions.js +3 -0
  65. package/lib/yoke/vendor-registry.js +61 -6
  66. package/package.json +1 -1
  67. package/lib/public/gemini-avatar.svg +0 -11
  68. package/lib/yoke/adapters/gemini.js +0 -7
@@ -13,33 +13,48 @@ import { showImageModal, showPasteModal } from './app-misc.js';
13
13
  import { sendMessage, hasSendableContent } from './input.js';
14
14
  import { getChatLayout } from './theme.js';
15
15
  import { getScheduledMsgEl } from './app-rate-limit.js';
16
+ export { VENDOR_ORDER, EXPERIMENTAL_VENDORS, isExperimentalVendor } from './vendor-priority.js';
16
17
 
17
18
  // Keep these client constants aligned with lib/yoke/vendor-registry.js. The
18
19
  // browser cannot import the server's CommonJS registry directly.
19
20
  export var VENDOR_AVATARS = {
20
21
  claude: "/claude-code-avatar.png",
21
22
  codex: "/codex-avatar.png",
22
- gemini: "/gemini-avatar.svg",
23
+ antigravity: "/antigravity-avatar.png",
23
24
  opencode: "/opencode-avatar.svg",
25
+ kimi: "/kimi-avatar.svg",
26
+ grok: "/grok-avatar.svg",
27
+ copilot: "/copilot-avatar.svg",
28
+ qwen: "/qwen-avatar.svg",
29
+ junie: "/junie-avatar.svg",
24
30
  kiro: "/kiro-avatar.svg",
25
31
  };
26
32
  export var VENDOR_NAMES = {
27
33
  claude: "Claude Code",
28
34
  codex: "Codex",
29
- gemini: "Gemini CLI",
35
+ antigravity: "Antigravity CLI",
30
36
  opencode: "OpenCode",
37
+ kimi: "Kimi Code",
38
+ grok: "Grok Build",
39
+ copilot: "GitHub Copilot CLI",
40
+ qwen: "Qwen Code",
41
+ junie: "Junie CLI",
31
42
  kiro: "Kiro CLI",
32
43
  };
33
44
  // Display order for every vendor Clay knows about, installed or not. Pickers
34
45
  // render the full list so a missing CLI reads as "not installed yet" rather
35
46
  // than "Clay doesn't support it".
36
- export var VENDOR_ORDER = ["claude", "codex", "gemini", "opencode", "kiro"];
37
47
  // Where to send the user when they pick a vendor whose CLI isn't installed.
38
48
  export var VENDOR_HOMEPAGES = {
39
49
  claude: "https://claude.com/product/claude-code",
40
50
  codex: "https://openai.com/codex/",
41
- gemini: "https://github.com/google-gemini/gemini-cli",
51
+ antigravity: "https://antigravity.google/product/antigravity-cli",
42
52
  opencode: "https://opencode.ai/",
53
+ kimi: "https://www.kimi.com/code/",
54
+ grok: "https://docs.x.ai/build/overview",
55
+ copilot: "https://github.com/features/copilot/cli",
56
+ qwen: "https://qwenlm.github.io/qwen-code-docs/",
57
+ junie: "https://junie.jetbrains.com/",
43
58
  kiro: "https://kiro.dev/",
44
59
  };
45
60
  var NEW_MSG_BTN_DEFAULT = "\u2193 Latest";
@@ -7,6 +7,7 @@ import { mateAvatarUrl } from './avatar.js';
7
7
  import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
8
8
  import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
9
9
  import { showToast } from './utils.js';
10
+ import { isShellCommandMode, submitShellCommand } from './shell-command.js';
10
11
 
11
12
  var ctx;
12
13
 
@@ -75,6 +76,37 @@ export var builtinCommands = [
75
76
  { name: "status", desc: "Process status and resource usage" },
76
77
  ];
77
78
 
79
+ function commitVendorForTurn() {
80
+ var committedVendor = store.get('currentVendor');
81
+ var vendorToggle = document.getElementById("vendor-toggle-wrap");
82
+ var activeIndicator = document.getElementById("active-vendor-indicator");
83
+ var activeIcon = document.getElementById("active-vendor-icon");
84
+ if (committedVendor) {
85
+ if (vendorToggle) {
86
+ vendorToggle.classList.add("hidden");
87
+ vendorToggle.classList.remove("locked");
88
+ }
89
+ if (activeIndicator && activeIcon) {
90
+ activeIcon.src = VENDOR_AVATARS[committedVendor] || VENDOR_AVATARS.claude;
91
+ activeIcon.alt = VENDOR_NAMES[committedVendor] || VENDOR_NAMES.claude;
92
+ activeIndicator.title = (VENDOR_NAMES[committedVendor] || VENDOR_NAMES.claude) + " session";
93
+ activeIndicator.classList.remove("hidden");
94
+ }
95
+ } else if (vendorToggle) {
96
+ vendorToggle.classList.remove("hidden");
97
+ vendorToggle.classList.add("locked");
98
+ }
99
+ store.set({ vendorSelectionLocked: false, sessionHasHistory: true });
100
+ }
101
+
102
+ function showPreThinkingForTurn() {
103
+ if (ctx.isMateDm && ctx.isMateDm()) {
104
+ ctx.showMatePreThinking();
105
+ } else if (ctx.showClaudePreThinking) {
106
+ ctx.showClaudePreThinking();
107
+ }
108
+ }
109
+
78
110
  // --- Send ---
79
111
  export function sendMessage() {
80
112
  // Debate ended mode intercept: route to resume handler
@@ -92,6 +124,14 @@ export function sendMessage() {
92
124
  ctx.handleDebateFloorSend();
93
125
  return;
94
126
  }
127
+ if (isShellCommandMode()) {
128
+ var shellText = ctx.inputEl.value.trim();
129
+ if (!shellText || !submitShellCommand(shellText)) return;
130
+ ctx.inputEl.value = "";
131
+ sendInputSync();
132
+ autoResize();
133
+ return;
134
+ }
95
135
  // DM mode intercept: if in DM mode, route to DM handler instead
96
136
  if (ctx.isDmMode && ctx.isDmMode() && ctx.handleDmSend) {
97
137
  ctx.handleDmSend();
@@ -304,36 +344,10 @@ export function sendMessage() {
304
344
  // Bumping sessionHasHistory drives in-session lock states (e.g. the
305
345
  // Codex model picker that becomes informational once the thread is
306
346
  // bound to a model).
307
- var _committedVendor = store.get('currentVendor');
308
- var _vtw2 = document.getElementById("vendor-toggle-wrap");
309
- var _avi = document.getElementById("active-vendor-indicator");
310
- var _avIcon = document.getElementById("active-vendor-icon");
311
- if (_committedVendor) {
312
- if (_vtw2) {
313
- _vtw2.classList.add("hidden");
314
- _vtw2.classList.remove("locked");
315
- }
316
- if (_avi && _avIcon) {
317
- _avIcon.src = VENDOR_AVATARS[_committedVendor] || VENDOR_AVATARS.claude;
318
- _avIcon.alt = VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude;
319
- _avi.title = (VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude) + " session";
320
- _avi.classList.remove("hidden");
321
- }
322
- } else if (_vtw2) {
323
- // No committed vendor (defensive — shouldn't happen because the
324
- // input is otherwise gated on a vendor pick). Fall back to the
325
- // locked toggle so the running vendor is still visible.
326
- _vtw2.classList.remove("hidden");
327
- _vtw2.classList.add("locked");
328
- }
329
- store.set({ vendorSelectionLocked: false, sessionHasHistory: true });
347
+ commitVendorForTurn();
330
348
 
331
349
  // Show pre-thinking dots before server responds
332
- if (ctx.isMateDm && ctx.isMateDm()) {
333
- ctx.showMatePreThinking();
334
- } else if (ctx.showClaudePreThinking) {
335
- ctx.showClaudePreThinking();
336
- }
350
+ showPreThinkingForTurn();
337
351
 
338
352
  ctx.inputEl.value = "";
339
353
  sendInputSync();
@@ -354,6 +368,22 @@ export function sendTextMessage(text) {
354
368
  return true;
355
369
  }
356
370
 
371
+ export function sendShellResultToAgent(msg) {
372
+ if (!ctx || !ctx.connected || !msg || msg.error || !msg.command) return false;
373
+ if (String(msg.sessionId) !== String(store.get("activeSessionId"))) return false;
374
+ var payload = {
375
+ type: "message",
376
+ text: "$ " + msg.command,
377
+ shellCommandResponse: true,
378
+ };
379
+ var selectedVendor = store.get("currentVendor") || null;
380
+ if (selectedVendor) payload.vendor = selectedVendor;
381
+ ctx.ws.send(JSON.stringify(payload));
382
+ commitVendorForTurn();
383
+ showPreThinkingForTurn();
384
+ return true;
385
+ }
386
+
357
387
  export function autoResize() {
358
388
  ctx.inputEl.style.height = "auto";
359
389
  ctx.inputEl.style.height = Math.min(ctx.inputEl.scrollHeight, 120) + "px";
@@ -6,6 +6,7 @@ import { hideKnowledge } from './mate-knowledge.js';
6
6
  import { isSchedulerOpen, closeScheduler } from './scheduler.js';
7
7
  import { hideNotes } from './sticky-notes.js';
8
8
  import { openSearch as openSessionSearch } from './session-search.js';
9
+ import { isExperimentalVendor } from './vendor-priority.js';
9
10
 
10
11
  var getWs = null;
11
12
  var currentMateId = null;
@@ -152,19 +153,26 @@ export function showMateSidebar(mateId, mateData) {
152
153
  var vendorIcons = {
153
154
  claude: "/claude-code-avatar.png",
154
155
  codex: "/codex-avatar.png",
155
- gemini: "/gemini-avatar.svg",
156
+ antigravity: "/antigravity-avatar.png",
156
157
  opencode: "/opencode-avatar.svg",
158
+ kimi: "/kimi-avatar.svg",
159
+ grok: "/grok-avatar.svg",
160
+ copilot: "/copilot-avatar.svg",
161
+ qwen: "/qwen-avatar.svg",
162
+ junie: "/junie-avatar.svg",
157
163
  kiro: "/kiro-avatar.svg",
158
164
  };
159
- var vendorNames = { claude: "Claude Code", codex: "Codex", gemini: "Gemini CLI", opencode: "OpenCode", kiro: "Kiro CLI" };
160
- var vendorKeys = ["claude", "codex", "gemini", "opencode", "kiro"];
165
+ var vendorNames = { claude: "Claude Code", codex: "Codex", grok: "Grok Build", kimi: "Kimi Code", copilot: "GitHub Copilot CLI", qwen: "Qwen Code", junie: "Junie CLI", antigravity: "Antigravity CLI", opencode: "OpenCode", kiro: "Kiro CLI" };
166
+ var vendorKeys = ["claude", "codex", "grok", "kimi", "copilot", "qwen", "junie", "antigravity", "opencode", "kiro"];
161
167
  mateVendorWrap.innerHTML = "";
162
168
  for (var vi = 0; vi < vendorKeys.length; vi++) {
163
169
  var vk = vendorKeys[vi];
164
170
  var vBtn = document.createElement("button");
165
171
  vBtn.className = "mate-vendor-btn" + (vk === mateVendor ? " active" : "");
172
+ if (isExperimentalVendor(vk)) vBtn.classList.add("experimental");
166
173
  if (available.indexOf(vk) === -1) vBtn.classList.add("disabled");
167
174
  vBtn.dataset.vendor = vk;
175
+ vBtn.title = vendorNames[vk] + (isExperimentalVendor(vk) ? " · Experimental integration; not yet validated through direct use testing" : "");
168
176
  vBtn.innerHTML = '<img src="' + vendorIcons[vk] + '" class="mate-vendor-icon" alt="' + vendorNames[vk] + '"><span class="mate-vendor-label">' + vendorNames[vk] + '</span>';
169
177
  vBtn.addEventListener("click", function() {
170
178
  var v = this.dataset.vendor;
@@ -72,6 +72,7 @@ export function initNotifications(_ctx) {
72
72
  if (window.visualViewport) {
73
73
  var layout = $("layout");
74
74
  var mobileTabBar = document.getElementById("mobile-tab-bar");
75
+ var keyboardWasOpen = false;
75
76
  // Capture initial viewport height before any keyboard interaction.
76
77
  // In iOS PWA standalone mode, window.innerHeight shrinks when the
77
78
  // keyboard opens, so we need a stable baseline for comparison.
@@ -86,7 +87,12 @@ export function initNotifications(_ctx) {
86
87
  // Toggle class so CSS can remove the tab-bar bottom padding while keyboard is up
87
88
  var keyboardOpen = vv.height < stableViewportHeight - 100;
88
89
  document.body.classList.toggle("keyboard-open", keyboardOpen);
89
- if (!keyboardOpen) ctx.scrollToBottom();
90
+ if (keyboardOpen && !keyboardWasOpen && ctx.forceScrollToBottom) {
91
+ requestAnimationFrame(function() { ctx.forceScrollToBottom(); });
92
+ } else if (!keyboardOpen) {
93
+ ctx.scrollToBottom();
94
+ }
95
+ keyboardWasOpen = keyboardOpen;
90
96
  // Hide tab bar when software keyboard is open
91
97
  if (mobileTabBar) {
92
98
  if (keyboardOpen) {
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { store } from './store.js';
7
7
  import { applyContextView, getContextView } from './app-panels.js';
8
+ import { forceExternalLinkToNewTab } from './pane-links.js';
8
9
 
9
10
  var panelOpen = false;
10
11
 
@@ -29,9 +30,19 @@ function togglePaneContextPanel() {
29
30
  applyContextView(panelOpen ? "panel" : "off");
30
31
  }
31
32
 
33
+ function preparePaneLink(event) {
34
+ var target = event.target;
35
+ if (!target || typeof target.closest !== "function") return;
36
+ var anchor = target.closest("a[href]");
37
+ if (!anchor || anchor.hasAttribute("download")) return;
38
+ forceExternalLinkToNewTab(anchor, window.location.href);
39
+ }
40
+
32
41
  export function initPaneBridge() {
33
42
  if (!store.get('paneMode')) return;
34
43
  panelOpen = getContextView() === "panel";
44
+ document.addEventListener("click", preparePaneLink, true);
45
+ document.addEventListener("auxclick", preparePaneLink, true);
35
46
  window.addEventListener("message", function (event) {
36
47
  if (event.origin !== window.location.origin) return;
37
48
  var msg = event.data;
@@ -0,0 +1,23 @@
1
+ export function isExternalWebLink(href, currentHref) {
2
+ if (!href) return false;
3
+ try {
4
+ var url = new URL(href, currentHref);
5
+ var currentUrl = new URL(currentHref);
6
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
7
+ return url.origin !== currentUrl.origin;
8
+ } catch (err) {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ export function forceExternalLinkToNewTab(anchor, currentHref) {
14
+ if (!anchor || !isExternalWebLink(anchor.href, currentHref)) return false;
15
+
16
+ var rel = (anchor.getAttribute("rel") || "").split(/\s+/).filter(Boolean);
17
+ if (rel.indexOf("noopener") === -1) rel.push("noopener");
18
+ if (rel.indexOf("noreferrer") === -1) rel.push("noreferrer");
19
+
20
+ anchor.setAttribute("target", "_blank");
21
+ anchor.setAttribute("rel", rel.join(" "));
22
+ return true;
23
+ }
@@ -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,148 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { iconHtml, refreshIcons } from './icons.js';
4
+ import { showToast } from './utils.js';
5
+
6
+ var defaultPlaceholder = "";
7
+
8
+ function getInput() {
9
+ return document.getElementById("input");
10
+ }
11
+
12
+ function appendToMessages(element) {
13
+ var messages = document.getElementById("messages");
14
+ if (!messages) return;
15
+ messages.appendChild(element);
16
+ requestAnimationFrame(function () { messages.scrollTop = messages.scrollHeight; });
17
+ }
18
+
19
+ function setMode(active) {
20
+ var input = getInput();
21
+ var button = document.getElementById("shell-command-btn");
22
+ var row = document.getElementById("input-row");
23
+ store.set({ shellCommandMode: active });
24
+ if (button) {
25
+ button.classList.toggle("active", active);
26
+ button.setAttribute("aria-pressed", active ? "true" : "false");
27
+ }
28
+ if (row) row.classList.toggle("shell-command-mode", active);
29
+ if (input) {
30
+ if (!defaultPlaceholder) defaultPlaceholder = input.placeholder;
31
+ input.placeholder = active ? "Run a shell command in this project…" : defaultPlaceholder;
32
+ input.focus();
33
+ }
34
+ }
35
+
36
+ export function isShellCommandMode() {
37
+ return !!store.get("shellCommandMode");
38
+ }
39
+
40
+ export function toggleShellCommandMode() {
41
+ if (store.get("shellCommandRunning")) return;
42
+ var target = store.get("dmTargetUser");
43
+ if (store.get("dmMode") && target && !target.isMate) {
44
+ showToast("Shell commands are available in agent sessions, not user DMs.", "error");
45
+ return;
46
+ }
47
+ setMode(!isShellCommandMode());
48
+ }
49
+
50
+ function renderPendingCommand(requestId, command) {
51
+ var card = document.createElement("div");
52
+ card.className = "shell-command-card running";
53
+ card.dataset.requestId = requestId;
54
+ card.innerHTML =
55
+ '<div class="shell-command-header">' +
56
+ '<span class="shell-command-icon">' + iconHtml("square-terminal") + '</span>' +
57
+ '<code></code><span class="shell-command-status">Running…</span>' +
58
+ '</div>' +
59
+ '<pre class="shell-command-output">Waiting for output…</pre>';
60
+ card.querySelector("code").textContent = "$ " + command;
61
+ appendToMessages(card);
62
+ refreshIcons();
63
+ }
64
+
65
+ export function submitShellCommand(command) {
66
+ command = String(command || "").trim();
67
+ if (!command || store.get("shellCommandRunning")) return false;
68
+ var ws = getWs();
69
+ if (!ws || ws.readyState !== 1) {
70
+ showToast("Not connected — command not run.", "error");
71
+ return false;
72
+ }
73
+
74
+ var requestId = "shell_" + Date.now() + "_" + Math.random().toString(36).slice(2, 9);
75
+ store.set({ shellCommandRunning: true, pendingShellCommandId: requestId });
76
+ var input = getInput();
77
+ if (input) {
78
+ input.disabled = true;
79
+ input.placeholder = "Running command…";
80
+ }
81
+ renderPendingCommand(requestId, command);
82
+ ws.send(JSON.stringify({ type: "shell_command", requestId: requestId, command: command }));
83
+ return true;
84
+ }
85
+
86
+ export function handleShellCommandResult(msg) {
87
+ var cards = document.querySelectorAll(".shell-command-card[data-request-id]");
88
+ var card = null;
89
+ for (var i = 0; i < cards.length; i++) {
90
+ if (cards[i].dataset.requestId === (msg.requestId || "")) {
91
+ card = cards[i];
92
+ break;
93
+ }
94
+ }
95
+ if (card) {
96
+ var status = card.querySelector(".shell-command-status");
97
+ var output = card.querySelector(".shell-command-output");
98
+ card.classList.remove("running");
99
+ if (msg.error) {
100
+ card.classList.add("error");
101
+ if (status) status.textContent = "Failed";
102
+ if (output) output.textContent = msg.error;
103
+ } else {
104
+ card.classList.toggle("error", msg.exitCode !== 0);
105
+ if (status) status.textContent = msg.timedOut ? "Timed out" : "Exit " + (msg.exitCode == null ? "—" : msg.exitCode);
106
+ if (output) output.textContent = msg.output || "(no output)";
107
+ }
108
+ }
109
+
110
+ store.set({ shellCommandRunning: false, pendingShellCommandId: null });
111
+ var input = getInput();
112
+ if (input) input.disabled = false;
113
+ if (msg.error) {
114
+ setMode(true);
115
+ } else {
116
+ setMode(false);
117
+ }
118
+ var messages = document.getElementById("messages");
119
+ if (messages) requestAnimationFrame(function () { messages.scrollTop = messages.scrollHeight; });
120
+ }
121
+
122
+ export function initShellCommand() {
123
+ var button = document.getElementById("shell-command-btn");
124
+ var mobileButton = document.getElementById("input-more-shell");
125
+ if (button) button.addEventListener("click", toggleShellCommandMode);
126
+ if (mobileButton) {
127
+ mobileButton.addEventListener("click", function () {
128
+ var sheet = document.getElementById("input-more-sheet");
129
+ if (sheet) {
130
+ sheet.classList.remove("open");
131
+ setTimeout(function () { sheet.classList.add("hidden"); }, 250);
132
+ }
133
+ toggleShellCommandMode();
134
+ });
135
+ }
136
+ store.subscribe(function (state, previous) {
137
+ if (previous.connected && !state.connected && state.shellCommandRunning) {
138
+ resetShellCommand();
139
+ }
140
+ });
141
+ }
142
+
143
+ export function resetShellCommand() {
144
+ store.set({ shellCommandMode: false, shellCommandRunning: false, pendingShellCommandId: null });
145
+ var input = getInput();
146
+ if (input) input.disabled = false;
147
+ setMode(false);
148
+ }
@@ -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
  }