clay-server 3.3.2-beta.3 → 3.4.0-beta.10

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 (77) 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-file-watch.js +77 -16
  6. package/lib/project-mate-interaction.js +21 -1
  7. package/lib/project-memory.js +3 -0
  8. package/lib/project-shell-command.js +160 -0
  9. package/lib/project-user-message.js +10 -0
  10. package/lib/project.js +19 -4
  11. package/lib/public/antigravity-avatar.png +0 -0
  12. package/lib/public/app.js +28 -0
  13. package/lib/public/copilot-avatar.svg +5 -0
  14. package/lib/public/css/command-palette.css +22 -2
  15. package/lib/public/css/icon-strip.css +29 -13
  16. package/lib/public/css/input.css +79 -0
  17. package/lib/public/css/mates.css +7 -0
  18. package/lib/public/css/menus.css +48 -14
  19. package/lib/public/css/messages.css +9 -0
  20. package/lib/public/css/pane.css +4 -0
  21. package/lib/public/css/pwa-mobile.css +17 -0
  22. package/lib/public/css/title-bar.css +19 -0
  23. package/lib/public/grok-avatar.svg +4 -0
  24. package/lib/public/index.html +43 -4
  25. package/lib/public/junie-avatar.svg +11 -0
  26. package/lib/public/kimi-avatar.svg +4 -0
  27. package/lib/public/modules/app-header.js +4 -0
  28. package/lib/public/modules/app-messages.js +28 -5
  29. package/lib/public/modules/app-panels.js +37 -5
  30. package/lib/public/modules/app-projects.js +3 -1
  31. package/lib/public/modules/app-rendering.js +22 -1
  32. package/lib/public/modules/input.js +58 -28
  33. package/lib/public/modules/mate-sidebar.js +17 -3
  34. package/lib/public/modules/notifications.js +7 -1
  35. package/lib/public/modules/pane-bridge.js +11 -0
  36. package/lib/public/modules/pane-links.js +23 -0
  37. package/lib/public/modules/project-switcher.js +7 -6
  38. package/lib/public/modules/shell-command.js +148 -0
  39. package/lib/public/modules/sidebar-mates.js +7 -1
  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 +6 -6
  43. package/lib/public/modules/split-pair-ui.js +3 -2
  44. package/lib/public/modules/tools.js +12 -1
  45. package/lib/public/modules/vendor-priority.js +14 -0
  46. package/lib/public/modules/vendor-selection.js +20 -0
  47. package/lib/public/modules/worktree-location.js +17 -0
  48. package/lib/public/opencode-avatar.svg +4 -0
  49. package/lib/public/qwen-avatar.svg +4 -0
  50. package/lib/public/style.css +3 -2
  51. package/lib/sdk-bridge.js +59 -19
  52. package/lib/sdk-message-processor.js +10 -1
  53. package/lib/session-notes-mcp-server.js +7 -1
  54. package/lib/worktree.js +9 -4
  55. package/lib/ws-schema.js +3 -0
  56. package/lib/yoke/acp-agent-profiles.js +238 -0
  57. package/lib/yoke/acp-driver-runtime.js +50 -0
  58. package/lib/yoke/acp-event-normalizer.js +179 -0
  59. package/lib/yoke/acp-process-manager.js +264 -0
  60. package/lib/yoke/acp-query-handle.js +487 -0
  61. package/lib/yoke/adapters/acp.js +317 -0
  62. package/lib/yoke/adapters/antigravity.js +417 -0
  63. package/lib/yoke/adapters/codex.js +98 -3
  64. package/lib/yoke/adapters/copilot.js +7 -0
  65. package/lib/yoke/adapters/grok.js +7 -0
  66. package/lib/yoke/adapters/junie.js +7 -0
  67. package/lib/yoke/adapters/kimi.js +7 -0
  68. package/lib/yoke/adapters/kiro.js +4 -4
  69. package/lib/yoke/adapters/opencode.js +7 -0
  70. package/lib/yoke/adapters/qwen.js +7 -0
  71. package/lib/yoke/codex-app-server.js +25 -8
  72. package/lib/yoke/index.js +81 -13
  73. package/lib/yoke/instructions.js +3 -0
  74. package/lib/yoke/interface.js +2 -0
  75. package/lib/yoke/kiro-acp-server.js +30 -276
  76. package/lib/yoke/vendor-registry.js +77 -0
  77. package/package.json +1 -1
@@ -22,7 +22,7 @@ import { handleFindInSessionResults } from './session-search.js';
22
22
  import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
23
23
  import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
24
24
  import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
25
- import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
25
+ import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent } from './input.js';
26
26
  import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
27
27
  import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
28
28
  import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch, presentMarkdownEdit } from './filebrowser.js';
@@ -42,6 +42,7 @@ import { checkAdminAccess } from './admin.js';
42
42
  import { mateAvatarUrl } from './avatar.js';
43
43
  import { showImageModal, sendExtensionCommand, handleMcpToolCallMessage } from './app-misc.js';
44
44
  import { handleMcpServersState } from './mcp-ui.js';
45
+ import { handleShellCommandResult } from './shell-command.js';
45
46
  import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
46
47
 
47
48
  // --- App module imports ---
@@ -51,6 +52,7 @@ import { setStatus } from './app-connection.js';
51
52
  import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
52
53
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
53
54
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
55
+ import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
54
56
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
55
57
  import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
56
58
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
@@ -413,6 +415,15 @@ export function processMessage(msg) {
413
415
  break;
414
416
 
415
417
  case "model_info": {
418
+ // Installation state applies across vendors. Keep it even when the
419
+ // model payload itself is stale for the active session.
420
+ var _vendorState = {};
421
+ if (msg.availableVendors) _vendorState.availableVendors = msg.availableVendors;
422
+ if (msg.installedVendors) _vendorState.installedVendors = msg.installedVendors;
423
+ if (Object.keys(_vendorState).length > 0) {
424
+ store.set(_vendorState);
425
+ selectDefaultVendorForBlankSession();
426
+ }
416
427
  // Drop stale model_info from a vendor that doesn't match the active
417
428
  // session's vendor. On high-latency connections, the server's default-
418
429
  // adapter model_info can arrive after session_switched has already
@@ -446,8 +457,6 @@ export function processMessage(msg) {
446
457
  _miUpdate.currentModel = store.get('currentModel');
447
458
  }
448
459
  if (msg.vendor && !store.get('vendorSelectionLocked')) _miUpdate.currentVendor = msg.vendor;
449
- if (msg.availableVendors) _miUpdate.availableVendors = msg.availableVendors;
450
- if (msg.installedVendors) _miUpdate.installedVendors = msg.installedVendors;
451
460
  if (msg.capabilities) _miUpdate.vendorCapabilities = msg.capabilities;
452
461
  store.set(_miUpdate);
453
462
  updateSettingsModels(_modelVal, msg.models || []);
@@ -660,7 +669,7 @@ export function processMessage(msg) {
660
669
  var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
661
670
  ? msg.runtimeTerminalId
662
671
  : (typeof msg.terminalId === "number" ? msg.terminalId : null);
663
- store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || store.get('currentModel'), currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode') });
672
+ store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || store.get('currentModel'), currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionVendorBound: !!msg.vendor || !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode') });
664
673
  // TUI sessions swap the chat UI for an embedded xterm running
665
674
  // `claude` inside a real PTY. Mount or tear down before the rest of
666
675
  // the chat-side bookkeeping runs so we don't waste work on hidden DOM.
@@ -705,7 +714,7 @@ export function processMessage(msg) {
705
714
  }
706
715
  }
707
716
  if (!msg.hasHistory && !msg.vendor) {
708
- // Preserve explicit pre-message vendor choice on brand-new sessions.
717
+ selectDefaultVendorForBlankSession();
709
718
  }
710
719
  // Vendor toggle visibility + active-vendor indicator next to the
711
720
  // model chip.
@@ -1209,6 +1218,15 @@ export function processMessage(msg) {
1209
1218
  addConflictMessage(msg);
1210
1219
  break;
1211
1220
 
1221
+ case "session_writer_conflict":
1222
+ removeMatePreThinking();
1223
+ setActivity(null);
1224
+ stopThinking();
1225
+ markAllToolsDone();
1226
+ closeToolGroup();
1227
+ addSystemMessage(msg.text || "This session is already open in another process.", true);
1228
+ break;
1229
+
1212
1230
  case "context_overflow":
1213
1231
  removeMatePreThinking();
1214
1232
  setActivity(null);
@@ -1371,6 +1389,11 @@ export function processMessage(msg) {
1371
1389
  updateTerminalList(msg.terminals);
1372
1390
  break;
1373
1391
 
1392
+ case "shell_command_result":
1393
+ handleShellCommandResult(msg);
1394
+ if (!msg.error) sendShellResultToAgent(msg);
1395
+ break;
1396
+
1374
1397
  case "context_sources_state":
1375
1398
  handleContextSourcesState(msg);
1376
1399
  break;
@@ -5,7 +5,7 @@ import { refreshIcons } from "./icons.js";
5
5
  import { escapeHtml, showToast } from "./utils.js";
6
6
  import { store } from './store.js';
7
7
  import { getWs } from './ws-ref.js';
8
- import { VENDOR_NAMES } from './app-rendering.js';
8
+ import { VENDOR_NAMES, isExperimentalVendor } from './app-rendering.js';
9
9
  import { reportPaneContext } from './pane-bridge.js';
10
10
 
11
11
  // --- Module-owned state (not in store) ---
@@ -89,6 +89,8 @@ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
89
89
  var EFFORT_LEVELS_BY_VENDOR = {
90
90
  claude: ["low", "medium", "high", "xhigh", "max"],
91
91
  codex: ["minimal", "low", "medium", "high", "xhigh"],
92
+ antigravity: [],
93
+ opencode: [],
92
94
  kiro: ["low", "medium", "high", "xhigh", "max"],
93
95
  };
94
96
  var THINKING_OPTIONS = ["disabled", "adaptive", "budget"];
@@ -441,8 +443,26 @@ export function initPanels() {
441
443
  var vendorToggleWrap = $("vendor-toggle-wrap");
442
444
  var vendorBtnClaude = $("vendor-btn-claude");
443
445
  var vendorBtnCodex = $("vendor-btn-codex");
446
+ var vendorBtnAntigravity = $("vendor-btn-antigravity");
447
+ var vendorBtnOpenCode = $("vendor-btn-opencode");
448
+ var vendorBtnKimi = $("vendor-btn-kimi");
449
+ var vendorBtnGrok = $("vendor-btn-grok");
450
+ var vendorBtnCopilot = $("vendor-btn-copilot");
451
+ var vendorBtnQwen = $("vendor-btn-qwen");
452
+ var vendorBtnJunie = $("vendor-btn-junie");
444
453
  var vendorBtnKiro = $("vendor-btn-kiro");
445
- var vendorBtns = { claude: vendorBtnClaude, codex: vendorBtnCodex, kiro: vendorBtnKiro };
454
+ var vendorBtns = {
455
+ claude: vendorBtnClaude,
456
+ codex: vendorBtnCodex,
457
+ antigravity: vendorBtnAntigravity,
458
+ opencode: vendorBtnOpenCode,
459
+ kimi: vendorBtnKimi,
460
+ grok: vendorBtnGrok,
461
+ copilot: vendorBtnCopilot,
462
+ qwen: vendorBtnQwen,
463
+ junie: vendorBtnJunie,
464
+ kiro: vendorBtnKiro,
465
+ };
446
466
 
447
467
  function updateVendorToggle() {
448
468
  var installed = store.get('installedVendors') || [];
@@ -456,7 +476,10 @@ export function initPanels() {
456
476
  var isInstalled = installed.indexOf(v) !== -1;
457
477
  btn.classList.toggle("active", v === current);
458
478
  btn.classList.toggle("disabled", !isInstalled);
459
- btn.title = isInstalled ? (VENDOR_NAMES[v] || v) : (VENDOR_NAMES[v] || v) + " is not installed";
479
+ btn.classList.toggle("experimental", isExperimentalVendor(v));
480
+ var title = isInstalled ? (VENDOR_NAMES[v] || v) : (VENDOR_NAMES[v] || v) + " is not installed";
481
+ if (isExperimentalVendor(v)) title += " · Experimental integration; not yet validated through direct use testing";
482
+ btn.title = title;
460
483
  }
461
484
  }
462
485
 
@@ -464,13 +487,20 @@ export function initPanels() {
464
487
  if (vendor === (store.get('currentVendor') || "claude")) return;
465
488
  var installed = store.get('installedVendors') || [];
466
489
  if (installed.indexOf(vendor) === -1) return;
467
- store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorSelectionLocked: true });
490
+ store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorSelectionLocked: true, sessionVendorBound: true });
468
491
  var ws = getWs();
469
492
  if (ws) ws.send(JSON.stringify({ type: "set_vendor", vendor: vendor }));
470
493
  }
471
494
 
472
495
  if (vendorBtnClaude) vendorBtnClaude.addEventListener("click", function() { onVendorClick("claude"); });
473
496
  if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
497
+ if (vendorBtnAntigravity) vendorBtnAntigravity.addEventListener("click", function() { onVendorClick("antigravity"); });
498
+ if (vendorBtnOpenCode) vendorBtnOpenCode.addEventListener("click", function() { onVendorClick("opencode"); });
499
+ if (vendorBtnKimi) vendorBtnKimi.addEventListener("click", function() { onVendorClick("kimi"); });
500
+ if (vendorBtnGrok) vendorBtnGrok.addEventListener("click", function() { onVendorClick("grok"); });
501
+ if (vendorBtnCopilot) vendorBtnCopilot.addEventListener("click", function() { onVendorClick("copilot"); });
502
+ if (vendorBtnQwen) vendorBtnQwen.addEventListener("click", function() { onVendorClick("qwen"); });
503
+ if (vendorBtnJunie) vendorBtnJunie.addEventListener("click", function() { onVendorClick("junie"); });
474
504
  if (vendorBtnKiro) vendorBtnKiro.addEventListener("click", function() { onVendorClick("kiro"); });
475
505
 
476
506
  // --- Reactive UI sync ---
@@ -849,7 +879,9 @@ export function updateContextPanel() {
849
879
  hCtxEl = document.createElement("div");
850
880
  hCtxEl.className = "header-context";
851
881
  hCtxEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
852
- statusArea.insertBefore(hCtxEl, statusArea.firstChild);
882
+ var workerBtn = document.getElementById("header-add-worker-btn");
883
+ var contextAnchor = workerBtn && workerBtn.parentNode === statusArea ? workerBtn.nextSibling : statusArea.firstChild;
884
+ statusArea.insertBefore(hCtxEl, contextAnchor);
853
885
  hCtxEl.addEventListener("mouseenter", function() {
854
886
  if (store.get('richContextUsage')) {
855
887
  showCtxPopover();
@@ -24,6 +24,7 @@ import { isSchedulerOpen, closeScheduler, resetScheduler } from './scheduler.js'
24
24
  import { connect, cancelReconnect, setStatus } from './app-connection.js';
25
25
  import { setTurnCounter, setPrependAnchor, setActivityEl, setIsUserScrolledUp, hideSuggestionChips } from './app-rendering.js';
26
26
  import { resetToolState, enableMainInput, resetTurnMetaCost } from './tools.js';
27
+ import { resetShellCommand } from './shell-command.js';
27
28
  import { clearPendingImages } from './input.js';
28
29
  import { clearAllMentionActive } from './sidebar-mates.js';
29
30
  import { setRewindMode } from './rewind.js';
@@ -321,7 +322,7 @@ export function renderProjectList() {
321
322
  isWorktree: p.isWorktree || false,
322
323
  parentSlug: p.parentSlug || null,
323
324
  branch: p.branch || null,
324
- worktreeAccessible: p.worktreeAccessible !== undefined ? p.worktreeAccessible : true,
325
+ worktreeExternal: p.worktreeExternal === true,
325
326
  };
326
327
  });
327
328
  var st = store.snap();
@@ -368,6 +369,7 @@ export function resetClientState() {
368
369
  store.set({ currentFullText: "" });
369
370
  resetToolState();
370
371
  clearPendingImages();
372
+ resetShellCommand();
371
373
  clearAllMentionActive();
372
374
  setActivityEl(null);
373
375
  store.set({ processing: false });
@@ -13,27 +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",
23
+ antigravity: "/antigravity-avatar.png",
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",
22
30
  kiro: "/kiro-avatar.svg",
23
31
  };
24
32
  export var VENDOR_NAMES = {
25
33
  claude: "Claude Code",
26
34
  codex: "Codex",
35
+ antigravity: "Antigravity CLI",
36
+ opencode: "OpenCode",
37
+ kimi: "Kimi Code",
38
+ grok: "Grok Build",
39
+ copilot: "GitHub Copilot CLI",
40
+ qwen: "Qwen Code",
41
+ junie: "Junie CLI",
27
42
  kiro: "Kiro CLI",
28
43
  };
29
44
  // Display order for every vendor Clay knows about, installed or not. Pickers
30
45
  // render the full list so a missing CLI reads as "not installed yet" rather
31
46
  // than "Clay doesn't support it".
32
- export var VENDOR_ORDER = ["claude", "codex", "kiro"];
33
47
  // Where to send the user when they pick a vendor whose CLI isn't installed.
34
48
  export var VENDOR_HOMEPAGES = {
35
49
  claude: "https://claude.com/product/claude-code",
36
50
  codex: "https://openai.com/codex/",
51
+ antigravity: "https://antigravity.google/product/antigravity-cli",
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/",
37
58
  kiro: "https://kiro.dev/",
38
59
  };
39
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;
@@ -149,16 +150,29 @@ export function showMateSidebar(mateId, mateData) {
149
150
  if (mateVendorWrap) {
150
151
  var available = store.get('availableVendors') || [];
151
152
  var mateVendor = mateData.vendor || "claude";
152
- var vendorIcons = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png", kiro: "/kiro-avatar.svg" };
153
- var vendorNames = { claude: "Claude Code", codex: "Codex", kiro: "Kiro CLI" };
154
- var vendorKeys = ["claude", "codex", "kiro"];
153
+ var vendorIcons = {
154
+ claude: "/claude-code-avatar.png",
155
+ codex: "/codex-avatar.png",
156
+ antigravity: "/antigravity-avatar.png",
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",
163
+ kiro: "/kiro-avatar.svg",
164
+ };
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"];
155
167
  mateVendorWrap.innerHTML = "";
156
168
  for (var vi = 0; vi < vendorKeys.length; vi++) {
157
169
  var vk = vendorKeys[vi];
158
170
  var vBtn = document.createElement("button");
159
171
  vBtn.className = "mate-vendor-btn" + (vk === mateVendor ? " active" : "");
172
+ if (isExperimentalVendor(vk)) vBtn.classList.add("experimental");
160
173
  if (available.indexOf(vk) === -1) vBtn.classList.add("disabled");
161
174
  vBtn.dataset.vendor = vk;
175
+ vBtn.title = vendorNames[vk] + (isExperimentalVendor(vk) ? " · Experimental integration; not yet validated through direct use testing" : "");
162
176
  vBtn.innerHTML = '<img src="' + vendorIcons[vk] + '" class="mate-vendor-icon" alt="' + vendorNames[vk] + '"><span class="mate-vendor-label">' + vendorNames[vk] + '</span>';
163
177
  vBtn.addEventListener("click", function() {
164
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');