clay-server 3.4.0-beta.1 → 3.4.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/project-shell-command.js +160 -0
- package/lib/project-user-message.js +10 -0
- package/lib/project.js +13 -0
- package/lib/public/app.js +10 -0
- package/lib/public/css/input.css +63 -0
- package/lib/public/index.html +5 -0
- package/lib/public/modules/app-messages.js +16 -1
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/input.js +58 -28
- package/lib/public/modules/shell-command.js +148 -0
- package/lib/sdk-message-processor.js +10 -1
- package/lib/ws-schema.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
var { spawn } = require("child_process");
|
|
2
|
+
var { buildUserEnv } = require("./build-user-env");
|
|
3
|
+
var { wrapSpawnAsUser } = require("./os-users");
|
|
4
|
+
|
|
5
|
+
var MAX_COMMAND_LENGTH = 16 * 1024;
|
|
6
|
+
var MAX_OUTPUT_LENGTH = 64 * 1024;
|
|
7
|
+
var COMMAND_TIMEOUT_MS = 30 * 1000;
|
|
8
|
+
|
|
9
|
+
function stripTerminalCodes(value) {
|
|
10
|
+
return String(value || "")
|
|
11
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
12
|
+
.replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, "")
|
|
13
|
+
.replace(/\r/g, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function attachShellCommand(ctx) {
|
|
17
|
+
var runningBySocket = new WeakMap();
|
|
18
|
+
|
|
19
|
+
function sendResult(ws, data) {
|
|
20
|
+
ctx.sendTo(ws, Object.assign({ type: "shell_command_result" }, data));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function handleShellCommand(ws, msg) {
|
|
24
|
+
if (msg.type !== "shell_command") return false;
|
|
25
|
+
|
|
26
|
+
var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 100) : "";
|
|
27
|
+
var command = typeof msg.command === "string" ? msg.command.trim() : "";
|
|
28
|
+
if (!requestId || !command) {
|
|
29
|
+
sendResult(ws, { requestId: requestId, error: "Enter a command to run." });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (command.length > MAX_COMMAND_LENGTH) {
|
|
33
|
+
sendResult(ws, { requestId: requestId, error: "Command is too long." });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (ws._clayUser) {
|
|
38
|
+
var permissions = ctx.usersModule.getEffectivePermissions(ws._clayUser, ctx.osUsers);
|
|
39
|
+
if (!permissions.terminal) {
|
|
40
|
+
sendResult(ws, { requestId: requestId, error: "Terminal access is not permitted." });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (runningBySocket.get(ws)) {
|
|
45
|
+
sendResult(ws, { requestId: requestId, error: "Another shell command is still running." });
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var session = ctx.getSessionForWs(ws);
|
|
50
|
+
if (!session) {
|
|
51
|
+
sendResult(ws, { requestId: requestId, error: "No active session." });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
var osUserInfo = ctx.getOsUserInfoForWs(ws);
|
|
56
|
+
var shell = (osUserInfo && osUserInfo.shell)
|
|
57
|
+
|| process.env.SHELL
|
|
58
|
+
|| (process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : "/bin/sh");
|
|
59
|
+
var args = process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-lc", command];
|
|
60
|
+
var spawnOptions = {
|
|
61
|
+
cwd: ctx.cwd,
|
|
62
|
+
env: buildUserEnv(osUserInfo),
|
|
63
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
64
|
+
};
|
|
65
|
+
if (osUserInfo) {
|
|
66
|
+
spawnOptions.uid = osUserInfo.uid;
|
|
67
|
+
spawnOptions.gid = osUserInfo.gid;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var wrapped = wrapSpawnAsUser(shell, args, spawnOptions);
|
|
71
|
+
var child;
|
|
72
|
+
try {
|
|
73
|
+
child = spawn(wrapped.command, wrapped.args, wrapped.options);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
sendResult(ws, { requestId: requestId, error: e.message || "Failed to start command." });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
runningBySocket.set(ws, child);
|
|
80
|
+
var chunks = [];
|
|
81
|
+
var outputLength = 0;
|
|
82
|
+
var truncated = false;
|
|
83
|
+
var timedOut = false;
|
|
84
|
+
var finished = false;
|
|
85
|
+
|
|
86
|
+
function collect(data) {
|
|
87
|
+
if (outputLength >= MAX_OUTPUT_LENGTH) {
|
|
88
|
+
truncated = true;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
var text = data.toString("utf8");
|
|
92
|
+
var remaining = MAX_OUTPUT_LENGTH - outputLength;
|
|
93
|
+
if (text.length > remaining) {
|
|
94
|
+
text = text.slice(0, remaining);
|
|
95
|
+
truncated = true;
|
|
96
|
+
}
|
|
97
|
+
chunks.push(text);
|
|
98
|
+
outputLength += text.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
child.stdout.on("data", collect);
|
|
102
|
+
child.stderr.on("data", collect);
|
|
103
|
+
|
|
104
|
+
var timer = setTimeout(function () {
|
|
105
|
+
timedOut = true;
|
|
106
|
+
try { child.kill("SIGTERM"); } catch (e) {}
|
|
107
|
+
setTimeout(function () {
|
|
108
|
+
if (!finished) {
|
|
109
|
+
try { child.kill("SIGKILL"); } catch (e) {}
|
|
110
|
+
}
|
|
111
|
+
}, 1000).unref();
|
|
112
|
+
}, COMMAND_TIMEOUT_MS);
|
|
113
|
+
timer.unref();
|
|
114
|
+
|
|
115
|
+
child.on("error", function (error) {
|
|
116
|
+
if (finished) return;
|
|
117
|
+
finished = true;
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
runningBySocket.delete(ws);
|
|
120
|
+
sendResult(ws, { requestId: requestId, command: command, error: error.message || "Command failed." });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
child.on("close", function (code, signal) {
|
|
124
|
+
if (finished) return;
|
|
125
|
+
finished = true;
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
runningBySocket.delete(ws);
|
|
128
|
+
|
|
129
|
+
var output = stripTerminalCodes(chunks.join(""));
|
|
130
|
+
if (truncated) output += "\n… output truncated at 64 KB";
|
|
131
|
+
if (timedOut) output += (output ? "\n" : "") + "Command timed out after 30 seconds.";
|
|
132
|
+
var exitCode = typeof code === "number" ? code : null;
|
|
133
|
+
var context = [
|
|
134
|
+
"[Shell command executed by the user]",
|
|
135
|
+
"$ " + command,
|
|
136
|
+
output || "(no output)",
|
|
137
|
+
"[Exit code: " + (exitCode == null ? (signal || "unknown") : exitCode) + "]",
|
|
138
|
+
].join("\n");
|
|
139
|
+
if (!session.pendingShellContexts) session.pendingShellContexts = [];
|
|
140
|
+
session.pendingShellContexts.push(context);
|
|
141
|
+
|
|
142
|
+
sendResult(ws, {
|
|
143
|
+
requestId: requestId,
|
|
144
|
+
sessionId: session.localId,
|
|
145
|
+
command: command,
|
|
146
|
+
output: output,
|
|
147
|
+
exitCode: exitCode,
|
|
148
|
+
signal: signal || null,
|
|
149
|
+
timedOut: timedOut,
|
|
150
|
+
truncated: truncated,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { handleShellCommand: handleShellCommand };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { attachShellCommand: attachShellCommand };
|
|
@@ -374,6 +374,16 @@ function attachUserMessage(ctx) {
|
|
|
374
374
|
fullText = mentionPrefix + "\n\n" + fullText;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// Inject one-shot shell results captured from the composer command mode.
|
|
378
|
+
if (session.pendingShellContexts && session.pendingShellContexts.length > 0) {
|
|
379
|
+
var shellPrefix = session.pendingShellContexts.join("\n\n");
|
|
380
|
+
session.pendingShellContexts = [];
|
|
381
|
+
fullText = shellPrefix + "\n\n" + fullText;
|
|
382
|
+
}
|
|
383
|
+
if (msg.shellCommandResponse) {
|
|
384
|
+
fullText += "\n\n[Respond to the shell command result above now.]";
|
|
385
|
+
}
|
|
386
|
+
|
|
377
387
|
// Inject active terminal context sources (delta only: send new output since last message)
|
|
378
388
|
var TERM_CONTEXT_MAX = 8192; // 8KB max per terminal per message
|
|
379
389
|
var TERM_HEAD_SIZE = 2048; // keep first 2KB for error context
|
package/lib/project.js
CHANGED
|
@@ -28,6 +28,7 @@ var { attachKnowledge } = require("./project-knowledge");
|
|
|
28
28
|
var { attachFilesystem } = require("./project-filesystem");
|
|
29
29
|
var { attachSessions } = require("./project-sessions");
|
|
30
30
|
var { attachUserMessage } = require("./project-user-message");
|
|
31
|
+
var { attachShellCommand } = require("./project-shell-command");
|
|
31
32
|
var { attachConnection } = require("./project-connection");
|
|
32
33
|
var { attachMcp } = require("./project-mcp");
|
|
33
34
|
var { createLocalMcp } = require("./mcp-local");
|
|
@@ -1176,6 +1177,9 @@ function createProjectContext(opts) {
|
|
|
1176
1177
|
// --- Filesystem, settings, env (delegated to project-filesystem.js) ---
|
|
1177
1178
|
if (_filesystem.handleFilesystemMessage(ws, msg)) return;
|
|
1178
1179
|
|
|
1180
|
+
// --- Shell command context ---
|
|
1181
|
+
if (_shellCommand.handleShellCommand(ws, msg)) return;
|
|
1182
|
+
|
|
1179
1183
|
// --- Notes, terminals, context, user message (delegated to project-user-message.js) ---
|
|
1180
1184
|
if (_userMessage.handleUserMessage(ws, msg)) return;
|
|
1181
1185
|
}
|
|
@@ -1424,6 +1428,15 @@ function createProjectContext(opts) {
|
|
|
1424
1428
|
_email: _email,
|
|
1425
1429
|
});
|
|
1426
1430
|
|
|
1431
|
+
var _shellCommand = attachShellCommand({
|
|
1432
|
+
cwd: cwd,
|
|
1433
|
+
osUsers: osUsers,
|
|
1434
|
+
usersModule: usersModule,
|
|
1435
|
+
sendTo: sendTo,
|
|
1436
|
+
getSessionForWs: getSessionForWs,
|
|
1437
|
+
getOsUserInfoForWs: getOsUserInfoForWs,
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1427
1440
|
// --- Filesystem handler (delegated to project-filesystem.js) ---
|
|
1428
1441
|
var _filesystem = attachFilesystem({
|
|
1429
1442
|
cwd: cwd,
|
package/lib/public/app.js
CHANGED
|
@@ -51,6 +51,7 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
|
|
|
51
51
|
import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } from './modules/mate-wizard.js';
|
|
52
52
|
import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
|
|
53
53
|
import { initLongPress } from './modules/longpress.js';
|
|
54
|
+
import { initShellCommand } from './modules/shell-command.js';
|
|
54
55
|
import { initConnection, connect as _connConnect, setStatus as _connSetStatus, scheduleReconnect as _connScheduleReconnect, cancelReconnect as _connCancelReconnect } from './modules/app-connection.js';
|
|
55
56
|
import { processMessage as _msgProcessMessage } from './modules/app-messages.js';
|
|
56
57
|
import { getWs as _getWsRef, setWs as _setWsRef } from './modules/ws-ref.js';
|
|
@@ -308,6 +309,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
308
309
|
returningFromMateDm: false,
|
|
309
310
|
pendingMateInterview: null,
|
|
310
311
|
pendingTermCommand: null,
|
|
312
|
+
shellCommandMode: false,
|
|
313
|
+
shellCommandRunning: false,
|
|
314
|
+
pendingShellCommandId: null,
|
|
311
315
|
mateProjectSlug: null,
|
|
312
316
|
myUserId: null,
|
|
313
317
|
isMultiUserMode: false,
|
|
@@ -793,6 +797,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
793
797
|
},
|
|
794
798
|
});
|
|
795
799
|
|
|
800
|
+
initShellCommand();
|
|
801
|
+
|
|
796
802
|
// --- @Mention module ---
|
|
797
803
|
initMention({
|
|
798
804
|
get ws() { return _getWsRef(); },
|
|
@@ -879,6 +885,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
879
885
|
if (termBtn) termBtn.style.display = "none";
|
|
880
886
|
var termSideBtn = document.getElementById("terminal-sidebar-btn");
|
|
881
887
|
if (termSideBtn) termSideBtn.style.display = "none";
|
|
888
|
+
var shellCommandBtn = document.getElementById("shell-command-btn");
|
|
889
|
+
if (shellCommandBtn) shellCommandBtn.style.display = "none";
|
|
890
|
+
var mobileShellCommandBtn = document.getElementById("input-more-shell");
|
|
891
|
+
if (mobileShellCommandBtn) mobileShellCommandBtn.style.display = "none";
|
|
882
892
|
}
|
|
883
893
|
if (!_perms.fileBrowser) {
|
|
884
894
|
var fbBtn = document.getElementById("file-browser-btn");
|
package/lib/public/css/input.css
CHANGED
|
@@ -826,6 +826,7 @@
|
|
|
826
826
|
|
|
827
827
|
#attach-file-btn,
|
|
828
828
|
#attach-image-btn,
|
|
829
|
+
#shell-command-btn,
|
|
829
830
|
#schedule-btn,
|
|
830
831
|
#input-more-btn {
|
|
831
832
|
width: 36px;
|
|
@@ -844,14 +845,76 @@
|
|
|
844
845
|
|
|
845
846
|
#attach-file-btn .lucide,
|
|
846
847
|
#attach-image-btn .lucide,
|
|
848
|
+
#shell-command-btn .lucide,
|
|
847
849
|
#schedule-btn .lucide,
|
|
848
850
|
#input-more-btn .lucide { width: 20px; height: 20px; flex-shrink: 0; }
|
|
849
851
|
|
|
850
852
|
#attach-file-btn:hover,
|
|
851
853
|
#attach-image-btn:hover,
|
|
854
|
+
#shell-command-btn:hover,
|
|
852
855
|
#schedule-btn:hover,
|
|
853
856
|
#input-more-btn:hover { background: rgba(var(--overlay-rgb), 0.06); color: var(--text); }
|
|
854
857
|
|
|
858
|
+
#shell-command-btn.active {
|
|
859
|
+
background: var(--accent-12);
|
|
860
|
+
color: var(--accent);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
#input-row.shell-command-mode {
|
|
864
|
+
box-shadow: 0 0 0 1px var(--accent);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
#input-row.shell-command-mode #input {
|
|
868
|
+
font-family: var(--font-mono, "SFMono-Regular", Consolas, monospace);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.shell-command-card {
|
|
872
|
+
width: min(760px, calc(100% - 24px));
|
|
873
|
+
margin: 10px auto;
|
|
874
|
+
border: 1px solid var(--border);
|
|
875
|
+
border-radius: 8px;
|
|
876
|
+
background: var(--input-bg);
|
|
877
|
+
overflow: hidden;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.shell-command-card.error { border-color: rgba(239, 68, 68, 0.45); }
|
|
881
|
+
|
|
882
|
+
.shell-command-header {
|
|
883
|
+
display: flex;
|
|
884
|
+
align-items: center;
|
|
885
|
+
gap: 8px;
|
|
886
|
+
padding: 9px 12px;
|
|
887
|
+
border-bottom: 1px solid var(--border);
|
|
888
|
+
color: var(--text-secondary);
|
|
889
|
+
font-size: 12px;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.shell-command-header code {
|
|
893
|
+
min-width: 0;
|
|
894
|
+
flex: 1;
|
|
895
|
+
overflow: hidden;
|
|
896
|
+
text-overflow: ellipsis;
|
|
897
|
+
white-space: nowrap;
|
|
898
|
+
color: var(--text);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.shell-command-icon { display: inline-flex; }
|
|
902
|
+
.shell-command-icon .lucide { width: 16px; height: 16px; }
|
|
903
|
+
.shell-command-status { flex-shrink: 0; color: var(--text-muted); }
|
|
904
|
+
|
|
905
|
+
.shell-command-card.running .shell-command-status { color: var(--accent); }
|
|
906
|
+
|
|
907
|
+
.shell-command-output {
|
|
908
|
+
max-height: 320px;
|
|
909
|
+
margin: 0;
|
|
910
|
+
padding: 12px;
|
|
911
|
+
overflow: auto;
|
|
912
|
+
color: var(--text-secondary);
|
|
913
|
+
font: 12px/1.5 var(--font-mono, "SFMono-Regular", Consolas, monospace);
|
|
914
|
+
white-space: pre-wrap;
|
|
915
|
+
word-break: break-word;
|
|
916
|
+
}
|
|
917
|
+
|
|
855
918
|
#ask-mate-btn {
|
|
856
919
|
width: 36px;
|
|
857
920
|
height: 36px;
|
package/lib/public/index.html
CHANGED
|
@@ -505,6 +505,7 @@
|
|
|
505
505
|
<button id="input-more-btn" type="button" aria-label="More options" title="More options" class="mobile-only"><i data-lucide="plus"></i></button>
|
|
506
506
|
<button id="attach-file-btn" type="button" aria-label="Attach file" title="Attach file" class="desktop-only"><i data-lucide="paperclip"></i></button>
|
|
507
507
|
<button id="attach-image-btn" type="button" aria-label="Attach image" title="Attach image" class="desktop-only"><i data-lucide="image"></i></button>
|
|
508
|
+
<button id="shell-command-btn" type="button" aria-label="Run shell command" aria-pressed="false" title="Run a shell command and add its output to the next agent message" class="desktop-only"><i data-lucide="square-terminal"></i></button>
|
|
508
509
|
<button id="stt-btn" type="button" aria-label="Voice input" title="Voice input"><i data-lucide="mic"></i></button>
|
|
509
510
|
<button id="schedule-btn" type="button" aria-label="Schedule message" title="Schedule message"><i data-lucide="clock"></i></button>
|
|
510
511
|
<button id="ask-mate-btn" type="button" aria-label="Ask Mate" title="Ask a Mate for advice on this session"><i data-lucide="at-sign"></i></button>
|
|
@@ -615,6 +616,10 @@
|
|
|
615
616
|
<i data-lucide="image"></i>
|
|
616
617
|
<span>Image</span>
|
|
617
618
|
</button>
|
|
619
|
+
<button class="input-more-action" id="input-more-shell">
|
|
620
|
+
<i data-lucide="square-terminal"></i>
|
|
621
|
+
<span>Shell command</span>
|
|
622
|
+
</button>
|
|
618
623
|
</div>
|
|
619
624
|
<div class="input-more-divider"></div>
|
|
620
625
|
<div class="input-more-section-label">
|
|
@@ -22,7 +22,7 @@ import { handleFindInSessionResults } from './session-search.js';
|
|
|
22
22
|
import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
|
|
23
23
|
import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
|
|
24
24
|
import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
|
|
25
|
-
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
|
|
25
|
+
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent } from './input.js';
|
|
26
26
|
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
|
|
27
27
|
import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
|
|
28
28
|
import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch, presentMarkdownEdit } from './filebrowser.js';
|
|
@@ -42,6 +42,7 @@ import { checkAdminAccess } from './admin.js';
|
|
|
42
42
|
import { mateAvatarUrl } from './avatar.js';
|
|
43
43
|
import { showImageModal, sendExtensionCommand, handleMcpToolCallMessage } from './app-misc.js';
|
|
44
44
|
import { handleMcpServersState } from './mcp-ui.js';
|
|
45
|
+
import { handleShellCommandResult } from './shell-command.js';
|
|
45
46
|
import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
|
|
46
47
|
|
|
47
48
|
// --- App module imports ---
|
|
@@ -1209,6 +1210,15 @@ export function processMessage(msg) {
|
|
|
1209
1210
|
addConflictMessage(msg);
|
|
1210
1211
|
break;
|
|
1211
1212
|
|
|
1213
|
+
case "session_writer_conflict":
|
|
1214
|
+
removeMatePreThinking();
|
|
1215
|
+
setActivity(null);
|
|
1216
|
+
stopThinking();
|
|
1217
|
+
markAllToolsDone();
|
|
1218
|
+
closeToolGroup();
|
|
1219
|
+
addSystemMessage(msg.text || "This session is already open in another process.", true);
|
|
1220
|
+
break;
|
|
1221
|
+
|
|
1212
1222
|
case "context_overflow":
|
|
1213
1223
|
removeMatePreThinking();
|
|
1214
1224
|
setActivity(null);
|
|
@@ -1371,6 +1381,11 @@ export function processMessage(msg) {
|
|
|
1371
1381
|
updateTerminalList(msg.terminals);
|
|
1372
1382
|
break;
|
|
1373
1383
|
|
|
1384
|
+
case "shell_command_result":
|
|
1385
|
+
handleShellCommandResult(msg);
|
|
1386
|
+
if (!msg.error) sendShellResultToAgent(msg);
|
|
1387
|
+
break;
|
|
1388
|
+
|
|
1374
1389
|
case "context_sources_state":
|
|
1375
1390
|
handleContextSourcesState(msg);
|
|
1376
1391
|
break;
|
|
@@ -24,6 +24,7 @@ import { isSchedulerOpen, closeScheduler, resetScheduler } from './scheduler.js'
|
|
|
24
24
|
import { connect, cancelReconnect, setStatus } from './app-connection.js';
|
|
25
25
|
import { setTurnCounter, setPrependAnchor, setActivityEl, setIsUserScrolledUp, hideSuggestionChips } from './app-rendering.js';
|
|
26
26
|
import { resetToolState, enableMainInput, resetTurnMetaCost } from './tools.js';
|
|
27
|
+
import { resetShellCommand } from './shell-command.js';
|
|
27
28
|
import { clearPendingImages } from './input.js';
|
|
28
29
|
import { clearAllMentionActive } from './sidebar-mates.js';
|
|
29
30
|
import { setRewindMode } from './rewind.js';
|
|
@@ -368,6 +369,7 @@ export function resetClientState() {
|
|
|
368
369
|
store.set({ currentFullText: "" });
|
|
369
370
|
resetToolState();
|
|
370
371
|
clearPendingImages();
|
|
372
|
+
resetShellCommand();
|
|
371
373
|
clearAllMentionActive();
|
|
372
374
|
setActivityEl(null);
|
|
373
375
|
store.set({ processing: false });
|
|
@@ -7,6 +7,7 @@ import { mateAvatarUrl } from './avatar.js';
|
|
|
7
7
|
import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
|
|
8
8
|
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
9
9
|
import { showToast } from './utils.js';
|
|
10
|
+
import { isShellCommandMode, submitShellCommand } from './shell-command.js';
|
|
10
11
|
|
|
11
12
|
var ctx;
|
|
12
13
|
|
|
@@ -75,6 +76,37 @@ export var builtinCommands = [
|
|
|
75
76
|
{ name: "status", desc: "Process status and resource usage" },
|
|
76
77
|
];
|
|
77
78
|
|
|
79
|
+
function commitVendorForTurn() {
|
|
80
|
+
var committedVendor = store.get('currentVendor');
|
|
81
|
+
var vendorToggle = document.getElementById("vendor-toggle-wrap");
|
|
82
|
+
var activeIndicator = document.getElementById("active-vendor-indicator");
|
|
83
|
+
var activeIcon = document.getElementById("active-vendor-icon");
|
|
84
|
+
if (committedVendor) {
|
|
85
|
+
if (vendorToggle) {
|
|
86
|
+
vendorToggle.classList.add("hidden");
|
|
87
|
+
vendorToggle.classList.remove("locked");
|
|
88
|
+
}
|
|
89
|
+
if (activeIndicator && activeIcon) {
|
|
90
|
+
activeIcon.src = VENDOR_AVATARS[committedVendor] || VENDOR_AVATARS.claude;
|
|
91
|
+
activeIcon.alt = VENDOR_NAMES[committedVendor] || VENDOR_NAMES.claude;
|
|
92
|
+
activeIndicator.title = (VENDOR_NAMES[committedVendor] || VENDOR_NAMES.claude) + " session";
|
|
93
|
+
activeIndicator.classList.remove("hidden");
|
|
94
|
+
}
|
|
95
|
+
} else if (vendorToggle) {
|
|
96
|
+
vendorToggle.classList.remove("hidden");
|
|
97
|
+
vendorToggle.classList.add("locked");
|
|
98
|
+
}
|
|
99
|
+
store.set({ vendorSelectionLocked: false, sessionHasHistory: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function showPreThinkingForTurn() {
|
|
103
|
+
if (ctx.isMateDm && ctx.isMateDm()) {
|
|
104
|
+
ctx.showMatePreThinking();
|
|
105
|
+
} else if (ctx.showClaudePreThinking) {
|
|
106
|
+
ctx.showClaudePreThinking();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
78
110
|
// --- Send ---
|
|
79
111
|
export function sendMessage() {
|
|
80
112
|
// Debate ended mode intercept: route to resume handler
|
|
@@ -92,6 +124,14 @@ export function sendMessage() {
|
|
|
92
124
|
ctx.handleDebateFloorSend();
|
|
93
125
|
return;
|
|
94
126
|
}
|
|
127
|
+
if (isShellCommandMode()) {
|
|
128
|
+
var shellText = ctx.inputEl.value.trim();
|
|
129
|
+
if (!shellText || !submitShellCommand(shellText)) return;
|
|
130
|
+
ctx.inputEl.value = "";
|
|
131
|
+
sendInputSync();
|
|
132
|
+
autoResize();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
95
135
|
// DM mode intercept: if in DM mode, route to DM handler instead
|
|
96
136
|
if (ctx.isDmMode && ctx.isDmMode() && ctx.handleDmSend) {
|
|
97
137
|
ctx.handleDmSend();
|
|
@@ -304,36 +344,10 @@ export function sendMessage() {
|
|
|
304
344
|
// Bumping sessionHasHistory drives in-session lock states (e.g. the
|
|
305
345
|
// Codex model picker that becomes informational once the thread is
|
|
306
346
|
// bound to a model).
|
|
307
|
-
|
|
308
|
-
var _vtw2 = document.getElementById("vendor-toggle-wrap");
|
|
309
|
-
var _avi = document.getElementById("active-vendor-indicator");
|
|
310
|
-
var _avIcon = document.getElementById("active-vendor-icon");
|
|
311
|
-
if (_committedVendor) {
|
|
312
|
-
if (_vtw2) {
|
|
313
|
-
_vtw2.classList.add("hidden");
|
|
314
|
-
_vtw2.classList.remove("locked");
|
|
315
|
-
}
|
|
316
|
-
if (_avi && _avIcon) {
|
|
317
|
-
_avIcon.src = VENDOR_AVATARS[_committedVendor] || VENDOR_AVATARS.claude;
|
|
318
|
-
_avIcon.alt = VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude;
|
|
319
|
-
_avi.title = (VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude) + " session";
|
|
320
|
-
_avi.classList.remove("hidden");
|
|
321
|
-
}
|
|
322
|
-
} else if (_vtw2) {
|
|
323
|
-
// No committed vendor (defensive — shouldn't happen because the
|
|
324
|
-
// input is otherwise gated on a vendor pick). Fall back to the
|
|
325
|
-
// locked toggle so the running vendor is still visible.
|
|
326
|
-
_vtw2.classList.remove("hidden");
|
|
327
|
-
_vtw2.classList.add("locked");
|
|
328
|
-
}
|
|
329
|
-
store.set({ vendorSelectionLocked: false, sessionHasHistory: true });
|
|
347
|
+
commitVendorForTurn();
|
|
330
348
|
|
|
331
349
|
// Show pre-thinking dots before server responds
|
|
332
|
-
|
|
333
|
-
ctx.showMatePreThinking();
|
|
334
|
-
} else if (ctx.showClaudePreThinking) {
|
|
335
|
-
ctx.showClaudePreThinking();
|
|
336
|
-
}
|
|
350
|
+
showPreThinkingForTurn();
|
|
337
351
|
|
|
338
352
|
ctx.inputEl.value = "";
|
|
339
353
|
sendInputSync();
|
|
@@ -354,6 +368,22 @@ export function sendTextMessage(text) {
|
|
|
354
368
|
return true;
|
|
355
369
|
}
|
|
356
370
|
|
|
371
|
+
export function sendShellResultToAgent(msg) {
|
|
372
|
+
if (!ctx || !ctx.connected || !msg || msg.error || !msg.command) return false;
|
|
373
|
+
if (String(msg.sessionId) !== String(store.get("activeSessionId"))) return false;
|
|
374
|
+
var payload = {
|
|
375
|
+
type: "message",
|
|
376
|
+
text: "$ " + msg.command,
|
|
377
|
+
shellCommandResponse: true,
|
|
378
|
+
};
|
|
379
|
+
var selectedVendor = store.get("currentVendor") || null;
|
|
380
|
+
if (selectedVendor) payload.vendor = selectedVendor;
|
|
381
|
+
ctx.ws.send(JSON.stringify(payload));
|
|
382
|
+
commitVendorForTurn();
|
|
383
|
+
showPreThinkingForTurn();
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
357
387
|
export function autoResize() {
|
|
358
388
|
ctx.inputEl.style.height = "auto";
|
|
359
389
|
ctx.inputEl.style.height = Math.min(ctx.inputEl.scrollHeight, 120) + "px";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
4
|
+
import { showToast } from './utils.js';
|
|
5
|
+
|
|
6
|
+
var defaultPlaceholder = "";
|
|
7
|
+
|
|
8
|
+
function getInput() {
|
|
9
|
+
return document.getElementById("input");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appendToMessages(element) {
|
|
13
|
+
var messages = document.getElementById("messages");
|
|
14
|
+
if (!messages) return;
|
|
15
|
+
messages.appendChild(element);
|
|
16
|
+
requestAnimationFrame(function () { messages.scrollTop = messages.scrollHeight; });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function setMode(active) {
|
|
20
|
+
var input = getInput();
|
|
21
|
+
var button = document.getElementById("shell-command-btn");
|
|
22
|
+
var row = document.getElementById("input-row");
|
|
23
|
+
store.set({ shellCommandMode: active });
|
|
24
|
+
if (button) {
|
|
25
|
+
button.classList.toggle("active", active);
|
|
26
|
+
button.setAttribute("aria-pressed", active ? "true" : "false");
|
|
27
|
+
}
|
|
28
|
+
if (row) row.classList.toggle("shell-command-mode", active);
|
|
29
|
+
if (input) {
|
|
30
|
+
if (!defaultPlaceholder) defaultPlaceholder = input.placeholder;
|
|
31
|
+
input.placeholder = active ? "Run a shell command in this project…" : defaultPlaceholder;
|
|
32
|
+
input.focus();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isShellCommandMode() {
|
|
37
|
+
return !!store.get("shellCommandMode");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toggleShellCommandMode() {
|
|
41
|
+
if (store.get("shellCommandRunning")) return;
|
|
42
|
+
var target = store.get("dmTargetUser");
|
|
43
|
+
if (store.get("dmMode") && target && !target.isMate) {
|
|
44
|
+
showToast("Shell commands are available in agent sessions, not user DMs.", "error");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setMode(!isShellCommandMode());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderPendingCommand(requestId, command) {
|
|
51
|
+
var card = document.createElement("div");
|
|
52
|
+
card.className = "shell-command-card running";
|
|
53
|
+
card.dataset.requestId = requestId;
|
|
54
|
+
card.innerHTML =
|
|
55
|
+
'<div class="shell-command-header">' +
|
|
56
|
+
'<span class="shell-command-icon">' + iconHtml("square-terminal") + '</span>' +
|
|
57
|
+
'<code></code><span class="shell-command-status">Running…</span>' +
|
|
58
|
+
'</div>' +
|
|
59
|
+
'<pre class="shell-command-output">Waiting for output…</pre>';
|
|
60
|
+
card.querySelector("code").textContent = "$ " + command;
|
|
61
|
+
appendToMessages(card);
|
|
62
|
+
refreshIcons();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function submitShellCommand(command) {
|
|
66
|
+
command = String(command || "").trim();
|
|
67
|
+
if (!command || store.get("shellCommandRunning")) return false;
|
|
68
|
+
var ws = getWs();
|
|
69
|
+
if (!ws || ws.readyState !== 1) {
|
|
70
|
+
showToast("Not connected — command not run.", "error");
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var requestId = "shell_" + Date.now() + "_" + Math.random().toString(36).slice(2, 9);
|
|
75
|
+
store.set({ shellCommandRunning: true, pendingShellCommandId: requestId });
|
|
76
|
+
var input = getInput();
|
|
77
|
+
if (input) {
|
|
78
|
+
input.disabled = true;
|
|
79
|
+
input.placeholder = "Running command…";
|
|
80
|
+
}
|
|
81
|
+
renderPendingCommand(requestId, command);
|
|
82
|
+
ws.send(JSON.stringify({ type: "shell_command", requestId: requestId, command: command }));
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function handleShellCommandResult(msg) {
|
|
87
|
+
var cards = document.querySelectorAll(".shell-command-card[data-request-id]");
|
|
88
|
+
var card = null;
|
|
89
|
+
for (var i = 0; i < cards.length; i++) {
|
|
90
|
+
if (cards[i].dataset.requestId === (msg.requestId || "")) {
|
|
91
|
+
card = cards[i];
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (card) {
|
|
96
|
+
var status = card.querySelector(".shell-command-status");
|
|
97
|
+
var output = card.querySelector(".shell-command-output");
|
|
98
|
+
card.classList.remove("running");
|
|
99
|
+
if (msg.error) {
|
|
100
|
+
card.classList.add("error");
|
|
101
|
+
if (status) status.textContent = "Failed";
|
|
102
|
+
if (output) output.textContent = msg.error;
|
|
103
|
+
} else {
|
|
104
|
+
card.classList.toggle("error", msg.exitCode !== 0);
|
|
105
|
+
if (status) status.textContent = msg.timedOut ? "Timed out" : "Exit " + (msg.exitCode == null ? "—" : msg.exitCode);
|
|
106
|
+
if (output) output.textContent = msg.output || "(no output)";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
store.set({ shellCommandRunning: false, pendingShellCommandId: null });
|
|
111
|
+
var input = getInput();
|
|
112
|
+
if (input) input.disabled = false;
|
|
113
|
+
if (msg.error) {
|
|
114
|
+
setMode(true);
|
|
115
|
+
} else {
|
|
116
|
+
setMode(false);
|
|
117
|
+
}
|
|
118
|
+
var messages = document.getElementById("messages");
|
|
119
|
+
if (messages) requestAnimationFrame(function () { messages.scrollTop = messages.scrollHeight; });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function initShellCommand() {
|
|
123
|
+
var button = document.getElementById("shell-command-btn");
|
|
124
|
+
var mobileButton = document.getElementById("input-more-shell");
|
|
125
|
+
if (button) button.addEventListener("click", toggleShellCommandMode);
|
|
126
|
+
if (mobileButton) {
|
|
127
|
+
mobileButton.addEventListener("click", function () {
|
|
128
|
+
var sheet = document.getElementById("input-more-sheet");
|
|
129
|
+
if (sheet) {
|
|
130
|
+
sheet.classList.remove("open");
|
|
131
|
+
setTimeout(function () { sheet.classList.add("hidden"); }, 250);
|
|
132
|
+
}
|
|
133
|
+
toggleShellCommandMode();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
store.subscribe(function (state, previous) {
|
|
137
|
+
if (previous.connected && !state.connected && state.shellCommandRunning) {
|
|
138
|
+
resetShellCommand();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resetShellCommand() {
|
|
144
|
+
store.set({ shellCommandMode: false, shellCommandRunning: false, pendingShellCommandId: null });
|
|
145
|
+
var input = getInput();
|
|
146
|
+
if (input) input.disabled = false;
|
|
147
|
+
setMode(false);
|
|
148
|
+
}
|
|
@@ -792,7 +792,16 @@ function attachMessageProcessor(ctx) {
|
|
|
792
792
|
// processQueryStream can finish the turn when the iterator closes.
|
|
793
793
|
var adapterErrorText = parsed.text || parsed.message || parsed.error || "Agent runtime error";
|
|
794
794
|
session._lastAdapterError = adapterErrorText;
|
|
795
|
-
|
|
795
|
+
var isSessionWriterConflict = /thread-store conflict|already has an active writer/i.test(adapterErrorText);
|
|
796
|
+
if (isSessionWriterConflict) {
|
|
797
|
+
sendAndRecord(session, {
|
|
798
|
+
type: "session_writer_conflict",
|
|
799
|
+
vendor: session.vendor || "codex",
|
|
800
|
+
text: "This Codex session is already open in another Clay or Codex process. Stop the other server or close the other session, then try again.",
|
|
801
|
+
});
|
|
802
|
+
} else {
|
|
803
|
+
sendAndRecord(session, { type: "error", text: adapterErrorText });
|
|
804
|
+
}
|
|
796
805
|
|
|
797
806
|
} else if (parsed.yokeType === "model_refusal") {
|
|
798
807
|
// Model declined the request. "fallback" => the CLI retried on another
|
package/lib/ws-schema.js
CHANGED
|
@@ -145,6 +145,7 @@ var schema = {
|
|
|
145
145
|
"kill_process": { direction: "c2s", handler: "lib/project-sessions.js", description: "Kill a system process by PID" },
|
|
146
146
|
"process_killed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Process was successfully killed" },
|
|
147
147
|
"process_conflict": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Conflict: another process is using the session" },
|
|
148
|
+
"session_writer_conflict": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Codex session is already open in another process" },
|
|
148
149
|
|
|
149
150
|
// -----------------------------------------------------------------------
|
|
150
151
|
// Context / usage
|
|
@@ -353,6 +354,8 @@ var schema = {
|
|
|
353
354
|
"term_closed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Terminal session was closed" },
|
|
354
355
|
"term_list": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full list of open terminals" },
|
|
355
356
|
"term_error": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Terminal error (e.g. access denied)" },
|
|
357
|
+
"shell_command": { direction: "c2s", handler: "lib/project-shell-command.js", description: "Run a one-shot shell command for agent context" },
|
|
358
|
+
"shell_command_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "One-shot shell command output and exit status" },
|
|
356
359
|
|
|
357
360
|
// -----------------------------------------------------------------------
|
|
358
361
|
// Sticky notes
|
package/package.json
CHANGED