clay-server 3.4.0-beta.5 → 3.4.0-beta.7

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,6 +22,17 @@ function dispatchMessageSafely(handleMessage, sendTo, slug, ws, msg) {
22
22
  }
23
23
  }
24
24
 
25
+ function buildInitialModelInfo(sm, initialVendor, initialModel, initialModels) {
26
+ return {
27
+ type: "model_info",
28
+ model: initialModel || "",
29
+ models: initialModels || [],
30
+ vendor: initialVendor,
31
+ availableVendors: sm.availableVendors || [],
32
+ installedVendors: sm.installedVendors || [],
33
+ };
34
+ }
35
+
25
36
  /**
26
37
  * Attach connection/disconnection handlers to a project context.
27
38
  *
@@ -167,9 +178,9 @@ function attachConnection(ctx) {
167
178
  sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
168
179
  }
169
180
  var initialModel = (restoredActive && restoredActive.model) || sm.currentModel || "";
170
- if (initialModel) {
171
- sendTo(ws, { type: "model_info", model: initialModel, models: initialModels, vendor: initialVendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
172
- }
181
+ // Vendor installation state is needed even before a new project has a
182
+ // model. Always send it so reconnects cannot fall back to "Not installed."
183
+ sendTo(ws, buildInitialModelInfo(sm, initialVendor, initialModel, initialModels));
173
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 });
174
185
  sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
175
186
  sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
@@ -333,4 +344,8 @@ function attachConnection(ctx) {
333
344
  };
334
345
  }
335
346
 
336
- module.exports = { attachConnection: attachConnection, dispatchMessageSafely: dispatchMessageSafely };
347
+ module.exports = {
348
+ attachConnection: attachConnection,
349
+ buildInitialModelInfo: buildInitialModelInfo,
350
+ dispatchMessageSafely: dispatchMessageSafely,
351
+ };
package/lib/public/app.js CHANGED
@@ -295,6 +295,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
295
295
  slashCommands: [],
296
296
  processing: false,
297
297
  activeSessionId: null,
298
+ sessionVendorBound: false,
298
299
  cliSessionId: null,
299
300
  isHeadlessMode: false,
300
301
  savedMainSlug: null,
@@ -52,6 +52,7 @@ import { setStatus } from './app-connection.js';
52
52
  import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
53
53
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
54
54
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
55
+ import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
55
56
  import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
56
57
  import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
57
58
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
@@ -414,6 +415,15 @@ export function processMessage(msg) {
414
415
  break;
415
416
 
416
417
  case "model_info": {
418
+ // Installation state applies across vendors. Keep it even when the
419
+ // model payload itself is stale for the active session.
420
+ var _vendorState = {};
421
+ if (msg.availableVendors) _vendorState.availableVendors = msg.availableVendors;
422
+ if (msg.installedVendors) _vendorState.installedVendors = msg.installedVendors;
423
+ if (Object.keys(_vendorState).length > 0) {
424
+ store.set(_vendorState);
425
+ selectDefaultVendorForBlankSession();
426
+ }
417
427
  // Drop stale model_info from a vendor that doesn't match the active
418
428
  // session's vendor. On high-latency connections, the server's default-
419
429
  // adapter model_info can arrive after session_switched has already
@@ -447,8 +457,6 @@ export function processMessage(msg) {
447
457
  _miUpdate.currentModel = store.get('currentModel');
448
458
  }
449
459
  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
460
  if (msg.capabilities) _miUpdate.vendorCapabilities = msg.capabilities;
453
461
  store.set(_miUpdate);
454
462
  updateSettingsModels(_modelVal, msg.models || []);
@@ -661,7 +669,7 @@ export function processMessage(msg) {
661
669
  var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
662
670
  ? msg.runtimeTerminalId
663
671
  : (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') });
672
+ store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || store.get('currentModel'), currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionVendorBound: !!msg.vendor || !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode') });
665
673
  // TUI sessions swap the chat UI for an embedded xterm running
666
674
  // `claude` inside a real PTY. Mount or tear down before the rest of
667
675
  // the chat-side bookkeeping runs so we don't waste work on hidden DOM.
@@ -706,7 +714,7 @@ export function processMessage(msg) {
706
714
  }
707
715
  }
708
716
  if (!msg.hasHistory && !msg.vendor) {
709
- // Preserve explicit pre-message vendor choice on brand-new sessions.
717
+ selectDefaultVendorForBlankSession();
710
718
  }
711
719
  // Vendor toggle visibility + active-vendor indicator next to the
712
720
  // model chip.
@@ -474,7 +474,7 @@ export function initPanels() {
474
474
  if (vendor === (store.get('currentVendor') || "claude")) return;
475
475
  var installed = store.get('installedVendors') || [];
476
476
  if (installed.indexOf(vendor) === -1) return;
477
- store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorSelectionLocked: true });
477
+ store.set({ currentVendor: vendor, currentModel: "", currentModels: [], vendorSelectionLocked: true, sessionVendorBound: true });
478
478
  var ws = getWs();
479
479
  if (ws) ws.send(JSON.stringify({ type: "set_vendor", vendor: vendor }));
480
480
  }
@@ -13,6 +13,7 @@ import { showImageModal, showPasteModal } from './app-misc.js';
13
13
  import { sendMessage, hasSendableContent } from './input.js';
14
14
  import { getChatLayout } from './theme.js';
15
15
  import { getScheduledMsgEl } from './app-rate-limit.js';
16
+ export { VENDOR_ORDER } from './vendor-priority.js';
16
17
 
17
18
  // Keep these client constants aligned with lib/yoke/vendor-registry.js. The
18
19
  // browser cannot import the server's CommonJS registry directly.
@@ -33,7 +34,6 @@ export var VENDOR_NAMES = {
33
34
  // Display order for every vendor Clay knows about, installed or not. Pickers
34
35
  // render the full list so a missing CLI reads as "not installed yet" rather
35
36
  // than "Clay doesn't support it".
36
- export var VENDOR_ORDER = ["claude", "codex", "antigravity", "opencode", "kiro"];
37
37
  // Where to send the user when they pick a vendor whose CLI isn't installed.
38
38
  export var VENDOR_HOMEPAGES = {
39
39
  claude: "https://claude.com/product/claude-code",
@@ -299,13 +299,10 @@ function appendSessionCloseButton(el, session) {
299
299
  el.appendChild(closeBtn);
300
300
  }
301
301
 
302
- // Resolve the vendor the "New session" button should launch: the project's
303
- // last-used vendor when its CLI is still installed, else the first installed
304
- // vendor, else Claude (so the button always has a sane label to render).
302
+ // Resolve the vendor the "New session" button should launch from the stable
303
+ // installed-vendor priority (Claude, then Codex, then the remaining vendors).
305
304
  export function resolveDefaultVendor() {
306
305
  var installed = store.get('installedVendors') || [];
307
- var last = store.get('lastVendor') || "";
308
- if (last && installed.indexOf(last) !== -1) return last;
309
306
  for (var i = 0; i < VENDOR_ORDER.length; i++) {
310
307
  if (installed.indexOf(VENDOR_ORDER[i]) !== -1) return VENDOR_ORDER[i];
311
308
  }
@@ -330,6 +327,7 @@ export function startNewSession(vendor, extra) {
330
327
  currentModel: "",
331
328
  currentModels: [],
332
329
  vendorSelectionLocked: true,
330
+ sessionVendorBound: true,
333
331
  });
334
332
  }
335
333
 
@@ -0,0 +1,9 @@
1
+ export var VENDOR_ORDER = ["claude", "codex", "antigravity", "opencode", "kiro"];
2
+
3
+ export function firstInstalledVendor(installedVendors) {
4
+ var installed = Array.isArray(installedVendors) ? installedVendors : [];
5
+ for (var i = 0; i < VENDOR_ORDER.length; i++) {
6
+ if (installed.indexOf(VENDOR_ORDER[i]) !== -1) return VENDOR_ORDER[i];
7
+ }
8
+ return "";
9
+ }
@@ -0,0 +1,20 @@
1
+ import { store } from './store.js';
2
+ import { firstInstalledVendor } from './vendor-priority.js';
3
+
4
+ export function selectDefaultVendorForBlankSession() {
5
+ var state = store.snap();
6
+ if (!state.activeSessionId || state.sessionHasHistory || state.sessionVendorBound || state.dmMode) return "";
7
+ var vendor = firstInstalledVendor(state.installedVendors);
8
+ if (!vendor) return "";
9
+ if (vendor === state.currentVendor) {
10
+ if (state.vendorSelectionLocked) store.set({ vendorSelectionLocked: false });
11
+ return vendor;
12
+ }
13
+ store.set({
14
+ currentVendor: vendor,
15
+ currentModel: "",
16
+ currentModels: [],
17
+ vendorSelectionLocked: false,
18
+ });
19
+ return vendor;
20
+ }
package/lib/sdk-bridge.js CHANGED
@@ -1859,6 +1859,13 @@ function createSDKBridge(opts) {
1859
1859
  var defaultVendor = adapter ? adapter.vendor : "claude";
1860
1860
  sm.defaultVendor = defaultVendor;
1861
1861
 
1862
+ // Installation detection is independent of adapter initialization. Publish
1863
+ // it before warmup so a new project does not render the initial empty state
1864
+ // as if every CLI were missing while the default adapter starts up.
1865
+ sm.installedVendors = detectInstalledVendors(linuxUser);
1866
+ sm.availableVendors = getAvailableVendors(linuxUser);
1867
+ sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1868
+
1862
1869
  // Initialize default adapter first (provides skills, slash commands, etc.)
1863
1870
  if (adapter) {
1864
1871
  try {
@@ -1910,19 +1917,8 @@ function createSDKBridge(opts) {
1910
1917
  // actually issues a query with it.
1911
1918
  sm.modelsByVendor = sm.modelsByVendor || {};
1912
1919
 
1913
- // Detect installed vendors per-user (binary existence check)
1914
- sm.installedVendors = detectInstalledVendors(linuxUser);
1915
- sm.availableVendors = getAvailableVendors(linuxUser);
1916
-
1917
- // Send initial state to client
1918
- send({
1919
- type: "model_info",
1920
- model: sm.currentModel || "",
1921
- models: getModelsForVendor(defaultVendor),
1922
- vendor: defaultVendor,
1923
- availableVendors: sm.availableVendors,
1924
- installedVendors: sm.installedVendors,
1925
- });
1920
+ // Send the fully initialized state to the client.
1921
+ sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1926
1922
  }
1927
1923
 
1928
1924
  async function setModel(session, model) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.4.0-beta.5",
3
+ "version": "3.4.0-beta.7",
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",