clay-server 3.4.0-beta.11 → 3.4.0-beta.12
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.
- package/lib/project-models.js +216 -0
- package/lib/project-sessions.js +0 -8
- package/lib/project.js +16 -73
- package/lib/public/app.js +6 -0
- package/lib/public/css/menus.css +35 -0
- package/lib/public/modules/app-messages.js +9 -5
- package/lib/public/modules/app-panels.js +12 -77
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/model-picker.js +275 -0
- package/lib/sdk-bridge.js +7 -3
- package/lib/ws-schema.js +3 -1
- package/package.json +1 -1
|
@@ -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
|
+
};
|
package/lib/project-sessions.js
CHANGED
|
@@ -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");
|
|
@@ -1053,79 +1054,6 @@ function createProjectContext(opts) {
|
|
|
1053
1054
|
return;
|
|
1054
1055
|
}
|
|
1055
1056
|
|
|
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
1057
|
// --- Debate ---
|
|
1130
1058
|
if (msg.type === "debate_start") {
|
|
1131
1059
|
handleDebateStart(ws, msg);
|
|
@@ -1174,6 +1102,7 @@ function createProjectContext(opts) {
|
|
|
1174
1102
|
// --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
|
|
1175
1103
|
if (_sessionPair.handleMessage(ws, msg)) return;
|
|
1176
1104
|
if (_splitGroups.handleMessage(ws, msg)) return;
|
|
1105
|
+
if (_models.handleMessage(ws, msg)) return;
|
|
1177
1106
|
if (_sessions.handleSessionsMessage(ws, msg)) return;
|
|
1178
1107
|
|
|
1179
1108
|
// --- Filesystem, settings, env (delegated to project-filesystem.js) ---
|
|
@@ -1386,6 +1315,20 @@ function createProjectContext(opts) {
|
|
|
1386
1315
|
_notifications: _notifications,
|
|
1387
1316
|
});
|
|
1388
1317
|
|
|
1318
|
+
var _models = attachModels({
|
|
1319
|
+
cwd: cwd,
|
|
1320
|
+
slug: slug,
|
|
1321
|
+
sm: sm,
|
|
1322
|
+
sdk: sdk,
|
|
1323
|
+
adapters: adapters,
|
|
1324
|
+
sendTo: sendTo,
|
|
1325
|
+
getSessionForWs: getSessionForWs,
|
|
1326
|
+
getLinuxUserForWs: getLinuxUserForWs,
|
|
1327
|
+
serverPort: serverPort,
|
|
1328
|
+
serverTls: serverTls,
|
|
1329
|
+
serverAuthToken: serverAuthToken,
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1389
1332
|
// --- User message handler (delegated to project-user-message.js) ---
|
|
1390
1333
|
var _userMessage = attachUserMessage({
|
|
1391
1334
|
cwd: cwd,
|
package/lib/public/app.js
CHANGED
|
@@ -339,6 +339,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
339
339
|
markdownSlideLevelExplicit: false,
|
|
340
340
|
currentModel: "",
|
|
341
341
|
currentModels: [],
|
|
342
|
+
modelListVendor: "",
|
|
343
|
+
modelListStatus: "idle",
|
|
344
|
+
modelListError: "",
|
|
345
|
+
modelRequestId: "",
|
|
346
|
+
modelSelectionPending: null,
|
|
347
|
+
modelSelectionError: "",
|
|
342
348
|
// Project's last-used vendor; seeds the sidebar's "New session" button.
|
|
343
349
|
lastVendor: "",
|
|
344
350
|
// Static adapter metadata sent before any vendor is initialized.
|
package/lib/public/css/menus.css
CHANGED
|
@@ -702,6 +702,41 @@ body.dm-mode #header-add-worker-btn { display: none; }
|
|
|
702
702
|
color: var(--text-dimmer);
|
|
703
703
|
font-style: italic;
|
|
704
704
|
}
|
|
705
|
+
.config-model-state {
|
|
706
|
+
display: flex;
|
|
707
|
+
align-items: center;
|
|
708
|
+
gap: 10px;
|
|
709
|
+
min-width: 280px;
|
|
710
|
+
padding: 10px 14px;
|
|
711
|
+
color: var(--text-secondary);
|
|
712
|
+
font-size: 12px;
|
|
713
|
+
white-space: normal;
|
|
714
|
+
}
|
|
715
|
+
.config-model-state > div { flex: 1; min-width: 0; }
|
|
716
|
+
.config-model-state-title { color: var(--text); font-weight: 600; }
|
|
717
|
+
.config-model-state-detail { margin-top: 2px; color: var(--text-muted); line-height: 1.35; }
|
|
718
|
+
.config-model-spinner {
|
|
719
|
+
width: 14px;
|
|
720
|
+
height: 14px;
|
|
721
|
+
flex: 0 0 auto;
|
|
722
|
+
border: 2px solid var(--border);
|
|
723
|
+
border-top-color: var(--accent);
|
|
724
|
+
border-radius: 50%;
|
|
725
|
+
animation: config-model-spin 0.8s linear infinite;
|
|
726
|
+
}
|
|
727
|
+
@keyframes config-model-spin { to { transform: rotate(360deg); } }
|
|
728
|
+
.config-model-retry {
|
|
729
|
+
flex: 0 0 auto;
|
|
730
|
+
border: 1px solid var(--border);
|
|
731
|
+
border-radius: 5px;
|
|
732
|
+
padding: 4px 8px;
|
|
733
|
+
background: var(--surface-raised);
|
|
734
|
+
color: var(--text-secondary);
|
|
735
|
+
font: inherit;
|
|
736
|
+
cursor: pointer;
|
|
737
|
+
}
|
|
738
|
+
.config-model-retry:hover { color: var(--text); border-color: var(--text-muted); }
|
|
739
|
+
.config-radio-item.pending { color: var(--accent); opacity: 0.75; }
|
|
705
740
|
|
|
706
741
|
/* Segmented control for effort */
|
|
707
742
|
.config-segmented {
|
|
@@ -53,6 +53,7 @@ import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWh
|
|
|
53
53
|
import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
|
|
54
54
|
import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
|
|
55
55
|
import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
|
|
56
|
+
import { getModelInfoUpdate, modelEntryMatches, handleModelSelectionResult } from './model-picker.js';
|
|
56
57
|
import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
|
|
57
58
|
import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
|
|
58
59
|
import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
|
|
@@ -431,12 +432,12 @@ export function processMessage(msg) {
|
|
|
431
432
|
// currentModels with the wrong vendor's list and trigger app-panels
|
|
432
433
|
// to request models for the "wrong" vendor, which feeds back into a
|
|
433
434
|
// ping-pong loop of vendor flapping. See issue #336.
|
|
434
|
-
var
|
|
435
|
-
if (
|
|
435
|
+
var _modelLoadUpdate = getModelInfoUpdate(msg);
|
|
436
|
+
if (!_modelLoadUpdate) break;
|
|
436
437
|
|
|
437
438
|
var _modelVal = msg.model;
|
|
438
439
|
if (_modelVal && typeof _modelVal === "object") _modelVal = _modelVal.value || _modelVal.displayName || "";
|
|
439
|
-
var _miUpdate = { currentModels: msg.models || [] };
|
|
440
|
+
var _miUpdate = Object.assign({ currentModels: msg.models || [] }, _modelLoadUpdate);
|
|
440
441
|
if (Object.prototype.hasOwnProperty.call(msg, "model")) {
|
|
441
442
|
// Keep the user's existing selection only if it actually belongs to
|
|
442
443
|
// this vendor's model list. Otherwise (e.g. switching to a Codex
|
|
@@ -445,8 +446,7 @@ export function processMessage(msg) {
|
|
|
445
446
|
// model.
|
|
446
447
|
var _curModel = store.get('currentModel');
|
|
447
448
|
var _curInList = !!_curModel && (msg.models || []).some(function (m) {
|
|
448
|
-
|
|
449
|
-
return v === _curModel;
|
|
449
|
+
return modelEntryMatches(m, _curModel);
|
|
450
450
|
});
|
|
451
451
|
if (store.get('vendorSelectionLocked') && _curInList) {
|
|
452
452
|
// Keep the user's existing (valid) selection; only update models list
|
|
@@ -463,6 +463,10 @@ export function processMessage(msg) {
|
|
|
463
463
|
break;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
case "model_selection_result":
|
|
467
|
+
handleModelSelectionResult(msg);
|
|
468
|
+
break;
|
|
469
|
+
|
|
466
470
|
case "config_state": {
|
|
467
471
|
var _cs = {};
|
|
468
472
|
if (msg.model) _cs.currentModel = msg.model;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
630
|
+
renderModelPicker();
|
|
696
631
|
rebuildModeList();
|
|
697
632
|
rebuildEffortBar();
|
|
698
633
|
|
|
@@ -5,6 +5,7 @@ import { escapeHtml, showToast } from './utils.js';
|
|
|
5
5
|
import { refreshIcons } from './icons.js';
|
|
6
6
|
import { parseEmojis } from './markdown.js';
|
|
7
7
|
import { store } from './store.js';
|
|
8
|
+
import { resetModelPickerState } from './model-picker.js';
|
|
8
9
|
import { getWs, setWs } from './ws-ref.js';
|
|
9
10
|
import { getMessagesEl, getStatusDot } from './dom-refs.js';
|
|
10
11
|
import { userAvatarUrl } from './avatar.js';
|
|
@@ -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 });
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
|
|
4
|
+
var configModelList = null;
|
|
5
|
+
var configPopover = null;
|
|
6
|
+
var configChip = null;
|
|
7
|
+
var requestCounter = 0;
|
|
8
|
+
var selectionCounter = 0;
|
|
9
|
+
var requestTimer = null;
|
|
10
|
+
|
|
11
|
+
export function modelEntryValue(entry) {
|
|
12
|
+
if (!entry) return "";
|
|
13
|
+
if (typeof entry === "string") return entry;
|
|
14
|
+
return entry.value || entry.id || "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function modelEntryMatches(entry, value) {
|
|
18
|
+
if (!entry || !value) return false;
|
|
19
|
+
if (typeof entry === "string") return entry === value;
|
|
20
|
+
return entry.value === value || entry.id === value || entry.resolvedModel === value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function modelDisplayName(value, models) {
|
|
24
|
+
if (!value) return "";
|
|
25
|
+
var list = models || [];
|
|
26
|
+
for (var i = 0; i < list.length; i++) {
|
|
27
|
+
if (modelEntryMatches(list[i], value)) {
|
|
28
|
+
return typeof list[i] === "string" ? list[i] : (list[i].displayName || modelEntryValue(list[i]));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function vendorDisplayName(vendor) {
|
|
35
|
+
var info = store.get('vendorInfo') || {};
|
|
36
|
+
return info[vendor] && info[vendor].displayName ? info[vendor].displayName : vendor;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clearRequestTimer() {
|
|
40
|
+
if (!requestTimer) return;
|
|
41
|
+
clearTimeout(requestTimer);
|
|
42
|
+
requestTimer = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function setupModelPicker() {
|
|
46
|
+
configModelList = document.getElementById("config-model-list");
|
|
47
|
+
configPopover = document.getElementById("config-popover");
|
|
48
|
+
configChip = document.getElementById("config-chip");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resetModelPickerState() {
|
|
52
|
+
clearRequestTimer();
|
|
53
|
+
store.set({
|
|
54
|
+
modelListVendor: "",
|
|
55
|
+
modelListStatus: "idle",
|
|
56
|
+
modelListError: "",
|
|
57
|
+
modelRequestId: "",
|
|
58
|
+
modelSelectionPending: null,
|
|
59
|
+
modelSelectionError: "",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function requestVendorModels(vendor, force) {
|
|
64
|
+
if (!vendor) return null;
|
|
65
|
+
var s = store.snap();
|
|
66
|
+
if (!force && s.modelListVendor === vendor) {
|
|
67
|
+
if (s.modelListStatus === "loading") return s.modelRequestId || null;
|
|
68
|
+
if (s.modelListStatus === "ready" && s.currentModels && s.currentModels.length > 0) return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var ws = getWs();
|
|
72
|
+
if (!ws || ws.readyState !== 1) {
|
|
73
|
+
store.set({
|
|
74
|
+
modelListVendor: vendor,
|
|
75
|
+
modelListStatus: "error",
|
|
76
|
+
modelListError: "The connection is not ready. Reconnect and retry.",
|
|
77
|
+
});
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
requestCounter++;
|
|
82
|
+
var requestId = "models-" + Date.now().toString(36) + "-" + requestCounter;
|
|
83
|
+
clearRequestTimer();
|
|
84
|
+
store.set({
|
|
85
|
+
modelListVendor: vendor,
|
|
86
|
+
modelListStatus: "loading",
|
|
87
|
+
modelListError: "",
|
|
88
|
+
modelRequestId: requestId,
|
|
89
|
+
});
|
|
90
|
+
ws.send(JSON.stringify({ type: "get_vendor_models", vendor: vendor, requestId: requestId }));
|
|
91
|
+
requestTimer = setTimeout(function() {
|
|
92
|
+
var current = store.snap();
|
|
93
|
+
if (current.modelRequestId !== requestId || current.modelListStatus !== "loading") return;
|
|
94
|
+
store.set({
|
|
95
|
+
modelListStatus: "error",
|
|
96
|
+
modelListError: vendorDisplayName(vendor) + " model loading timed out. Retry to try again.",
|
|
97
|
+
});
|
|
98
|
+
}, 35000);
|
|
99
|
+
return requestId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function prepareModelPickerOpen() {
|
|
103
|
+
var s = store.snap();
|
|
104
|
+
var vendor = s.currentVendor || "claude";
|
|
105
|
+
var needsModels = !s.currentModels || s.currentModels.length === 0;
|
|
106
|
+
var wrongVendor = s.modelListVendor !== vendor;
|
|
107
|
+
var retryable = s.modelListStatus === "error" || s.modelListStatus === "empty" || s.modelListStatus === "idle";
|
|
108
|
+
requestVendorModels(vendor, needsModels || wrongVendor || retryable);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getModelInfoUpdate(msg) {
|
|
112
|
+
var vendor = msg.vendor || store.get('currentVendor') || "claude";
|
|
113
|
+
var currentVendor = store.get('currentVendor');
|
|
114
|
+
if (currentVendor && vendor !== currentVendor) return null;
|
|
115
|
+
|
|
116
|
+
var s = store.snap();
|
|
117
|
+
if (msg.requestId && s.modelRequestId && msg.requestId !== s.modelRequestId) return null;
|
|
118
|
+
|
|
119
|
+
var models = Array.isArray(msg.models) ? msg.models : [];
|
|
120
|
+
var unsolicitedEmptyWhileLoading = s.modelListStatus === "loading"
|
|
121
|
+
&& s.modelListVendor === vendor
|
|
122
|
+
&& !msg.requestId
|
|
123
|
+
&& !msg.modelStatus
|
|
124
|
+
&& !msg.error
|
|
125
|
+
&& models.length === 0;
|
|
126
|
+
if (unsolicitedEmptyWhileLoading) {
|
|
127
|
+
return {
|
|
128
|
+
modelListVendor: vendor,
|
|
129
|
+
modelListStatus: "loading",
|
|
130
|
+
modelListError: "",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clearRequestTimer();
|
|
135
|
+
var status = msg.modelStatus || (msg.error ? "error" : (models.length > 0 ? "ready" : "empty"));
|
|
136
|
+
return {
|
|
137
|
+
modelListVendor: vendor,
|
|
138
|
+
modelListStatus: status,
|
|
139
|
+
modelListError: msg.error || "",
|
|
140
|
+
modelRequestId: "",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function requestModelSelection(model) {
|
|
145
|
+
var ws = getWs();
|
|
146
|
+
if (!ws || ws.readyState !== 1) {
|
|
147
|
+
store.set({ modelSelectionError: "The connection is not ready. Reconnect before selecting a model." });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
var s = store.snap();
|
|
151
|
+
selectionCounter++;
|
|
152
|
+
var requestId = "model-select-" + Date.now().toString(36) + "-" + selectionCounter;
|
|
153
|
+
store.set({
|
|
154
|
+
currentModel: model,
|
|
155
|
+
modelSelectionPending: { requestId: requestId, model: model, previousModel: s.currentModel || "" },
|
|
156
|
+
modelSelectionError: "",
|
|
157
|
+
});
|
|
158
|
+
ws.send(JSON.stringify({
|
|
159
|
+
type: "set_model",
|
|
160
|
+
model: model,
|
|
161
|
+
vendor: s.currentVendor || "claude",
|
|
162
|
+
requestId: requestId,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function handleModelSelectionResult(msg) {
|
|
167
|
+
var pending = store.get('modelSelectionPending');
|
|
168
|
+
if (!pending || (msg.requestId && msg.requestId !== pending.requestId)) return false;
|
|
169
|
+
if (msg.ok) {
|
|
170
|
+
store.set({
|
|
171
|
+
currentModel: msg.model || pending.model,
|
|
172
|
+
modelSelectionPending: null,
|
|
173
|
+
modelSelectionError: "",
|
|
174
|
+
});
|
|
175
|
+
if (configPopover) configPopover.classList.add("hidden");
|
|
176
|
+
if (configChip) configChip.classList.remove("active");
|
|
177
|
+
} else {
|
|
178
|
+
store.set({
|
|
179
|
+
currentModel: pending.previousModel,
|
|
180
|
+
modelSelectionPending: null,
|
|
181
|
+
modelSelectionError: msg.error || "The model could not be selected.",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function appendState(title, detail, retry) {
|
|
188
|
+
var stateEl = document.createElement("div");
|
|
189
|
+
stateEl.className = "config-model-state";
|
|
190
|
+
if (title === "Loading models…") {
|
|
191
|
+
var spinner = document.createElement("span");
|
|
192
|
+
spinner.className = "config-model-spinner";
|
|
193
|
+
spinner.setAttribute("aria-hidden", "true");
|
|
194
|
+
stateEl.appendChild(spinner);
|
|
195
|
+
}
|
|
196
|
+
var textWrap = document.createElement("div");
|
|
197
|
+
var titleEl = document.createElement("div");
|
|
198
|
+
titleEl.className = "config-model-state-title";
|
|
199
|
+
titleEl.textContent = title;
|
|
200
|
+
textWrap.appendChild(titleEl);
|
|
201
|
+
if (detail) {
|
|
202
|
+
var detailEl = document.createElement("div");
|
|
203
|
+
detailEl.className = "config-model-state-detail";
|
|
204
|
+
detailEl.textContent = detail;
|
|
205
|
+
textWrap.appendChild(detailEl);
|
|
206
|
+
}
|
|
207
|
+
stateEl.appendChild(textWrap);
|
|
208
|
+
if (retry) {
|
|
209
|
+
var retryBtn = document.createElement("button");
|
|
210
|
+
retryBtn.type = "button";
|
|
211
|
+
retryBtn.className = "config-model-retry";
|
|
212
|
+
retryBtn.textContent = "Retry";
|
|
213
|
+
retryBtn.addEventListener("click", function() {
|
|
214
|
+
requestVendorModels(store.get('currentVendor') || "claude", true);
|
|
215
|
+
});
|
|
216
|
+
stateEl.appendChild(retryBtn);
|
|
217
|
+
}
|
|
218
|
+
configModelList.appendChild(stateEl);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function renderModelPicker() {
|
|
222
|
+
if (!configModelList) return;
|
|
223
|
+
var s = store.snap();
|
|
224
|
+
var vendor = s.currentVendor || "claude";
|
|
225
|
+
var status = s.modelListVendor === vendor ? s.modelListStatus : "idle";
|
|
226
|
+
configModelList.innerHTML = "";
|
|
227
|
+
|
|
228
|
+
if (status === "loading") {
|
|
229
|
+
appendState("Loading models…", "Starting " + vendorDisplayName(vendor) + " and requesting its model catalog.", false);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (status === "error") {
|
|
233
|
+
appendState("Couldn’t load models", s.modelListError || "An unexpected error occurred.", true);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (status === "empty" || !s.currentModels || s.currentModels.length === 0) {
|
|
237
|
+
appendState("No models available", s.modelListError || "The vendor returned an empty model catalog.", true);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
var caps = s.vendorCapabilities || {};
|
|
242
|
+
var locked = s.activeSessionMode === "gui" && !!s.sessionHasHistory && !caps.midSessionModelSwitch;
|
|
243
|
+
for (var i = 0; i < s.currentModels.length; i++) {
|
|
244
|
+
var item = s.currentModels[i];
|
|
245
|
+
var value = modelEntryValue(item);
|
|
246
|
+
if (!value) continue;
|
|
247
|
+
var button = document.createElement("button");
|
|
248
|
+
button.className = "config-radio-item";
|
|
249
|
+
if (modelEntryMatches(item, s.currentModel)) button.classList.add("active");
|
|
250
|
+
if (locked) button.classList.add("locked");
|
|
251
|
+
button.dataset.model = value;
|
|
252
|
+
button.textContent = typeof item === "string" ? item : (item.displayName || value);
|
|
253
|
+
if (s.modelSelectionPending && s.modelSelectionPending.model === value) {
|
|
254
|
+
button.classList.add("pending");
|
|
255
|
+
button.textContent += " · Selecting…";
|
|
256
|
+
}
|
|
257
|
+
button.disabled = locked || !!s.modelSelectionPending;
|
|
258
|
+
if (locked) {
|
|
259
|
+
button.title = "This vendor binds the model when the session starts. Start a new session to change it.";
|
|
260
|
+
} else {
|
|
261
|
+
button.addEventListener("click", function() { requestModelSelection(this.dataset.model); });
|
|
262
|
+
}
|
|
263
|
+
configModelList.appendChild(button);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (s.modelSelectionError) {
|
|
267
|
+
appendState("Model selection failed", s.modelSelectionError, false);
|
|
268
|
+
}
|
|
269
|
+
if (locked) {
|
|
270
|
+
var hint = document.createElement("div");
|
|
271
|
+
hint.className = "config-model-hint";
|
|
272
|
+
hint.textContent = "Locked after first message. Start a new session to change models.";
|
|
273
|
+
configModelList.appendChild(hint);
|
|
274
|
+
}
|
|
275
|
+
}
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -1931,10 +1931,12 @@ function createSDKBridge(opts) {
|
|
|
1931
1931
|
session.model = model;
|
|
1932
1932
|
sm.saveSessionFile(session);
|
|
1933
1933
|
sm.currentModel = model;
|
|
1934
|
-
//
|
|
1935
|
-
|
|
1934
|
+
// Confirm against the session vendor so a non-default vendor does not
|
|
1935
|
+
// receive model metadata labeled as the project's default adapter.
|
|
1936
|
+
var pendingVendor = session.vendor || (adapter && adapter.vendor) || "claude";
|
|
1937
|
+
sendModelInfoForVendor(pendingVendor, model);
|
|
1936
1938
|
sendToSession(session, { type: "config_state", model: sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1937
|
-
return;
|
|
1939
|
+
return { ok: true, model: model };
|
|
1938
1940
|
}
|
|
1939
1941
|
try {
|
|
1940
1942
|
await session.queryInstance.setModel(model);
|
|
@@ -1944,8 +1946,10 @@ function createSDKBridge(opts) {
|
|
|
1944
1946
|
var sessionVendor = session.vendor || (adapter && adapter.vendor) || "claude";
|
|
1945
1947
|
sendModelInfoForVendor(sessionVendor, model);
|
|
1946
1948
|
sendToSession(session, { type: "config_state", model: sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
|
|
1949
|
+
return { ok: true, model: model };
|
|
1947
1950
|
} catch (e) {
|
|
1948
1951
|
send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
|
|
1952
|
+
return { ok: false, model: session.model || "", error: e.message || String(e) };
|
|
1949
1953
|
}
|
|
1950
1954
|
}
|
|
1951
1955
|
|
package/lib/ws-schema.js
CHANGED
|
@@ -175,7 +175,8 @@ var schema = {
|
|
|
175
175
|
// -----------------------------------------------------------------------
|
|
176
176
|
// Model / config
|
|
177
177
|
// -----------------------------------------------------------------------
|
|
178
|
-
"set_model": { direction: "c2s", handler: "lib/project-
|
|
178
|
+
"set_model": { direction: "c2s", handler: "lib/project-models.js", description: "Set the model for the current session" },
|
|
179
|
+
"get_vendor_models": { direction: "c2s", handler: "lib/project-models.js", description: "Load a vendor model catalog" },
|
|
179
180
|
"set_server_default_model": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the server-wide default model" },
|
|
180
181
|
"set_project_default_model": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set the project default model" },
|
|
181
182
|
"set_permission_mode": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set permission mode for the current session" },
|
|
@@ -192,6 +193,7 @@ var schema = {
|
|
|
192
193
|
"set_betas": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set beta feature flags" },
|
|
193
194
|
"set_thinking": { direction: "c2s", handler: "lib/project-sessions.js", description: "Set thinking mode and budget" },
|
|
194
195
|
"model_info": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Current model and available models list" },
|
|
196
|
+
"model_selection_result": { direction: "s2c", handler: "lib/public/modules/model-picker.js", description: "Acknowledgement or error for a model selection" },
|
|
195
197
|
"config_state": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full config state: model, mode, effort, betas, thinking" },
|
|
196
198
|
"slash_commands": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Available slash commands list" },
|
|
197
199
|
"fast_mode_state": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Fast mode toggle state" },
|
package/package.json
CHANGED