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.
- package/lib/project-models.js +216 -0
- package/lib/project-session-handoff.js +162 -0
- package/lib/project-sessions.js +0 -8
- package/lib/project.js +30 -73
- package/lib/public/app.js +8 -0
- package/lib/public/css/menus.css +35 -56
- package/lib/public/css/pane.css +7 -1
- package/lib/public/css/session-actions.css +98 -0
- package/lib/public/index.html +4 -8
- package/lib/public/modules/agent-config-selects.js +69 -0
- package/lib/public/modules/app-header.js +0 -22
- package/lib/public/modules/app-messages.js +17 -5
- package/lib/public/modules/app-panels.js +13 -80
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/model-picker.js +262 -0
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/split-pair-ui.js +16 -78
- package/lib/public/modules/split-view.js +3 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +7 -3
- package/lib/session-handoff-context.js +110 -0
- package/lib/sessions.js +2 -0
- package/lib/ws-schema.js +8 -1
- package/lib/yoke/adapters/codex.js +3 -2
- package/lib/yoke/adapters/kiro.js +2 -2
- 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
|
+
};
|
|
@@ -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
|
+
};
|
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");
|
|
@@ -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();
|
package/lib/public/css/menus.css
CHANGED
|
@@ -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 {
|