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.
@@ -0,0 +1,216 @@
1
+ var yoke = require("./yoke");
2
+
3
+ function modelEntryValue(entry) {
4
+ if (!entry) return "";
5
+ if (typeof entry === "string") return entry;
6
+ return entry.value || entry.id || "";
7
+ }
8
+
9
+ function modelEntryMatches(entry, value) {
10
+ if (!entry || !value) return false;
11
+ if (typeof entry === "string") return entry === value;
12
+ return entry.value === value || entry.id === value || entry.resolvedModel === value;
13
+ }
14
+
15
+ function attachModels(ctx) {
16
+ var cwd = ctx.cwd;
17
+ var slug = ctx.slug;
18
+ var sm = ctx.sm;
19
+ var sdk = ctx.sdk;
20
+ var adapters = ctx.adapters;
21
+ var sendTo = ctx.sendTo;
22
+ var getSessionForWs = ctx.getSessionForWs;
23
+ var getLinuxUserForWs = ctx.getLinuxUserForWs;
24
+ var serverPort = ctx.serverPort;
25
+ var serverTls = ctx.serverTls;
26
+ var serverAuthToken = ctx.serverAuthToken;
27
+
28
+ async function loadVendorModels(ws, msg) {
29
+ var vendor = msg.vendor || "";
30
+ var requestId = msg.requestId || null;
31
+ var loadError = null;
32
+ var vendorAdapter = null;
33
+
34
+ if (!vendor) {
35
+ sendTo(ws, {
36
+ type: "model_info",
37
+ vendor: vendor,
38
+ requestId: requestId,
39
+ modelStatus: "error",
40
+ error: "No vendor was selected.",
41
+ model: "",
42
+ models: [],
43
+ });
44
+ return;
45
+ }
46
+
47
+ try {
48
+ var modelLinuxUser = getLinuxUserForWs(ws);
49
+ vendorAdapter = adapters[vendor] || null;
50
+ if (!vendorAdapter) {
51
+ vendorAdapter = await yoke.lazyCreateAdapter(adapters, vendor, {
52
+ cwd: cwd,
53
+ linuxUser: modelLinuxUser || undefined,
54
+ clayPort: serverPort,
55
+ clayTls: serverTls,
56
+ clayAuthToken: serverAuthToken,
57
+ slug: slug,
58
+ });
59
+ }
60
+
61
+ var cachedModels = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
62
+ var cachedCapabilities = sm.capabilitiesByVendor && sm.capabilitiesByVendor[vendor];
63
+ var needsReadyMetadata = !cachedCapabilities || cachedModels.length === 0;
64
+ if (vendorAdapter && needsReadyMetadata && typeof vendorAdapter.init === "function") {
65
+ try {
66
+ var readyResult = await vendorAdapter.init({
67
+ cwd: cwd,
68
+ linuxUser: modelLinuxUser || undefined,
69
+ clayPort: serverPort,
70
+ clayTls: serverTls,
71
+ clayAuthToken: serverAuthToken,
72
+ slug: slug,
73
+ });
74
+ sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
75
+ sm.capabilitiesByVendor[vendor] = readyResult.capabilities || {};
76
+ sm.modelsByVendor = sm.modelsByVendor || {};
77
+ if (Array.isArray(readyResult.models) && readyResult.models.length > 0) {
78
+ sm.modelsByVendor[vendor] = readyResult.models;
79
+ }
80
+ } catch (e) {
81
+ loadError = e;
82
+ console.error("[project-models] " + vendor + " init failed:", e.message || e);
83
+ }
84
+ }
85
+
86
+ if (vendorAdapter) {
87
+ sm.availableVendors = Object.keys(adapters);
88
+ sm.modelsByVendor = sm.modelsByVendor || {};
89
+ var currentModels = sm.modelsByVendor[vendor] || [];
90
+ if (currentModels.length === 0 && typeof vendorAdapter.supportedModels === "function") {
91
+ try {
92
+ var supported = await vendorAdapter.supportedModels();
93
+ if (Array.isArray(supported) && supported.length > 0) {
94
+ sm.modelsByVendor[vendor] = supported;
95
+ loadError = null;
96
+ }
97
+ } catch (e) {
98
+ if (!loadError) loadError = e;
99
+ console.error("[project-models] " + vendor + " model listing failed:", e.message || e);
100
+ }
101
+ }
102
+ } else {
103
+ loadError = new Error("The vendor adapter is unavailable.");
104
+ }
105
+ } catch (e) {
106
+ loadError = e;
107
+ console.error("[project-models] model loading failed for " + vendor + ":", e.message || e);
108
+ }
109
+
110
+ var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
111
+ var modelToSend = modelEntryValue(vendorModels[0]);
112
+ if (sm.currentModel) {
113
+ for (var i = 0; i < vendorModels.length; i++) {
114
+ if (modelEntryMatches(vendorModels[i], sm.currentModel)) {
115
+ modelToSend = modelEntryValue(vendorModels[i]);
116
+ break;
117
+ }
118
+ }
119
+ }
120
+ var vendorInfo = yoke.getVendorInfo(vendor);
121
+ var displayName = vendorInfo ? vendorInfo.displayName : vendor;
122
+ var status = vendorModels.length > 0 ? "ready" : (loadError ? "error" : "empty");
123
+ var errorText = "";
124
+ if (status === "error") {
125
+ errorText = "Could not load " + displayName + " models: " + (loadError.message || loadError);
126
+ } else if (status === "empty") {
127
+ errorText = displayName + " returned no models. Check CLI authentication and retry.";
128
+ }
129
+ sendTo(ws, {
130
+ type: "model_info",
131
+ model: modelToSend,
132
+ models: vendorModels,
133
+ vendor: vendor,
134
+ capabilities: (sm.capabilitiesByVendor && sm.capabilitiesByVendor[vendor]) || {},
135
+ availableVendors: sm.availableVendors || [],
136
+ installedVendors: sm.installedVendors || [],
137
+ requestId: requestId,
138
+ modelStatus: status,
139
+ error: errorText,
140
+ });
141
+ }
142
+
143
+ async function selectModel(ws, msg) {
144
+ var session = getSessionForWs(ws);
145
+ if (!session) {
146
+ sendTo(ws, { type: "model_selection_result", requestId: msg.requestId || null, ok: false, error: "No active session." });
147
+ return;
148
+ }
149
+ var sessionVendor = session.vendor || sm.defaultVendor || "claude";
150
+ if (msg.vendor && msg.vendor !== sessionVendor) {
151
+ sendTo(ws, {
152
+ type: "model_selection_result",
153
+ requestId: msg.requestId || null,
154
+ ok: false,
155
+ error: "The session vendor changed before the model selection completed.",
156
+ });
157
+ return;
158
+ }
159
+ var knownModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
160
+ if (knownModels.length > 0) {
161
+ var known = false;
162
+ for (var i = 0; i < knownModels.length; i++) {
163
+ if (modelEntryMatches(knownModels[i], msg.model)) {
164
+ known = true;
165
+ break;
166
+ }
167
+ }
168
+ if (!known) {
169
+ sendTo(ws, {
170
+ type: "model_selection_result",
171
+ requestId: msg.requestId || null,
172
+ ok: false,
173
+ error: "That model is not available for the selected vendor.",
174
+ });
175
+ return;
176
+ }
177
+ }
178
+ var result = await sdk.setModel(session, msg.model);
179
+ sendTo(ws, {
180
+ type: "model_selection_result",
181
+ requestId: msg.requestId || null,
182
+ ok: !!(result && result.ok),
183
+ model: result && result.model ? result.model : msg.model,
184
+ vendor: sessionVendor,
185
+ error: result && result.error ? result.error : "",
186
+ });
187
+ }
188
+
189
+ function handleMessage(ws, msg) {
190
+ if (msg.type === "get_vendor_models") {
191
+ loadVendorModels(ws, msg).catch(function(e) {
192
+ console.error("[project-models] Unexpected model loading failure:", e.message || e);
193
+ });
194
+ return true;
195
+ }
196
+ if (msg.type === "set_model" && msg.model) {
197
+ selectModel(ws, msg).catch(function(e) {
198
+ sendTo(ws, { type: "model_selection_result", requestId: msg.requestId || null, ok: false, error: e.message || String(e) });
199
+ });
200
+ return true;
201
+ }
202
+ return false;
203
+ }
204
+
205
+ return {
206
+ handleMessage: handleMessage,
207
+ loadVendorModels: loadVendorModels,
208
+ selectModel: selectModel,
209
+ };
210
+ }
211
+
212
+ module.exports = {
213
+ attachModels: attachModels,
214
+ modelEntryMatches: modelEntryMatches,
215
+ modelEntryValue: modelEntryValue,
216
+ };
@@ -0,0 +1,162 @@
1
+ var yoke = require("./yoke");
2
+ var contextBuilder = require("./session-handoff-context");
3
+
4
+ function hasUserContext(session) {
5
+ var history = (session && session.history) || [];
6
+ for (var i = 0; i < history.length; i++) {
7
+ if (history[i] && history[i].type === "user_message" && history[i].text) return true;
8
+ if (history[i] && history[i].type === "handoff_context" && history[i].request) return true;
9
+ }
10
+ return false;
11
+ }
12
+
13
+ function attachSessionHandoff(ctx) {
14
+ var sm = ctx.sm;
15
+
16
+ function sendResult(ws, ok, details) {
17
+ ctx.sendTo(ws, Object.assign({ type: "session_handoff_result", ok: ok }, details || {}));
18
+ }
19
+
20
+ function validate(ws, source, targetVendor) {
21
+ if (ctx.isMate) throw new Error("Session handoff is only available in projects.");
22
+ if (!source) throw new Error("No active session was found.");
23
+ if (ctx.splitStore && ctx.splitStore.groupForMember(source.localId)) {
24
+ throw new Error("Open the session by itself before continuing it in another agent.");
25
+ }
26
+ if (source.isProcessing || source._queryStarting) {
27
+ throw new Error("Wait for the current response to finish before continuing in another agent.");
28
+ }
29
+ if (!hasUserContext(source)) throw new Error("This session does not have any conversation to continue.");
30
+ if (!targetVendor) throw new Error("Choose an agent for the handoff.");
31
+ if (!ctx.adapters[targetVendor] || (sm.installedVendors || []).indexOf(targetVendor) === -1) {
32
+ throw new Error("The selected coding agent is not installed.");
33
+ }
34
+ var linuxUser = ctx.getLinuxUserForSession(source);
35
+ var vendorInfo = yoke.getVendorInfo(targetVendor);
36
+ if (linuxUser && vendorInfo && vendorInfo.osUserIsolation === false) {
37
+ throw new Error((vendorInfo.displayName || targetVendor) + " is not available for OS-isolated users.");
38
+ }
39
+ var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
40
+ if ((source.ownerId || null) !== ownerId) throw new Error("Session access denied.");
41
+ return linuxUser;
42
+ }
43
+
44
+ function quietRecord(session, entry) {
45
+ session.history.push(entry);
46
+ sm.appendToSessionFile(session, entry);
47
+ }
48
+
49
+ function handoff(ws, msg) {
50
+ var source = sm.sessions.get(ws._clayActiveSession) || null;
51
+ var targetVendor = typeof msg.targetVendor === "string" ? msg.targetVendor.trim() : "";
52
+ var targetModel = typeof msg.model === "string" ? msg.model.trim() : "";
53
+ var linuxUser;
54
+ try {
55
+ linuxUser = validate(ws, source, targetVendor);
56
+ } catch (err) {
57
+ sendResult(ws, false, { error: err.message || String(err) });
58
+ return;
59
+ }
60
+
61
+ var targetEffort = yoke.clampEffort(
62
+ targetVendor,
63
+ msg.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[targetVendor]) || sm.currentEffort || "medium"
64
+ ) || null;
65
+ var target = sm.createSessionRaw({
66
+ ownerId: source.ownerId || null,
67
+ sessionVisibility: source.sessionVisibility || "shared",
68
+ vendor: targetVendor,
69
+ model: targetModel || null,
70
+ effort: targetEffort,
71
+ });
72
+ var targetName = (yoke.getVendorInfo(targetVendor) || {}).displayName || targetVendor;
73
+ target.title = (source.title || "Continued session") + " · " + targetName;
74
+ target.handoff = {
75
+ sourceSessionId: source.localId,
76
+ sourceVendor: source.vendor || sm.defaultVendor || "claude",
77
+ createdAt: Date.now(),
78
+ mode: "context",
79
+ sourceHistoryIndex: source.history.length,
80
+ };
81
+
82
+ var sourceEntry = {
83
+ type: "handoff_created",
84
+ targetSessionId: target.localId,
85
+ targetVendor: targetVendor,
86
+ targetTitle: target.title,
87
+ _ts: Date.now(),
88
+ };
89
+ var targetEntry = {
90
+ type: "handoff_context",
91
+ sourceSessionId: source.localId,
92
+ sourceVendor: target.handoff.sourceVendor,
93
+ sourceTitle: source.title || "Untitled session",
94
+ targetVendor: targetVendor,
95
+ request: contextBuilder.latestUserRequest(source.history),
96
+ _ts: Date.now(),
97
+ };
98
+ quietRecord(source, sourceEntry);
99
+ quietRecord(target, targetEntry);
100
+
101
+ var prompt = contextBuilder.buildHandoffContext({
102
+ cwd: ctx.cwd,
103
+ source: source,
104
+ targetVendor: targetVendor,
105
+ });
106
+ target.isProcessing = true;
107
+ target.lastActivity = Date.now();
108
+ target.sentToolResults = {};
109
+ sm.switchSession(target.localId, ws);
110
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
111
+ sendResult(ws, true, {
112
+ sourceSessionId: source.localId,
113
+ targetSessionId: target.localId,
114
+ targetVendor: targetVendor,
115
+ });
116
+
117
+ var sdk = ctx.getSdk();
118
+ function failStart(err) {
119
+ target.isProcessing = false;
120
+ sm.sendAndRecord(target, { type: "error", text: "Could not start the handoff session: " + (err.message || String(err)) });
121
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
122
+ sm.broadcastSessionList();
123
+ }
124
+ if (!sdk || typeof sdk.startQuery !== "function") {
125
+ failStart(new Error("SDK bridge is not ready."));
126
+ return;
127
+ }
128
+ target._queryStartTs = Date.now();
129
+ var startPromise;
130
+ try {
131
+ startPromise = sdk.startQuery(target, prompt, undefined, linuxUser);
132
+ } catch (err) {
133
+ failStart(err);
134
+ return;
135
+ }
136
+ Promise.resolve(startPromise).catch(failStart);
137
+ }
138
+
139
+ function handleMessage(ws, msg) {
140
+ if (msg.type === "handoff_session_options") {
141
+ ctx.sendTo(ws, {
142
+ type: "handoff_session_options",
143
+ installedVendors: sm.installedVendors || [],
144
+ modelsByVendor: sm.modelsByVendor || {},
145
+ capabilitiesByVendor: sm.capabilitiesByVendor || {},
146
+ });
147
+ return true;
148
+ }
149
+ if (msg.type === "handoff_session") {
150
+ handoff(ws, msg);
151
+ return true;
152
+ }
153
+ return false;
154
+ }
155
+
156
+ return { handleMessage: handleMessage };
157
+ }
158
+
159
+ module.exports = {
160
+ attachSessionHandoff: attachSessionHandoff,
161
+ hasUserContext: hasUserContext,
162
+ };
@@ -958,14 +958,6 @@ function attachSessions(ctx) {
958
958
  return true;
959
959
  }
960
960
 
961
- if (msg.type === "set_model" && msg.model) {
962
- var session = getSessionForWs(ws);
963
- if (session) {
964
- sdk.setModel(session, msg.model);
965
- }
966
- return true;
967
- }
968
-
969
961
  if (msg.type === "reload_skills") {
970
962
  var session = getSessionForWs(ws);
971
963
  if (session && sdk.reloadSkills) {
package/lib/project.js CHANGED
@@ -27,6 +27,7 @@ var { attachImage } = require("./project-image");
27
27
  var { attachKnowledge } = require("./project-knowledge");
28
28
  var { attachFilesystem } = require("./project-filesystem");
29
29
  var { attachSessions } = require("./project-sessions");
30
+ var { attachModels } = require("./project-models");
30
31
  var { attachUserMessage } = require("./project-user-message");
31
32
  var { attachShellCommand } = require("./project-shell-command");
32
33
  var { attachConnection } = require("./project-connection");
@@ -35,6 +36,7 @@ var { createLocalMcp } = require("./mcp-local");
35
36
  var { attachEmail: attachEmailModule } = require("./project-email");
36
37
  var { attachSessionSpawn } = require("./project-session-spawn");
37
38
  var { attachSessionPair } = require("./project-session-pair");
39
+ var { attachSessionHandoff } = require("./project-session-handoff");
38
40
  var { attachSessionNotes, composeSystemPrompts } = require("./project-session-notes");
39
41
  var { attachSessionDocument } = require("./project-session-document");
40
42
  var { attachSplitGroups } = require("./session-split-groups");
@@ -569,6 +571,18 @@ function createProjectContext(opts) {
569
571
  getLinuxUserForSession: getLinuxUserForSession,
570
572
  getPairToolDefs: function (boundSession) { return _sessionPair.getToolDefs(boundSession); },
571
573
  });
574
+ var _sessionHandoff = attachSessionHandoff({
575
+ cwd: cwd,
576
+ sm: sm,
577
+ isMate: isMate,
578
+ splitStore: _splitGroups.store,
579
+ getSdk: function () { return sdk; },
580
+ sendTo: sendTo,
581
+ usersModule: usersModule,
582
+ adapters: adapters,
583
+ getLinuxUserForSession: getLinuxUserForSession,
584
+ onProcessingChanged: onProcessingChanged,
585
+ });
572
586
  var _debate = null;
573
587
  var _debateProposal = attachDebateProposal({
574
588
  cwd: cwd,
@@ -1053,79 +1067,6 @@ function createProjectContext(opts) {
1053
1067
  return;
1054
1068
  }
1055
1069
 
1056
- // --- Vendor model switching ---
1057
- if (msg.type === "get_vendor_models") {
1058
- (async function() {
1059
- if (msg.vendor) {
1060
- try {
1061
- var modelLinuxUser = getLinuxUserForWs(ws);
1062
- var vendorAdapter = adapters[msg.vendor] || null;
1063
- if (!vendorAdapter) {
1064
- vendorAdapter = await yoke.lazyCreateAdapter(adapters, msg.vendor, {
1065
- cwd: cwd,
1066
- linuxUser: modelLinuxUser || undefined,
1067
- clayPort: serverPort,
1068
- clayTls: serverTls,
1069
- clayAuthToken: serverAuthToken,
1070
- slug: slug,
1071
- });
1072
- }
1073
- var needsReadyMetadata = !sm.capabilitiesByVendor || !sm.capabilitiesByVendor[msg.vendor]
1074
- || !sm.modelsByVendor || !sm.modelsByVendor[msg.vendor];
1075
- if (vendorAdapter && needsReadyMetadata && typeof vendorAdapter.init === "function") {
1076
- // Init warms the adapter, but a slow/failed init must not block
1077
- // model listing (e.g. Codex models are a fixed list). Keep going
1078
- // to supportedModels() even if init throws.
1079
- try {
1080
- var readyResult = await vendorAdapter.init({
1081
- cwd: cwd,
1082
- linuxUser: modelLinuxUser || undefined,
1083
- clayPort: serverPort,
1084
- clayTls: serverTls,
1085
- clayAuthToken: serverAuthToken,
1086
- slug: slug,
1087
- });
1088
- sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
1089
- sm.capabilitiesByVendor[msg.vendor] = readyResult.capabilities || {};
1090
- sm.modelsByVendor = sm.modelsByVendor || {};
1091
- if (Array.isArray(readyResult.models)) sm.modelsByVendor[msg.vendor] = readyResult.models;
1092
- } catch (e) {
1093
- console.error("[project] " + msg.vendor + " init failed (continuing to model list):", e.message || e);
1094
- }
1095
- }
1096
- if (vendorAdapter) {
1097
- sm.availableVendors = Object.keys(adapters);
1098
- sm.modelsByVendor = sm.modelsByVendor || {};
1099
- if (!sm.modelsByVendor[msg.vendor] && typeof vendorAdapter.supportedModels === "function") {
1100
- sm.modelsByVendor[msg.vendor] = await vendorAdapter.supportedModels();
1101
- }
1102
- }
1103
- } catch (e) {
1104
- console.error("[project] get_vendor_models lazy init failed for " + msg.vendor + ":", e.message || e);
1105
- }
1106
- }
1107
- var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[msg.vendor]) || [];
1108
- var firstModel = vendorModels[0] || "";
1109
- // model value can be string or {value, displayName} object
1110
- var defaultModel = typeof firstModel === "string" ? firstModel : (firstModel.value || "");
1111
- // Preserve the user's current model selection if it belongs to this
1112
- // vendor, rather than always snapping back to the vendor's default.
1113
- var modelToSend = defaultModel;
1114
- if (sm.currentModel) {
1115
- for (var mi = 0; mi < vendorModels.length; mi++) {
1116
- var mv = typeof vendorModels[mi] === "string" ? vendorModels[mi] : (vendorModels[mi].value || "");
1117
- if (mv === sm.currentModel) {
1118
- modelToSend = sm.currentModel;
1119
- break;
1120
- }
1121
- }
1122
- }
1123
- var vendorCapabilities = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[msg.vendor]) || {};
1124
- sendTo(ws, { type: "model_info", model: modelToSend, models: vendorModels, vendor: msg.vendor, capabilities: vendorCapabilities, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
1125
- })();
1126
- return;
1127
- }
1128
-
1129
1070
  // --- Debate ---
1130
1071
  if (msg.type === "debate_start") {
1131
1072
  handleDebateStart(ws, msg);
@@ -1173,7 +1114,9 @@ function createProjectContext(opts) {
1173
1114
 
1174
1115
  // --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
1175
1116
  if (_sessionPair.handleMessage(ws, msg)) return;
1117
+ if (_sessionHandoff.handleMessage(ws, msg)) return;
1176
1118
  if (_splitGroups.handleMessage(ws, msg)) return;
1119
+ if (_models.handleMessage(ws, msg)) return;
1177
1120
  if (_sessions.handleSessionsMessage(ws, msg)) return;
1178
1121
 
1179
1122
  // --- Filesystem, settings, env (delegated to project-filesystem.js) ---
@@ -1386,6 +1329,20 @@ function createProjectContext(opts) {
1386
1329
  _notifications: _notifications,
1387
1330
  });
1388
1331
 
1332
+ var _models = attachModels({
1333
+ cwd: cwd,
1334
+ slug: slug,
1335
+ sm: sm,
1336
+ sdk: sdk,
1337
+ adapters: adapters,
1338
+ sendTo: sendTo,
1339
+ getSessionForWs: getSessionForWs,
1340
+ getLinuxUserForWs: getLinuxUserForWs,
1341
+ serverPort: serverPort,
1342
+ serverTls: serverTls,
1343
+ serverAuthToken: serverAuthToken,
1344
+ });
1345
+
1389
1346
  // --- User message handler (delegated to project-user-message.js) ---
1390
1347
  var _userMessage = attachUserMessage({
1391
1348
  cwd: cwd,
package/lib/public/app.js CHANGED
@@ -60,6 +60,7 @@ import { initRateLimit, handleRateLimitEvent as _rlHandleRateLimitEvent, updateR
60
60
  import { initCursors, handleRemoteCursorMove as _curHandleRemoteCursorMove, handleRemoteCursorLeave as _curHandleRemoteCursorLeave, handleRemoteSelection as _curHandleRemoteSelection, clearRemoteCursors as _curClearRemoteCursors, initCursorToggle } from './modules/app-cursors.js';
61
61
  import { initFavicon, updateFavicon as _favUpdateFavicon, setSendBtnMode as _favSetSendBtnMode, blinkIO as _favBlinkIO, blinkSessionDot as _favBlinkSessionDot, updateCrossProjectBlink as _favUpdateCrossProjectBlink, startUrgentBlink as _favStartUrgentBlink, stopUrgentBlink as _favStopUrgentBlink, setActivity as _favSetActivity, drawFaviconAnimFrame as _favDrawFaviconAnimFrame } from './modules/app-favicon.js';
62
62
  import { initHeader, closeSessionInfoPopover as _hdrCloseSessionInfoPopover, updateHistorySentinel as _hdrUpdateHistorySentinel, requestMoreHistory as _hdrRequestMoreHistory, prependOlderHistory as _hdrPrependOlderHistory } from './modules/app-header.js';
63
+ import { initSessionActions } from './modules/session-actions.js';
63
64
  import { initMisc, flushPendingExtMessages, showImageModal as _miscShowImageModal, closeImageModal as _miscCloseImageModal, showPasteModal as _miscShowPasteModal, closePasteModal as _miscClosePasteModal, showConfirm as _miscShowConfirm, hideConfirm as _miscHideConfirm, showForceChangePinOverlay as _miscShowForceChangePinOverlay, sendExtensionCommand as _miscSendExtensionCommand, handleExtensionResult as _miscHandleExtensionResult } from './modules/app-misc.js';
64
65
  import { initSkillInstall, requireSkills as _siRequireSkills, requireClayMateInterview as _siRequireClayMateInterview, handleSkillInstallWs as _siHandleSkillInstallWs } from './modules/app-skills-install.js';
65
66
  import { initDebateUi, showDebateConcludeConfirm as _debShowDebateConcludeConfirm, exitDebateConcludeMode as _debExitDebateConcludeMode, handleDebateConcludeSend as _debHandleDebateConcludeSend, showDebateEndedMode as _debShowDebateEndedMode, exitDebateEndedMode as _debExitDebateEndedMode, showDebateUserFloor as _debShowDebateUserFloor, exitDebateFloorMode as _debExitDebateFloorMode, handleDebateFloorSend as _debHandleDebateFloorSend, renderDebateUserFloorDone as _debRenderDebateUserFloorDone, showDebateSticky as _debShowDebateSticky, showDebateBottomBar as _debShowDebateBottomBar, removeDebateBottomBar as _debRemoveDebateBottomBar, sendDebateStickyComment as _debSendDebateStickyComment, updateDebateRound as _debUpdateDebateRound } from './modules/app-debate-ui.js';
@@ -339,6 +340,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
339
340
  markdownSlideLevelExplicit: false,
340
341
  currentModel: "",
341
342
  currentModels: [],
343
+ modelListVendor: "",
344
+ modelListStatus: "idle",
345
+ modelListError: "",
346
+ modelRequestId: "",
347
+ modelSelectionPending: null,
348
+ modelSelectionError: "",
342
349
  // Project's last-used vendor; seeds the sidebar's "New session" button.
343
350
  lastVendor: "",
344
351
  // Static adapter metadata sent before any vendor is initialized.
@@ -633,6 +640,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
633
640
 
634
641
  // --- Header module (rename, info popover, history) ---
635
642
  initHeader();
643
+ initSessionActions();
636
644
 
637
645
  // --- Skill Install module ---
638
646
  initSkillInstall();
@@ -43,62 +43,6 @@
43
43
  #header-rename-btn:hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.04); border-color: var(--border); }
44
44
  #header-rename-btn .lucide { width: 13px; height: 13px; }
45
45
 
46
- #header-add-worker-btn {
47
- display: inline-flex;
48
- align-items: center;
49
- justify-content: center;
50
- gap: 7px;
51
- width: auto;
52
- height: 28px;
53
- border: 1px solid color-mix(in srgb, var(--accent2) 28%, var(--border));
54
- background: var(--accent2-8);
55
- color: var(--accent2);
56
- cursor: pointer;
57
- border-radius: 8px;
58
- flex-shrink: 0;
59
- padding: 0 9px 0 7px;
60
- margin-left: 0;
61
- font-family: inherit;
62
- font-size: 11px;
63
- font-weight: 700;
64
- line-height: 1;
65
- white-space: nowrap;
66
- transition: color 0.15s, background 0.15s, border-color 0.15s;
67
- }
68
- #header-add-worker-btn:hover {
69
- color: var(--accent2-hover);
70
- background: var(--accent2-15);
71
- border-color: color-mix(in srgb, var(--accent2) 48%, var(--border));
72
- }
73
- .header-worker-icon {
74
- position: relative;
75
- display: inline-flex;
76
- align-items: center;
77
- justify-content: center;
78
- width: 16px;
79
- height: 16px;
80
- flex-shrink: 0;
81
- }
82
- .header-worker-icon > .lucide { width: 15px; height: 15px; }
83
- .header-worker-add-badge {
84
- position: absolute;
85
- right: -4px;
86
- bottom: -3px;
87
- display: inline-flex;
88
- align-items: center;
89
- justify-content: center;
90
- width: 9px;
91
- height: 9px;
92
- border-radius: 50%;
93
- background: var(--accent2);
94
- color: var(--bg);
95
- box-shadow: 0 0 0 2px var(--bg);
96
- }
97
- .header-worker-add-badge .lucide { width: 7px; height: 7px; stroke-width: 3; }
98
- .header-worker-label { letter-spacing: 0.01em; }
99
- #header-add-worker-btn.hidden { display: none; }
100
- body.dm-mode #header-add-worker-btn { display: none; }
101
-
102
46
  /* "Close terminal" button for a live TUI session. Matches the session-control
103
47
  chrome; reds out on hover since it stops the running agent while preserving
104
48
  the resumable session. */
@@ -702,6 +646,41 @@ body.dm-mode #header-add-worker-btn { display: none; }
702
646
  color: var(--text-dimmer);
703
647
  font-style: italic;
704
648
  }
649
+ .config-model-state {
650
+ display: flex;
651
+ align-items: center;
652
+ gap: 10px;
653
+ min-width: 280px;
654
+ padding: 10px 14px;
655
+ color: var(--text-secondary);
656
+ font-size: 12px;
657
+ white-space: normal;
658
+ }
659
+ .config-model-state > div { flex: 1; min-width: 0; }
660
+ .config-model-state-title { color: var(--text); font-weight: 600; }
661
+ .config-model-state-detail { margin-top: 2px; color: var(--text-muted); line-height: 1.35; }
662
+ .config-model-spinner {
663
+ width: 14px;
664
+ height: 14px;
665
+ flex: 0 0 auto;
666
+ border: 2px solid var(--border);
667
+ border-top-color: var(--accent);
668
+ border-radius: 50%;
669
+ animation: config-model-spin 0.8s linear infinite;
670
+ }
671
+ @keyframes config-model-spin { to { transform: rotate(360deg); } }
672
+ .config-model-retry {
673
+ flex: 0 0 auto;
674
+ border: 1px solid var(--border);
675
+ border-radius: 5px;
676
+ padding: 4px 8px;
677
+ background: var(--surface-raised);
678
+ color: var(--text-secondary);
679
+ font: inherit;
680
+ cursor: pointer;
681
+ }
682
+ .config-model-retry:hover { color: var(--text); border-color: var(--text-muted); }
683
+ .config-radio-item.pending { color: var(--accent); opacity: 0.75; }
705
684
 
706
685
  /* Segmented control for effort */
707
686
  .config-segmented {