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

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.
@@ -100,7 +100,8 @@ body.pane-mode #input-area {
100
100
  }
101
101
 
102
102
  .split-pane-title {
103
- flex: 1;
103
+ flex: 0 1 auto;
104
+ max-width: 46%;
104
105
  min-width: 0;
105
106
  overflow: hidden;
106
107
  text-overflow: ellipsis;
@@ -283,12 +284,17 @@ button.split-pair-role:hover { filter: brightness(1.25); }
283
284
  color: var(--text-dimmer);
284
285
  cursor: pointer;
285
286
  flex-shrink: 0;
287
+ margin-left: auto;
286
288
  }
287
289
 
288
290
  .split-pane-context.has-data {
289
291
  display: inline-flex;
290
292
  }
291
293
 
294
+ .split-pane-context:not(.has-data) + .split-pane-close {
295
+ margin-left: auto;
296
+ }
297
+
292
298
  .split-pane-context:hover {
293
299
  background: var(--bg-alt);
294
300
  color: var(--text);
@@ -0,0 +1,98 @@
1
+ #header-session-actions-btn {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ width: 28px;
6
+ height: 28px;
7
+ padding: 0;
8
+ border: 1px solid transparent;
9
+ border-radius: 8px;
10
+ background: transparent;
11
+ color: var(--text-dimmer);
12
+ cursor: pointer;
13
+ flex-shrink: 0;
14
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
15
+ }
16
+
17
+ #header-session-actions-btn:hover,
18
+ #header-session-actions-btn[aria-expanded="true"] {
19
+ color: var(--text);
20
+ background: rgba(var(--overlay-rgb), 0.05);
21
+ border-color: var(--border);
22
+ }
23
+
24
+ #header-session-actions-btn.hidden { display: none; }
25
+ #header-session-actions-btn .lucide { width: 16px; height: 16px; }
26
+
27
+ .session-actions-menu {
28
+ position: fixed;
29
+ z-index: 10020;
30
+ width: 286px;
31
+ padding: 6px;
32
+ border: 1px solid var(--border);
33
+ border-radius: 12px;
34
+ background: color-mix(in srgb, var(--bg) 96%, transparent);
35
+ box-shadow: 0 14px 38px rgba(var(--shadow-rgb), 0.32), 0 2px 8px rgba(var(--shadow-rgb), 0.2);
36
+ backdrop-filter: blur(18px);
37
+ }
38
+
39
+ .session-actions-row {
40
+ width: 100%;
41
+ border: 0;
42
+ background: transparent;
43
+ color: var(--text);
44
+ font: inherit;
45
+ text-align: left;
46
+ cursor: pointer;
47
+ }
48
+
49
+ .session-actions-row {
50
+ display: grid;
51
+ grid-template-columns: 30px minmax(0, 1fr) 16px;
52
+ align-items: center;
53
+ gap: 8px;
54
+ min-height: 54px;
55
+ padding: 7px 8px;
56
+ border-radius: 8px;
57
+ }
58
+
59
+ .session-actions-row:hover { background: var(--bg-alt); }
60
+ .session-actions-row:disabled { cursor: default; opacity: 0.42; }
61
+ .session-actions-row:disabled:hover { background: transparent; }
62
+
63
+ .session-actions-row-icon {
64
+ display: grid;
65
+ place-items: center;
66
+ width: 28px;
67
+ height: 28px;
68
+ border: 1px solid var(--border-subtle);
69
+ border-radius: 8px;
70
+ background: rgba(var(--overlay-rgb), 0.035);
71
+ color: var(--accent2);
72
+ }
73
+
74
+ .session-actions-row-icon .lucide { width: 15px; height: 15px; }
75
+ .session-actions-row-copy { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
76
+ .session-actions-row-copy strong { font-size: 12px; font-weight: 750; }
77
+ .session-actions-row-copy > span { color: var(--text-dimmer); font-size: 10px; line-height: 1.25; }
78
+ .session-actions-row-chevron { color: var(--text-dimmer); }
79
+ .session-actions-row-chevron .lucide { width: 13px; height: 13px; }
80
+
81
+ .handoff-modal { width: min(420px, calc(100vw - 32px)); }
82
+ .handoff-modal-desc { margin: -10px 0 14px; color: var(--text-dimmer); font-size: 12px; line-height: 1.45; }
83
+ .handoff-modal-source {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 10px;
87
+ margin-bottom: 14px;
88
+ padding: 10px;
89
+ border: 1px solid var(--border-subtle);
90
+ border-radius: 9px;
91
+ background: var(--bg-alt);
92
+ }
93
+ .handoff-modal-source > img { width: 26px; height: 26px; object-fit: contain; border-radius: 6px; }
94
+ .handoff-modal-source > span { display: grid; min-width: 0; gap: 1px; }
95
+ .handoff-modal-source strong { overflow: hidden; color: var(--text); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
96
+ .handoff-modal-source span span { color: var(--text-dimmer); font-size: 10px; }
97
+ .handoff-modal-field + .handoff-modal-field { margin-top: 10px; }
98
+ .handoff-modal-field .wt-modal-label { margin-top: 0; }
@@ -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=20260824f">
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 */
@@ -388,12 +388,8 @@
388
388
  <div id="ralph-sticky" class="hidden"></div>
389
389
  <div id="debate-sticky" class="hidden"></div>
390
390
  <div class="status">
391
- <button id="header-add-worker-btn" type="button" aria-label="Add AI Worker" title="Add an AI Worker — this session becomes the Driver of a pair">
392
- <span class="header-worker-icon" aria-hidden="true">
393
- <i data-lucide="bot"></i>
394
- <span class="header-worker-add-badge"><i data-lucide="plus"></i></span>
395
- </span>
396
- <span class="header-worker-label">Add Worker</span>
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>
397
393
  </button>
398
394
  <button type="button" id="header-tui-font-btn" class="header-tui-font-btn hidden" title="Terminal font" aria-label="Terminal font">
399
395
  <i data-lucide="type"></i>
@@ -2364,7 +2360,7 @@
2364
2360
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2365
2361
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2366
2362
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2367
- <script type="module" src="app.js?v=20260824g"></script>
2363
+ <script type="module" src="app.js?v=20260826a"></script>
2368
2364
  <div id="pwa-install-modal" class="pwa-modal hidden">
2369
2365
  <div class="pwa-modal-backdrop"></div>
2370
2366
  <div class="pwa-modal-card">
@@ -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() {
@@ -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';
@@ -53,6 +54,7 @@ import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWh
53
54
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
54
55
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
55
56
  import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
57
+ import { getModelInfoUpdate, modelEntryMatches, handleModelSelectionResult } from './model-picker.js';
56
58
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
57
59
  import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
58
60
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
@@ -431,12 +433,12 @@ export function processMessage(msg) {
431
433
  // currentModels with the wrong vendor's list and trigger app-panels
432
434
  // to request models for the "wrong" vendor, which feeds back into a
433
435
  // ping-pong loop of vendor flapping. See issue #336.
434
- var _curV = store.get('currentVendor');
435
- if (msg.vendor && _curV && msg.vendor !== _curV) break;
436
+ var _modelLoadUpdate = getModelInfoUpdate(msg);
437
+ if (!_modelLoadUpdate) break;
436
438
 
437
439
  var _modelVal = msg.model;
438
440
  if (_modelVal && typeof _modelVal === "object") _modelVal = _modelVal.value || _modelVal.displayName || "";
439
- var _miUpdate = { currentModels: msg.models || [] };
441
+ var _miUpdate = Object.assign({ currentModels: msg.models || [] }, _modelLoadUpdate);
440
442
  if (Object.prototype.hasOwnProperty.call(msg, "model")) {
441
443
  // Keep the user's existing selection only if it actually belongs to
442
444
  // this vendor's model list. Otherwise (e.g. switching to a Codex
@@ -445,8 +447,7 @@ export function processMessage(msg) {
445
447
  // model.
446
448
  var _curModel = store.get('currentModel');
447
449
  var _curInList = !!_curModel && (msg.models || []).some(function (m) {
448
- var v = typeof m === "string" ? m : (m && m.value);
449
- return v === _curModel;
450
+ return modelEntryMatches(m, _curModel);
450
451
  });
451
452
  if (store.get('vendorSelectionLocked') && _curInList) {
452
453
  // Keep the user's existing (valid) selection; only update models list
@@ -463,6 +464,10 @@ export function processMessage(msg) {
463
464
  break;
464
465
  }
465
466
 
467
+ case "model_selection_result":
468
+ handleModelSelectionResult(msg);
469
+ break;
470
+
466
471
  case "config_state": {
467
472
  var _cs = {};
468
473
  if (msg.model) _cs.currentModel = msg.model;
@@ -597,6 +602,13 @@ export function processMessage(msg) {
597
602
  if (createdPairGroup) openGroup(createdPairGroup);
598
603
  break;
599
604
 
605
+ case "session_handoff_result":
606
+ case "handoff_session_options":
607
+ case "handoff_context":
608
+ case "handoff_created":
609
+ handleSessionActionMessage(msg);
610
+ break;
611
+
600
612
  case "worker_proposal":
601
613
  renderWorkerProposal(msg);
602
614
  break;
@@ -7,6 +7,7 @@ import { store } from './store.js';
7
7
  import { getWs } from './ws-ref.js';
8
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;
@@ -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");
@@ -487,7 +416,7 @@ export function initPanels() {
487
416
  if (vendor === (store.get('currentVendor') || "claude")) return;
488
417
  var installed = store.get('installedVendors') || [];
489
418
  if (installed.indexOf(vendor) === -1) return;
490
- store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorSelectionLocked: true, sessionVendorBound: true });
419
+ store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorCapabilities: {}, vendorSelectionLocked: true, sessionVendorBound: true });
491
420
  var ws = getWs();
492
421
  if (ws) ws.send(JSON.stringify({ type: "set_vendor", vendor: vendor }));
493
422
  }
@@ -528,8 +457,7 @@ export function initPanels() {
528
457
 
529
458
  // Vendor changed -> switch model list and current model to match
530
459
  if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
531
- var ws = getWs();
532
- if (ws) ws.send(JSON.stringify({ type: "get_vendor_models", vendor: state.currentVendor }));
460
+ requestVendorModels(state.currentVendor, true);
533
461
  }
534
462
 
535
463
  // config chip
@@ -539,6 +467,12 @@ export function initPanels() {
539
467
  state.currentBetas !== prev.currentBetas ||
540
468
  state.currentThinking !== prev.currentThinking ||
541
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 ||
542
476
  state.codexApproval !== prev.codexApproval ||
543
477
  state.codexSandbox !== prev.codexSandbox ||
544
478
  state.codexWebSearch !== prev.codexWebSearch ||
@@ -639,6 +573,7 @@ export function initPanels() {
639
573
  e.stopPropagation();
640
574
  var wasHidden = configPopover.classList.toggle("hidden");
641
575
  configChip.classList.toggle("active", !wasHidden);
576
+ if (!wasHidden) prepareModelPickerOpen();
642
577
  });
643
578
  }
644
579
 
@@ -692,7 +627,7 @@ export function updateConfigChip() {
692
627
  var s = store.snap();
693
628
  var vendor = s.currentVendor || "claude";
694
629
  configChipLabel.textContent = modelDisplayName(s.currentModel, s.currentModels);
695
- rebuildModelList();
630
+ renderModelPicker();
696
631
  rebuildModeList();
697
632
  rebuildEffortBar();
698
633
 
@@ -879,9 +814,7 @@ export function updateContextPanel() {
879
814
  hCtxEl = document.createElement("div");
880
815
  hCtxEl.className = "header-context";
881
816
  hCtxEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
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);
817
+ statusArea.insertBefore(hCtxEl, statusArea.firstChild);
885
818
  hCtxEl.addEventListener("mouseenter", function() {
886
819
  if (store.get('richContextUsage')) {
887
820
  showCtxPopover();
@@ -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';
@@ -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 });