clay-server 3.4.0-beta.18 → 3.4.0-beta.19

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.
@@ -22,12 +22,23 @@ function dispatchMessageSafely(handleMessage, sendTo, slug, ws, msg) {
22
22
  }
23
23
  }
24
24
 
25
- function buildInitialModelInfo(sm, initialVendor, initialModel, initialModels) {
25
+ function buildInitialModelInfo(sm, initialVendor, initialModel, initialModels, sessionId) {
26
+ var model = initialModel || "";
27
+ var models = initialModels || [];
28
+ if (model && models.length > 0) {
29
+ var found = false;
30
+ for (var i = 0; i < models.length; i++) {
31
+ var entry = models[i];
32
+ if (typeof entry === "string" ? entry === model : (entry.value === model || entry.id === model || entry.resolvedModel === model)) found = true;
33
+ }
34
+ if (!found) model = typeof models[0] === "string" ? models[0] : (models[0].value || models[0].id || "");
35
+ }
26
36
  return {
27
37
  type: "model_info",
28
- model: initialModel || "",
29
- models: initialModels || [],
38
+ model: model,
39
+ models: models,
30
40
  vendor: initialVendor,
41
+ sessionId: sessionId || null,
31
42
  availableVendors: sm.availableVendors || [],
32
43
  installedVendors: sm.installedVendors || [],
33
44
  };
@@ -177,11 +188,12 @@ function attachConnection(ctx) {
177
188
  if (sm.slashCommands) {
178
189
  sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
179
190
  }
180
- var initialModel = (restoredActive && restoredActive.model) || sm.currentModel || "";
191
+ var initialModel = (restoredActive && restoredActive.model) || ((sm.defaultModelByVendor || {})[initialVendor]) || "";
181
192
  // Vendor installation state is needed even before a new project has a
182
193
  // model. Always send it so reconnects cannot fall back to "Not installed."
183
- sendTo(ws, buildInitialModelInfo(sm, initialVendor, initialModel, initialModels));
184
- sendTo(ws, { type: "config_state", model: initialModel, mode: sm.currentPermissionMode || "default", effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
194
+ var initialModelInfo = buildInitialModelInfo(sm, initialVendor, initialModel, initialModels, restoredActive ? restoredActive.localId : null);
195
+ sendTo(ws, initialModelInfo);
196
+ sendTo(ws, { type: "config_state", model: initialModelInfo.model, vendor: initialVendor, sessionId: restoredActive ? restoredActive.localId : null, mode: sm.currentPermissionMode || "default", effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
185
197
  sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
186
198
  sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
187
199
  sendTo(ws, { type: "term_list", terminals: tm.list() });
@@ -30,6 +30,7 @@ function attachModels(ctx) {
30
30
  var requestId = msg.requestId || null;
31
31
  var loadError = null;
32
32
  var vendorAdapter = null;
33
+ var session = getSessionForWs(ws);
33
34
 
34
35
  if (!vendor) {
35
36
  sendTo(ws, {
@@ -40,6 +41,7 @@ function attachModels(ctx) {
40
41
  error: "No vendor was selected.",
41
42
  model: "",
42
43
  models: [],
44
+ sessionId: session ? session.localId : null,
43
45
  });
44
46
  return;
45
47
  }
@@ -109,9 +111,10 @@ function attachModels(ctx) {
109
111
 
110
112
  var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
111
113
  var modelToSend = modelEntryValue(vendorModels[0]);
112
- if (sm.currentModel) {
114
+ var preferredModel = (session && session.vendor === vendor && session.model) || (sm.defaultModelByVendor && sm.defaultModelByVendor[vendor]) || "";
115
+ if (preferredModel) {
113
116
  for (var i = 0; i < vendorModels.length; i++) {
114
- if (modelEntryMatches(vendorModels[i], sm.currentModel)) {
117
+ if (modelEntryMatches(vendorModels[i], preferredModel)) {
115
118
  modelToSend = modelEntryValue(vendorModels[i]);
116
119
  break;
117
120
  }
@@ -131,6 +134,7 @@ function attachModels(ctx) {
131
134
  model: modelToSend,
132
135
  models: vendorModels,
133
136
  vendor: vendor,
137
+ sessionId: session ? session.localId : null,
134
138
  capabilities: (sm.capabilitiesByVendor && sm.capabilitiesByVendor[vendor]) || {},
135
139
  availableVendors: sm.availableVendors || [],
136
140
  installedVendors: sm.installedVendors || [],
@@ -658,10 +658,6 @@ function attachSessions(ctx) {
658
658
  // (resume_tui_session), and born-GUI never auto-converts to a terminal.
659
659
  resolveSessionForView(xmTarget, ws);
660
660
  }
661
- // If the target session's vendor doesn't own the currently cached
662
- // model, clear sm.currentModel so the UI and next query don't leak
663
- // the previous session's vendor-specific model into this one. A
664
- // session-specific model always wins and survives daemon restarts.
665
661
  var switchTargetSess = sm.sessions.get(msg.id);
666
662
  var switchTargetVendor = switchTargetSess && (switchTargetSess.vendor || sm.defaultVendor || "claude");
667
663
  if (switchTargetSess && !switchTargetSess.effort) {
@@ -671,22 +667,6 @@ function attachSessions(ctx) {
671
667
  ) || null;
672
668
  sm.saveSessionFile(switchTargetSess);
673
669
  }
674
- if (switchTargetSess && switchTargetSess.model) {
675
- sm.currentModel = switchTargetSess.model;
676
- } else if (switchTargetSess && sm.currentModel) {
677
- var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
678
- var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
679
- var found = false;
680
- var _curLc = sm.currentModel.toLowerCase();
681
- for (var tvi = 0; tvi < tvModels.length; tvi++) {
682
- var tvEntry = tvModels[tvi];
683
- var tvVal = typeof tvEntry === "string" ? tvEntry : (tvEntry && (tvEntry.value || tvEntry.id)) || "";
684
- if (tvVal === sm.currentModel || (tvVal && (tvVal.toLowerCase().indexOf(_curLc) !== -1 || _curLc.indexOf(tvVal.toLowerCase()) !== -1))) { found = true; break; }
685
- }
686
- if (tvModels.length > 0 && !found) {
687
- sm.currentModel = "";
688
- }
689
- }
690
670
  // Check access in multi-user mode
691
671
  if (usersModule.isMultiUser() && ws._clayUser) {
692
672
  var switchTarget = sm.sessions.get(msg.id);
@@ -989,11 +969,7 @@ function attachSessions(ctx) {
989
969
  " is bound to '" + vendorSession.vendor + "', refused rebind to '" + msg.vendor + "'");
990
970
  } else {
991
971
  vendorSession.vendor = msg.vendor;
992
- // Clear the shared model so the next query uses the vendor's default
993
- // instead of leaking the previous vendor's model into a fresh session.
994
- if (sm.currentModel) {
995
- sm.currentModel = "";
996
- }
972
+ if (vendorSession.vendor !== msg.vendor) vendorSession.model = "";
997
973
  sm.saveSessionFile(vendorSession);
998
974
  sm.broadcastSessionList();
999
975
  }
@@ -1004,11 +980,11 @@ function attachSessions(ctx) {
1004
980
  type: "model_info",
1005
981
  model: "",
1006
982
  models: vendorModels,
1007
- vendor: msg.vendor,
983
+ vendor: msg.vendor, sessionId: vendorSession ? vendorSession.localId : null,
1008
984
  availableVendors: sm.availableVendors || [],
1009
985
  installedVendors: sm.installedVendors || [],
1010
986
  });
1011
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
987
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1012
988
  }
1013
989
  return true;
1014
990
  }
@@ -1044,7 +1020,7 @@ function attachSessions(ctx) {
1044
1020
  sm.broadcastSessionList();
1045
1021
  sdk.setPermissionMode(session, msg.mode);
1046
1022
  }
1047
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1023
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1048
1024
  return true;
1049
1025
  }
1050
1026
 
@@ -1105,7 +1081,7 @@ function attachSessions(ctx) {
1105
1081
  if (session) {
1106
1082
  sdk.setPermissionMode(session, msg.mode);
1107
1083
  }
1108
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1084
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1109
1085
  return true;
1110
1086
  }
1111
1087
 
@@ -1118,7 +1094,7 @@ function attachSessions(ctx) {
1118
1094
  if (session) {
1119
1095
  sdk.setPermissionMode(session, msg.mode);
1120
1096
  }
1121
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1097
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1122
1098
  return true;
1123
1099
  }
1124
1100
 
@@ -1134,7 +1110,7 @@ function attachSessions(ctx) {
1134
1110
  sdk.setEffort(session, sessionEffort).catch(function (e) {
1135
1111
  sendTo(ws, { type: "error", text: "Failed to change reasoning effort: " + (e.message || e) });
1136
1112
  });
1137
- sm.sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode || "default", effort: sessionEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1113
+ sm.sendToSession(session, { type: "config_state", model: session.model || ((sm.defaultModelByVendor || {})[effortVendor]) || "", vendor: effortVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: sessionEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1138
1114
  }
1139
1115
  return true;
1140
1116
  }
@@ -1144,7 +1120,7 @@ function attachSessions(ctx) {
1144
1120
  opts.onSetServerDefaultEffort(msg.effort);
1145
1121
  }
1146
1122
  sm.currentEffort = msg.effort;
1147
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1123
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1148
1124
  return true;
1149
1125
  }
1150
1126
 
@@ -1153,20 +1129,20 @@ function attachSessions(ctx) {
1153
1129
  opts.onSetProjectDefaultEffort(slug, msg.effort);
1154
1130
  }
1155
1131
  sm.currentEffort = msg.effort;
1156
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1132
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1157
1133
  return true;
1158
1134
  }
1159
1135
 
1160
1136
  if (msg.type === "set_betas") {
1161
1137
  sm.currentBetas = msg.betas || [];
1162
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1138
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1163
1139
  return true;
1164
1140
  }
1165
1141
 
1166
1142
  if (msg.type === "set_thinking") {
1167
1143
  sm.currentThinking = msg.thinking || "adaptive";
1168
1144
  if (msg.budgetTokens) sm.currentThinkingBudget = msg.budgetTokens;
1169
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1145
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1170
1146
  return true;
1171
1147
  }
1172
1148
 
@@ -1448,7 +1424,7 @@ function attachSessions(ctx) {
1448
1424
  if (decision === "allow_accept_edits") {
1449
1425
  sdk.setPermissionMode(session, "acceptEdits");
1450
1426
  sm.currentPermissionMode = "acceptEdits";
1451
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1427
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1452
1428
  pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
1453
1429
  sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
1454
1430
  return true;
@@ -1477,7 +1453,7 @@ function attachSessions(ctx) {
1477
1453
 
1478
1454
  // Update permission mode for the new session
1479
1455
  sm.currentPermissionMode = "acceptEdits";
1480
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1456
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1481
1457
 
1482
1458
  // Build prompt from plan content (sent from client) or plan file path
1483
1459
  var clientPlanContent = msg.planContent || "";
@@ -40,7 +40,7 @@ function attachWorkerProposal(ctx) {
40
40
  function isEligible(session) {
41
41
  if (ctx.isMate || !session || session.mode === "tui") return false;
42
42
  if (store.groupForMember(session.localId)) return false;
43
- return isFableSession(session, sm.modelsByVendor, sm.currentModel);
43
+ return isFableSession(session, sm.modelsByVendor, (sm.defaultModelByVendor || {})[session.vendor || "claude"]);
44
44
  }
45
45
 
46
46
  function safeModelsByVendor(installed) {
@@ -112,7 +112,7 @@ function attachWorkerProposal(ctx) {
112
112
  }
113
113
  var models = options.modelsByVendor[vendor] || [];
114
114
  var model = modelIsAvailable(options, vendor, args.recommendedModel) ? (args.recommendedModel || "") : "";
115
- var currentModel = session.model || sm.currentModel || "";
115
+ var currentModel = session.model || (sm.defaultModelByVendor || {})[session.vendor || "claude"] || "";
116
116
  if (vendor === session.vendor && model === currentModel) model = "";
117
117
  if (!model && models.length > 0) {
118
118
  for (var j = 0; j < models.length; j++) {
package/lib/project.js CHANGED
@@ -472,10 +472,9 @@ function createProjectContext(opts) {
472
472
 
473
473
  var _projModel = typeof opts.onGetProjectDefaultModel === "function" ? opts.onGetProjectDefaultModel(slug) : null;
474
474
  var _srvModel = typeof opts.onGetServerDefaultModel === "function" ? opts.onGetServerDefaultModel() : null;
475
- sm._savedDefaultModel = (_projModel && _projModel.model) || (_srvModel && _srvModel.model) || null;
476
- // Immediately apply the saved default so config_state on connect reflects it
477
- // before the SDK has warmed up and fired system/init.
478
- if (sm._savedDefaultModel) sm.currentModel = sm._savedDefaultModel;
475
+ var savedDefaultModel = (_projModel && _projModel.model) || (_srvModel && _srvModel.model) || null;
476
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
477
+ if (savedDefaultModel) sm.defaultModelByVendor[sm.defaultVendor || "claude"] = savedDefaultModel;
479
478
 
480
479
  // --- Local MCP (direct process management for localhost clients) ---
481
480
  var _localMcp = createLocalMcp();
@@ -54,7 +54,7 @@ import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWh
54
54
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
55
55
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
56
56
  import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
57
- import { getModelInfoUpdate, modelEntryMatches, handleModelSelectionResult } from './model-picker.js';
57
+ import { getModelInfoUpdate, modelEntryValue, modelEntryMatches, handleModelSelectionResult, requestVendorModels } from './model-picker.js';
58
58
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
59
59
  import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
60
60
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
@@ -449,7 +449,13 @@ export function processMessage(msg) {
449
449
  var _curInList = !!_curModel && (msg.models || []).some(function (m) {
450
450
  return modelEntryMatches(m, _curModel);
451
451
  });
452
- if (store.get('vendorSelectionLocked') && _curInList) {
452
+ var _incomingInList = !_modelVal || (msg.models || []).length === 0 || (msg.models || []).some(function (m) {
453
+ return modelEntryMatches(m, _modelVal);
454
+ });
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) {
453
459
  // Keep the user's existing (valid) selection; only update models list
454
460
  } else {
455
461
  _miUpdate.currentModel = _modelVal || "";
@@ -469,8 +475,20 @@ export function processMessage(msg) {
469
475
  break;
470
476
 
471
477
  case "config_state": {
478
+ if ((msg.sessionId != null && msg.sessionId !== store.get('activeSessionId')) || (msg.vendor && msg.vendor !== store.get('currentVendor'))) break;
472
479
  var _cs = {};
473
- 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
+ }
474
492
  if (msg.mode) _cs.currentMode = msg.mode;
475
493
  if (msg.effort) _cs.currentEffort = msg.effort;
476
494
  if (msg.betas) _cs.currentBetas = msg.betas;
@@ -681,7 +699,7 @@ export function processMessage(msg) {
681
699
  var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
682
700
  ? msg.runtimeTerminalId
683
701
  : (typeof msg.terminalId === "number" ? msg.terminalId : null);
684
- 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') });
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') });
685
703
  // TUI sessions swap the chat UI for an embedded xterm running
686
704
  // `claude` inside a real PTY. Mount or tear down before the rest of
687
705
  // the chat-side bookkeeping runs so we don't waste work on hidden DOM.
@@ -728,6 +746,7 @@ export function processMessage(msg) {
728
746
  if (!msg.hasHistory && !msg.vendor) {
729
747
  selectDefaultVendorForBlankSession();
730
748
  }
749
+ if (!msg.model) requestVendorModels(store.get('currentVendor') || "claude", true);
731
750
  // Vendor toggle visibility + active-vendor indicator next to the
732
751
  // model chip.
733
752
  // - Session has an explicit vendor: hide the toggle (it's
@@ -112,6 +112,7 @@ export function getModelInfoUpdate(msg) {
112
112
  var vendor = msg.vendor || store.get('currentVendor') || "claude";
113
113
  var currentVendor = store.get('currentVendor');
114
114
  if (currentVendor && vendor !== currentVendor) return null;
115
+ if (msg.sessionId != null && msg.sessionId !== store.get('activeSessionId')) return null;
115
116
 
116
117
  var s = store.snap();
117
118
  if (msg.requestId && s.modelRequestId && msg.requestId !== s.modelRequestId) return null;
package/lib/sdk-bridge.js CHANGED
@@ -245,17 +245,24 @@ function createSDKBridge(opts) {
245
245
  return null;
246
246
  }
247
247
 
248
- function sendModelInfoForVendor(vendor, model) {
248
+ function defaultModelForVendor(vendor) {
249
+ return (sm.defaultModelByVendor && sm.defaultModelByVendor[vendor]) || "";
250
+ }
251
+
252
+ function sendModelInfoForVendor(session, vendor, model) {
249
253
  var resolvedVendor = vendor || (adapter && adapter.vendor) || "claude";
250
- send({
254
+ var message = {
251
255
  type: "model_info",
252
256
  model: model || "",
253
257
  models: getModelsForVendor(resolvedVendor),
254
258
  vendor: resolvedVendor,
259
+ sessionId: session ? session.localId : null,
255
260
  capabilities: (sm.capabilitiesByVendor && sm.capabilitiesByVendor[resolvedVendor]) || {},
256
261
  availableVendors: sm.availableVendors || [],
257
262
  installedVendors: sm.installedVendors || [],
258
- });
263
+ };
264
+ if (session) sendToSession(session, message);
265
+ else send(message);
259
266
  }
260
267
 
261
268
  function rememberAdapterReady(vendor, result) {
@@ -849,19 +856,20 @@ function createSDKBridge(opts) {
849
856
  case "model_changed":
850
857
  session.model = metaData.model;
851
858
  sm.saveSessionFile(session);
852
- sm.currentModel = metaData.model;
853
- sendModelInfoForVendor(session.vendor || (adapter && adapter.vendor) || "claude", metaData.model);
854
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
859
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
860
+ sm.defaultModelByVendor[session.vendor || "claude"] = metaData.model;
861
+ sendModelInfoForVendor(session, session.vendor || (adapter && adapter.vendor) || "claude", metaData.model);
862
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
855
863
  break;
856
864
  case "effort_changed":
857
865
  session.effort = metaData.effort;
858
866
  sm.saveSessionFile(session);
859
867
  sm.currentEffort = metaData.effort;
860
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort, betas: sm.currentBetas || [] });
868
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort, betas: sm.currentBetas || [] });
861
869
  break;
862
870
  case "permission_mode_changed":
863
871
  sm.currentPermissionMode = metaData.mode;
864
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
872
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
865
873
  break;
866
874
  case "worker_error":
867
875
  send({ type: "error", text: metaData.error });
@@ -1232,10 +1240,10 @@ function createSDKBridge(opts) {
1232
1240
  sm.modelsByVendor[vendor] = await vendorAdapter.supportedModels();
1233
1241
  }
1234
1242
  var vendorModels = getModelsForVendor(vendor);
1235
- var modelForVendor = resolveModelInList(vendorModels, sm.currentModel)
1243
+ var modelForVendor = resolveModelInList(vendorModels, defaultModelForVendor(vendor))
1236
1244
  || modelEntryValue(vendorModels[0])
1237
1245
  || "";
1238
- sendModelInfoForVendor(vendor, modelForVendor);
1246
+ sendModelInfoForVendor(session, vendor, modelForVendor);
1239
1247
  }
1240
1248
  return vendorAdapter;
1241
1249
  }
@@ -1438,14 +1446,9 @@ function createSDKBridge(opts) {
1438
1446
  }
1439
1447
  }
1440
1448
 
1441
- // Pick a model that belongs to the session's vendor. sm.currentModel is
1442
- // shared project-wide, so a Codex session that last set it to
1443
- // "gpt-5.4-mini" would otherwise leak into a Claude session in the same
1444
- // project (or in another session that switches vendor to claude) and
1445
- // Claude would reject the unknown model. We validate against the
1446
- // session vendor's model list regardless of which vendor happens to be
1447
- // the project's default adapter.
1448
- var queryModel = (ls.model && ls.model !== "default" ? ls.model : null) || session.model || sm.currentModel || undefined;
1449
+ // Pick a model from the session or its vendor-specific default, then
1450
+ // validate it against the session vendor's known model list.
1451
+ var queryModel = (ls.model && ls.model !== "default" ? ls.model : null) || session.model || defaultModelForVendor(session.vendor || "claude") || undefined;
1449
1452
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
1450
1453
  if (sessionVendor) {
1451
1454
  var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
@@ -1461,8 +1464,7 @@ function createSDKBridge(opts) {
1461
1464
  }
1462
1465
 
1463
1466
  // Bind the resolved model to the session so it survives a daemon
1464
- // restart. Without this, a session started under sm.currentModel (an
1465
- // in-memory value) came back after restart on the adapter default.
1467
+ // restart. This preserves a session-specific selection across restarts.
1466
1468
  if (queryModel && session.model !== queryModel) {
1467
1469
  session.model = queryModel;
1468
1470
  sm.saveSessionFile(session);
@@ -1874,7 +1876,7 @@ function createSDKBridge(opts) {
1874
1876
  // as if every CLI were missing while the default adapter starts up.
1875
1877
  sm.installedVendors = detectInstalledVendors(linuxUser);
1876
1878
  sm.availableVendors = getAvailableVendors(linuxUser);
1877
- sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1879
+ sendModelInfoForVendor(null, defaultVendor, defaultModelForVendor(defaultVendor));
1878
1880
 
1879
1881
  // Initialize default adapter first (provides skills, slash commands, etc.)
1880
1882
  if (adapter) {
@@ -1906,7 +1908,8 @@ function createSDKBridge(opts) {
1906
1908
  send({ type: "slash_commands", commands: combined, vendor: defaultVendor });
1907
1909
  }
1908
1910
  if (result.defaultModel) {
1909
- sm.currentModel = sm.currentModel || sm._savedDefaultModel || result.defaultModel;
1911
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
1912
+ sm.defaultModelByVendor[defaultVendor] = sm.defaultModelByVendor[defaultVendor] || result.defaultModel;
1910
1913
  }
1911
1914
  sm.availableModels = result.models || [];
1912
1915
  // Store per-vendor models and capabilities
@@ -1928,7 +1931,7 @@ function createSDKBridge(opts) {
1928
1931
  sm.modelsByVendor = sm.modelsByVendor || {};
1929
1932
 
1930
1933
  // Send the fully initialized state to the client.
1931
- sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1934
+ sendModelInfoForVendor(null, defaultVendor, defaultModelForVendor(defaultVendor));
1932
1935
  }
1933
1936
 
1934
1937
  async function setModel(session, model) {
@@ -1940,22 +1943,24 @@ function createSDKBridge(opts) {
1940
1943
  // No active query — just store the model for next startQuery
1941
1944
  session.model = model;
1942
1945
  sm.saveSessionFile(session);
1943
- sm.currentModel = model;
1946
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
1944
1947
  // Confirm against the session vendor so a non-default vendor does not
1945
1948
  // receive model metadata labeled as the project's default adapter.
1946
1949
  var pendingVendor = session.vendor || (adapter && adapter.vendor) || "claude";
1947
- sendModelInfoForVendor(pendingVendor, model);
1948
- sendToSession(session, { type: "config_state", model: sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1950
+ sm.defaultModelByVendor[pendingVendor] = model;
1951
+ sendModelInfoForVendor(session, pendingVendor, model);
1952
+ sendToSession(session, { type: "config_state", model: model, vendor: pendingVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1949
1953
  return { ok: true, model: model };
1950
1954
  }
1951
1955
  try {
1952
1956
  await session.queryInstance.setModel(model);
1953
1957
  session.model = model;
1954
1958
  sm.saveSessionFile(session);
1955
- sm.currentModel = model;
1959
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
1956
1960
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || "claude";
1957
- sendModelInfoForVendor(sessionVendor, model);
1958
- sendToSession(session, { type: "config_state", model: sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1961
+ sm.defaultModelByVendor[sessionVendor] = model;
1962
+ sendModelInfoForVendor(session, sessionVendor, model);
1963
+ sendToSession(session, { type: "config_state", model: model, vendor: sessionVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1959
1964
  return { ok: true, model: model };
1960
1965
  } catch (e) {
1961
1966
  send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
@@ -1988,14 +1993,14 @@ function createSDKBridge(opts) {
1988
1993
  if (!session.queryInstance) {
1989
1994
  // No active query — just store the mode for next startQuery
1990
1995
  sm.currentPermissionMode = mode;
1991
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1996
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1992
1997
  return;
1993
1998
  }
1994
1999
  try {
1995
2000
  // Route through QueryHandle (works for both in-process and worker paths)
1996
2001
  await session.queryInstance.setPermissionMode(mode);
1997
2002
  sm.currentPermissionMode = mode;
1998
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
2003
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1999
2004
  } catch (e) {
2000
2005
  sendToSession(session, { type: "error", text: "Failed to set permission mode: " + (e.message || e) });
2001
2006
  }
@@ -196,13 +196,20 @@ function attachMessageProcessor(ctx) {
196
196
  send({ type: "slash_commands", commands: sm.slashCommands });
197
197
  }
198
198
  if (parsed.model) {
199
- sm.currentModel = sm.currentModel || sm._savedDefaultModel || parsed.model;
200
199
  var initVendor = session.vendor || (adapter && adapter.vendor) || "claude";
201
- send({
200
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
201
+ sm.defaultModelByVendor[initVendor] = sm.defaultModelByVendor[initVendor] || parsed.model;
202
+ var initModels = getModelsForVendor(initVendor);
203
+ var initModel = session.model || sm.defaultModelByVendor[initVendor] || "";
204
+ if (initModels.length > 0 && !initModels.some(function(entry) {
205
+ return entry === initModel || (entry && (entry.value === initModel || entry.id === initModel || entry.resolvedModel === initModel));
206
+ })) initModel = typeof initModels[0] === "string" ? initModels[0] : (initModels[0].value || initModels[0].id || "");
207
+ sendToSession(session, {
202
208
  type: "model_info",
203
- model: sm.currentModel,
204
- models: getModelsForVendor(initVendor),
209
+ model: initModel,
210
+ models: initModels,
205
211
  vendor: initVendor,
212
+ sessionId: session.localId,
206
213
  availableVendors: sm.availableVendors || [],
207
214
  installedVendors: sm.installedVendors || [],
208
215
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.4.0-beta.18",
3
+ "version": "3.4.0-beta.19",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",