clay-server 3.4.0-beta.2 → 3.4.0-beta.20
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/daemon-projects.js +3 -3
- package/lib/daemon.js +59 -59
- package/lib/project-connection.js +35 -7
- package/lib/project-debate.js +4 -0
- package/lib/project-mate-interaction.js +21 -1
- package/lib/project-memory.js +3 -0
- package/lib/project-models.js +220 -0
- package/lib/project-session-handoff.js +162 -0
- package/lib/project-sessions.js +13 -45
- package/lib/project-worker-proposal.js +49 -14
- package/lib/project.js +38 -80
- package/lib/public/antigravity-avatar.png +0 -0
- package/lib/public/app.js +29 -0
- package/lib/public/copilot-avatar.svg +5 -0
- package/lib/public/css/command-palette.css +22 -2
- package/lib/public/css/icon-strip.css +29 -13
- package/lib/public/css/input.css +56 -0
- package/lib/public/css/mates.css +6 -0
- package/lib/public/css/menus.css +38 -25
- package/lib/public/css/messages.css +9 -0
- package/lib/public/css/pane.css +16 -3
- package/lib/public/css/pwa-mobile.css +17 -0
- package/lib/public/css/session-actions.css +98 -0
- package/lib/public/css/title-bar.css +19 -0
- package/lib/public/grok-avatar.svg +4 -0
- package/lib/public/index.html +29 -7
- package/lib/public/junie-avatar.svg +11 -0
- package/lib/public/kimi-avatar.svg +4 -0
- package/lib/public/modules/agent-config-selects.js +69 -0
- package/lib/public/modules/app-header.js +4 -22
- package/lib/public/modules/app-messages.js +54 -11
- package/lib/public/modules/app-panels.js +36 -83
- package/lib/public/modules/app-projects.js +3 -1
- package/lib/public/modules/app-rendering.js +19 -4
- package/lib/public/modules/background-tasks-ui.js +55 -0
- package/lib/public/modules/mate-sidebar.js +11 -3
- package/lib/public/modules/model-picker.js +263 -0
- package/lib/public/modules/notifications.js +7 -1
- package/lib/public/modules/pane-bridge.js +11 -0
- package/lib/public/modules/pane-links.js +23 -0
- package/lib/public/modules/project-switcher.js +7 -6
- package/lib/public/modules/session-actions.js +294 -0
- package/lib/public/modules/sidebar-mates.js +1 -1
- package/lib/public/modules/sidebar-mobile.js +2 -1
- package/lib/public/modules/sidebar-projects.js +34 -43
- package/lib/public/modules/sidebar-sessions.js +20 -6
- package/lib/public/modules/split-pair-ui.js +16 -77
- package/lib/public/modules/split-view.js +3 -0
- package/lib/public/modules/tools.js +6 -1
- package/lib/public/modules/vendor-priority.js +14 -0
- package/lib/public/modules/vendor-selection.js +20 -0
- package/lib/public/modules/worker-proposal.js +5 -1
- package/lib/public/modules/worktree-location.js +17 -0
- package/lib/public/qwen-avatar.svg +4 -0
- package/lib/public/style.css +4 -2
- package/lib/sdk-bridge.js +99 -56
- package/lib/sdk-message-processor.js +26 -4
- package/lib/session-handoff-context.js +110 -0
- package/lib/session-notes-mcp-server.js +8 -1
- package/lib/sessions.js +4 -0
- package/lib/worktree.js +9 -4
- package/lib/ws-schema.js +9 -1
- package/lib/yoke/acp-agent-profiles.js +64 -14
- package/lib/yoke/adapters/antigravity.js +417 -0
- package/lib/yoke/adapters/claude.js +34 -0
- package/lib/yoke/adapters/codex.js +101 -5
- package/lib/yoke/adapters/copilot.js +7 -0
- package/lib/yoke/adapters/grok.js +7 -0
- package/lib/yoke/adapters/junie.js +7 -0
- package/lib/yoke/adapters/kimi.js +7 -0
- package/lib/yoke/adapters/kiro.js +2 -2
- package/lib/yoke/adapters/qwen.js +7 -0
- package/lib/yoke/codex-app-server.js +25 -8
- package/lib/yoke/index.js +67 -28
- package/lib/yoke/instructions.js +3 -0
- package/lib/yoke/interface.js +12 -0
- package/lib/yoke/vendor-registry.js +61 -6
- package/package.json +1 -1
- package/lib/public/gemini-avatar.svg +0 -11
- package/lib/yoke/adapters/gemini.js +0 -7
|
@@ -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
|
@@ -658,10 +658,6 @@ function attachSessions(ctx) {
|
|
|
658
658
|
// (resume_tui_session), and born-GUI never auto-converts to a terminal.
|
|
659
659
|
resolveSessionForView(xmTarget, ws);
|
|
660
660
|
}
|
|
661
|
-
// If the target session's vendor doesn't own the currently cached
|
|
662
|
-
// model, clear sm.currentModel so the UI and next query don't leak
|
|
663
|
-
// the previous session's vendor-specific model into this one. A
|
|
664
|
-
// session-specific model always wins and survives daemon restarts.
|
|
665
661
|
var switchTargetSess = sm.sessions.get(msg.id);
|
|
666
662
|
var switchTargetVendor = switchTargetSess && (switchTargetSess.vendor || sm.defaultVendor || "claude");
|
|
667
663
|
if (switchTargetSess && !switchTargetSess.effort) {
|
|
@@ -671,22 +667,6 @@ function attachSessions(ctx) {
|
|
|
671
667
|
) || null;
|
|
672
668
|
sm.saveSessionFile(switchTargetSess);
|
|
673
669
|
}
|
|
674
|
-
if (switchTargetSess && switchTargetSess.model) {
|
|
675
|
-
sm.currentModel = switchTargetSess.model;
|
|
676
|
-
} else if (switchTargetSess && sm.currentModel) {
|
|
677
|
-
var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
|
|
678
|
-
var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
|
|
679
|
-
var found = false;
|
|
680
|
-
var _curLc = sm.currentModel.toLowerCase();
|
|
681
|
-
for (var tvi = 0; tvi < tvModels.length; tvi++) {
|
|
682
|
-
var tvEntry = tvModels[tvi];
|
|
683
|
-
var tvVal = typeof tvEntry === "string" ? tvEntry : (tvEntry && (tvEntry.value || tvEntry.id)) || "";
|
|
684
|
-
if (tvVal === sm.currentModel || (tvVal && (tvVal.toLowerCase().indexOf(_curLc) !== -1 || _curLc.indexOf(tvVal.toLowerCase()) !== -1))) { found = true; break; }
|
|
685
|
-
}
|
|
686
|
-
if (tvModels.length > 0 && !found) {
|
|
687
|
-
sm.currentModel = "";
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
670
|
// Check access in multi-user mode
|
|
691
671
|
if (usersModule.isMultiUser() && ws._clayUser) {
|
|
692
672
|
var switchTarget = sm.sessions.get(msg.id);
|
|
@@ -958,14 +938,6 @@ function attachSessions(ctx) {
|
|
|
958
938
|
return true;
|
|
959
939
|
}
|
|
960
940
|
|
|
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
941
|
if (msg.type === "reload_skills") {
|
|
970
942
|
var session = getSessionForWs(ws);
|
|
971
943
|
if (session && sdk.reloadSkills) {
|
|
@@ -997,11 +969,7 @@ function attachSessions(ctx) {
|
|
|
997
969
|
" is bound to '" + vendorSession.vendor + "', refused rebind to '" + msg.vendor + "'");
|
|
998
970
|
} else {
|
|
999
971
|
vendorSession.vendor = msg.vendor;
|
|
1000
|
-
|
|
1001
|
-
// instead of leaking the previous vendor's model into a fresh session.
|
|
1002
|
-
if (sm.currentModel) {
|
|
1003
|
-
sm.currentModel = "";
|
|
1004
|
-
}
|
|
972
|
+
if (vendorSession.vendor !== msg.vendor) vendorSession.model = "";
|
|
1005
973
|
sm.saveSessionFile(vendorSession);
|
|
1006
974
|
sm.broadcastSessionList();
|
|
1007
975
|
}
|
|
@@ -1012,11 +980,11 @@ function attachSessions(ctx) {
|
|
|
1012
980
|
type: "model_info",
|
|
1013
981
|
model: "",
|
|
1014
982
|
models: vendorModels,
|
|
1015
|
-
vendor: msg.vendor,
|
|
983
|
+
vendor: msg.vendor, sessionId: vendorSession ? vendorSession.localId : null,
|
|
1016
984
|
availableVendors: sm.availableVendors || [],
|
|
1017
985
|
installedVendors: sm.installedVendors || [],
|
|
1018
986
|
});
|
|
1019
|
-
send({ type: "config_state",
|
|
987
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1020
988
|
}
|
|
1021
989
|
return true;
|
|
1022
990
|
}
|
|
@@ -1052,7 +1020,7 @@ function attachSessions(ctx) {
|
|
|
1052
1020
|
sm.broadcastSessionList();
|
|
1053
1021
|
sdk.setPermissionMode(session, msg.mode);
|
|
1054
1022
|
}
|
|
1055
|
-
send({ type: "config_state",
|
|
1023
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1056
1024
|
return true;
|
|
1057
1025
|
}
|
|
1058
1026
|
|
|
@@ -1113,7 +1081,7 @@ function attachSessions(ctx) {
|
|
|
1113
1081
|
if (session) {
|
|
1114
1082
|
sdk.setPermissionMode(session, msg.mode);
|
|
1115
1083
|
}
|
|
1116
|
-
send({ type: "config_state",
|
|
1084
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1117
1085
|
return true;
|
|
1118
1086
|
}
|
|
1119
1087
|
|
|
@@ -1126,7 +1094,7 @@ function attachSessions(ctx) {
|
|
|
1126
1094
|
if (session) {
|
|
1127
1095
|
sdk.setPermissionMode(session, msg.mode);
|
|
1128
1096
|
}
|
|
1129
|
-
send({ type: "config_state",
|
|
1097
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1130
1098
|
return true;
|
|
1131
1099
|
}
|
|
1132
1100
|
|
|
@@ -1142,7 +1110,7 @@ function attachSessions(ctx) {
|
|
|
1142
1110
|
sdk.setEffort(session, sessionEffort).catch(function (e) {
|
|
1143
1111
|
sendTo(ws, { type: "error", text: "Failed to change reasoning effort: " + (e.message || e) });
|
|
1144
1112
|
});
|
|
1145
|
-
sm.sendToSession(session, { type: "config_state", model: session.model || sm.
|
|
1113
|
+
sm.sendToSession(session, { type: "config_state", model: session.model || ((sm.defaultModelByVendor || {})[effortVendor]) || "", vendor: effortVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: sessionEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1146
1114
|
}
|
|
1147
1115
|
return true;
|
|
1148
1116
|
}
|
|
@@ -1152,7 +1120,7 @@ function attachSessions(ctx) {
|
|
|
1152
1120
|
opts.onSetServerDefaultEffort(msg.effort);
|
|
1153
1121
|
}
|
|
1154
1122
|
sm.currentEffort = msg.effort;
|
|
1155
|
-
send({ type: "config_state",
|
|
1123
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1156
1124
|
return true;
|
|
1157
1125
|
}
|
|
1158
1126
|
|
|
@@ -1161,20 +1129,20 @@ function attachSessions(ctx) {
|
|
|
1161
1129
|
opts.onSetProjectDefaultEffort(slug, msg.effort);
|
|
1162
1130
|
}
|
|
1163
1131
|
sm.currentEffort = msg.effort;
|
|
1164
|
-
send({ type: "config_state",
|
|
1132
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1165
1133
|
return true;
|
|
1166
1134
|
}
|
|
1167
1135
|
|
|
1168
1136
|
if (msg.type === "set_betas") {
|
|
1169
1137
|
sm.currentBetas = msg.betas || [];
|
|
1170
|
-
send({ type: "config_state",
|
|
1138
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1171
1139
|
return true;
|
|
1172
1140
|
}
|
|
1173
1141
|
|
|
1174
1142
|
if (msg.type === "set_thinking") {
|
|
1175
1143
|
sm.currentThinking = msg.thinking || "adaptive";
|
|
1176
1144
|
if (msg.budgetTokens) sm.currentThinkingBudget = msg.budgetTokens;
|
|
1177
|
-
send({ type: "config_state",
|
|
1145
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1178
1146
|
return true;
|
|
1179
1147
|
}
|
|
1180
1148
|
|
|
@@ -1456,7 +1424,7 @@ function attachSessions(ctx) {
|
|
|
1456
1424
|
if (decision === "allow_accept_edits") {
|
|
1457
1425
|
sdk.setPermissionMode(session, "acceptEdits");
|
|
1458
1426
|
sm.currentPermissionMode = "acceptEdits";
|
|
1459
|
-
send({ type: "config_state",
|
|
1427
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1460
1428
|
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
|
|
1461
1429
|
sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
|
|
1462
1430
|
return true;
|
|
@@ -1485,7 +1453,7 @@ function attachSessions(ctx) {
|
|
|
1485
1453
|
|
|
1486
1454
|
// Update permission mode for the new session
|
|
1487
1455
|
sm.currentPermissionMode = "acceptEdits";
|
|
1488
|
-
send({ type: "config_state",
|
|
1456
|
+
send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
1489
1457
|
|
|
1490
1458
|
// Build prompt from plan content (sent from client) or plan file path
|
|
1491
1459
|
var clientPlanContent = msg.planContent || "";
|
|
@@ -40,7 +40,7 @@ function attachWorkerProposal(ctx) {
|
|
|
40
40
|
function isEligible(session) {
|
|
41
41
|
if (ctx.isMate || !session || session.mode === "tui") return false;
|
|
42
42
|
if (store.groupForMember(session.localId)) return false;
|
|
43
|
-
return isFableSession(session, sm.modelsByVendor, sm.
|
|
43
|
+
return isFableSession(session, sm.modelsByVendor, (sm.defaultModelByVendor || {})[session.vendor || "claude"]);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function safeModelsByVendor(installed) {
|
|
@@ -112,7 +112,7 @@ function attachWorkerProposal(ctx) {
|
|
|
112
112
|
}
|
|
113
113
|
var models = options.modelsByVendor[vendor] || [];
|
|
114
114
|
var model = modelIsAvailable(options, vendor, args.recommendedModel) ? (args.recommendedModel || "") : "";
|
|
115
|
-
var currentModel = session.model || sm.
|
|
115
|
+
var currentModel = session.model || (sm.defaultModelByVendor || {})[session.vendor || "claude"] || "";
|
|
116
116
|
if (vendor === session.vendor && model === currentModel) model = "";
|
|
117
117
|
if (!model && models.length > 0) {
|
|
118
118
|
for (var j = 0; j < models.length; j++) {
|
|
@@ -154,6 +154,18 @@ function attachWorkerProposal(ctx) {
|
|
|
154
154
|
}, patch));
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function skipPermissionsEnabled(session) {
|
|
158
|
+
return !!session && (session.permissionMode === "bypassPermissions" || session.dangerouslySkipPermissions === true);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function autoApprovalWs(session) {
|
|
162
|
+
return {
|
|
163
|
+
_clayActiveSession: session.localId,
|
|
164
|
+
_clayUser: session.ownerId ? { id: session.ownerId } : null,
|
|
165
|
+
_autoApproval: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
157
169
|
async function propose(args, session) {
|
|
158
170
|
if (!isEligible(session)) return toolResult({ error: "Worker suggestions are only available in an unpaired Fable session." });
|
|
159
171
|
if (hasPendingProposal(session)) return toolResult({ error: "A Worker suggestion is already awaiting a decision." });
|
|
@@ -166,6 +178,7 @@ function attachWorkerProposal(ctx) {
|
|
|
166
178
|
var options = proposalOptions();
|
|
167
179
|
if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Worker session." });
|
|
168
180
|
var recommendation = chooseRecommendation(args, session, options);
|
|
181
|
+
var autoApprove = skipPermissionsEnabled(session);
|
|
169
182
|
var proposal = {
|
|
170
183
|
type: "worker_proposal",
|
|
171
184
|
proposalId: "worker_" + crypto.randomUUID(),
|
|
@@ -178,7 +191,22 @@ function attachWorkerProposal(ctx) {
|
|
|
178
191
|
recommendedEffort: recommendation.effort,
|
|
179
192
|
options: options,
|
|
180
193
|
};
|
|
194
|
+
if (autoApprove) proposal.autoApproved = true;
|
|
181
195
|
sm.sendAndRecord(session, proposal);
|
|
196
|
+
if (autoApprove) {
|
|
197
|
+
var accepted = await acceptProposal(session, proposal, {
|
|
198
|
+
vendor: recommendation.vendor,
|
|
199
|
+
model: recommendation.model,
|
|
200
|
+
effort: recommendation.effort,
|
|
201
|
+
autoApproved: true,
|
|
202
|
+
}, autoApprovalWs(session));
|
|
203
|
+
if (!accepted.ok) return toolResult({ error: accepted.error || "Could not start the Worker." });
|
|
204
|
+
return toolResult({
|
|
205
|
+
status: "running",
|
|
206
|
+
proposalId: proposal.proposalId,
|
|
207
|
+
instruction: "The Worker was auto-approved and started because skip permissions is enabled. Its result will return for review.",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
182
210
|
return toolResult({
|
|
183
211
|
status: "posted",
|
|
184
212
|
proposalId: proposal.proposalId,
|
|
@@ -238,30 +266,24 @@ function attachWorkerProposal(ctx) {
|
|
|
238
266
|
return session;
|
|
239
267
|
}
|
|
240
268
|
|
|
241
|
-
async function
|
|
242
|
-
var session = sessionForResponse(ws);
|
|
243
|
-
var proposal = findProposal(session, msg.proposalId);
|
|
244
|
-
if (!proposal) throw new Error("Worker suggestion not found");
|
|
245
|
-
if (proposal.status !== "pending") throw new Error("Worker suggestion has already been resolved");
|
|
246
|
-
if (!msg.accepted) {
|
|
247
|
-
updateProposal(session, proposal, { status: "declined" });
|
|
248
|
-
await resumeDriver(session, "[Worker suggestion declined]\nContinue this task in the current session using the plan you already prepared.");
|
|
249
|
-
return { ok: true, status: "declined" };
|
|
250
|
-
}
|
|
269
|
+
async function acceptProposal(session, proposal, msg, ws) {
|
|
251
270
|
var options = proposal.options || proposalOptions();
|
|
252
271
|
var vendor = msg.vendor || proposal.recommendedVendor;
|
|
253
272
|
var model = msg.model || "";
|
|
254
273
|
if (options.installedVendors.indexOf(vendor) === -1) throw new Error("Selected Worker vendor is not installed");
|
|
255
274
|
if (!modelIsAvailable(options, vendor, model)) throw new Error("Selected Worker model is unavailable");
|
|
256
275
|
var effort = yoke.clampEffort(vendor, msg.effort || proposal.recommendedEffort || "medium") || "";
|
|
257
|
-
|
|
276
|
+
var startingPatch = { status: "starting", selectedVendor: vendor, selectedModel: model, selectedEffort: effort };
|
|
277
|
+
if (msg.autoApproved) startingPatch.autoApproved = true;
|
|
278
|
+
updateProposal(session, proposal, startingPatch);
|
|
258
279
|
try {
|
|
259
280
|
var created = ctx.createPairRecord(ws, {
|
|
260
281
|
driver: { sessionId: session.localId },
|
|
261
282
|
worker: { vendor: vendor, model: model, effort: effort },
|
|
262
283
|
});
|
|
263
284
|
updateProposal(session, proposal, { status: "running", groupId: created.group.id, workerId: created.worker.localId });
|
|
264
|
-
|
|
285
|
+
if (ws._autoApproval) sm.sendToSession(session, { type: "pair_session_created", ok: true, group: created.group });
|
|
286
|
+
else ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
|
|
265
287
|
runWorker(session, proposal).catch(function (err) {
|
|
266
288
|
updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
|
|
267
289
|
resumeDriver(session, "[Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
|
|
@@ -273,6 +295,19 @@ function attachWorkerProposal(ctx) {
|
|
|
273
295
|
}
|
|
274
296
|
}
|
|
275
297
|
|
|
298
|
+
async function respondToProposal(ws, msg) {
|
|
299
|
+
var session = sessionForResponse(ws);
|
|
300
|
+
var proposal = findProposal(session, msg.proposalId);
|
|
301
|
+
if (!proposal) throw new Error("Worker suggestion not found");
|
|
302
|
+
if (proposal.status !== "pending") throw new Error("Worker suggestion has already been resolved");
|
|
303
|
+
if (!msg.accepted) {
|
|
304
|
+
updateProposal(session, proposal, { status: "declined" });
|
|
305
|
+
await resumeDriver(session, "[Worker suggestion declined]\nContinue this task in the current session using the plan you already prepared.");
|
|
306
|
+
return { ok: true, status: "declined" };
|
|
307
|
+
}
|
|
308
|
+
return acceptProposal(session, proposal, msg, ws);
|
|
309
|
+
}
|
|
310
|
+
|
|
276
311
|
function handleMessage(ws, msg) {
|
|
277
312
|
if (msg.type !== "worker_proposal_response") return false;
|
|
278
313
|
respondToProposal(ws, msg).catch(function (err) {
|
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");
|
|
@@ -163,7 +165,7 @@ function createProjectContext(opts) {
|
|
|
163
165
|
var updateChannel = opts.updateChannel || "stable";
|
|
164
166
|
var osUsers = opts.osUsers || false;
|
|
165
167
|
var projectOwnerId = opts.projectOwnerId || null;
|
|
166
|
-
var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch,
|
|
168
|
+
var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, external }
|
|
167
169
|
var isMate = opts.isMate || false;
|
|
168
170
|
var onCreateWorktree = opts.onCreateWorktree || null;
|
|
169
171
|
var serverPort = opts.port || 2633;
|
|
@@ -470,10 +472,9 @@ function createProjectContext(opts) {
|
|
|
470
472
|
|
|
471
473
|
var _projModel = typeof opts.onGetProjectDefaultModel === "function" ? opts.onGetProjectDefaultModel(slug) : null;
|
|
472
474
|
var _srvModel = typeof opts.onGetServerDefaultModel === "function" ? opts.onGetServerDefaultModel() : null;
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (sm._savedDefaultModel) sm.currentModel = sm._savedDefaultModel;
|
|
475
|
+
var savedDefaultModel = (_projModel && _projModel.model) || (_srvModel && _srvModel.model) || null;
|
|
476
|
+
sm.defaultModelByVendor = sm.defaultModelByVendor || {};
|
|
477
|
+
if (savedDefaultModel) sm.defaultModelByVendor[sm.defaultVendor || "claude"] = savedDefaultModel;
|
|
477
478
|
|
|
478
479
|
// --- Local MCP (direct process management for localhost clients) ---
|
|
479
480
|
var _localMcp = createLocalMcp();
|
|
@@ -569,6 +570,18 @@ function createProjectContext(opts) {
|
|
|
569
570
|
getLinuxUserForSession: getLinuxUserForSession,
|
|
570
571
|
getPairToolDefs: function (boundSession) { return _sessionPair.getToolDefs(boundSession); },
|
|
571
572
|
});
|
|
573
|
+
var _sessionHandoff = attachSessionHandoff({
|
|
574
|
+
cwd: cwd,
|
|
575
|
+
sm: sm,
|
|
576
|
+
isMate: isMate,
|
|
577
|
+
splitStore: _splitGroups.store,
|
|
578
|
+
getSdk: function () { return sdk; },
|
|
579
|
+
sendTo: sendTo,
|
|
580
|
+
usersModule: usersModule,
|
|
581
|
+
adapters: adapters,
|
|
582
|
+
getLinuxUserForSession: getLinuxUserForSession,
|
|
583
|
+
onProcessingChanged: onProcessingChanged,
|
|
584
|
+
});
|
|
572
585
|
var _debate = null;
|
|
573
586
|
var _debateProposal = attachDebateProposal({
|
|
574
587
|
cwd: cwd,
|
|
@@ -835,6 +848,7 @@ function createProjectContext(opts) {
|
|
|
835
848
|
getNotificationsModule: function () { return _notifications; },
|
|
836
849
|
mateDisplayName: opts.mateDisplayName || "",
|
|
837
850
|
isMate: isMate,
|
|
851
|
+
osUsers: osUsers,
|
|
838
852
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
839
853
|
mcpServers: getLocalMcpServers,
|
|
840
854
|
getRemoteMcpServers: function () { return _mcp.getMcpServers(); },
|
|
@@ -1052,78 +1066,6 @@ function createProjectContext(opts) {
|
|
|
1052
1066
|
return;
|
|
1053
1067
|
}
|
|
1054
1068
|
|
|
1055
|
-
// --- Vendor model switching ---
|
|
1056
|
-
if (msg.type === "get_vendor_models") {
|
|
1057
|
-
(async function() {
|
|
1058
|
-
if (msg.vendor) {
|
|
1059
|
-
try {
|
|
1060
|
-
var modelLinuxUser = getLinuxUserForWs(ws);
|
|
1061
|
-
var vendorAdapter = adapters[msg.vendor] || null;
|
|
1062
|
-
if (!vendorAdapter) {
|
|
1063
|
-
vendorAdapter = await yoke.lazyCreateAdapter(adapters, msg.vendor, {
|
|
1064
|
-
cwd: cwd,
|
|
1065
|
-
linuxUser: modelLinuxUser || undefined,
|
|
1066
|
-
clayPort: serverPort,
|
|
1067
|
-
clayTls: serverTls,
|
|
1068
|
-
clayAuthToken: serverAuthToken,
|
|
1069
|
-
slug: slug,
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
var needsReadyMetadata = !sm.capabilitiesByVendor || !sm.capabilitiesByVendor[msg.vendor]
|
|
1073
|
-
|| !sm.modelsByVendor || !sm.modelsByVendor[msg.vendor];
|
|
1074
|
-
if (vendorAdapter && needsReadyMetadata && typeof vendorAdapter.init === "function") {
|
|
1075
|
-
// Init warms the adapter, but a slow/failed init must not block
|
|
1076
|
-
// model listing (e.g. Codex models are a fixed list). Keep going
|
|
1077
|
-
// to supportedModels() even if init throws.
|
|
1078
|
-
try {
|
|
1079
|
-
var readyResult = await vendorAdapter.init({
|
|
1080
|
-
cwd: cwd,
|
|
1081
|
-
clayPort: serverPort,
|
|
1082
|
-
clayTls: serverTls,
|
|
1083
|
-
clayAuthToken: serverAuthToken,
|
|
1084
|
-
slug: slug,
|
|
1085
|
-
});
|
|
1086
|
-
sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
|
|
1087
|
-
sm.capabilitiesByVendor[msg.vendor] = readyResult.capabilities || {};
|
|
1088
|
-
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
1089
|
-
if (Array.isArray(readyResult.models)) sm.modelsByVendor[msg.vendor] = readyResult.models;
|
|
1090
|
-
} catch (e) {
|
|
1091
|
-
console.error("[project] " + msg.vendor + " init failed (continuing to model list):", e.message || e);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
if (vendorAdapter) {
|
|
1095
|
-
sm.availableVendors = Object.keys(adapters);
|
|
1096
|
-
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
1097
|
-
if (!sm.modelsByVendor[msg.vendor] && typeof vendorAdapter.supportedModels === "function") {
|
|
1098
|
-
sm.modelsByVendor[msg.vendor] = await vendorAdapter.supportedModels();
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
} catch (e) {
|
|
1102
|
-
console.error("[project] get_vendor_models lazy init failed for " + msg.vendor + ":", e.message || e);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[msg.vendor]) || [];
|
|
1106
|
-
var firstModel = vendorModels[0] || "";
|
|
1107
|
-
// model value can be string or {value, displayName} object
|
|
1108
|
-
var defaultModel = typeof firstModel === "string" ? firstModel : (firstModel.value || "");
|
|
1109
|
-
// Preserve the user's current model selection if it belongs to this
|
|
1110
|
-
// vendor, rather than always snapping back to the vendor's default.
|
|
1111
|
-
var modelToSend = defaultModel;
|
|
1112
|
-
if (sm.currentModel) {
|
|
1113
|
-
for (var mi = 0; mi < vendorModels.length; mi++) {
|
|
1114
|
-
var mv = typeof vendorModels[mi] === "string" ? vendorModels[mi] : (vendorModels[mi].value || "");
|
|
1115
|
-
if (mv === sm.currentModel) {
|
|
1116
|
-
modelToSend = sm.currentModel;
|
|
1117
|
-
break;
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
var vendorCapabilities = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[msg.vendor]) || {};
|
|
1122
|
-
sendTo(ws, { type: "model_info", model: modelToSend, models: vendorModels, vendor: msg.vendor, capabilities: vendorCapabilities, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
|
|
1123
|
-
})();
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
1069
|
// --- Debate ---
|
|
1128
1070
|
if (msg.type === "debate_start") {
|
|
1129
1071
|
handleDebateStart(ws, msg);
|
|
@@ -1171,7 +1113,9 @@ function createProjectContext(opts) {
|
|
|
1171
1113
|
|
|
1172
1114
|
// --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
|
|
1173
1115
|
if (_sessionPair.handleMessage(ws, msg)) return;
|
|
1116
|
+
if (_sessionHandoff.handleMessage(ws, msg)) return;
|
|
1174
1117
|
if (_splitGroups.handleMessage(ws, msg)) return;
|
|
1118
|
+
if (_models.handleMessage(ws, msg)) return;
|
|
1175
1119
|
if (_sessions.handleSessionsMessage(ws, msg)) return;
|
|
1176
1120
|
|
|
1177
1121
|
// --- Filesystem, settings, env (delegated to project-filesystem.js) ---
|
|
@@ -1384,6 +1328,20 @@ function createProjectContext(opts) {
|
|
|
1384
1328
|
_notifications: _notifications,
|
|
1385
1329
|
});
|
|
1386
1330
|
|
|
1331
|
+
var _models = attachModels({
|
|
1332
|
+
cwd: cwd,
|
|
1333
|
+
slug: slug,
|
|
1334
|
+
sm: sm,
|
|
1335
|
+
sdk: sdk,
|
|
1336
|
+
adapters: adapters,
|
|
1337
|
+
sendTo: sendTo,
|
|
1338
|
+
getSessionForWs: getSessionForWs,
|
|
1339
|
+
getLinuxUserForWs: getLinuxUserForWs,
|
|
1340
|
+
serverPort: serverPort,
|
|
1341
|
+
serverTls: serverTls,
|
|
1342
|
+
serverAuthToken: serverAuthToken,
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1387
1345
|
// --- User message handler (delegated to project-user-message.js) ---
|
|
1388
1346
|
var _userMessage = attachUserMessage({
|
|
1389
1347
|
cwd: cwd,
|
|
@@ -1686,8 +1644,8 @@ function createProjectContext(opts) {
|
|
|
1686
1644
|
getProject: function () { return project; },
|
|
1687
1645
|
// Exposed so the first websocket connection can lazily warm up the
|
|
1688
1646
|
// adapters for this project (see project-connection handleConnection).
|
|
1689
|
-
warmup: function () {
|
|
1690
|
-
sdk.warmup();
|
|
1647
|
+
warmup: function (linuxUser) {
|
|
1648
|
+
sdk.warmup(osUsers ? linuxUser : null);
|
|
1691
1649
|
sdk.startIdleReaper();
|
|
1692
1650
|
if (!osUsers && !sessionTitleMigrationScheduled) {
|
|
1693
1651
|
sessionTitleMigrationScheduled = true;
|
|
@@ -1813,7 +1771,7 @@ function createProjectContext(opts) {
|
|
|
1813
1771
|
status.isWorktree = true;
|
|
1814
1772
|
status.parentSlug = worktreeMeta.parentSlug;
|
|
1815
1773
|
status.branch = worktreeMeta.branch;
|
|
1816
|
-
status.
|
|
1774
|
+
status.worktreeExternal = worktreeMeta.external === true;
|
|
1817
1775
|
}
|
|
1818
1776
|
if (usersModule.isMultiUser()) {
|
|
1819
1777
|
var seen = {};
|
|
Binary file
|