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
@@ -1,7 +1,7 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
- import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER } from './app-rendering.js';
4
- import { effortLevelsFor, effortDisplayName } from './app-panels.js';
3
+ import { VENDOR_AVATARS } from './app-rendering.js';
4
+ import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAgentEffort } from './agent-config-selects.js';
5
5
  import { showToast } from './utils.js';
6
6
 
7
7
  var pairDialog = null;
@@ -9,77 +9,14 @@ var pairDialog = null;
9
9
  // that session becomes the Driver and the dialog only configures the Worker.
10
10
  var pendingDriver = null;
11
11
 
12
- function optionValue(entry) {
13
- if (typeof entry === "string") return entry;
14
- return entry && (entry.value || entry.id) || "";
15
- }
16
-
17
- function optionLabel(entry) {
18
- if (typeof entry === "string") return entry;
19
- return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
20
- }
21
-
22
- function fillModels(select, vendor, options) {
23
- var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
24
- select.innerHTML = "";
25
- var automatic = document.createElement("option");
26
- automatic.value = "";
27
- automatic.textContent = "Automatic";
28
- select.appendChild(automatic);
29
- for (var i = 0; i < models.length; i++) {
30
- var option = document.createElement("option");
31
- option.value = optionValue(models[i]);
32
- option.textContent = optionLabel(models[i]);
33
- select.appendChild(option);
34
- }
35
- }
36
-
37
12
  function closePairDialog() {
38
13
  if (pairDialog) pairDialog.remove();
39
14
  pairDialog = null;
40
15
  }
41
16
 
42
- function buildVendorSelect(installed, preferred) {
43
- var select = document.createElement("select");
44
- select.className = "wt-modal-input";
45
- for (var i = 0; i < VENDOR_ORDER.length; i++) {
46
- if (installed.indexOf(VENDOR_ORDER[i]) === -1) continue;
47
- var option = document.createElement("option");
48
- option.value = VENDOR_ORDER[i];
49
- option.textContent = VENDOR_NAMES[VENDOR_ORDER[i]] || VENDOR_ORDER[i];
50
- select.appendChild(option);
51
- }
52
- if (preferred && installed.indexOf(preferred) !== -1) select.value = preferred;
53
- return select;
54
- }
55
-
56
17
  // Effort levels are vendor- and model-specific (codex: minimal..xhigh,
57
18
  // claude/kiro: low..max, plus per-model supportedEffortLevels overrides), so
58
19
  // options are rebuilt whenever the role's vendor or model changes.
59
- function fillEffortOptions(select, vendor, options, modelValue) {
60
- var previous = select.value;
61
- var models = (options.modelsByVendor && options.modelsByVendor[vendor]) || [];
62
- var levels = effortLevelsFor(vendor, models, modelValue);
63
- select.innerHTML = "";
64
- var automatic = document.createElement("option");
65
- automatic.value = "";
66
- automatic.textContent = "Default";
67
- select.appendChild(automatic);
68
- for (var i = 0; i < levels.length; i++) {
69
- var option = document.createElement("option");
70
- option.value = levels[i];
71
- option.textContent = effortDisplayName(levels[i]);
72
- select.appendChild(option);
73
- }
74
- select.value = levels.indexOf(previous) !== -1 ? previous : "";
75
- }
76
-
77
- function buildEffortSelect() {
78
- var select = document.createElement("select");
79
- select.className = "wt-modal-input";
80
- return select;
81
- }
82
-
83
20
  function fieldLabel(text, control) {
84
21
  var label = document.createElement("label");
85
22
  label.className = "wt-modal-label";
@@ -154,25 +91,25 @@ export function showPairDialog(options) {
154
91
  : "One Driver plans and directs a visible Worker session.";
155
92
  modal.appendChild(desc);
156
93
 
157
- var driverVendor = buildVendorSelect(installed, installed.indexOf("claude") !== -1 ? "claude" : installed[0]);
94
+ var driverVendor = buildAgentVendorSelect(installed, installed.indexOf("claude") !== -1 ? "claude" : installed[0]);
158
95
  var workerDefault = options.lastVendor && options.lastVendor !== driverVendor.value ? options.lastVendor : (installed.indexOf("codex") !== -1 ? "codex" : installed[0]);
159
- var workerVendor = buildVendorSelect(installed, workerDefault);
96
+ var workerVendor = buildAgentVendorSelect(installed, workerDefault);
160
97
  var driverModel = document.createElement("select");
161
98
  driverModel.className = "wt-modal-input";
162
99
  var workerModel = document.createElement("select");
163
100
  workerModel.className = "wt-modal-input";
164
- var driverEffort = buildEffortSelect();
165
- var workerEffort = buildEffortSelect();
166
- function refreshDriverEffort() { fillEffortOptions(driverEffort, driverVendor.value, options, driverModel.value); }
167
- function refreshWorkerEffort() { fillEffortOptions(workerEffort, workerVendor.value, options, workerModel.value); }
168
- fillModels(driverModel, driverVendor.value, options);
169
- fillModels(workerModel, workerVendor.value, options);
101
+ var driverEffort = buildAgentEffortSelect();
102
+ var workerEffort = buildAgentEffortSelect();
103
+ function refreshDriverEffort() { fillAgentEffort(driverEffort, driverVendor.value, options, driverModel.value); }
104
+ function refreshWorkerEffort() { fillAgentEffort(workerEffort, workerVendor.value, options, workerModel.value); }
105
+ fillAgentModels(driverModel, driverVendor.value, options);
106
+ fillAgentModels(workerModel, workerVendor.value, options);
170
107
  refreshDriverEffort();
171
108
  refreshWorkerEffort();
172
109
  workerEffort.value = "medium";
173
110
  if (workerEffort.value !== "medium") workerEffort.value = "";
174
- driverVendor.addEventListener("change", function () { fillModels(driverModel, driverVendor.value, options); refreshDriverEffort(); });
175
- workerVendor.addEventListener("change", function () { fillModels(workerModel, workerVendor.value, options); refreshWorkerEffort(); });
111
+ driverVendor.addEventListener("change", function () { fillAgentModels(driverModel, driverVendor.value, options); refreshDriverEffort(); });
112
+ workerVendor.addEventListener("change", function () { fillAgentModels(workerModel, workerVendor.value, options); refreshWorkerEffort(); });
176
113
  driverModel.addEventListener("change", refreshDriverEffort);
177
114
  workerModel.addEventListener("change", refreshWorkerEffort);
178
115
 
@@ -318,7 +255,8 @@ export function syncPairChrome(host, split) {
318
255
  setBtn.title = "Make this session the Driver; the other pane becomes its Worker";
319
256
  setBtn.addEventListener("click", function () { sendSetPair(group.id, paneSessionId); });
320
257
  var titleEl = header.querySelector(".split-pane-title");
321
- header.insertBefore(setBtn, titleEl ? titleEl.nextSibling : null);
258
+ var accessEl = header.querySelector(".split-pane-full-access");
259
+ header.insertBefore(setBtn, accessEl ? accessEl.nextSibling : (titleEl ? titleEl.nextSibling : null));
322
260
  })(split.panes[ai].sessionId, headers[ai]);
323
261
  }
324
262
  return;
@@ -338,7 +276,8 @@ export function syncPairChrome(host, split) {
338
276
  sendSetPair(group.id, isDriver ? null : sessionId);
339
277
  });
340
278
  var title = header.querySelector(".split-pane-title");
341
- header.insertBefore(badge, title ? title.nextSibling : null);
279
+ var access = header.querySelector(".split-pane-full-access");
280
+ header.insertBefore(badge, access ? access.nextSibling : (title ? title.nextSibling : null));
342
281
  })(split.panes[pi].sessionId, headers[pi]);
343
282
  }
344
283
  var active = (store.get('splitDelegations') || {})[group.id];
@@ -10,6 +10,7 @@ import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
10
10
  import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
11
11
  import { showConfirm } from './app-misc.js';
12
12
  import { syncPairChrome } from './split-pair-ui.js';
13
+ import { presentMarkdownEdit } from './filebrowser.js';
13
14
 
14
15
  var host = null;
15
16
  var nativeApp = null;
@@ -231,6 +232,9 @@ function createPane(pane, index) {
231
232
  }, "Skip Permissions", true);
232
233
  });
233
234
  header.appendChild(fullAccess);
235
+ // Permission state belongs to the session identity on the left. Keep it
236
+ // immediately after the title instead of grouping it with usage and close.
237
+ header.insertBefore(fullAccess, ctxChip);
234
238
 
235
239
  var close = document.createElement("button");
236
240
  close.type = "button";
@@ -266,7 +270,13 @@ function updatePaneContextChip(paneEl, msg) {
266
270
  function handlePaneMessage(event) {
267
271
  if (event.origin !== window.location.origin) return;
268
272
  var msg = event.data;
269
- if (!msg || msg.type !== "clay-pane-context" || !host) return;
273
+ if (!msg || !host) return;
274
+ if (msg.type === "clay-pane-present-markdown") {
275
+ var present = msg.message;
276
+ if (present) presentMarkdownEdit(present);
277
+ return;
278
+ }
279
+ if (msg.type !== "clay-pane-context") return;
270
280
  var frames = host.querySelectorAll(".split-pane-frame");
271
281
  for (var i = 0; i < frames.length; i++) {
272
282
  if (frames[i].contentWindow === event.source) {
@@ -819,6 +819,11 @@ function resolvePermissionIdentity(mateId, vendor) {
819
819
  codex: "/codex-avatar.png",
820
820
  antigravity: "/antigravity-avatar.png",
821
821
  opencode: "/opencode-avatar.svg",
822
+ kimi: "/kimi-avatar.svg",
823
+ grok: "/grok-avatar.svg",
824
+ copilot: "/copilot-avatar.svg",
825
+ qwen: "/qwen-avatar.svg",
826
+ junie: "/junie-avatar.svg",
822
827
  kiro: "/kiro-avatar.svg",
823
828
  };
824
829
  var vendorName = (vendor && VENDOR_NAMES[vendor]) || VENDOR_NAMES.claude;
@@ -1,4 +1,9 @@
1
- export var VENDOR_ORDER = ["claude", "codex", "antigravity", "opencode", "kiro"];
1
+ export var VENDOR_ORDER = ["claude", "codex", "grok", "kimi", "copilot", "qwen", "junie", "antigravity", "opencode", "kiro"];
2
+ export var EXPERIMENTAL_VENDORS = ["grok", "kimi", "copilot", "qwen", "junie", "antigravity", "opencode"];
3
+
4
+ export function isExperimentalVendor(vendor) {
5
+ return EXPERIMENTAL_VENDORS.indexOf(vendor) !== -1;
6
+ }
2
7
 
3
8
  export function firstInstalledVendor(installedVendors) {
4
9
  var installed = Array.isArray(installedVendors) ? installedVendors : [];
@@ -95,6 +95,7 @@ function statusLabel(status) {
95
95
  if (status === "starting") return "Starting Worker";
96
96
  if (status === "running") return "Worker running";
97
97
  if (status === "completed") return "Completed";
98
+ if (status === "interrupted") return "Interrupted";
98
99
  if (status === "declined") return "Continuing here";
99
100
  if (status === "error") return "Needs attention";
100
101
  return "Suggested by Fable";
@@ -103,8 +104,10 @@ function statusLabel(status) {
103
104
  function applyState(card, msg) {
104
105
  var status = msg.status || "pending";
105
106
  card.dataset.status = status;
107
+ if (msg.autoApproved) card.dataset.autoApproved = "true";
108
+ var autoApproved = card.dataset.autoApproved === "true";
106
109
  var badge = card.querySelector(".worker-proposal-status");
107
- if (badge) badge.textContent = statusLabel(status);
110
+ if (badge) badge.textContent = statusLabel(status) + (autoApproved ? " · auto-approved" : "");
108
111
  var error = card.querySelector(".worker-proposal-error");
109
112
  if (error) {
110
113
  error.textContent = msg.error || "";
@@ -116,6 +119,8 @@ function applyState(card, msg) {
116
119
  preview.classList.remove("hidden");
117
120
  }
118
121
  setControlsDisabled(card, status !== "pending");
122
+ var actions = card.querySelector(".worker-proposal-actions");
123
+ if (actions) actions.classList.toggle("hidden", autoApproved);
119
124
  }
120
125
 
121
126
  function sendDecision(card, accepted) {
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Qwen Code">
2
+ <rect width="64" height="64" rx="14" fill="#6b4eff"/>
3
+ <path d="M32 12a20 20 0 1 0 12 36l7 5-3-11a20 20 0 0 0-16-30zm0 9a11 11 0 1 1-7.8 18.8A11 11 0 0 1 32 21z" fill="#fff"/>
4
+ </svg>
@@ -3,6 +3,7 @@
3
3
  @import url("css/title-bar.css?v=20260824");
4
4
  @import url("css/pane.css");
5
5
  @import url("css/worker-proposal.css");
6
+ @import url("css/session-actions.css");
6
7
  @import url("css/sidebar.css");
7
8
  @import url("css/overlays.css");
8
9
  @import url("css/menus.css");
package/lib/sdk-bridge.js CHANGED
@@ -245,17 +245,24 @@ function createSDKBridge(opts) {
245
245
  return null;
246
246
  }
247
247
 
248
- function sendModelInfoForVendor(vendor, model) {
248
+ function defaultModelForVendor(vendor) {
249
+ return (sm.defaultModelByVendor && sm.defaultModelByVendor[vendor]) || "";
250
+ }
251
+
252
+ function sendModelInfoForVendor(session, vendor, model) {
249
253
  var resolvedVendor = vendor || (adapter && adapter.vendor) || "claude";
250
- send({
254
+ var message = {
251
255
  type: "model_info",
252
256
  model: model || "",
253
257
  models: getModelsForVendor(resolvedVendor),
254
258
  vendor: resolvedVendor,
259
+ sessionId: session ? session.localId : null,
255
260
  capabilities: (sm.capabilitiesByVendor && sm.capabilitiesByVendor[resolvedVendor]) || {},
256
261
  availableVendors: sm.availableVendors || [],
257
262
  installedVendors: sm.installedVendors || [],
258
- });
263
+ };
264
+ if (session) sendToSession(session, message);
265
+ else send(message);
259
266
  }
260
267
 
261
268
  function rememberAdapterReady(vendor, result) {
@@ -849,19 +856,20 @@ function createSDKBridge(opts) {
849
856
  case "model_changed":
850
857
  session.model = metaData.model;
851
858
  sm.saveSessionFile(session);
852
- sm.currentModel = metaData.model;
853
- sendModelInfoForVendor(session.vendor || (adapter && adapter.vendor) || "claude", metaData.model);
854
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
859
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
860
+ sm.defaultModelByVendor[session.vendor || "claude"] = metaData.model;
861
+ sendModelInfoForVendor(session, session.vendor || (adapter && adapter.vendor) || "claude", metaData.model);
862
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
855
863
  break;
856
864
  case "effort_changed":
857
865
  session.effort = metaData.effort;
858
866
  sm.saveSessionFile(session);
859
867
  sm.currentEffort = metaData.effort;
860
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort, betas: sm.currentBetas || [] });
868
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort, betas: sm.currentBetas || [] });
861
869
  break;
862
870
  case "permission_mode_changed":
863
871
  sm.currentPermissionMode = metaData.mode;
864
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
872
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
865
873
  break;
866
874
  case "worker_error":
867
875
  send({ type: "error", text: metaData.error });
@@ -877,7 +885,10 @@ function createSDKBridge(opts) {
877
885
  console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
878
886
  var stillOwnsRuntime = session.queryInstance === myQueryInstance;
879
887
  if (session.isProcessing && session.taskStopRequested && stillOwnsRuntime) {
888
+ session._lastTurnInterrupted = true;
880
889
  session.isProcessing = false;
890
+ session._awaitingTurnResult = false;
891
+ session._queuedTurnCount = 0;
881
892
  onProcessingChanged();
882
893
  send({ type: "status", processing: false });
883
894
  sendAndRecord(session, { type: "thinking_stop" });
@@ -892,6 +903,8 @@ function createSDKBridge(opts) {
892
903
  // iterator without a result event. Treat any result-less stream end as
893
904
  // a failed turn so the UI cannot remain on processing forever.
894
905
  session.isProcessing = false;
906
+ session._awaitingTurnResult = false;
907
+ session._queuedTurnCount = 0;
895
908
  onProcessingChanged();
896
909
  if (!session._lastAdapterError) {
897
910
  sendAndRecord(session, {
@@ -906,8 +919,11 @@ function createSDKBridge(opts) {
906
919
  } catch (err) {
907
920
  if (session.isProcessing && session.queryInstance === myQueryInstance) {
908
921
  session.isProcessing = false;
922
+ session._awaitingTurnResult = false;
923
+ session._queuedTurnCount = 0;
909
924
  onProcessingChanged();
910
925
  if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
926
+ session._lastTurnInterrupted = true;
911
927
  if (!session.destroying) {
912
928
  sendAndRecord(session, { type: "thinking_stop" });
913
929
  var interruptMsg2 = (session.vendor && session.vendor !== "claude")
@@ -1175,6 +1191,7 @@ function createSDKBridge(opts) {
1175
1191
  // Wrapper: marks the boot window so pushMessage can buffer instead of
1176
1192
  // dropping messages that arrive while createQuery is still awaiting.
1177
1193
  async function startQuery(session, text, images, linuxUser) {
1194
+ delete session._lastTurnInterrupted;
1178
1195
  session._queryStarting = true;
1179
1196
  try {
1180
1197
  return await startQueryInner(session, text, images, linuxUser);
@@ -1226,10 +1243,10 @@ function createSDKBridge(opts) {
1226
1243
  sm.modelsByVendor[vendor] = await vendorAdapter.supportedModels();
1227
1244
  }
1228
1245
  var vendorModels = getModelsForVendor(vendor);
1229
- var modelForVendor = resolveModelInList(vendorModels, sm.currentModel)
1246
+ var modelForVendor = resolveModelInList(vendorModels, defaultModelForVendor(vendor))
1230
1247
  || modelEntryValue(vendorModels[0])
1231
1248
  || "";
1232
- sendModelInfoForVendor(vendor, modelForVendor);
1249
+ sendModelInfoForVendor(session, vendor, modelForVendor);
1233
1250
  }
1234
1251
  return vendorAdapter;
1235
1252
  }
@@ -1432,14 +1449,9 @@ function createSDKBridge(opts) {
1432
1449
  }
1433
1450
  }
1434
1451
 
1435
- // Pick a model that belongs to the session's vendor. sm.currentModel is
1436
- // shared project-wide, so a Codex session that last set it to
1437
- // "gpt-5.4-mini" would otherwise leak into a Claude session in the same
1438
- // project (or in another session that switches vendor to claude) and
1439
- // Claude would reject the unknown model. We validate against the
1440
- // session vendor's model list regardless of which vendor happens to be
1441
- // the project's default adapter.
1442
- var queryModel = (ls.model && ls.model !== "default" ? ls.model : null) || session.model || sm.currentModel || undefined;
1452
+ // Pick a model from the session or its vendor-specific default, then
1453
+ // validate it against the session vendor's known model list.
1454
+ var queryModel = (ls.model && ls.model !== "default" ? ls.model : null) || session.model || defaultModelForVendor(session.vendor || "claude") || undefined;
1443
1455
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
1444
1456
  if (sessionVendor) {
1445
1457
  var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
@@ -1455,8 +1467,7 @@ function createSDKBridge(opts) {
1455
1467
  }
1456
1468
 
1457
1469
  // Bind the resolved model to the session so it survives a daemon
1458
- // restart. Without this, a session started under sm.currentModel (an
1459
- // in-memory value) came back after restart on the adapter default.
1470
+ // restart. This preserves a session-specific selection across restarts.
1460
1471
  if (queryModel && session.model !== queryModel) {
1461
1472
  session.model = queryModel;
1462
1473
  sm.saveSessionFile(session);
@@ -1584,6 +1595,8 @@ function createSDKBridge(opts) {
1584
1595
  console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!session.cliSessionId);
1585
1596
  console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
1586
1597
  session.isProcessing = false;
1598
+ session._awaitingTurnResult = false;
1599
+ session._queuedTurnCount = 0;
1587
1600
  onProcessingChanged();
1588
1601
  session.queryInstance = null;
1589
1602
  session.messageQueue = null;
@@ -1625,6 +1638,8 @@ function createSDKBridge(opts) {
1625
1638
  session.messageQueue = null;
1626
1639
  session.abortController = null;
1627
1640
  session.isProcessing = false;
1641
+ session._awaitingTurnResult = false;
1642
+ session._queuedTurnCount = 0;
1628
1643
  onProcessingChanged();
1629
1644
  sendAndRecord(session, { type: "error", text: "The agent connection closed before it received your message. Please send it again." });
1630
1645
  sendAndRecord(session, { type: "done", code: 1 });
@@ -1695,6 +1710,7 @@ function createSDKBridge(opts) {
1695
1710
  console.error("[sdk-bridge] QueryHandle rejected message for session " + session.localId + ":", e.message || e);
1696
1711
  }
1697
1712
  if (delivered) {
1713
+ delete session._lastTurnInterrupted;
1698
1714
  if (session._awaitingTurnResult) {
1699
1715
  session._queuedTurnCount = (session._queuedTurnCount || 0) + 1;
1700
1716
  } else {
@@ -1813,7 +1829,7 @@ function createSDKBridge(opts) {
1813
1829
  } catch (e) {}
1814
1830
  if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
1815
1831
 
1816
- var subprocessVendorKeys = ["antigravity", "opencode"];
1832
+ var subprocessVendorKeys = ["antigravity", "opencode", "kimi", "grok", "copilot", "qwen", "junie"];
1817
1833
  var acpProfiles = require("./yoke/acp-agent-profiles");
1818
1834
  for (var subprocessVendorIndex = 0; subprocessVendorIndex < subprocessVendorKeys.length; subprocessVendorIndex++) {
1819
1835
  var subprocessVendor = subprocessVendorKeys[subprocessVendorIndex];
@@ -1864,7 +1880,7 @@ function createSDKBridge(opts) {
1864
1880
  // as if every CLI were missing while the default adapter starts up.
1865
1881
  sm.installedVendors = detectInstalledVendors(linuxUser);
1866
1882
  sm.availableVendors = getAvailableVendors(linuxUser);
1867
- sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1883
+ sendModelInfoForVendor(null, defaultVendor, defaultModelForVendor(defaultVendor));
1868
1884
 
1869
1885
  // Initialize default adapter first (provides skills, slash commands, etc.)
1870
1886
  if (adapter) {
@@ -1896,7 +1912,8 @@ function createSDKBridge(opts) {
1896
1912
  send({ type: "slash_commands", commands: combined, vendor: defaultVendor });
1897
1913
  }
1898
1914
  if (result.defaultModel) {
1899
- sm.currentModel = sm.currentModel || sm._savedDefaultModel || result.defaultModel;
1915
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
1916
+ sm.defaultModelByVendor[defaultVendor] = sm.defaultModelByVendor[defaultVendor] || result.defaultModel;
1900
1917
  }
1901
1918
  sm.availableModels = result.models || [];
1902
1919
  // Store per-vendor models and capabilities
@@ -1918,7 +1935,7 @@ function createSDKBridge(opts) {
1918
1935
  sm.modelsByVendor = sm.modelsByVendor || {};
1919
1936
 
1920
1937
  // Send the fully initialized state to the client.
1921
- sendModelInfoForVendor(defaultVendor, sm.currentModel || "");
1938
+ sendModelInfoForVendor(null, defaultVendor, defaultModelForVendor(defaultVendor));
1922
1939
  }
1923
1940
 
1924
1941
  async function setModel(session, model) {
@@ -1930,22 +1947,28 @@ function createSDKBridge(opts) {
1930
1947
  // No active query — just store the model for next startQuery
1931
1948
  session.model = model;
1932
1949
  sm.saveSessionFile(session);
1933
- sm.currentModel = model;
1934
- // Don't send vendor here: session vendor not yet bound, let client keep its selection
1935
- sendModelInfoForVendor(null, model);
1936
- sendToSession(session, { type: "config_state", model: sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1937
- return;
1950
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
1951
+ // Confirm against the session vendor so a non-default vendor does not
1952
+ // receive model metadata labeled as the project's default adapter.
1953
+ var pendingVendor = session.vendor || (adapter && adapter.vendor) || "claude";
1954
+ sm.defaultModelByVendor[pendingVendor] = model;
1955
+ sendModelInfoForVendor(session, pendingVendor, model);
1956
+ sendToSession(session, { type: "config_state", model: model, vendor: pendingVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1957
+ return { ok: true, model: model };
1938
1958
  }
1939
1959
  try {
1940
1960
  await session.queryInstance.setModel(model);
1941
1961
  session.model = model;
1942
1962
  sm.saveSessionFile(session);
1943
- sm.currentModel = model;
1963
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
1944
1964
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || "claude";
1945
- sendModelInfoForVendor(sessionVendor, model);
1946
- sendToSession(session, { type: "config_state", model: sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1965
+ sm.defaultModelByVendor[sessionVendor] = model;
1966
+ sendModelInfoForVendor(session, sessionVendor, model);
1967
+ sendToSession(session, { type: "config_state", model: model, vendor: sessionVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1968
+ return { ok: true, model: model };
1947
1969
  } catch (e) {
1948
1970
  send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
1971
+ return { ok: false, model: session.model || "", error: e.message || String(e) };
1949
1972
  }
1950
1973
  }
1951
1974
 
@@ -1974,14 +1997,14 @@ function createSDKBridge(opts) {
1974
1997
  if (!session.queryInstance) {
1975
1998
  // No active query — just store the mode for next startQuery
1976
1999
  sm.currentPermissionMode = mode;
1977
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
2000
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1978
2001
  return;
1979
2002
  }
1980
2003
  try {
1981
2004
  // Route through QueryHandle (works for both in-process and worker paths)
1982
2005
  await session.queryInstance.setPermissionMode(mode);
1983
2006
  sm.currentPermissionMode = mode;
1984
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
2007
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1985
2008
  } catch (e) {
1986
2009
  sendToSession(session, { type: "error", text: "Failed to set permission mode: " + (e.message || e) });
1987
2010
  }
@@ -175,6 +175,10 @@ function attachMessageProcessor(ctx) {
175
175
 
176
176
  // Cache slash_commands and model from CLI init message
177
177
  if (parsed.yokeType === "init") {
178
+ var previousBackgroundTaskCount = (session.activeBackgroundTasks || []).length;
179
+ session.activeBackgroundTasks = [];
180
+ sendToSession(session, { type: "active_background_tasks", tasks: [] });
181
+ if (previousBackgroundTaskCount !== 0) sm.broadcastSessionList();
178
182
  var fsSkills = discoverSkillDirs();
179
183
  sm.skillNames = mergeSkills(parsed.skills, fsSkills);
180
184
  if (parsed.slashCommands) {
@@ -192,13 +196,20 @@ function attachMessageProcessor(ctx) {
192
196
  send({ type: "slash_commands", commands: sm.slashCommands });
193
197
  }
194
198
  if (parsed.model) {
195
- sm.currentModel = sm.currentModel || sm._savedDefaultModel || parsed.model;
196
199
  var initVendor = session.vendor || (adapter && adapter.vendor) || "claude";
197
- send({
200
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
201
+ sm.defaultModelByVendor[initVendor] = sm.defaultModelByVendor[initVendor] || parsed.model;
202
+ var initModels = getModelsForVendor(initVendor);
203
+ var initModel = session.model || sm.defaultModelByVendor[initVendor] || "";
204
+ if (initModels.length > 0 && !initModels.some(function(entry) {
205
+ return entry === initModel || (entry && (entry.value === initModel || entry.id === initModel || entry.resolvedModel === initModel));
206
+ })) initModel = typeof initModels[0] === "string" ? initModels[0] : (initModels[0].value || initModels[0].id || "");
207
+ sendToSession(session, {
198
208
  type: "model_info",
199
- model: sm.currentModel,
200
- models: getModelsForVendor(initVendor),
209
+ model: initModel,
210
+ models: initModels,
201
211
  vendor: initVendor,
212
+ sessionId: session.localId,
202
213
  availableVendors: sm.availableVendors || [],
203
214
  installedVendors: sm.installedVendors || [],
204
215
  });
@@ -612,6 +623,15 @@ function attachMessageProcessor(ctx) {
612
623
  });
613
624
  }
614
625
 
626
+ } else if (parsed.yokeType === "background_tasks_changed") {
627
+ var previousBackgroundTaskCount = (session.activeBackgroundTasks || []).length;
628
+ var activeBackgroundTasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
629
+ session.activeBackgroundTasks = activeBackgroundTasks;
630
+ sendToSession(session, { type: "active_background_tasks", tasks: activeBackgroundTasks });
631
+ if (previousBackgroundTaskCount !== activeBackgroundTasks.length) {
632
+ sm.broadcastSessionList();
633
+ }
634
+
615
635
  } else if (parsed.yokeType === "tool_progress") {
616
636
  // Sub-agent tool_progress: forward as activity update
617
637
  var parentId = parsed.parentToolId;
@@ -783,6 +803,8 @@ function attachMessageProcessor(ctx) {
783
803
  // app-server returned an unauthorized/token-revoked error). Trigger the
784
804
  // same login flow as the Claude login-prompt path.
785
805
  session.isProcessing = false;
806
+ session._awaitingTurnResult = false;
807
+ session._queuedTurnCount = 0;
786
808
  onProcessingChanged();
787
809
  emitAuthRequired(session);
788
810