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.
Files changed (80) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +59 -59
  3. package/lib/project-connection.js +35 -7
  4. package/lib/project-debate.js +4 -0
  5. package/lib/project-mate-interaction.js +21 -1
  6. package/lib/project-memory.js +3 -0
  7. package/lib/project-models.js +220 -0
  8. package/lib/project-session-handoff.js +162 -0
  9. package/lib/project-sessions.js +13 -45
  10. package/lib/project-worker-proposal.js +49 -14
  11. package/lib/project.js +38 -80
  12. package/lib/public/antigravity-avatar.png +0 -0
  13. package/lib/public/app.js +29 -0
  14. package/lib/public/copilot-avatar.svg +5 -0
  15. package/lib/public/css/command-palette.css +22 -2
  16. package/lib/public/css/icon-strip.css +29 -13
  17. package/lib/public/css/input.css +56 -0
  18. package/lib/public/css/mates.css +6 -0
  19. package/lib/public/css/menus.css +38 -25
  20. package/lib/public/css/messages.css +9 -0
  21. package/lib/public/css/pane.css +16 -3
  22. package/lib/public/css/pwa-mobile.css +17 -0
  23. package/lib/public/css/session-actions.css +98 -0
  24. package/lib/public/css/title-bar.css +19 -0
  25. package/lib/public/grok-avatar.svg +4 -0
  26. package/lib/public/index.html +29 -7
  27. package/lib/public/junie-avatar.svg +11 -0
  28. package/lib/public/kimi-avatar.svg +4 -0
  29. package/lib/public/modules/agent-config-selects.js +69 -0
  30. package/lib/public/modules/app-header.js +4 -22
  31. package/lib/public/modules/app-messages.js +54 -11
  32. package/lib/public/modules/app-panels.js +36 -83
  33. package/lib/public/modules/app-projects.js +3 -1
  34. package/lib/public/modules/app-rendering.js +19 -4
  35. package/lib/public/modules/background-tasks-ui.js +55 -0
  36. package/lib/public/modules/mate-sidebar.js +11 -3
  37. package/lib/public/modules/model-picker.js +263 -0
  38. package/lib/public/modules/notifications.js +7 -1
  39. package/lib/public/modules/pane-bridge.js +11 -0
  40. package/lib/public/modules/pane-links.js +23 -0
  41. package/lib/public/modules/project-switcher.js +7 -6
  42. package/lib/public/modules/session-actions.js +294 -0
  43. package/lib/public/modules/sidebar-mates.js +1 -1
  44. package/lib/public/modules/sidebar-mobile.js +2 -1
  45. package/lib/public/modules/sidebar-projects.js +34 -43
  46. package/lib/public/modules/sidebar-sessions.js +20 -6
  47. package/lib/public/modules/split-pair-ui.js +16 -77
  48. package/lib/public/modules/split-view.js +3 -0
  49. package/lib/public/modules/tools.js +6 -1
  50. package/lib/public/modules/vendor-priority.js +14 -0
  51. package/lib/public/modules/vendor-selection.js +20 -0
  52. package/lib/public/modules/worker-proposal.js +5 -1
  53. package/lib/public/modules/worktree-location.js +17 -0
  54. package/lib/public/qwen-avatar.svg +4 -0
  55. package/lib/public/style.css +4 -2
  56. package/lib/sdk-bridge.js +99 -56
  57. package/lib/sdk-message-processor.js +26 -4
  58. package/lib/session-handoff-context.js +110 -0
  59. package/lib/session-notes-mcp-server.js +8 -1
  60. package/lib/sessions.js +4 -0
  61. package/lib/worktree.js +9 -4
  62. package/lib/ws-schema.js +9 -1
  63. package/lib/yoke/acp-agent-profiles.js +64 -14
  64. package/lib/yoke/adapters/antigravity.js +417 -0
  65. package/lib/yoke/adapters/claude.js +34 -0
  66. package/lib/yoke/adapters/codex.js +101 -5
  67. package/lib/yoke/adapters/copilot.js +7 -0
  68. package/lib/yoke/adapters/grok.js +7 -0
  69. package/lib/yoke/adapters/junie.js +7 -0
  70. package/lib/yoke/adapters/kimi.js +7 -0
  71. package/lib/yoke/adapters/kiro.js +2 -2
  72. package/lib/yoke/adapters/qwen.js +7 -0
  73. package/lib/yoke/codex-app-server.js +25 -8
  74. package/lib/yoke/index.js +67 -28
  75. package/lib/yoke/instructions.js +3 -0
  76. package/lib/yoke/interface.js +12 -0
  77. package/lib/yoke/vendor-registry.js +61 -6
  78. package/package.json +1 -1
  79. package/lib/public/gemini-avatar.svg +0 -11
  80. 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
+ };
@@ -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
- // Clear the shared model so the next query uses the vendor's default
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", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode || "default", effort: sessionEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
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.currentModel);
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.currentModel || "";
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 respondToProposal(ws, msg) {
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
- updateProposal(session, proposal, { status: "starting", selectedVendor: vendor, selectedModel: model, selectedEffort: effort });
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
- ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
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, accessible }
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
- sm._savedDefaultModel = (_projModel && _projModel.model) || (_srvModel && _srvModel.model) || null;
474
- // Immediately apply the saved default so config_state on connect reflects it
475
- // before the SDK has warmed up and fired system/init.
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.worktreeAccessible = worktreeMeta.accessible;
1774
+ status.worktreeExternal = worktreeMeta.external === true;
1817
1775
  }
1818
1776
  if (usersModule.isMultiUser()) {
1819
1777
  var seen = {};