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.
- package/lib/ask-user-mcp-server.js +5 -119
- package/lib/builtin-mates.js +6 -6
- package/lib/claude-hook-installer.js +15 -0
- package/lib/clay-history-mcp-server.js +62 -297
- package/lib/daemon.js +1 -1
- package/lib/debate-model-selection.js +119 -0
- package/lib/home-debate-tool-policy.js +164 -0
- package/lib/home-response-notifications.js +26 -0
- package/lib/mate-creation-mcp-server.js +55 -0
- package/lib/mate-ready-creation.js +95 -0
- package/lib/mates.js +2 -0
- package/lib/notes.js +2 -1
- package/lib/project-ask-user.js +64 -0
- package/lib/project-capsule-catalog.js +101 -0
- package/lib/project-connection.js +4 -0
- package/lib/project-debate-proposal.js +133 -17
- package/lib/project-debate.js +214 -110
- package/lib/project-delegated-follow-up.js +84 -0
- package/lib/project-delegated-session.js +53 -0
- package/lib/project-filesystem.js +7 -9
- package/lib/project-knowledge.js +1 -0
- package/lib/project-mate-creation-proposal.js +120 -0
- package/lib/project-memory.js +14 -7
- package/lib/project-models.js +157 -32
- package/lib/project-session-notes.js +2 -2
- package/lib/project-session-pair.js +8 -8
- package/lib/project-sessions.js +66 -51
- package/lib/project-vendor-login.js +397 -0
- package/lib/project-worker-proposal.js +25 -25
- package/lib/project-workspace-query.js +144 -0
- package/lib/project.js +349 -102
- package/lib/public/app.js +85 -42
- package/lib/public/css/avatar-imprints.css +0 -1
- package/lib/public/css/base.css +4 -0
- package/lib/public/css/capsule-ui.css +389 -0
- package/lib/public/css/command-palette.css +306 -6
- package/lib/public/css/home-capsule-library.css +243 -0
- package/lib/public/css/home-capsule-source.css +192 -0
- package/lib/public/css/home-debate-live.css +146 -0
- package/lib/public/css/home-debate-planning.css +125 -0
- package/lib/public/css/home-debates-archive.css +281 -0
- package/lib/public/css/home-hub.css +896 -405
- package/lib/public/css/home-mate-creation.css +34 -0
- package/lib/public/css/home-mate-model-picker.css +131 -0
- package/lib/public/css/home-mate-settings.css +344 -0
- package/lib/public/css/home-session-actions.css +265 -0
- package/lib/public/css/home-sidebar.css +667 -0
- package/lib/public/css/icon-strip.css +1 -2
- package/lib/public/css/mates.css +7 -34
- package/lib/public/css/pane.css +1 -1
- package/lib/public/css/playbook.css +0 -80
- package/lib/public/css/sticky-notes.css +53 -117
- package/lib/public/css/tui-attention.css +0 -44
- package/lib/public/css/workspace-assignment.css +45 -0
- package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
- package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
- package/lib/public/index.html +93 -74
- package/lib/public/modules/app-connection.js +10 -1
- package/lib/public/modules/app-dm.js +43 -160
- package/lib/public/modules/app-home-hub.js +328 -585
- package/lib/public/modules/app-message-cards.js +242 -0
- package/lib/public/modules/app-message-router.js +84 -0
- package/lib/public/modules/app-messages.js +102 -37
- package/lib/public/modules/app-notifications.js +8 -119
- package/lib/public/modules/app-projects.js +9 -1
- package/lib/public/modules/app-rendering.js +21 -602
- package/lib/public/modules/avatar-imprint.js +11 -8
- package/lib/public/modules/chat-bubble-renderer.js +152 -0
- package/lib/public/modules/chat-render-runtime.js +198 -0
- package/lib/public/modules/command-palette.js +225 -436
- package/lib/public/modules/dm-render.js +82 -0
- package/lib/public/modules/home-capsule-creation-intent.js +31 -0
- package/lib/public/modules/home-capsule-library.js +146 -0
- package/lib/public/modules/home-capsule-source.js +383 -0
- package/lib/public/modules/home-chat-identity.js +35 -0
- package/lib/public/modules/home-chat-scroll.js +77 -0
- package/lib/public/modules/home-chat-stream-state.js +34 -0
- package/lib/public/modules/home-composer-focus.js +25 -0
- package/lib/public/modules/home-conversations-sheet.js +179 -0
- package/lib/public/modules/home-debate-activity.js +48 -0
- package/lib/public/modules/home-debate-controls.js +295 -0
- package/lib/public/modules/home-debate-launch.js +41 -0
- package/lib/public/modules/home-debate-live.js +284 -0
- package/lib/public/modules/home-debate-models.js +73 -0
- package/lib/public/modules/home-debate-planning.js +335 -0
- package/lib/public/modules/home-debates-archive.js +279 -0
- package/lib/public/modules/home-dock-resize.js +74 -0
- package/lib/public/modules/home-dock.js +446 -0
- package/lib/public/modules/home-mate-chat.js +496 -0
- package/lib/public/modules/home-mate-creation.js +109 -0
- package/lib/public/modules/home-mate-model-picker.js +281 -0
- package/lib/public/modules/home-mate-selection.js +38 -0
- package/lib/public/modules/home-mate-settings-menu.js +131 -0
- package/lib/public/modules/home-mate-settings.js +383 -0
- package/lib/public/modules/home-session-actions.js +373 -0
- package/lib/public/modules/home-session-model-confirmation.js +13 -0
- package/lib/public/modules/home-shell.js +9 -0
- package/lib/public/modules/home-sidebar-chat-list.js +63 -0
- package/lib/public/modules/home-sidebar.js +158 -0
- package/lib/public/modules/home-sub-surface.js +19 -0
- package/lib/public/modules/home-surface-boot.js +68 -0
- package/lib/public/modules/home-surface.js +141 -0
- package/lib/public/modules/home-tools.js +271 -0
- package/lib/public/modules/markdown.js +50 -2
- package/lib/public/modules/mate-management.js +86 -0
- package/lib/public/modules/mate-wizard.js +0 -7
- package/lib/public/modules/mention.js +17 -19
- package/lib/public/modules/pane-bridge.js +9 -0
- package/lib/public/modules/profile.js +4 -5
- package/lib/public/modules/project-settings.js +34 -28
- package/lib/public/modules/project-switcher.js +11 -5
- package/lib/public/modules/project-workspace-assignment.js +23 -0
- package/lib/public/modules/search-clay-chat.js +402 -0
- package/lib/public/modules/server-settings.js +14 -12
- package/lib/public/modules/session-actions.js +1 -1
- package/lib/public/modules/sidebar-mates.js +56 -552
- package/lib/public/modules/sidebar-mobile.js +7 -64
- package/lib/public/modules/sidebar-presence.js +37 -0
- package/lib/public/modules/split-pair-ui.js +13 -13
- package/lib/public/modules/split-view.js +7 -0
- package/lib/public/modules/sticky-notes.js +7 -6
- package/lib/public/modules/tool-input-composition.js +48 -0
- package/lib/public/modules/tool-llm-status.js +167 -0
- package/lib/public/modules/tool-renderer-advanced.js +285 -0
- package/lib/public/modules/tool-renderer-chart.js +158 -0
- package/lib/public/modules/tool-renderer-semantics.js +35 -0
- package/lib/public/modules/tool-renderer.js +464 -0
- package/lib/public/modules/tool-runtime.js +309 -0
- package/lib/public/modules/tool-ui-evaluator.js +97 -0
- package/lib/public/modules/tool-ui-tree.js +21 -0
- package/lib/public/modules/tui-attention.js +10 -0
- package/lib/public/modules/user-settings.js +16 -23
- package/lib/public/modules/vendor-login.js +287 -0
- package/lib/public/modules/worker-proposal.js +2 -2
- package/lib/public/modules/workspace-assignment-card.js +149 -0
- package/lib/public/modules/workspace-assignment-routing.js +4 -0
- package/lib/public/style.css +13 -2
- package/lib/runtime-env.js +65 -0
- package/lib/sdk-bridge.js +120 -32
- package/lib/sdk-message-processor.js +10 -6
- package/lib/server-home-capsule-creation.js +36 -0
- package/lib/server-home-chat-events.js +287 -0
- package/lib/server-home-chat.js +518 -0
- package/lib/server-home-clay-entry.js +65 -0
- package/lib/server-home-clay-session-links.js +26 -0
- package/lib/server-home-debate-planning.js +214 -0
- package/lib/server-home-debates.js +137 -0
- package/lib/server-home-mate-creation.js +261 -0
- package/lib/server-home-models.js +241 -0
- package/lib/server-home-preferences.js +61 -0
- package/lib/server-mates.js +37 -0
- package/lib/server-tools.js +377 -0
- package/lib/server.js +111 -59
- package/lib/session-pair-mcp-server.js +6 -6
- package/lib/sessions.js +18 -3
- package/lib/tool-capsule-source.js +142 -0
- package/lib/tool-control-mcp-server.js +126 -0
- package/lib/tool-llm.js +48 -0
- package/lib/tool-storage.js +77 -0
- package/lib/tool-ui-spec-advanced.js +82 -0
- package/lib/tool-ui-spec.js +261 -0
- package/lib/tools-registry.js +286 -0
- package/lib/users-home-dock-preferences.js +77 -0
- package/lib/users-home-surface-preferences.js +99 -0
- package/lib/users-preferences.js +4 -7
- package/lib/users.js +12 -0
- package/lib/whats-new-content.js +3 -3
- package/lib/workspace-assignment-service.js +420 -0
- package/lib/workspace-query-mcp-server.js +154 -0
- package/lib/workspace-query-service.js +492 -0
- package/lib/ws-schema.js +93 -12
- package/lib/yoke/adapters/acp.js +2 -1
- package/lib/yoke/adapters/antigravity.js +2 -1
- package/lib/yoke/adapters/claude-worker.js +11 -4
- package/lib/yoke/adapters/claude.js +73 -43
- package/lib/yoke/adapters/codex.js +40 -10
- package/lib/yoke/adapters/kiro.js +2 -1
- package/lib/yoke/claude-user-input.js +38 -0
- package/lib/yoke/codex-app-server.js +20 -2
- package/lib/yoke/codex-user-input.js +64 -0
- package/lib/yoke/complete-once.js +164 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/interface.js +19 -0
- package/lib/yoke/mcp-bridge-server.js +2 -6
- package/lib/yoke/user-input.js +254 -0
- package/package.json +3 -2
- package/lib/public/css/home-chat.css +0 -380
- package/lib/public/modules/home-chat.js +0 -438
- 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
|
|
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 = "
|
|
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
|
}
|
package/lib/project-sessions.js
CHANGED
|
@@ -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)
|
|
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
|
|
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 (
|
|
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
|
|
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 };
|