clay-server 3.8.1 → 4.0.0-beta.2

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 (189) hide show
  1. package/lib/ask-user-mcp-server.js +5 -119
  2. package/lib/builtin-mates.js +6 -6
  3. package/lib/claude-hook-installer.js +15 -0
  4. package/lib/clay-history-mcp-server.js +62 -297
  5. package/lib/daemon.js +1 -1
  6. package/lib/debate-model-selection.js +119 -0
  7. package/lib/home-debate-tool-policy.js +164 -0
  8. package/lib/home-response-notifications.js +26 -0
  9. package/lib/mate-creation-mcp-server.js +55 -0
  10. package/lib/mate-ready-creation.js +95 -0
  11. package/lib/mates.js +2 -0
  12. package/lib/notes.js +2 -1
  13. package/lib/project-ask-user.js +64 -0
  14. package/lib/project-capsule-catalog.js +101 -0
  15. package/lib/project-connection.js +4 -0
  16. package/lib/project-debate-proposal.js +133 -17
  17. package/lib/project-debate.js +214 -110
  18. package/lib/project-delegated-follow-up.js +84 -0
  19. package/lib/project-delegated-session.js +53 -0
  20. package/lib/project-filesystem.js +7 -9
  21. package/lib/project-knowledge.js +1 -0
  22. package/lib/project-mate-creation-proposal.js +120 -0
  23. package/lib/project-memory.js +14 -7
  24. package/lib/project-models.js +157 -32
  25. package/lib/project-session-notes.js +2 -2
  26. package/lib/project-session-pair.js +8 -8
  27. package/lib/project-sessions.js +66 -51
  28. package/lib/project-vendor-login.js +397 -0
  29. package/lib/project-worker-proposal.js +25 -25
  30. package/lib/project-workspace-query.js +144 -0
  31. package/lib/project.js +349 -102
  32. package/lib/public/app.js +85 -42
  33. package/lib/public/css/avatar-imprints.css +0 -1
  34. package/lib/public/css/base.css +4 -0
  35. package/lib/public/css/capsule-ui.css +389 -0
  36. package/lib/public/css/command-palette.css +306 -6
  37. package/lib/public/css/home-capsule-library.css +243 -0
  38. package/lib/public/css/home-capsule-source.css +192 -0
  39. package/lib/public/css/home-debate-live.css +146 -0
  40. package/lib/public/css/home-debate-planning.css +125 -0
  41. package/lib/public/css/home-debates-archive.css +281 -0
  42. package/lib/public/css/home-hub.css +896 -405
  43. package/lib/public/css/home-mate-creation.css +34 -0
  44. package/lib/public/css/home-mate-model-picker.css +131 -0
  45. package/lib/public/css/home-mate-settings.css +344 -0
  46. package/lib/public/css/home-session-actions.css +265 -0
  47. package/lib/public/css/home-sidebar.css +667 -0
  48. package/lib/public/css/icon-strip.css +1 -2
  49. package/lib/public/css/mates.css +7 -34
  50. package/lib/public/css/pane.css +1 -1
  51. package/lib/public/css/playbook.css +0 -80
  52. package/lib/public/css/sticky-notes.css +53 -117
  53. package/lib/public/css/tui-attention.css +0 -44
  54. package/lib/public/css/workspace-assignment.css +45 -0
  55. package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
  56. package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
  57. package/lib/public/index.html +93 -74
  58. package/lib/public/modules/app-connection.js +10 -1
  59. package/lib/public/modules/app-dm.js +43 -160
  60. package/lib/public/modules/app-home-hub.js +328 -585
  61. package/lib/public/modules/app-message-cards.js +242 -0
  62. package/lib/public/modules/app-message-router.js +84 -0
  63. package/lib/public/modules/app-messages.js +102 -37
  64. package/lib/public/modules/app-notifications.js +8 -119
  65. package/lib/public/modules/app-projects.js +9 -1
  66. package/lib/public/modules/app-rendering.js +21 -602
  67. package/lib/public/modules/avatar-imprint.js +11 -8
  68. package/lib/public/modules/chat-bubble-renderer.js +152 -0
  69. package/lib/public/modules/chat-render-runtime.js +198 -0
  70. package/lib/public/modules/command-palette.js +225 -436
  71. package/lib/public/modules/dm-render.js +82 -0
  72. package/lib/public/modules/home-capsule-creation-intent.js +31 -0
  73. package/lib/public/modules/home-capsule-library.js +146 -0
  74. package/lib/public/modules/home-capsule-source.js +383 -0
  75. package/lib/public/modules/home-chat-identity.js +35 -0
  76. package/lib/public/modules/home-chat-scroll.js +77 -0
  77. package/lib/public/modules/home-chat-stream-state.js +34 -0
  78. package/lib/public/modules/home-composer-focus.js +25 -0
  79. package/lib/public/modules/home-conversations-sheet.js +179 -0
  80. package/lib/public/modules/home-debate-activity.js +48 -0
  81. package/lib/public/modules/home-debate-controls.js +295 -0
  82. package/lib/public/modules/home-debate-launch.js +41 -0
  83. package/lib/public/modules/home-debate-live.js +284 -0
  84. package/lib/public/modules/home-debate-models.js +73 -0
  85. package/lib/public/modules/home-debate-planning.js +335 -0
  86. package/lib/public/modules/home-debates-archive.js +279 -0
  87. package/lib/public/modules/home-dock-resize.js +74 -0
  88. package/lib/public/modules/home-dock.js +446 -0
  89. package/lib/public/modules/home-mate-chat.js +496 -0
  90. package/lib/public/modules/home-mate-creation.js +109 -0
  91. package/lib/public/modules/home-mate-model-picker.js +281 -0
  92. package/lib/public/modules/home-mate-selection.js +38 -0
  93. package/lib/public/modules/home-mate-settings-menu.js +131 -0
  94. package/lib/public/modules/home-mate-settings.js +383 -0
  95. package/lib/public/modules/home-session-actions.js +373 -0
  96. package/lib/public/modules/home-session-model-confirmation.js +13 -0
  97. package/lib/public/modules/home-shell.js +9 -0
  98. package/lib/public/modules/home-sidebar-chat-list.js +63 -0
  99. package/lib/public/modules/home-sidebar.js +158 -0
  100. package/lib/public/modules/home-sub-surface.js +19 -0
  101. package/lib/public/modules/home-surface-boot.js +68 -0
  102. package/lib/public/modules/home-surface.js +141 -0
  103. package/lib/public/modules/home-tools.js +271 -0
  104. package/lib/public/modules/markdown.js +50 -2
  105. package/lib/public/modules/mate-management.js +86 -0
  106. package/lib/public/modules/mate-wizard.js +0 -7
  107. package/lib/public/modules/mention.js +17 -19
  108. package/lib/public/modules/pane-bridge.js +9 -0
  109. package/lib/public/modules/profile.js +4 -5
  110. package/lib/public/modules/project-settings.js +34 -28
  111. package/lib/public/modules/project-switcher.js +11 -5
  112. package/lib/public/modules/project-workspace-assignment.js +23 -0
  113. package/lib/public/modules/search-clay-chat.js +402 -0
  114. package/lib/public/modules/server-settings.js +14 -12
  115. package/lib/public/modules/session-actions.js +1 -1
  116. package/lib/public/modules/sidebar-mates.js +56 -552
  117. package/lib/public/modules/sidebar-mobile.js +7 -64
  118. package/lib/public/modules/sidebar-presence.js +37 -0
  119. package/lib/public/modules/split-pair-ui.js +13 -13
  120. package/lib/public/modules/split-view.js +7 -0
  121. package/lib/public/modules/sticky-notes.js +7 -6
  122. package/lib/public/modules/tool-input-composition.js +48 -0
  123. package/lib/public/modules/tool-llm-status.js +167 -0
  124. package/lib/public/modules/tool-renderer-advanced.js +285 -0
  125. package/lib/public/modules/tool-renderer-chart.js +158 -0
  126. package/lib/public/modules/tool-renderer-semantics.js +35 -0
  127. package/lib/public/modules/tool-renderer.js +464 -0
  128. package/lib/public/modules/tool-runtime.js +309 -0
  129. package/lib/public/modules/tool-ui-evaluator.js +97 -0
  130. package/lib/public/modules/tool-ui-tree.js +21 -0
  131. package/lib/public/modules/tui-attention.js +10 -0
  132. package/lib/public/modules/user-settings.js +16 -23
  133. package/lib/public/modules/vendor-login.js +287 -0
  134. package/lib/public/modules/worker-proposal.js +2 -2
  135. package/lib/public/modules/workspace-assignment-card.js +149 -0
  136. package/lib/public/modules/workspace-assignment-routing.js +4 -0
  137. package/lib/public/style.css +13 -2
  138. package/lib/runtime-env.js +65 -0
  139. package/lib/sdk-bridge.js +120 -32
  140. package/lib/sdk-message-processor.js +10 -6
  141. package/lib/server-home-capsule-creation.js +36 -0
  142. package/lib/server-home-chat-events.js +287 -0
  143. package/lib/server-home-chat.js +518 -0
  144. package/lib/server-home-clay-entry.js +65 -0
  145. package/lib/server-home-clay-session-links.js +26 -0
  146. package/lib/server-home-debate-planning.js +214 -0
  147. package/lib/server-home-debates.js +137 -0
  148. package/lib/server-home-mate-creation.js +261 -0
  149. package/lib/server-home-models.js +241 -0
  150. package/lib/server-home-preferences.js +61 -0
  151. package/lib/server-mates.js +37 -0
  152. package/lib/server-tools.js +377 -0
  153. package/lib/server.js +111 -59
  154. package/lib/session-pair-mcp-server.js +6 -6
  155. package/lib/sessions.js +18 -3
  156. package/lib/tool-capsule-source.js +142 -0
  157. package/lib/tool-control-mcp-server.js +126 -0
  158. package/lib/tool-llm.js +48 -0
  159. package/lib/tool-storage.js +77 -0
  160. package/lib/tool-ui-spec-advanced.js +82 -0
  161. package/lib/tool-ui-spec.js +261 -0
  162. package/lib/tools-registry.js +286 -0
  163. package/lib/users-home-dock-preferences.js +77 -0
  164. package/lib/users-home-surface-preferences.js +99 -0
  165. package/lib/users-preferences.js +4 -7
  166. package/lib/users.js +12 -0
  167. package/lib/whats-new-content.js +3 -3
  168. package/lib/workspace-assignment-service.js +420 -0
  169. package/lib/workspace-query-mcp-server.js +154 -0
  170. package/lib/workspace-query-service.js +492 -0
  171. package/lib/ws-schema.js +93 -12
  172. package/lib/yoke/adapters/acp.js +2 -1
  173. package/lib/yoke/adapters/antigravity.js +2 -1
  174. package/lib/yoke/adapters/claude-worker.js +11 -4
  175. package/lib/yoke/adapters/claude.js +73 -43
  176. package/lib/yoke/adapters/codex.js +40 -10
  177. package/lib/yoke/adapters/kiro.js +2 -1
  178. package/lib/yoke/claude-user-input.js +38 -0
  179. package/lib/yoke/codex-app-server.js +20 -2
  180. package/lib/yoke/codex-user-input.js +64 -0
  181. package/lib/yoke/complete-once.js +164 -0
  182. package/lib/yoke/index.js +5 -1
  183. package/lib/yoke/interface.js +19 -0
  184. package/lib/yoke/mcp-bridge-server.js +2 -6
  185. package/lib/yoke/user-input.js +254 -0
  186. package/package.json +3 -2
  187. package/lib/public/css/home-chat.css +0 -380
  188. package/lib/public/modules/home-chat.js +0 -438
  189. package/lib/server-clay-home.js +0 -245
@@ -100,11 +100,11 @@ function attachSessionPair(ctx) {
100
100
  var response = token.response || "";
101
101
  var text;
102
102
  if (token.interrupted) {
103
- text = "[Worker execution interrupted] The 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.";
103
+ text = "[Split Worker execution interrupted] The user interrupted the Split 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.";
104
104
  } else {
105
- text = "A Worker task delegated through send_to_partner has finished.\n\n" +
105
+ text = "A Split Worker task delegated through send_to_partner has finished.\n\n" +
106
106
  "Original task:\n" + token.message + "\n\n" +
107
- (failure ? "Worker error:\n" + failure : "Worker result:\n" + (response || "(No text response was recorded.)")) +
107
+ (failure ? "Split Worker error:\n" + failure : "Split Worker result:\n" + (response || "(No text response was recorded.)")) +
108
108
  "\n\nReview the result, verify it as needed, and continue the task.";
109
109
  }
110
110
  sm.sendAndRecord(caller, {
@@ -361,7 +361,7 @@ function attachSessionPair(ctx) {
361
361
  var driver;
362
362
  var groupName;
363
363
  if (Number.isInteger(driverSpec.sessionId)) {
364
- // "Add Worker" on an existing session: the current session becomes
364
+ // "Add Split Worker" on an existing session: the current session becomes
365
365
  // the Driver with its full conversation context intact.
366
366
  driver = sm.sessions.get(driverSpec.sessionId);
367
367
  if (!driver) throw new Error("driver session not found");
@@ -378,7 +378,7 @@ function attachSessionPair(ctx) {
378
378
  }
379
379
  var workerEffort = yoke.clampEffort(workerVendor, workerSpec.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[workerVendor]) || sm.currentEffort || "medium") || null;
380
380
  var worker = sm.createSessionRaw({ ownerId: ownerId, vendor: workerVendor, model: workerSpec.model || null, effort: workerEffort });
381
- worker.title = "Worker · " + ((yoke.getVendorInfo(workerVendor) || {}).displayName || workerVendor);
381
+ worker.title = "Split Worker · " + ((yoke.getVendorInfo(workerVendor) || {}).displayName || workerVendor);
382
382
  if (workerSpec.effort) worker.loopSettings = { effort: workerSpec.effort };
383
383
  var result = store.create(ws, {
384
384
  members: [driver.localId, worker.localId],
@@ -393,7 +393,7 @@ function attachSessionPair(ctx) {
393
393
  function createWorkerForDriver(driver, args) {
394
394
  if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
395
395
  var installed = sm.installedVendors || [];
396
- if (installed.length === 0) throw new Error("no coding agent is installed for a Worker session");
396
+ if (installed.length === 0) throw new Error("no coding agent is installed for a Split Worker session");
397
397
  var vendor = typeof args.workerVendor === "string" ? args.workerVendor.trim() : "";
398
398
  if (vendor) validateVendor(vendor);
399
399
  if (!vendor) {
@@ -468,9 +468,9 @@ function attachSessionPair(ctx) {
468
468
  var group = store.groupForMember(session.localId);
469
469
  var pairPrompt = "";
470
470
  if (group && group.pair && group.pair.driverId === session.localId) {
471
- pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner, read_partner, interrupt_partner, and close_partner are provided directly to you. Use send_to_partner to delegate concrete bounded work to the Worker. The human may also message or stop the Worker directly; treat that intervention as authoritative. Use close_partner immediately when the human asks to close, dismiss, or remove the Worker pane; it preserves the session history. A completed Worker turn leaves the Worker session available for more work. Use interrupt_partner when execution must stop or change direction but the pair should remain open; its work is partial and unverified until you review it. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If the Worker is no longer available, keep the work in the visible Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background session. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
471
+ pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner, read_partner, interrupt_partner, and close_partner are provided directly to you. Infer the user's intended target from conversational and UI context. References to the user-visible paired or split pane, its session, its activity or status, or a collaborator the user opened resolve to Clay's Split Worker and the appropriate partner tool. Internal Sub-agents are a distinct execution mechanism, not a lexical category; use them only when the user clearly intends internal or background parallel delegation rather than the visible paired session. When ambiguous and a visible pair exists, prefer the visible Split Worker. If the ambiguity would materially change where work runs, ask a concise clarification instead of guessing. Use send_to_partner to delegate concrete bounded work to the Split Worker. The human may also message or stop the Split Worker directly; treat that intervention as authoritative. Use close_partner immediately when the human asks to close, dismiss, or remove the Split Worker pane; it preserves the session history. A completed Split Worker turn leaves the Split Worker session available for more work. Use interrupt_partner when execution must stop or change direction but the pair should remain open; its work is partial and unverified until you review it. If review or user feedback requires corrections to the Split Worker's implementation, delegate a follow-up turn to that Split Worker instead of editing the Split Worker-owned files yourself. If the Split Worker is no longer available, keep the work in the visible Split Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background Sub-agent. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
472
472
  } else if (!group && !ctx.isMate) {
473
- pairPrompt = "When the user asks you to open or use a visible Worker, call send_to_partner directly. Clay will create the paired Worker session and open it in the right pane automatically before delivering the task. Do not ask the user to start split mode and do not use a background sub-agent as a substitute for a visible Worker.";
473
+ pairPrompt = "Infer the user's intended target from conversational and UI context. References to a user-visible paired or split pane, its session, its activity or status, or a collaborator the user wants opened resolve to Clay's Split Worker and its partner tools. Internal Sub-agents are a distinct execution mechanism, not a lexical category; use them only when the user clearly intends internal or background parallel delegation rather than a visible paired session. When ambiguity would materially change where work runs, ask a concise clarification instead of guessing. When the user intends a visible collaborator, call send_to_partner directly; Clay will create the paired Split Worker session and open it in the right pane automatically before delivering the task. Do not ask the user to start split mode and do not use a background Sub-agent as a substitute for a visible Split Worker.";
474
474
  }
475
475
  return [pairPrompt, workerProposal.getSystemPrompt(session)].filter(Boolean).join("\n\n");
476
476
  }
@@ -75,6 +75,7 @@ function attachSessions(ctx) {
75
75
  var sendToSessionOthers = ctx.sendToSessionOthers;
76
76
  var opts = ctx.opts;
77
77
  var usersModule = ctx.usersModule;
78
+ var getProjectAccess = ctx.getProjectAccess || function () { return { visibility: "public" }; };
78
79
  var userPresence = ctx.userPresence;
79
80
  var pushModule = ctx.pushModule;
80
81
  var getSessionForWs = ctx.getSessionForWs;
@@ -85,6 +86,60 @@ function attachSessions(ctx) {
85
86
  var onProcessingChanged = ctx.onProcessingChanged;
86
87
  var broadcastPresence = ctx.broadcastPresence;
87
88
  var adapter = ctx.adapter;
89
+
90
+ function ownsSearchSession(ws, session) {
91
+ if (!session) return false;
92
+ if (usersModule && typeof usersModule.isMultiUser === "function" && usersModule.isMultiUser()) {
93
+ return !!(ws && ws._clayUser && usersModule.canAccessSession(ws._clayUser.id, session, getProjectAccess()));
94
+ }
95
+ return !session.ownerId;
96
+ }
97
+
98
+ function respondToAskUser(session, msg) {
99
+ if (!session || !msg || !msg.toolId) return false;
100
+ var toolId = msg.toolId;
101
+ var answers = msg.answers || {};
102
+ var pending = session.pendingAskUser && session.pendingAskUser[toolId];
103
+ if (!pending) return false;
104
+ if (pending.mode === "yoke") {
105
+ try { yoke.userInput.normalizeAnswers(pending.request.questions, answers); }
106
+ catch (e) { return false; }
107
+ }
108
+ delete session.pendingAskUser[toolId];
109
+ sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId, answers: answers });
110
+ if (pending.mode === "yoke" && typeof pending.respond === "function") {
111
+ pending.respond(answers);
112
+ } else if (pending.mode === "mcp") {
113
+ var answerText = formatAskUserAnswerAsMessage(pending.input, answers);
114
+ var userMsg = { type: "user_message", text: answerText, askUserAnswer: true };
115
+ session.history.push(userMsg);
116
+ sm.appendToSessionFile(session, userMsg);
117
+ sendToSession(session.localId, userMsg);
118
+ if (!session.isProcessing) {
119
+ session.isProcessing = true;
120
+ onProcessingChanged();
121
+ session.sentToolResults = {};
122
+ sendToSession(session.localId, { type: "status", status: "processing" });
123
+ if (!session.queryInstance && !session.worker) sdk.startQuery(session, answerText, undefined, ensureProjectAccessForSession(session));
124
+ else sdk.pushMessage(session, answerText);
125
+ } else {
126
+ sdk.pushMessage(session, answerText);
127
+ }
128
+ } else {
129
+ pending.resolve({ behavior: "deny", message: formatAskUserAnswerAsMessage(pending.input, answers) });
130
+ }
131
+ return true;
132
+ }
133
+
134
+ function cancelPendingAskUser(session, reason) {
135
+ var pendingMap = session && session.pendingAskUser ? session.pendingAskUser : {};
136
+ Object.keys(pendingMap).forEach(function (toolId) {
137
+ var pending = pendingMap[toolId];
138
+ if (pending && pending.respond && typeof pending.respond.cancel === "function") pending.respond.cancel(reason || "Cancelled");
139
+ else if (pending && typeof pending.resolve === "function") pending.resolve({ behavior: "deny", message: reason || "Cancelled" });
140
+ });
141
+ if (session) session.pendingAskUser = {};
142
+ }
88
143
  var getProjectList = ctx.getProjectList;
89
144
  var getProjectCount = ctx.getProjectCount;
90
145
  var getScheduleCount = ctx.getScheduleCount;
@@ -821,14 +876,19 @@ function attachSessions(ctx) {
821
876
  }
822
877
 
823
878
  if (msg.type === "search_sessions") {
824
- var results = sm.searchSessions(msg.query || "");
879
+ var results = sm.searchSessions(msg.query || "", function (session) {
880
+ return ownsSearchSession(ws, session);
881
+ });
825
882
  sendTo(ws, { type: "search_results", query: msg.query || "", results: results });
826
883
  return true;
827
884
  }
828
885
 
829
886
  if (msg.type === "search_session_content") {
830
887
  var targetSession = msg.id ? sm.sessions.get(msg.id) : getSessionForWs(ws);
831
- if (!targetSession) return true;
888
+ if (!ownsSearchSession(ws, targetSession)) {
889
+ sendTo(ws, { type: "search_content_results", query: msg.query || "", sessionId: msg.id || null, hits: [], total: 0 });
890
+ return true;
891
+ }
832
892
  var contentResults = sm.searchSessionContent(targetSession.localId, msg.query || "");
833
893
  var searchResp = { type: "search_content_results", query: msg.query || "", sessionId: targetSession.localId, hits: contentResults.hits, total: contentResults.total };
834
894
  if (msg.source) searchResp.source = msg.source;
@@ -1256,7 +1316,7 @@ function attachSessions(ctx) {
1256
1316
  session.blocks = {};
1257
1317
  session.sentToolResults = {};
1258
1318
  session.pendingPermissions = {};
1259
- session.pendingAskUser = {};
1319
+ cancelPendingAskUser(session, "Conversation rewound");
1260
1320
  session.isProcessing = false;
1261
1321
  onProcessingChanged();
1262
1322
 
@@ -1328,53 +1388,7 @@ function attachSessions(ctx) {
1328
1388
 
1329
1389
  if (msg.type === "ask_user_response") {
1330
1390
  var session = getSessionForWs(ws);
1331
- if (!session) return true;
1332
- var toolId = msg.toolId;
1333
- var answers = msg.answers || {};
1334
- var pending = session.pendingAskUser[toolId];
1335
- if (!pending) return true;
1336
- delete session.pendingAskUser[toolId];
1337
- sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId, answers: answers });
1338
-
1339
- if (pending.mode === "mcp") {
1340
- // Stateless MCP path: the tool already returned. Inject the user's
1341
- // answer as a new user message so the conversation continues
1342
- // naturally on the next turn. This matches how the mate would see
1343
- // any other user input.
1344
- var answerText = formatAskUserAnswerAsMessage(pending.input, answers);
1345
- var userMsg = { type: "user_message", text: answerText };
1346
- session.history.push(userMsg);
1347
- sm.appendToSessionFile(session, userMsg);
1348
- sendToSession(session.localId, userMsg);
1349
-
1350
- if (!session.isProcessing) {
1351
- session.isProcessing = true;
1352
- onProcessingChanged();
1353
- session.sentToolResults = {};
1354
- sendToSession(session.localId, { type: "status", status: "processing" });
1355
- if (!session.queryInstance && !session.worker) {
1356
- sdk.startQuery(session, answerText, undefined, ensureProjectAccessForSession(session));
1357
- } else {
1358
- sdk.pushMessage(session, answerText);
1359
- }
1360
- } else {
1361
- // Turn is still running; queue for the next turn.
1362
- sdk.pushMessage(session, answerText);
1363
- }
1364
- } else {
1365
- // Claude native AskUserQuestion path (built-in tool, intercepted via
1366
- // canUseTool). In headless SDK mode the built-in can't render its own
1367
- // answer UI: resolving "allow" lets it return an EMPTY result, so the
1368
- // model continues the same turn with no answer ("I don't see an
1369
- // answer"). Injected user messages also lose the race against that
1370
- // empty result. Instead, deliver the answer as the tool_result by
1371
- // denying with the formatted answer text — the SDK surfaces a deny
1372
- // `message` to the model as the tool result, so the model receives the
1373
- // user's choice in the same turn. (updatedInput.answers is not read by
1374
- // the 0.3.x built-in, which is what caused answers to be dropped.)
1375
- var nativeAnswerText = formatAskUserAnswerAsMessage(pending.input, answers);
1376
- pending.resolve({ behavior: "deny", message: nativeAnswerText });
1377
- }
1391
+ if (session) respondToAskUser(session, msg);
1378
1392
  return true;
1379
1393
  }
1380
1394
 
@@ -1443,7 +1457,7 @@ function attachSessions(ctx) {
1443
1457
  session.isProcessing = false;
1444
1458
  onProcessingChanged();
1445
1459
  session.pendingPermissions = {};
1446
- session.pendingAskUser = {};
1460
+ cancelPendingAskUser(session, "Conversation restarted");
1447
1461
  sm.broadcastSessionList();
1448
1462
  setImmediate(function () {
1449
1463
  if (session.abortController) {
@@ -2011,6 +2025,7 @@ function attachSessions(ctx) {
2011
2025
  return {
2012
2026
  handleSessionsMessage: handleSessionsMessage,
2013
2027
  resolveSessionForView: resolveSessionForView,
2028
+ respondToAskUser: respondToAskUser,
2014
2029
  };
2015
2030
  }
2016
2031
 
@@ -0,0 +1,397 @@
1
+ // Vendor login flow coordinator.
2
+ //
3
+ // Owns the "the vendor CLI is not logged in" recovery path end to end so the
4
+ // client never has to guess. One login terminal per vendor per project is the
5
+ // single source of truth: repeated auth_required events (other sessions, split
6
+ // panes, a burst of 401 retries) re-use that record instead of spawning more
7
+ // terminals.
8
+ //
9
+ // Lifecycle:
10
+ // vendor_login_start -> create (or re-use) a PTY running the vendor login
11
+ // command, reply with vendor_login_ready
12
+ // PTY output/exit -> detect a successful login, restart the vendor's
13
+ // YOKE adapter so it re-reads the credential file,
14
+ // broadcast auth_refreshed, close the terminal
15
+ // vendor_login_cancel -> user dismissed the flow; kill the terminal
16
+ //
17
+ // The adapter restart is the actual bug fix: a Codex app-server is a long-lived
18
+ // child process that reads ~/.codex/auth.json once at spawn, so without a
19
+ // restart every query after a successful login keeps hitting 401 and re-arms
20
+ // auth_required forever.
21
+
22
+ var vendorRegistry = require("./yoke/vendor-registry");
23
+
24
+ // Login CLIs print a success line and then hand the shell back, so the PTY
25
+ // itself does not exit. Watch the output stream instead. Deliberately narrow:
26
+ // "Successfully logged out" and MCP-server logins must not match.
27
+ var LOGIN_SUCCESS_PATTERNS = [
28
+ /successfully logged in(?![ \t]+to[ \t]+mcp)/i,
29
+ /logged in successfully/i,
30
+ /login successful/i,
31
+ /successfully authenticated/i,
32
+ /authentication successful/i,
33
+ /signed in successfully/i,
34
+ /signed in with your [a-z ]+ account/i,
35
+ /you(?:'re| are) (?:now )?(?:logged|signed) in/i,
36
+ ];
37
+
38
+ // Rolling window kept per flow so a success line split across PTY chunks is
39
+ // still matched, without buffering the whole login transcript.
40
+ var OUTPUT_TAIL_MAX = 4096;
41
+
42
+ // Grace period between detecting success and killing the terminal, so the user
43
+ // actually sees the confirmation line before the modal closes.
44
+ var CLOSE_AFTER_SUCCESS_MS = 2000;
45
+
46
+ var ANSI_PATTERN = /\x1b\[[0-9;?]*[ -\/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\r/g;
47
+
48
+ function stripAnsi(text) {
49
+ return String(text).replace(ANSI_PATTERN, "");
50
+ }
51
+
52
+ function looksLikeLoginSuccess(text) {
53
+ for (var i = 0; i < LOGIN_SUCCESS_PATTERNS.length; i++) {
54
+ if (LOGIN_SUCCESS_PATTERNS[i].test(text)) return true;
55
+ }
56
+ return false;
57
+ }
58
+
59
+ /**
60
+ * ctx fields:
61
+ * slug, osUsers,
62
+ * sm, tm, adapters,
63
+ * send, sendTo,
64
+ * usersModule,
65
+ * getOsUserInfoForWs, getOsUserInfoForLinuxUser, getLinuxUserForSession
66
+ */
67
+ function attachVendorLogin(ctx) {
68
+ var slug = ctx.slug;
69
+ var osUsers = ctx.osUsers;
70
+
71
+ var sm = ctx.sm;
72
+ var tm = ctx.tm;
73
+ var adapters = ctx.adapters || {};
74
+
75
+ var send = ctx.send;
76
+ var sendTo = ctx.sendTo;
77
+
78
+ var usersModule = ctx.usersModule;
79
+
80
+ var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
81
+ var getOsUserInfoForLinuxUser = ctx.getOsUserInfoForLinuxUser;
82
+ var getLinuxUserForSession = ctx.getLinuxUserForSession;
83
+
84
+ // vendor -> flow record. At most one live login terminal per vendor.
85
+ var _flows = Object.create(null);
86
+
87
+ function loginCommandFor(vendor) {
88
+ var info = vendorRegistry.getVendorInfo(vendor);
89
+ return (info && info.loginCommand) || "claude login";
90
+ }
91
+
92
+ function vendorDisplayName(vendor) {
93
+ var info = vendorRegistry.getVendorInfo(vendor);
94
+ return (info && info.displayName) || vendor || "Vendor";
95
+ }
96
+
97
+ function listFlows() {
98
+ var result = [];
99
+ var vendors = Object.keys(_flows);
100
+ for (var i = 0; i < vendors.length; i++) {
101
+ var flow = _flows[vendors[i]];
102
+ result.push({
103
+ vendor: flow.vendor,
104
+ terminalId: flow.terminalId,
105
+ startedAt: flow.startedAt,
106
+ completed: !!flow.completed,
107
+ });
108
+ }
109
+ return result;
110
+ }
111
+
112
+ function broadcastState() {
113
+ send({ type: "vendor_login_state", slug: slug, flows: listFlows() });
114
+ }
115
+
116
+ function sendStateTo(ws) {
117
+ sendTo(ws, { type: "vendor_login_state", slug: slug, flows: listFlows() });
118
+ }
119
+
120
+ function hasTerminalPermission(ws) {
121
+ if (!ws || !ws._clayUser) return true;
122
+ if (!usersModule || typeof usersModule.getEffectivePermissions !== "function") return true;
123
+ var perms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
124
+ return !!(perms && perms.terminal);
125
+ }
126
+
127
+ // The login terminal must write credentials to the same HOME the adapter
128
+ // will read them from. With OS-user isolation the adapter runs as the
129
+ // *session owner's* Linux user, which is not necessarily the connected
130
+ // client, so prefer the session identity when the requester is entitled to
131
+ // it. Without isolation both sides run as the daemon user and this resolves
132
+ // to null on both paths.
133
+ function resolveLoginIdentity(ws, msg) {
134
+ if (!osUsers) return { linuxUser: null, osUserInfo: null };
135
+
136
+ var sessionLinuxUser = null;
137
+ var sessionId = msg && msg.sessionId;
138
+ if (sessionId && sm && sm.sessions && typeof getLinuxUserForSession === "function") {
139
+ var session = sm.sessions.get(sessionId);
140
+ if (session) {
141
+ var requesterId = ws && ws._clayUser ? ws._clayUser.id : null;
142
+ var isOwner = !session.ownerId || (requesterId && String(session.ownerId) === String(requesterId));
143
+ var isAdmin = !!(ws && ws._clayUser && ws._clayUser.role === "admin");
144
+ if (isOwner || isAdmin) sessionLinuxUser = getLinuxUserForSession(session);
145
+ }
146
+ }
147
+
148
+ if (sessionLinuxUser && typeof getOsUserInfoForLinuxUser === "function") {
149
+ return { linuxUser: sessionLinuxUser, osUserInfo: getOsUserInfoForLinuxUser(sessionLinuxUser) };
150
+ }
151
+
152
+ var wsInfo = typeof getOsUserInfoForWs === "function" ? getOsUserInfoForWs(ws) : null;
153
+ return { linuxUser: wsInfo ? wsInfo.user : null, osUserInfo: wsInfo };
154
+ }
155
+
156
+ function isFlowTerminalAlive(flow) {
157
+ return !!(flow && typeof flow.terminalId === "number" && tm.has(flow.terminalId));
158
+ }
159
+
160
+ function discardFlow(vendor) {
161
+ var flow = _flows[vendor];
162
+ if (!flow) return null;
163
+ delete _flows[vendor];
164
+ if (flow.closeTimer) {
165
+ clearTimeout(flow.closeTimer);
166
+ flow.closeTimer = null;
167
+ }
168
+ return flow;
169
+ }
170
+
171
+ // Kill the PTY and drop it from the sidebar list. `flow.closing` keeps the
172
+ // terminal's own exit hook from re-entering finalization.
173
+ function closeFlowTerminal(flow) {
174
+ if (!flow || typeof flow.terminalId !== "number") return;
175
+ flow.closing = true;
176
+ try { tm.close(flow.terminalId); } catch (e) {}
177
+ send({ type: "term_list", terminals: tm.list() });
178
+ }
179
+
180
+ // Restart the vendor adapter so the next query spawns a process that reads
181
+ // the credentials the login just wrote. Shared adapter instances (Claude) are
182
+ // reused by every project, so tearing one down here would kill unrelated
183
+ // sessions; those runtimes pick up new credentials on their own.
184
+ function refreshVendorAuth(vendor) {
185
+ var adapter = adapters[vendor];
186
+ if (!adapter || typeof adapter.shutdown !== "function") return Promise.resolve(false);
187
+ if (adapter.shared) {
188
+ console.log("[vendor-login] " + vendor + " adapter is shared; skipping restart for " + slug);
189
+ return Promise.resolve(false);
190
+ }
191
+ // adapter.shutdown() already fans out to every per-OS-user runtime it
192
+ // created (shutdownUserRuntimes), so one call covers isolated setups too.
193
+ return Promise.resolve()
194
+ .then(function () { return adapter.shutdown(); })
195
+ .then(function () {
196
+ console.log("[vendor-login] Restarted " + vendor + " adapter for " + slug + " after login");
197
+ return true;
198
+ })
199
+ .catch(function (err) {
200
+ console.error("[vendor-login] " + vendor + " adapter restart failed for " + slug + ":",
201
+ err && err.message ? err.message : err);
202
+ return false;
203
+ });
204
+ }
205
+
206
+ function finishFlow(vendor, succeeded) {
207
+ var flow = _flows[vendor];
208
+ if (!flow || flow.finishing) return;
209
+ flow.finishing = true;
210
+ flow.completed = !!succeeded;
211
+
212
+ var refresh = succeeded ? refreshVendorAuth(vendor) : Promise.resolve(false);
213
+ refresh.then(function (restarted) {
214
+ discardFlow(vendor);
215
+ closeFlowTerminal(flow);
216
+ if (succeeded) {
217
+ send({ type: "auth_refreshed", slug: slug, vendor: vendor, adapterRestarted: !!restarted });
218
+ }
219
+ broadcastState();
220
+ });
221
+ }
222
+
223
+ function handleFlowOutput(vendor, chunk) {
224
+ var flow = _flows[vendor];
225
+ if (!flow || flow.finishing || flow.succeeded) return;
226
+ flow.outputTail = (flow.outputTail + stripAnsi(chunk)).slice(-OUTPUT_TAIL_MAX);
227
+ if (!looksLikeLoginSuccess(flow.outputTail)) return;
228
+
229
+ flow.succeeded = true;
230
+ console.log("[vendor-login] Detected successful " + vendor + " login in " + slug);
231
+ // Let the success line land on screen before the terminal disappears.
232
+ flow.closeTimer = setTimeout(function () {
233
+ finishFlow(vendor, true);
234
+ }, CLOSE_AFTER_SUCCESS_MS);
235
+ if (flow.closeTimer && typeof flow.closeTimer.unref === "function") flow.closeTimer.unref();
236
+ }
237
+
238
+ // Fires for both a PTY that exited on its own and one the user closed from
239
+ // the sidebar. Either way the flow is over; refresh auth if the command had
240
+ // already reported success.
241
+ function handleFlowExit(vendor) {
242
+ var flow = _flows[vendor];
243
+ if (!flow || flow.closing || flow.finishing) return;
244
+ finishFlow(vendor, !!flow.succeeded);
245
+ }
246
+
247
+ function startFlow(ws, vendor, msg) {
248
+ var identity = resolveLoginIdentity(ws, msg);
249
+ var command = loginCommandFor(vendor);
250
+ var flow = {
251
+ vendor: vendor,
252
+ terminalId: null,
253
+ startedAt: Date.now(),
254
+ linuxUser: identity.linuxUser,
255
+ outputTail: "",
256
+ succeeded: false,
257
+ finishing: false,
258
+ closing: false,
259
+ completed: false,
260
+ closeTimer: null,
261
+ };
262
+ // Registered before create() so the PTY's first output chunk (hooks fire
263
+ // synchronously from spawn) already finds the record.
264
+ _flows[vendor] = flow;
265
+
266
+ var terminal = null;
267
+ try {
268
+ terminal = tm.create(100, 30, identity.osUserInfo, ws, {
269
+ initialInput: command + "\n",
270
+ title: vendorDisplayName(vendor) + " login",
271
+ kind: "vendor-login",
272
+ onData: function (data) { handleFlowOutput(vendor, data); },
273
+ onExit: function () { handleFlowExit(vendor); },
274
+ });
275
+ } catch (e) {
276
+ console.error("[vendor-login] Failed to spawn " + vendor + " login terminal in " + slug + ":",
277
+ e && e.message ? e.message : e);
278
+ }
279
+
280
+ if (!terminal) {
281
+ delete _flows[vendor];
282
+ return null;
283
+ }
284
+
285
+ flow.terminalId = terminal.id;
286
+ tm.attach(terminal.id, ws);
287
+ send({ type: "term_list", terminals: tm.list() });
288
+ return flow;
289
+ }
290
+
291
+ function handleVendorLoginMessage(ws, msg) {
292
+ if (msg.type === "vendor_login_state_request") {
293
+ sendStateTo(ws);
294
+ return true;
295
+ }
296
+
297
+ if (msg.type === "vendor_login_cancel") {
298
+ var cancelVendor = String(msg.vendor || "");
299
+ var cancelled = discardFlow(cancelVendor);
300
+ if (cancelled) closeFlowTerminal(cancelled);
301
+ broadcastState();
302
+ return true;
303
+ }
304
+
305
+ if (msg.type === "vendor_login_start") {
306
+ var vendor = String(msg.vendor || "claude");
307
+ if (!vendorRegistry.getVendorInfo(vendor)) {
308
+ sendTo(ws, { type: "vendor_login_error", vendor: vendor, error: "Unknown vendor: " + vendor });
309
+ return true;
310
+ }
311
+ if (!hasTerminalPermission(ws)) {
312
+ sendTo(ws, { type: "vendor_login_error", vendor: vendor, error: "Terminal access is not permitted" });
313
+ return true;
314
+ }
315
+
316
+ var existing = _flows[vendor];
317
+ if (existing && !isFlowTerminalAlive(existing)) {
318
+ // Terminal died without its exit hook clearing the record.
319
+ discardFlow(vendor);
320
+ existing = null;
321
+ }
322
+
323
+ if (existing) {
324
+ // An auto-triggered request (a fresh auth_required from another
325
+ // session or split pane) must not pop a second prompt; it only learns
326
+ // that a flow is already running. A deliberate request re-attaches.
327
+ if (!msg.auto) {
328
+ tm.attach(existing.terminalId, ws);
329
+ sendTo(ws, {
330
+ type: "vendor_login_ready",
331
+ slug: slug,
332
+ vendor: vendor,
333
+ terminalId: existing.terminalId,
334
+ reused: true,
335
+ });
336
+ }
337
+ sendStateTo(ws);
338
+ return true;
339
+ }
340
+
341
+ var started = startFlow(ws, vendor, msg);
342
+ if (!started) {
343
+ sendTo(ws, {
344
+ type: "vendor_login_error",
345
+ vendor: vendor,
346
+ error: "Cannot create terminal (node-pty not available or limit reached)",
347
+ });
348
+ return true;
349
+ }
350
+
351
+ console.log("[vendor-login] Started " + vendor + " login terminal " + started.terminalId + " in " + slug);
352
+ sendTo(ws, {
353
+ type: "vendor_login_ready",
354
+ slug: slug,
355
+ vendor: vendor,
356
+ terminalId: started.terminalId,
357
+ reused: false,
358
+ });
359
+ broadcastState();
360
+ return true;
361
+ }
362
+
363
+ return false;
364
+ }
365
+
366
+ // The sidebar "close terminal" path goes through tm.close() directly, which
367
+ // fires our exit hook. This is the explicit entry point for callers that
368
+ // remove a terminal without going through the PTY.
369
+ function forgetTerminal(terminalId) {
370
+ var vendors = Object.keys(_flows);
371
+ for (var i = 0; i < vendors.length; i++) {
372
+ if (_flows[vendors[i]].terminalId === terminalId) {
373
+ handleFlowExit(vendors[i]);
374
+ return true;
375
+ }
376
+ }
377
+ return false;
378
+ }
379
+
380
+ function isLoginTerminal(terminalId) {
381
+ var vendors = Object.keys(_flows);
382
+ for (var i = 0; i < vendors.length; i++) {
383
+ if (_flows[vendors[i]].terminalId === terminalId) return true;
384
+ }
385
+ return false;
386
+ }
387
+
388
+ return {
389
+ handleVendorLoginMessage: handleVendorLoginMessage,
390
+ sendStateTo: sendStateTo,
391
+ listFlows: listFlows,
392
+ forgetTerminal: forgetTerminal,
393
+ isLoginTerminal: isLoginTerminal,
394
+ };
395
+ }
396
+
397
+ module.exports = { attachVendorLogin: attachVendorLogin };