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.
@@ -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");
@@ -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;
@@ -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
- var _committedVendor = store.get('currentVendor');
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
- if (ctx.isMateDm && ctx.isMateDm()) {
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
- sendAndRecord(session, { type: "error", text: adapterErrorText });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.4.0-beta.1",
3
+ "version": "3.4.0-beta.2",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",