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
@@ -30,7 +30,7 @@
30
30
  (function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
31
31
  </script>
32
32
  <script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
33
- <link rel="stylesheet" href="style.css?v=20260407">
33
+ <link rel="stylesheet" href="style.css?v=20260826a">
34
34
  <style>
35
35
  @media(max-width:768px){
36
36
  /* User messages: vertical stack, avatar on top, right-aligned */
@@ -373,12 +373,11 @@
373
373
  </div>
374
374
  <button id="hamburger-btn" aria-label="Toggle sidebar" title="Toggle sidebar"><i data-lucide="menu"></i></button>
375
375
  <span class="header-title" id="header-title">Connecting...</span>
376
+ <button id="header-rename-btn" type="button" title="Rename session"><i data-lucide="pencil"></i></button>
376
377
  <button id="header-info-btn" type="button" title="Session info"><i data-lucide="info"></i></button>
377
378
  <button id="header-full-access-btn" type="button" class="session-full-access hidden" role="switch" aria-checked="false" title="Skip Permissions is off — permission prompts are on">
378
379
  <span class="session-full-access-label">Skip Permissions</span><span class="session-full-access-track" aria-hidden="true"><span></span></span>
379
380
  </button>
380
- <button id="header-rename-btn" type="button" title="Rename session"><i data-lucide="pencil"></i></button>
381
- <button id="header-add-worker-btn" type="button" title="Add Worker — this session becomes the Driver of a pair"><i data-lucide="user-plus"></i></button>
382
381
  <button type="button" id="header-tui-close-btn" class="header-tui-close-btn hidden" title="Close terminal (keeps history, resume anytime)" aria-label="Close terminal"><i data-lucide="power"></i></button>
383
382
  <div id="mate-mobile-title" class="mate-mobile-title hidden">
384
383
  <img id="mate-mobile-avatar" class="mate-mobile-avatar" alt="">
@@ -389,6 +388,9 @@
389
388
  <div id="ralph-sticky" class="hidden"></div>
390
389
  <div id="debate-sticky" class="hidden"></div>
391
390
  <div class="status">
391
+ <button id="header-session-actions-btn" type="button" aria-label="Session actions" title="Session actions" aria-haspopup="menu" aria-expanded="false">
392
+ <i data-lucide="ellipsis"></i>
393
+ </button>
392
394
  <button type="button" id="header-tui-font-btn" class="header-tui-font-btn hidden" title="Terminal font" aria-label="Terminal font">
393
395
  <i data-lucide="type"></i>
394
396
  </button>
@@ -528,14 +530,34 @@
528
530
  <img src="/codex-avatar.png" class="vendor-toggle-icon" alt="Codex">
529
531
  <span class="vendor-toggle-label">Codex</span>
530
532
  </button>
531
- <button id="vendor-btn-gemini" class="vendor-toggle-btn" data-vendor="gemini">
532
- <img src="/gemini-avatar.svg" class="vendor-toggle-icon" alt="Gemini">
533
- <span class="vendor-toggle-label">Gemini CLI</span>
533
+ <button id="vendor-btn-antigravity" class="vendor-toggle-btn" data-vendor="antigravity">
534
+ <img src="/antigravity-avatar.png" class="vendor-toggle-icon" alt="Antigravity">
535
+ <span class="vendor-toggle-label">Antigravity CLI</span>
534
536
  </button>
535
537
  <button id="vendor-btn-opencode" class="vendor-toggle-btn" data-vendor="opencode">
536
538
  <img src="/opencode-avatar.svg" class="vendor-toggle-icon" alt="OpenCode">
537
539
  <span class="vendor-toggle-label">OpenCode</span>
538
540
  </button>
541
+ <button id="vendor-btn-kimi" class="vendor-toggle-btn" data-vendor="kimi">
542
+ <img src="/kimi-avatar.svg" class="vendor-toggle-icon" alt="Kimi">
543
+ <span class="vendor-toggle-label">Kimi Code</span>
544
+ </button>
545
+ <button id="vendor-btn-grok" class="vendor-toggle-btn" data-vendor="grok">
546
+ <img src="/grok-avatar.svg" class="vendor-toggle-icon" alt="Grok">
547
+ <span class="vendor-toggle-label">Grok Build</span>
548
+ </button>
549
+ <button id="vendor-btn-copilot" class="vendor-toggle-btn" data-vendor="copilot">
550
+ <img src="/copilot-avatar.svg" class="vendor-toggle-icon" alt="GitHub Copilot">
551
+ <span class="vendor-toggle-label">GitHub Copilot CLI</span>
552
+ </button>
553
+ <button id="vendor-btn-qwen" class="vendor-toggle-btn" data-vendor="qwen">
554
+ <img src="/qwen-avatar.svg" class="vendor-toggle-icon" alt="Qwen">
555
+ <span class="vendor-toggle-label">Qwen Code</span>
556
+ </button>
557
+ <button id="vendor-btn-junie" class="vendor-toggle-btn" data-vendor="junie">
558
+ <img src="/junie-avatar.svg" class="vendor-toggle-icon" alt="Junie">
559
+ <span class="vendor-toggle-label">Junie CLI</span>
560
+ </button>
539
561
  <button id="vendor-btn-kiro" class="vendor-toggle-btn" data-vendor="kiro">
540
562
  <img src="/kiro-avatar.svg" class="vendor-toggle-icon" alt="Kiro">
541
563
  <span class="vendor-toggle-label">Kiro CLI</span>
@@ -2338,7 +2360,7 @@
2338
2360
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2339
2361
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2340
2362
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2341
- <script type="module" src="app.js"></script>
2363
+ <script type="module" src="app.js?v=20260826a"></script>
2342
2364
  <div id="pwa-install-modal" class="pwa-modal hidden">
2343
2365
  <div class="pwa-modal-backdrop"></div>
2344
2366
  <div class="pwa-modal-card">
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Junie">
2
+ <defs>
3
+ <linearGradient id="junie-gradient" x1="8" y1="8" x2="56" y2="56" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#8B5CF6"/>
5
+ <stop offset="1" stop-color="#F97316"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="15" fill="url(#junie-gradient)"/>
9
+ <path d="M39 15v26c0 7.7-5.4 12-13.2 12-5.3 0-9.4-2.2-12-6.4l7.1-5c1.3 2 2.7 3 4.6 3 2.5 0 4-1.4 4-4.4V15H39Z" fill="#fff"/>
10
+ <circle cx="47.5" cy="17.5" r="4.5" fill="#fff" fill-opacity=".9"/>
11
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Kimi Code">
2
+ <rect width="64" height="64" rx="14" fill="#1769ff"/>
3
+ <path d="M18 14h9v15l13-15h11L36 31l16 19H40L27 34v16h-9z" fill="#fff"/>
4
+ </svg>
@@ -0,0 +1,69 @@
1
+ import { VENDOR_NAMES, VENDOR_ORDER, isExperimentalVendor } from './app-rendering.js';
2
+ import { effortLevelsFor, effortDisplayName } from './app-panels.js';
3
+
4
+ function optionValue(entry) {
5
+ if (typeof entry === "string") return entry;
6
+ return entry && (entry.value || entry.id) || "";
7
+ }
8
+
9
+ function optionLabel(entry) {
10
+ if (typeof entry === "string") return entry;
11
+ return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
12
+ }
13
+
14
+ export function buildAgentVendorSelect(installed, preferred) {
15
+ var select = document.createElement("select");
16
+ select.className = "wt-modal-input";
17
+ for (var i = 0; i < VENDOR_ORDER.length; i++) {
18
+ var vendor = VENDOR_ORDER[i];
19
+ if (installed.indexOf(vendor) === -1) continue;
20
+ var option = document.createElement("option");
21
+ option.value = vendor;
22
+ option.textContent = (isExperimentalVendor(vendor) ? "🧪 " : "") + (VENDOR_NAMES[vendor] || vendor);
23
+ if (isExperimentalVendor(vendor)) option.title = "Experimental integration; not yet validated through direct use testing";
24
+ select.appendChild(option);
25
+ }
26
+ if (preferred && installed.indexOf(preferred) !== -1) select.value = preferred;
27
+ return select;
28
+ }
29
+
30
+ export function fillAgentModels(select, vendor, options, preferred) {
31
+ var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
32
+ select.innerHTML = "";
33
+ var automatic = document.createElement("option");
34
+ automatic.value = "";
35
+ automatic.textContent = "Automatic";
36
+ select.appendChild(automatic);
37
+ for (var i = 0; i < models.length; i++) {
38
+ var option = document.createElement("option");
39
+ option.value = optionValue(models[i]);
40
+ option.textContent = optionLabel(models[i]);
41
+ select.appendChild(option);
42
+ }
43
+ select.value = preferred || "";
44
+ if (select.selectedIndex === -1) select.value = "";
45
+ }
46
+
47
+ export function buildAgentEffortSelect() {
48
+ var select = document.createElement("select");
49
+ select.className = "wt-modal-input";
50
+ return select;
51
+ }
52
+
53
+ export function fillAgentEffort(select, vendor, options, modelValue, preferred) {
54
+ var previous = preferred === undefined ? select.value : preferred;
55
+ var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
56
+ var levels = effortLevelsFor(vendor, models, modelValue);
57
+ select.innerHTML = "";
58
+ var automatic = document.createElement("option");
59
+ automatic.value = "";
60
+ automatic.textContent = "Default";
61
+ select.appendChild(automatic);
62
+ for (var i = 0; i < levels.length; i++) {
63
+ var option = document.createElement("option");
64
+ option.value = levels[i];
65
+ option.textContent = effortDisplayName(levels[i]);
66
+ select.appendChild(option);
67
+ }
68
+ select.value = levels.indexOf(previous) !== -1 ? previous : "";
69
+ }
@@ -12,7 +12,6 @@ import { saveToolState, resetToolState, restoreToolState } from './tools.js';
12
12
  import { getSessionUsage, setSessionUsage, getContextData, setContextData, updateContextPanel, updateUsagePanel } from './app-panels.js';
13
13
  import { onHistoryPrepended as onSessionSearchHistoryPrepended } from './session-search.js';
14
14
  import { getCachedSessions } from './sidebar-sessions.js';
15
- import { openPairDialog } from './split-pair-ui.js';
16
15
  import { showConfirm } from './app-misc.js';
17
16
 
18
17
  // --- Module-owned state ---
@@ -23,27 +22,6 @@ export function initHeader() {
23
22
  var headerRenameBtn = document.getElementById("header-rename-btn");
24
23
  var headerTitleEl = document.getElementById("header-title");
25
24
  var headerInfoBtn = document.getElementById("header-info-btn");
26
- var headerAddWorkerBtn = document.getElementById("header-add-worker-btn");
27
-
28
- // --- Add Worker: the current session becomes the Driver of a new pair ---
29
- if (headerAddWorkerBtn) {
30
- headerAddWorkerBtn.addEventListener("click", function () {
31
- var sessionId = store.get('activeSessionId');
32
- if (!sessionId || store.get('splitPanes') || store.get('dmMode')) return;
33
- openPairDialog({
34
- sessionId: sessionId,
35
- title: headerTitleEl ? headerTitleEl.textContent : "",
36
- vendor: store.get('currentVendor') || "claude",
37
- });
38
- });
39
- // A split shows the GROUP in the title bar; adding a worker there makes
40
- // no sense (the group already has two members), so hide the button.
41
- store.subscribe(function (state, prev) {
42
- if (state.splitPanes !== prev.splitPanes) {
43
- headerAddWorkerBtn.classList.toggle("hidden", !!state.splitPanes);
44
- }
45
- });
46
- }
47
25
  var headerFullAccessBtn = document.getElementById("header-full-access-btn");
48
26
 
49
27
  function updateFullAccessButton() {
@@ -93,6 +71,8 @@ export function initHeader() {
93
71
  input.type = "text";
94
72
  input.className = "header-rename-input";
95
73
  input.value = currentText;
74
+ var titleBar = headerTitleEl.closest(".title-bar-content");
75
+ if (titleBar) titleBar.classList.add("session-renaming");
96
76
  headerTitleEl.style.display = "none";
97
77
  headerRenameBtn.style.display = "none";
98
78
  headerTitleEl.parentNode.insertBefore(input, headerTitleEl.nextSibling);
@@ -117,6 +97,7 @@ export function initHeader() {
117
97
  }
118
98
  }
119
99
  input.remove();
100
+ if (titleBar) titleBar.classList.remove("session-renaming");
120
101
  headerTitleEl.style.display = "";
121
102
  headerRenameBtn.style.display = "";
122
103
  }
@@ -126,6 +107,7 @@ export function initHeader() {
126
107
  if (e.key === "Escape") {
127
108
  e.preventDefault();
128
109
  input.remove();
110
+ if (titleBar) titleBar.classList.remove("session-renaming");
129
111
  headerTitleEl.style.display = "";
130
112
  headerRenameBtn.style.display = "";
131
113
  }
@@ -21,6 +21,7 @@ import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette
21
21
  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
+ import { handleSessionActionMessage } from './session-actions.js';
24
25
  import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
25
26
  import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent } from './input.js';
26
27
  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';
@@ -52,6 +53,8 @@ import { setStatus } from './app-connection.js';
52
53
  import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
53
54
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
54
55
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
56
+ import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
57
+ import { getModelInfoUpdate, modelEntryValue, modelEntryMatches, handleModelSelectionResult, requestVendorModels } from './model-picker.js';
55
58
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
56
59
  import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
57
60
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
@@ -414,6 +417,15 @@ export function processMessage(msg) {
414
417
  break;
415
418
 
416
419
  case "model_info": {
420
+ // Installation state applies across vendors. Keep it even when the
421
+ // model payload itself is stale for the active session.
422
+ var _vendorState = {};
423
+ if (msg.availableVendors) _vendorState.availableVendors = msg.availableVendors;
424
+ if (msg.installedVendors) _vendorState.installedVendors = msg.installedVendors;
425
+ if (Object.keys(_vendorState).length > 0) {
426
+ store.set(_vendorState);
427
+ selectDefaultVendorForBlankSession();
428
+ }
417
429
  // Drop stale model_info from a vendor that doesn't match the active
418
430
  // session's vendor. On high-latency connections, the server's default-
419
431
  // adapter model_info can arrive after session_switched has already
@@ -421,12 +433,12 @@ export function processMessage(msg) {
421
433
  // currentModels with the wrong vendor's list and trigger app-panels
422
434
  // to request models for the "wrong" vendor, which feeds back into a
423
435
  // ping-pong loop of vendor flapping. See issue #336.
424
- var _curV = store.get('currentVendor');
425
- if (msg.vendor && _curV && msg.vendor !== _curV) break;
436
+ var _modelLoadUpdate = getModelInfoUpdate(msg);
437
+ if (!_modelLoadUpdate) break;
426
438
 
427
439
  var _modelVal = msg.model;
428
440
  if (_modelVal && typeof _modelVal === "object") _modelVal = _modelVal.value || _modelVal.displayName || "";
429
- var _miUpdate = { currentModels: msg.models || [] };
441
+ var _miUpdate = Object.assign({ currentModels: msg.models || [] }, _modelLoadUpdate);
430
442
  if (Object.prototype.hasOwnProperty.call(msg, "model")) {
431
443
  // Keep the user's existing selection only if it actually belongs to
432
444
  // this vendor's model list. Otherwise (e.g. switching to a Codex
@@ -435,10 +447,15 @@ export function processMessage(msg) {
435
447
  // model.
436
448
  var _curModel = store.get('currentModel');
437
449
  var _curInList = !!_curModel && (msg.models || []).some(function (m) {
438
- var v = typeof m === "string" ? m : (m && m.value);
439
- return v === _curModel;
450
+ return modelEntryMatches(m, _curModel);
451
+ });
452
+ var _incomingInList = !_modelVal || (msg.models || []).length === 0 || (msg.models || []).some(function (m) {
453
+ return modelEntryMatches(m, _modelVal);
440
454
  });
441
- if (store.get('vendorSelectionLocked') && _curInList) {
455
+ if (!_incomingInList) {
456
+ console.warn("[model-state] Ignoring model not listed for vendor " + (msg.vendor || store.get('currentVendor')) + ": " + _modelVal);
457
+ _miUpdate.currentModel = _curInList ? _curModel : modelEntryValue((msg.models || [])[0]);
458
+ } else if (store.get('vendorSelectionLocked') && _curInList) {
442
459
  // Keep the user's existing (valid) selection; only update models list
443
460
  } else {
444
461
  _miUpdate.currentModel = _modelVal || "";
@@ -447,17 +464,31 @@ export function processMessage(msg) {
447
464
  _miUpdate.currentModel = store.get('currentModel');
448
465
  }
449
466
  if (msg.vendor && !store.get('vendorSelectionLocked')) _miUpdate.currentVendor = msg.vendor;
450
- if (msg.availableVendors) _miUpdate.availableVendors = msg.availableVendors;
451
- if (msg.installedVendors) _miUpdate.installedVendors = msg.installedVendors;
452
467
  if (msg.capabilities) _miUpdate.vendorCapabilities = msg.capabilities;
453
468
  store.set(_miUpdate);
454
469
  updateSettingsModels(_modelVal, msg.models || []);
455
470
  break;
456
471
  }
457
472
 
473
+ case "model_selection_result":
474
+ handleModelSelectionResult(msg);
475
+ break;
476
+
458
477
  case "config_state": {
478
+ if ((msg.sessionId != null && msg.sessionId !== store.get('activeSessionId')) || (msg.vendor && msg.vendor !== store.get('currentVendor'))) break;
459
479
  var _cs = {};
460
- if (msg.model) _cs.currentModel = msg.model;
480
+ if (msg.model) {
481
+ var _knownModels = store.get('currentModels') || [];
482
+ var _modelKnown = _knownModels.length === 0 || _knownModels.some(function(model) { return modelEntryMatches(model, msg.model); });
483
+ if (_modelKnown) {
484
+ _cs.currentModel = msg.model;
485
+ } else {
486
+ console.warn("[model-state] Ignoring model not listed for current vendor: " + msg.model);
487
+ var _existingModel = store.get('currentModel');
488
+ var _existingKnown = _knownModels.some(function(model) { return modelEntryMatches(model, _existingModel); });
489
+ _cs.currentModel = _existingKnown ? _existingModel : modelEntryValue(_knownModels[0]);
490
+ }
491
+ }
461
492
  if (msg.mode) _cs.currentMode = msg.mode;
462
493
  if (msg.effort) _cs.currentEffort = msg.effort;
463
494
  if (msg.betas) _cs.currentBetas = msg.betas;
@@ -589,6 +620,13 @@ export function processMessage(msg) {
589
620
  if (createdPairGroup) openGroup(createdPairGroup);
590
621
  break;
591
622
 
623
+ case "session_handoff_result":
624
+ case "handoff_session_options":
625
+ case "handoff_context":
626
+ case "handoff_created":
627
+ handleSessionActionMessage(msg);
628
+ break;
629
+
592
630
  case "worker_proposal":
593
631
  renderWorkerProposal(msg);
594
632
  break;
@@ -661,7 +699,7 @@ export function processMessage(msg) {
661
699
  var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
662
700
  ? msg.runtimeTerminalId
663
701
  : (typeof msg.terminalId === "number" ? msg.terminalId : null);
664
- 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') });
702
+ store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || "", 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') });
665
703
  // TUI sessions swap the chat UI for an embedded xterm running
666
704
  // `claude` inside a real PTY. Mount or tear down before the rest of
667
705
  // the chat-side bookkeeping runs so we don't waste work on hidden DOM.
@@ -706,8 +744,9 @@ export function processMessage(msg) {
706
744
  }
707
745
  }
708
746
  if (!msg.hasHistory && !msg.vendor) {
709
- // Preserve explicit pre-message vendor choice on brand-new sessions.
747
+ selectDefaultVendorForBlankSession();
710
748
  }
749
+ if (!msg.model) requestVendorModels(store.get('currentVendor') || "claude", true);
711
750
  // Vendor toggle visibility + active-vendor indicator next to the
712
751
  // model chip.
713
752
  // - Session has an explicit vendor: hide the toggle (it's
@@ -1108,6 +1147,10 @@ export function processMessage(msg) {
1108
1147
  updateSubagentTaskStatus(msg.parentToolId, msg.patch);
1109
1148
  break;
1110
1149
 
1150
+ case "active_background_tasks":
1151
+ store.set({ activeBackgroundTasks: Array.isArray(msg.tasks) ? msg.tasks : [] });
1152
+ break;
1153
+
1111
1154
  case "result":
1112
1155
  // Result marks turn end. Drop pre-thinking even if no delta/tool
1113
1156
  // event ever arrived (e.g. tool-only turn whose progress signals
@@ -5,8 +5,9 @@ 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
+ import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelPickerOpen, modelDisplayName } from './model-picker.js';
10
11
 
11
12
  // --- Module-owned state (not in store) ---
12
13
  var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
@@ -20,7 +21,6 @@ var configChipWrap = null;
20
21
  var configChip = null;
21
22
  var configChipLabel = null;
22
23
  var configPopover = null;
23
- var configModelList = null;
24
24
  var configModeList = null;
25
25
  var configEffortSection = null;
26
26
  var configEffortBar = null;
@@ -89,7 +89,7 @@ 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
- gemini: [],
92
+ antigravity: [],
93
93
  opencode: [],
94
94
  kiro: ["low", "medium", "high", "xhigh", "max"],
95
95
  };
@@ -132,18 +132,6 @@ export function setContextData(v) { contextData = v; }
132
132
 
133
133
  // --- Internal helpers ---
134
134
 
135
- function modelDisplayName(value, models) {
136
- if (!value) return "";
137
- if (models) {
138
- for (var i = 0; i < models.length; i++) {
139
- var m = models[i];
140
- if (typeof m === "string") { if (m === value) return m; }
141
- else if (m.value === value && m.displayName) return m.displayName;
142
- }
143
- }
144
- return value;
145
- }
146
-
147
135
  function modeDisplayName(value) {
148
136
  for (var i = 0; i < MODE_OPTIONS.length; i++) {
149
137
  if (MODE_OPTIONS[i].value === value) return MODE_OPTIONS[i].label;
@@ -198,65 +186,6 @@ function hasBeta(name) {
198
186
  return false;
199
187
  }
200
188
 
201
- function rebuildModelList() {
202
- if (!configModelList) return;
203
- // Picker lock policy is capability-based (yoke midSessionModelSwitch):
204
- // Claude -> free. The SDK supports mid-thread swaps on a live query
205
- // (query.setModel: "Change the model used for subsequent
206
- // responses"), and sdk-bridge already routes set_model to
207
- // queryInstance.setModel. Cache re-priming on switch is a
208
- // cost, not a breakage.
209
- // Codex / Kiro -> locked once the session has history: their threads
210
- // bind the model at creation and a mid-thread change would be
211
- // silently ignored. Picks made before the first message still
212
- // work everywhere (sdk-bridge stores into sm.currentModel when
213
- // there's no active queryInstance).
214
- var modelSection = configModelList.parentElement;
215
- var s = store.snap();
216
- var vendor = s.currentVendor || "claude";
217
- if (modelSection) modelSection.style.display = "";
218
- configModelList.innerHTML = "";
219
-
220
- var caps = s.vendorCapabilities || {};
221
- var lockedForGui = s.activeSessionMode === "gui" && !!s.sessionHasHistory && !caps.midSessionModelSwitch;
222
-
223
- var list = s.currentModels.length > 0 ? s.currentModels : (s.currentModel ? [{ value: s.currentModel, displayName: s.currentModel }] : []);
224
- for (var i = 0; i < list.length; i++) {
225
- var item = list[i];
226
- // Support both object { value, displayName } and plain string formats
227
- var value = typeof item === "string" ? item : (item.value || "");
228
- var label = typeof item === "string" ? item : (item.displayName || value);
229
- var btn = document.createElement("button");
230
- btn.className = "config-radio-item";
231
- if (value === s.currentModel) btn.classList.add("active");
232
- if (lockedForGui) btn.classList.add("locked");
233
- btn.dataset.model = value;
234
- btn.textContent = label;
235
- if (lockedForGui) {
236
- btn.disabled = true;
237
- btn.title = "This vendor binds the model when the session starts. Start a new session to change it.";
238
- } else {
239
- btn.addEventListener("click", function () {
240
- var model = this.dataset.model;
241
- var ws = getWs();
242
- if (ws && ws.readyState === 1) {
243
- ws.send(JSON.stringify({ type: "set_model", model: model }));
244
- }
245
- configPopover.classList.add("hidden");
246
- configChip.classList.remove("active");
247
- });
248
- }
249
- configModelList.appendChild(btn);
250
- }
251
-
252
- if (lockedForGui) {
253
- var hint = document.createElement("div");
254
- hint.className = "config-model-hint";
255
- hint.textContent = "Locked after first message. Start a new session to change models.";
256
- configModelList.appendChild(hint);
257
- }
258
- }
259
-
260
189
  function rebuildModeList() {
261
190
  if (!configModeList) return;
262
191
  configModeList.innerHTML = "";
@@ -422,7 +351,7 @@ export function initPanels() {
422
351
  configChip = $("config-chip");
423
352
  configChipLabel = $("config-chip-label");
424
353
  configPopover = $("config-popover");
425
- configModelList = $("config-model-list");
354
+ setupModelPicker();
426
355
  configModeList = $("config-mode-list");
427
356
  configEffortSection = $("config-effort-section");
428
357
  configEffortBar = $("config-effort-bar");
@@ -443,14 +372,24 @@ export function initPanels() {
443
372
  var vendorToggleWrap = $("vendor-toggle-wrap");
444
373
  var vendorBtnClaude = $("vendor-btn-claude");
445
374
  var vendorBtnCodex = $("vendor-btn-codex");
446
- var vendorBtnGemini = $("vendor-btn-gemini");
375
+ var vendorBtnAntigravity = $("vendor-btn-antigravity");
447
376
  var vendorBtnOpenCode = $("vendor-btn-opencode");
377
+ var vendorBtnKimi = $("vendor-btn-kimi");
378
+ var vendorBtnGrok = $("vendor-btn-grok");
379
+ var vendorBtnCopilot = $("vendor-btn-copilot");
380
+ var vendorBtnQwen = $("vendor-btn-qwen");
381
+ var vendorBtnJunie = $("vendor-btn-junie");
448
382
  var vendorBtnKiro = $("vendor-btn-kiro");
449
383
  var vendorBtns = {
450
384
  claude: vendorBtnClaude,
451
385
  codex: vendorBtnCodex,
452
- gemini: vendorBtnGemini,
386
+ antigravity: vendorBtnAntigravity,
453
387
  opencode: vendorBtnOpenCode,
388
+ kimi: vendorBtnKimi,
389
+ grok: vendorBtnGrok,
390
+ copilot: vendorBtnCopilot,
391
+ qwen: vendorBtnQwen,
392
+ junie: vendorBtnJunie,
454
393
  kiro: vendorBtnKiro,
455
394
  };
456
395
 
@@ -466,7 +405,10 @@ export function initPanels() {
466
405
  var isInstalled = installed.indexOf(v) !== -1;
467
406
  btn.classList.toggle("active", v === current);
468
407
  btn.classList.toggle("disabled", !isInstalled);
469
- btn.title = isInstalled ? (VENDOR_NAMES[v] || v) : (VENDOR_NAMES[v] || v) + " is not installed";
408
+ btn.classList.toggle("experimental", isExperimentalVendor(v));
409
+ var title = isInstalled ? (VENDOR_NAMES[v] || v) : (VENDOR_NAMES[v] || v) + " is not installed";
410
+ if (isExperimentalVendor(v)) title += " · Experimental integration; not yet validated through direct use testing";
411
+ btn.title = title;
470
412
  }
471
413
  }
472
414
 
@@ -474,15 +416,20 @@ export function initPanels() {
474
416
  if (vendor === (store.get('currentVendor') || "claude")) return;
475
417
  var installed = store.get('installedVendors') || [];
476
418
  if (installed.indexOf(vendor) === -1) return;
477
- store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorSelectionLocked: true });
419
+ store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorCapabilities: {}, vendorSelectionLocked: true, sessionVendorBound: true });
478
420
  var ws = getWs();
479
421
  if (ws) ws.send(JSON.stringify({ type: "set_vendor", vendor: vendor }));
480
422
  }
481
423
 
482
424
  if (vendorBtnClaude) vendorBtnClaude.addEventListener("click", function() { onVendorClick("claude"); });
483
425
  if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
484
- if (vendorBtnGemini) vendorBtnGemini.addEventListener("click", function() { onVendorClick("gemini"); });
426
+ if (vendorBtnAntigravity) vendorBtnAntigravity.addEventListener("click", function() { onVendorClick("antigravity"); });
485
427
  if (vendorBtnOpenCode) vendorBtnOpenCode.addEventListener("click", function() { onVendorClick("opencode"); });
428
+ if (vendorBtnKimi) vendorBtnKimi.addEventListener("click", function() { onVendorClick("kimi"); });
429
+ if (vendorBtnGrok) vendorBtnGrok.addEventListener("click", function() { onVendorClick("grok"); });
430
+ if (vendorBtnCopilot) vendorBtnCopilot.addEventListener("click", function() { onVendorClick("copilot"); });
431
+ if (vendorBtnQwen) vendorBtnQwen.addEventListener("click", function() { onVendorClick("qwen"); });
432
+ if (vendorBtnJunie) vendorBtnJunie.addEventListener("click", function() { onVendorClick("junie"); });
486
433
  if (vendorBtnKiro) vendorBtnKiro.addEventListener("click", function() { onVendorClick("kiro"); });
487
434
 
488
435
  // --- Reactive UI sync ---
@@ -510,8 +457,7 @@ export function initPanels() {
510
457
 
511
458
  // Vendor changed -> switch model list and current model to match
512
459
  if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
513
- var ws = getWs();
514
- if (ws) ws.send(JSON.stringify({ type: "get_vendor_models", vendor: state.currentVendor }));
460
+ requestVendorModels(state.currentVendor, true);
515
461
  }
516
462
 
517
463
  // config chip
@@ -521,6 +467,12 @@ export function initPanels() {
521
467
  state.currentBetas !== prev.currentBetas ||
522
468
  state.currentThinking !== prev.currentThinking ||
523
469
  state.currentVendor !== prev.currentVendor ||
470
+ state.currentModels !== prev.currentModels ||
471
+ state.vendorCapabilities !== prev.vendorCapabilities ||
472
+ state.modelListStatus !== prev.modelListStatus ||
473
+ state.modelListError !== prev.modelListError ||
474
+ state.modelSelectionPending !== prev.modelSelectionPending ||
475
+ state.modelSelectionError !== prev.modelSelectionError ||
524
476
  state.codexApproval !== prev.codexApproval ||
525
477
  state.codexSandbox !== prev.codexSandbox ||
526
478
  state.codexWebSearch !== prev.codexWebSearch ||
@@ -621,6 +573,7 @@ export function initPanels() {
621
573
  e.stopPropagation();
622
574
  var wasHidden = configPopover.classList.toggle("hidden");
623
575
  configChip.classList.toggle("active", !wasHidden);
576
+ if (!wasHidden) prepareModelPickerOpen();
624
577
  });
625
578
  }
626
579
 
@@ -674,7 +627,7 @@ export function updateConfigChip() {
674
627
  var s = store.snap();
675
628
  var vendor = s.currentVendor || "claude";
676
629
  configChipLabel.textContent = modelDisplayName(s.currentModel, s.currentModels);
677
- rebuildModelList();
630
+ renderModelPicker();
678
631
  rebuildModeList();
679
632
  rebuildEffortBar();
680
633
 
@@ -5,6 +5,7 @@ import { escapeHtml, showToast } from './utils.js';
5
5
  import { refreshIcons } from './icons.js';
6
6
  import { parseEmojis } from './markdown.js';
7
7
  import { store } from './store.js';
8
+ import { resetModelPickerState } from './model-picker.js';
8
9
  import { getWs, setWs } from './ws-ref.js';
9
10
  import { getMessagesEl, getStatusDot } from './dom-refs.js';
10
11
  import { userAvatarUrl } from './avatar.js';
@@ -322,7 +323,7 @@ export function renderProjectList() {
322
323
  isWorktree: p.isWorktree || false,
323
324
  parentSlug: p.parentSlug || null,
324
325
  branch: p.branch || null,
325
- worktreeAccessible: p.worktreeAccessible !== undefined ? p.worktreeAccessible : true,
326
+ worktreeExternal: p.worktreeExternal === true,
326
327
  };
327
328
  });
328
329
  var st = store.snap();
@@ -363,6 +364,7 @@ export function renderProjectList() {
363
364
  }
364
365
 
365
366
  export function resetClientState() {
367
+ resetModelPickerState();
366
368
  if (isSearchOpen()) closeSearch();
367
369
  getMessagesEl().innerHTML = "";
368
370
  store.set({ currentMsgEl: null });