clay-server 3.4.0-beta.9 → 3.4.0

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 (66) hide show
  1. package/lib/daemon.js +58 -58
  2. package/lib/project-connection.js +19 -6
  3. package/lib/project-models.js +220 -0
  4. package/lib/project-session-handoff.js +162 -0
  5. package/lib/project-session-pair.js +14 -7
  6. package/lib/project-session-spawn.js +1 -1
  7. package/lib/project-sessions.js +13 -45
  8. package/lib/project-worker-proposal.js +51 -14
  9. package/lib/project.js +33 -77
  10. package/lib/public/app.js +11 -0
  11. package/lib/public/copilot-avatar.svg +5 -0
  12. package/lib/public/css/filebrowser.css +6 -1
  13. package/lib/public/css/input.css +56 -0
  14. package/lib/public/css/mates.css +6 -0
  15. package/lib/public/css/menus.css +35 -56
  16. package/lib/public/css/pane.css +18 -3
  17. package/lib/public/css/session-actions.css +98 -0
  18. package/lib/public/grok-avatar.svg +4 -0
  19. package/lib/public/index.html +26 -9
  20. package/lib/public/junie-avatar.svg +11 -0
  21. package/lib/public/kimi-avatar.svg +4 -0
  22. package/lib/public/modules/agent-config-selects.js +69 -0
  23. package/lib/public/modules/app-connection.js +6 -0
  24. package/lib/public/modules/app-header.js +0 -22
  25. package/lib/public/modules/app-messages.js +48 -9
  26. package/lib/public/modules/app-panels.js +33 -82
  27. package/lib/public/modules/app-projects.js +2 -0
  28. package/lib/public/modules/app-rendering.js +16 -1
  29. package/lib/public/modules/background-tasks-ui.js +55 -0
  30. package/lib/public/modules/filebrowser-tabs.js +65 -0
  31. package/lib/public/modules/filebrowser.js +23 -3
  32. package/lib/public/modules/mate-sidebar.js +10 -2
  33. package/lib/public/modules/model-picker.js +263 -0
  34. package/lib/public/modules/pane-bridge.js +6 -0
  35. package/lib/public/modules/session-actions.js +294 -0
  36. package/lib/public/modules/sidebar-mobile.js +2 -1
  37. package/lib/public/modules/sidebar-sessions.js +17 -1
  38. package/lib/public/modules/split-pair-ui.js +16 -77
  39. package/lib/public/modules/split-view.js +11 -1
  40. package/lib/public/modules/tools.js +5 -0
  41. package/lib/public/modules/vendor-priority.js +6 -1
  42. package/lib/public/modules/worker-proposal.js +6 -1
  43. package/lib/public/qwen-avatar.svg +4 -0
  44. package/lib/public/style.css +1 -0
  45. package/lib/sdk-bridge.js +57 -34
  46. package/lib/sdk-message-processor.js +26 -4
  47. package/lib/session-handoff-context.js +110 -0
  48. package/lib/session-notes-mcp-server.js +1 -0
  49. package/lib/sessions.js +4 -0
  50. package/lib/ws-schema.js +9 -1
  51. package/lib/yoke/acp-agent-profiles.js +64 -0
  52. package/lib/yoke/adapters/acp.js +2 -1
  53. package/lib/yoke/adapters/claude.js +34 -0
  54. package/lib/yoke/adapters/codex.js +38 -3
  55. package/lib/yoke/adapters/copilot.js +7 -0
  56. package/lib/yoke/adapters/grok.js +7 -0
  57. package/lib/yoke/adapters/junie.js +7 -0
  58. package/lib/yoke/adapters/kimi.js +7 -0
  59. package/lib/yoke/adapters/kiro.js +4 -3
  60. package/lib/yoke/adapters/qwen.js +7 -0
  61. package/lib/yoke/codex-background-tasks.js +84 -0
  62. package/lib/yoke/index.js +43 -14
  63. package/lib/yoke/instructions.js +3 -0
  64. package/lib/yoke/interface.js +21 -0
  65. package/lib/yoke/vendor-registry.js +55 -0
  66. package/package.json +1 -1
@@ -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,
@@ -220,6 +248,8 @@ function attachWorkerProposal(ctx) {
220
248
  var followup;
221
249
  if (result.status === "complete") {
222
250
  followup = "[Worker execution completed]\nReview and verify the Worker's result. The Worker session remains available: if the implementation needs corrections or additional edits, send a follow-up with send_to_partner instead of taking over the Worker-owned files yourself. If that Worker is no longer available, create a replacement with spawn_sessions for the remaining implementation.\n\n" + (result.response || "The Worker completed without a text summary.");
251
+ } else if (result.status === "interrupted") {
252
+ followup = "[Worker execution interrupted]\nThe user interrupted the Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
223
253
  } else if (result.status === "running") {
224
254
  followup = "[Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
225
255
  } else {
@@ -238,30 +268,24 @@ function attachWorkerProposal(ctx) {
238
268
  return session;
239
269
  }
240
270
 
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
- }
271
+ async function acceptProposal(session, proposal, msg, ws) {
251
272
  var options = proposal.options || proposalOptions();
252
273
  var vendor = msg.vendor || proposal.recommendedVendor;
253
274
  var model = msg.model || "";
254
275
  if (options.installedVendors.indexOf(vendor) === -1) throw new Error("Selected Worker vendor is not installed");
255
276
  if (!modelIsAvailable(options, vendor, model)) throw new Error("Selected Worker model is unavailable");
256
277
  var effort = yoke.clampEffort(vendor, msg.effort || proposal.recommendedEffort || "medium") || "";
257
- updateProposal(session, proposal, { status: "starting", selectedVendor: vendor, selectedModel: model, selectedEffort: effort });
278
+ var startingPatch = { status: "starting", selectedVendor: vendor, selectedModel: model, selectedEffort: effort };
279
+ if (msg.autoApproved) startingPatch.autoApproved = true;
280
+ updateProposal(session, proposal, startingPatch);
258
281
  try {
259
282
  var created = ctx.createPairRecord(ws, {
260
283
  driver: { sessionId: session.localId },
261
284
  worker: { vendor: vendor, model: model, effort: effort },
262
285
  });
263
286
  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 });
287
+ if (ws._autoApproval) sm.sendToSession(session, { type: "pair_session_created", ok: true, group: created.group });
288
+ else ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
265
289
  runWorker(session, proposal).catch(function (err) {
266
290
  updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
267
291
  resumeDriver(session, "[Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
@@ -273,6 +297,19 @@ function attachWorkerProposal(ctx) {
273
297
  }
274
298
  }
275
299
 
300
+ async function respondToProposal(ws, msg) {
301
+ var session = sessionForResponse(ws);
302
+ var proposal = findProposal(session, msg.proposalId);
303
+ if (!proposal) throw new Error("Worker suggestion not found");
304
+ if (proposal.status !== "pending") throw new Error("Worker suggestion has already been resolved");
305
+ if (!msg.accepted) {
306
+ updateProposal(session, proposal, { status: "declined" });
307
+ await resumeDriver(session, "[Worker suggestion declined]\nContinue this task in the current session using the plan you already prepared.");
308
+ return { ok: true, status: "declined" };
309
+ }
310
+ return acceptProposal(session, proposal, msg, ws);
311
+ }
312
+
276
313
  function handleMessage(ws, msg) {
277
314
  if (msg.type !== "worker_proposal_response") return false;
278
315
  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");
@@ -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,
@@ -1053,79 +1066,6 @@ function createProjectContext(opts) {
1053
1066
  return;
1054
1067
  }
1055
1068
 
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
1069
  // --- Debate ---
1130
1070
  if (msg.type === "debate_start") {
1131
1071
  handleDebateStart(ws, msg);
@@ -1173,7 +1113,9 @@ function createProjectContext(opts) {
1173
1113
 
1174
1114
  // --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
1175
1115
  if (_sessionPair.handleMessage(ws, msg)) return;
1116
+ if (_sessionHandoff.handleMessage(ws, msg)) return;
1176
1117
  if (_splitGroups.handleMessage(ws, msg)) return;
1118
+ if (_models.handleMessage(ws, msg)) return;
1177
1119
  if (_sessions.handleSessionsMessage(ws, msg)) return;
1178
1120
 
1179
1121
  // --- Filesystem, settings, env (delegated to project-filesystem.js) ---
@@ -1386,6 +1328,20 @@ function createProjectContext(opts) {
1386
1328
  _notifications: _notifications,
1387
1329
  });
1388
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
+
1389
1345
  // --- User message handler (delegated to project-user-message.js) ---
1390
1346
  var _userMessage = attachUserMessage({
1391
1347
  cwd: cwd,
package/lib/public/app.js CHANGED
@@ -60,10 +60,12 @@ 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';
66
67
  import { initLoopUi, updateLoopInputVisibility as _loopUpdateLoopInputVisibility, updateLoopButton as _loopUpdateLoopButton, showLoopBanner as _loopShowLoopBanner, updateLoopBanner as _loopUpdateLoopBanner, updateRalphBars as _loopUpdateRalphBars, showRalphCraftingBar as _loopShowRalphCraftingBar, showRalphApprovalBar as _loopShowRalphApprovalBar, updateRalphApprovalStatus as _loopUpdateRalphApprovalStatus, openRalphPreviewModal as _loopOpenRalphPreviewModal, showExecModal as _loopShowExecModal, closeExecModal as _loopCloseExecModal, updateExecModalStatus as _loopUpdateExecModalStatus } from './modules/app-loop-ui.js';
68
+ import { initBackgroundTasksUi } from './modules/background-tasks-ui.js';
67
69
  import { initLoopWizard, openRalphWizard as _loopOpenRalphWizard, closeRalphWizard as _loopCloseRalphWizard, getWizardSource as _loopGetWizardSource } from './modules/app-loop-wizard.js';
68
70
  import { initAppNotifications, handleNotificationsState as _notifHandleState, handleNotificationCreated as _notifHandleCreated, handleNotificationDismissed as _notifHandleDismissed, handleNotificationDismissedAll as _notifHandleDismissedAll } from './modules/app-notifications.js';
69
71
  import { initWhatsNew, handleWhatsNewState as _wnHandleState, handleWhatsNewSeenResult as _wnHandleSeenResult } from './modules/whats-new.js';
@@ -295,6 +297,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
295
297
  slashCommands: [],
296
298
  processing: false,
297
299
  activeSessionId: null,
300
+ activeBackgroundTasks: [],
298
301
  sessionVendorBound: false,
299
302
  cliSessionId: null,
300
303
  isHeadlessMode: false,
@@ -339,6 +342,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
339
342
  markdownSlideLevelExplicit: false,
340
343
  currentModel: "",
341
344
  currentModels: [],
345
+ modelListVendor: "",
346
+ modelListStatus: "idle",
347
+ modelListError: "",
348
+ modelRequestId: "",
349
+ modelSelectionPending: null,
350
+ modelSelectionError: "",
342
351
  // Project's last-used vendor; seeds the sidebar's "New session" button.
343
352
  lastVendor: "",
344
353
  // Static adapter metadata sent before any vendor is initialized.
@@ -633,6 +642,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
633
642
 
634
643
  // --- Header module (rename, info popover, history) ---
635
644
  initHeader();
645
+ initSessionActions();
636
646
 
637
647
  // --- Skill Install module ---
638
648
  initSkillInstall();
@@ -641,6 +651,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
641
651
  initDebateUi();
642
652
 
643
653
  initLoopUi();
654
+ initBackgroundTasksUi();
644
655
  initLoopWizard();
645
656
 
646
657
  // --- Notifications module ---
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="GitHub Copilot CLI">
2
+ <rect width="64" height="64" rx="14" fill="#24292f"/>
3
+ <path d="M15 25c0-8 7-14 17-14s17 6 17 14v15c0 8-7 13-17 13s-17-5-17-13zm8 1v13c0 4 4 7 9 7s9-3 9-7V26c0-4-4-7-9-7s-9 3-9 7z" fill="#fff"/>
4
+ <circle cx="27" cy="30" r="3" fill="#24292f"/><circle cx="37" cy="30" r="3" fill="#24292f"/>
5
+ </svg>
@@ -960,7 +960,7 @@
960
960
  width: 50%;
961
961
  max-width: 720px;
962
962
  min-width: 360px;
963
- border-left: 1px solid var(--border);
963
+ border-right: 1px solid var(--border);
964
964
  background: var(--bg);
965
965
  display: flex;
966
966
  flex-direction: column;
@@ -1011,6 +1011,11 @@
1011
1011
  min-height: 44px;
1012
1012
  }
1013
1013
 
1014
+ .file-viewer-tabs { display: flex; min-width: 0; flex: 1; overflow-x: auto; gap: 2px; }
1015
+ .file-viewer-tab { display: inline-flex; align-items: center; gap: 6px; max-width: 170px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 0; border-radius: 5px; padding: 4px 7px; background: transparent; color: var(--text-secondary); font: inherit; font-size: 12px; cursor: pointer; }
1016
+ .file-viewer-tab.active { background: var(--sidebar-hover); color: var(--text); }
1017
+ .file-viewer-tab-close { font-size: 16px; line-height: 12px; color: var(--text-muted); }
1018
+
1014
1019
  .file-viewer-path {
1015
1020
  flex: 1;
1016
1021
  font-family: "Roboto Mono", monospace;
@@ -667,6 +667,50 @@
667
667
  position: relative;
668
668
  }
669
669
 
670
+ .background-tasks-bar {
671
+ max-width: var(--content-width);
672
+ margin: 0 auto 8px;
673
+ border: 1px solid var(--border);
674
+ border-radius: 8px;
675
+ background: var(--input-bg);
676
+ overflow: hidden;
677
+ }
678
+
679
+ .background-tasks-toggle {
680
+ width: 100%;
681
+ display: flex;
682
+ align-items: center;
683
+ gap: 7px;
684
+ padding: 8px 10px;
685
+ border: 0;
686
+ background: transparent;
687
+ color: var(--text-secondary);
688
+ cursor: pointer;
689
+ font: inherit;
690
+ text-align: left;
691
+ }
692
+
693
+ .background-tasks-chip, .session-background-task-count {
694
+ display: inline-flex;
695
+ align-items: center;
696
+ justify-content: center;
697
+ min-width: 16px;
698
+ height: 16px;
699
+ padding: 0 4px;
700
+ border-radius: 8px;
701
+ background: var(--accent);
702
+ color: var(--bg);
703
+ font-size: 10px;
704
+ font-weight: 700;
705
+ }
706
+
707
+ .background-tasks-list { padding: 0 10px 8px; }
708
+ .background-task-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-top: 1px solid var(--border-subtle); }
709
+ .background-task-description { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
710
+ .background-task-type { padding: 2px 6px; border-radius: 5px; background: var(--code-bg); color: var(--text-muted); font-size: 11px; }
711
+ .background-task-stop { border: 1px solid var(--border); border-radius: 5px; padding: 3px 7px; background: transparent; color: var(--text-secondary); cursor: pointer; font: inherit; font-size: 12px; }
712
+ .background-task-stop:hover { color: var(--text); border-color: var(--text-muted); }
713
+
670
714
  #input-row {
671
715
  display: flex;
672
716
  flex-direction: column;
@@ -790,6 +834,7 @@
790
834
  opacity: 0.35;
791
835
  cursor: default;
792
836
  }
837
+ .vendor-toggle-btn.disabled:not(.active) { display: none; }
793
838
  .vendor-toggle-icon {
794
839
  width: 16px;
795
840
  height: 16px;
@@ -799,6 +844,17 @@
799
844
  .vendor-toggle-label {
800
845
  pointer-events: none;
801
846
  }
847
+ .vendor-toggle-btn.experimental::after,
848
+ .vendor-experimental-badge {
849
+ content: "🧪";
850
+ font-size: 10px;
851
+ line-height: 1;
852
+ font-weight: 400;
853
+ filter: saturate(0.85);
854
+ }
855
+ .vendor-experimental-badge {
856
+ margin-left: auto;
857
+ }
802
858
  @media (min-width: 901px) {
803
859
  .vendor-toggle-btn:not(.active) .vendor-toggle-label { display: none; }
804
860
  .vendor-toggle-btn:not(.active) { padding: 0 8px; }
@@ -682,6 +682,12 @@
682
682
  .mate-vendor-label {
683
683
  pointer-events: none;
684
684
  }
685
+ .mate-vendor-btn.experimental::after {
686
+ content: "🧪";
687
+ font-size: 9px;
688
+ line-height: 1;
689
+ font-weight: 400;
690
+ }
685
691
  .mate-vendor-btn:not(.active) .mate-vendor-label { display: none; }
686
692
 
687
693
  /* Collapse button inside mate header: white to match header style */