clay-server 3.3.2-beta.3 → 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.
Files changed (36) hide show
  1. package/lib/project-file-watch.js +77 -16
  2. package/lib/project-shell-command.js +160 -0
  3. package/lib/project-user-message.js +10 -0
  4. package/lib/project.js +13 -0
  5. package/lib/public/app.js +10 -0
  6. package/lib/public/css/input.css +67 -0
  7. package/lib/public/css/mates.css +1 -0
  8. package/lib/public/gemini-avatar.svg +11 -0
  9. package/lib/public/index.html +13 -0
  10. package/lib/public/modules/app-messages.js +16 -1
  11. package/lib/public/modules/app-panels.js +13 -1
  12. package/lib/public/modules/app-projects.js +2 -0
  13. package/lib/public/modules/app-rendering.js +7 -1
  14. package/lib/public/modules/input.js +58 -28
  15. package/lib/public/modules/mate-sidebar.js +9 -3
  16. package/lib/public/modules/shell-command.js +148 -0
  17. package/lib/public/modules/sidebar-mates.js +7 -1
  18. package/lib/public/modules/tools.js +7 -1
  19. package/lib/public/opencode-avatar.svg +4 -0
  20. package/lib/sdk-bridge.js +18 -2
  21. package/lib/sdk-message-processor.js +10 -1
  22. package/lib/ws-schema.js +3 -0
  23. package/lib/yoke/acp-agent-profiles.js +188 -0
  24. package/lib/yoke/acp-driver-runtime.js +50 -0
  25. package/lib/yoke/acp-event-normalizer.js +179 -0
  26. package/lib/yoke/acp-process-manager.js +264 -0
  27. package/lib/yoke/acp-query-handle.js +487 -0
  28. package/lib/yoke/adapters/acp.js +317 -0
  29. package/lib/yoke/adapters/gemini.js +7 -0
  30. package/lib/yoke/adapters/kiro.js +4 -4
  31. package/lib/yoke/adapters/opencode.js +7 -0
  32. package/lib/yoke/index.js +45 -11
  33. package/lib/yoke/interface.js +2 -0
  34. package/lib/yoke/kiro-acp-server.js +30 -276
  35. package/lib/yoke/vendor-registry.js +22 -0
  36. package/package.json +1 -1
@@ -21,12 +21,22 @@ function attachFileWatch(ctx) {
21
21
  // one browser tab silently replace another tab's live preview subscription.
22
22
  var fileWatchers = new Map();
23
23
 
24
+ function settleFileWatchReady(entry, ready) {
25
+ if (!entry || !entry.resolveReady) return;
26
+ var resolve = entry.resolveReady;
27
+ entry.resolveReady = null;
28
+ resolve(ready);
29
+ }
30
+
24
31
  function closeFileWatch(key) {
25
32
  var entry = fileWatchers.get(key);
26
33
  if (!entry) return;
27
34
  clearTimeout(entry.debounce);
35
+ if (entry.reconcile) clearImmediate(entry.reconcile);
36
+ if (entry.pollTimer) clearInterval(entry.pollTimer);
28
37
  try { entry.watcher.close(); } catch (e) {}
29
38
  fileWatchers.delete(key);
39
+ settleFileWatchReady(entry, false);
30
40
  }
31
41
 
32
42
  function sendFileChanged(client, message) {
@@ -37,6 +47,36 @@ function attachFileWatch(ctx) {
37
47
  }
38
48
  }
39
49
 
50
+ function readFileSnapshot(absPath) {
51
+ var stat = fs.statSync(absPath);
52
+ var ext = path.extname(absPath).toLowerCase();
53
+ if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return null;
54
+ return { content: fs.readFileSync(absPath, "utf8"), size: stat.size };
55
+ }
56
+
57
+ function publishFileChanged(key, client, relPath, absPath) {
58
+ var latest = fileWatchers.get(key);
59
+ if (!latest || latest.relPath !== relPath) return;
60
+ try {
61
+ var snapshot = readFileSnapshot(absPath);
62
+ if (!snapshot) return;
63
+ if (latest.hasSnapshot && latest.content === snapshot.content && latest.size === snapshot.size) return;
64
+ latest.hasSnapshot = true;
65
+ latest.content = snapshot.content;
66
+ latest.size = snapshot.size;
67
+ sendFileChanged(client, {
68
+ type: "fs_file_changed",
69
+ path: relPath,
70
+ content: snapshot.content,
71
+ size: snapshot.size,
72
+ });
73
+ } catch (e) {
74
+ // Atomic saves can briefly remove the destination path between rename
75
+ // events. Keep the parent watcher alive for the next event.
76
+ if (e.code !== "ENOENT") closeFileWatch(key);
77
+ }
78
+ }
79
+
40
80
  function startFileWatch(client, relPath) {
41
81
  // Preserve the old single-argument API for callers outside the websocket
42
82
  // file browser. They share one legacy subscription.
@@ -45,10 +85,10 @@ function attachFileWatch(ctx) {
45
85
  client = null;
46
86
  }
47
87
  var absPath = safePath(cwd, relPath);
48
- if (!absPath) return;
88
+ if (!absPath) return Promise.resolve(false);
49
89
  var key = client || "_legacy";
50
90
  var existing = fileWatchers.get(key);
51
- if (existing && existing.relPath === relPath) return;
91
+ if (existing && existing.relPath === relPath) return existing.ready;
52
92
  closeFileWatch(key);
53
93
 
54
94
  // Watch the parent directory rather than the file inode. Editors and agent
@@ -56,6 +96,8 @@ function attachFileWatch(ctx) {
56
96
  // misses later edits even though the path still exists.
57
97
  var parentPath = path.dirname(absPath);
58
98
  var baseName = path.basename(absPath);
99
+ var initialSnapshot = null;
100
+ try { initialSnapshot = readFileSnapshot(absPath); } catch (e) {}
59
101
  try {
60
102
  var watcher = fs.watch(parentPath, function (eventType, filename) {
61
103
  if (filename && String(filename) !== baseName) return;
@@ -63,25 +105,44 @@ function attachFileWatch(ctx) {
63
105
  if (!active || active.relPath !== relPath) return;
64
106
  clearTimeout(active.debounce);
65
107
  active.debounce = setTimeout(function () {
66
- var latest = fileWatchers.get(key);
67
- if (!latest || latest.relPath !== relPath) return;
68
- try {
69
- var stat = fs.statSync(absPath);
70
- var ext = path.extname(absPath).toLowerCase();
71
- if (stat.size > FS_MAX_SIZE || BINARY_EXTS.has(ext)) return;
72
- var content = fs.readFileSync(absPath, "utf8");
73
- sendFileChanged(client, { type: "fs_file_changed", path: relPath, content: content, size: stat.size });
74
- } catch (e) {
75
- // Atomic saves can briefly remove the destination path between
76
- // rename events. Keep the parent watcher alive for the next event.
77
- if (e.code !== "ENOENT") closeFileWatch(key);
78
- }
108
+ publishFileChanged(key, client, relPath, absPath);
79
109
  }, 200);
80
110
  });
81
- fileWatchers.set(key, { watcher: watcher, relPath: relPath, debounce: null });
111
+ var resolveReady = null;
112
+ var ready = new Promise(function (resolve) { resolveReady = resolve; });
113
+ var entry = {
114
+ watcher: watcher,
115
+ relPath: relPath,
116
+ debounce: null,
117
+ reconcile: null,
118
+ pollTimer: null,
119
+ ready: ready,
120
+ resolveReady: resolveReady,
121
+ hasSnapshot: !!initialSnapshot,
122
+ content: initialSnapshot ? initialSnapshot.content : null,
123
+ size: initialSnapshot ? initialSnapshot.size : null,
124
+ };
125
+ fileWatchers.set(key, entry);
126
+ // Directory events are the low-latency path, but macOS can coalesce or
127
+ // drop them under load. Periodic content reconciliation is the source of
128
+ // truth and also survives atomic replacements with identical metadata.
129
+ entry.pollTimer = setInterval(function () {
130
+ publishFileChanged(key, client, relPath, absPath);
131
+ }, 1000);
132
+ // fs.watch has no readiness event. Reconcile once on the next event-loop
133
+ // turn so a change between the initial read and native watcher activation
134
+ // cannot leave the browser showing stale content.
135
+ entry.reconcile = setImmediate(function () {
136
+ var active = fileWatchers.get(key);
137
+ if (active) active.reconcile = null;
138
+ publishFileChanged(key, client, relPath, absPath);
139
+ if (fileWatchers.get(key) === entry) settleFileWatchReady(entry, true);
140
+ });
82
141
  watcher.on("error", function () { closeFileWatch(key); });
142
+ return ready;
83
143
  } catch (e) {
84
144
  closeFileWatch(key);
145
+ return Promise.resolve(false);
85
146
  }
86
147
  }
87
148
 
@@ -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");
@@ -799,6 +799,10 @@
799
799
  .vendor-toggle-label {
800
800
  pointer-events: none;
801
801
  }
802
+ @media (min-width: 901px) {
803
+ .vendor-toggle-btn:not(.active) .vendor-toggle-label { display: none; }
804
+ .vendor-toggle-btn:not(.active) { padding: 0 8px; }
805
+ }
802
806
  @media (max-width: 900px) {
803
807
  .vendor-toggle-label { display: none; }
804
808
  .vendor-toggle-btn { padding: 0 8px; }
@@ -822,6 +826,7 @@
822
826
 
823
827
  #attach-file-btn,
824
828
  #attach-image-btn,
829
+ #shell-command-btn,
825
830
  #schedule-btn,
826
831
  #input-more-btn {
827
832
  width: 36px;
@@ -840,14 +845,76 @@
840
845
 
841
846
  #attach-file-btn .lucide,
842
847
  #attach-image-btn .lucide,
848
+ #shell-command-btn .lucide,
843
849
  #schedule-btn .lucide,
844
850
  #input-more-btn .lucide { width: 20px; height: 20px; flex-shrink: 0; }
845
851
 
846
852
  #attach-file-btn:hover,
847
853
  #attach-image-btn:hover,
854
+ #shell-command-btn:hover,
848
855
  #schedule-btn:hover,
849
856
  #input-more-btn:hover { background: rgba(var(--overlay-rgb), 0.06); color: var(--text); }
850
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
+
851
918
  #ask-mate-btn {
852
919
  width: 36px;
853
920
  height: 36px;
@@ -682,6 +682,7 @@
682
682
  .mate-vendor-label {
683
683
  pointer-events: none;
684
684
  }
685
+ .mate-vendor-btn:not(.active) .mate-vendor-label { display: none; }
685
686
 
686
687
  /* Collapse button inside mate header: white to match header style */
687
688
  .mate-sidebar-header .sidebar-collapse-btn {
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Gemini CLI">
2
+ <defs>
3
+ <linearGradient id="g" x1="8" y1="56" x2="56" y2="8" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#4f7cff"/>
5
+ <stop offset="0.5" stop-color="#9b5cff"/>
6
+ <stop offset="1" stop-color="#ff65b3"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <rect width="64" height="64" rx="16" fill="#10131d"/>
10
+ <path d="M32 8c2.3 14.1 9.9 21.7 24 24-14.1 2.3-21.7 9.9-24 24-2.3-14.1-9.9-21.7-24-24 14.1-2.3 21.7-9.9 24-24Z" fill="url(#g)"/>
11
+ </svg>
@@ -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>
@@ -527,6 +528,14 @@
527
528
  <img src="/codex-avatar.png" class="vendor-toggle-icon" alt="Codex">
528
529
  <span class="vendor-toggle-label">Codex</span>
529
530
  </button>
531
+ <button id="vendor-btn-gemini" class="vendor-toggle-btn" data-vendor="gemini">
532
+ <img src="/gemini-avatar.svg" class="vendor-toggle-icon" alt="Gemini">
533
+ <span class="vendor-toggle-label">Gemini CLI</span>
534
+ </button>
535
+ <button id="vendor-btn-opencode" class="vendor-toggle-btn" data-vendor="opencode">
536
+ <img src="/opencode-avatar.svg" class="vendor-toggle-icon" alt="OpenCode">
537
+ <span class="vendor-toggle-label">OpenCode</span>
538
+ </button>
530
539
  <button id="vendor-btn-kiro" class="vendor-toggle-btn" data-vendor="kiro">
531
540
  <img src="/kiro-avatar.svg" class="vendor-toggle-icon" alt="Kiro">
532
541
  <span class="vendor-toggle-label">Kiro CLI</span>
@@ -607,6 +616,10 @@
607
616
  <i data-lucide="image"></i>
608
617
  <span>Image</span>
609
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>
610
623
  </div>
611
624
  <div class="input-more-divider"></div>
612
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;
@@ -89,6 +89,8 @@ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
89
89
  var EFFORT_LEVELS_BY_VENDOR = {
90
90
  claude: ["low", "medium", "high", "xhigh", "max"],
91
91
  codex: ["minimal", "low", "medium", "high", "xhigh"],
92
+ gemini: [],
93
+ opencode: [],
92
94
  kiro: ["low", "medium", "high", "xhigh", "max"],
93
95
  };
94
96
  var THINKING_OPTIONS = ["disabled", "adaptive", "budget"];
@@ -441,8 +443,16 @@ export function initPanels() {
441
443
  var vendorToggleWrap = $("vendor-toggle-wrap");
442
444
  var vendorBtnClaude = $("vendor-btn-claude");
443
445
  var vendorBtnCodex = $("vendor-btn-codex");
446
+ var vendorBtnGemini = $("vendor-btn-gemini");
447
+ var vendorBtnOpenCode = $("vendor-btn-opencode");
444
448
  var vendorBtnKiro = $("vendor-btn-kiro");
445
- var vendorBtns = { claude: vendorBtnClaude, codex: vendorBtnCodex, kiro: vendorBtnKiro };
449
+ var vendorBtns = {
450
+ claude: vendorBtnClaude,
451
+ codex: vendorBtnCodex,
452
+ gemini: vendorBtnGemini,
453
+ opencode: vendorBtnOpenCode,
454
+ kiro: vendorBtnKiro,
455
+ };
446
456
 
447
457
  function updateVendorToggle() {
448
458
  var installed = store.get('installedVendors') || [];
@@ -471,6 +481,8 @@ export function initPanels() {
471
481
 
472
482
  if (vendorBtnClaude) vendorBtnClaude.addEventListener("click", function() { onVendorClick("claude"); });
473
483
  if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
484
+ if (vendorBtnGemini) vendorBtnGemini.addEventListener("click", function() { onVendorClick("gemini"); });
485
+ if (vendorBtnOpenCode) vendorBtnOpenCode.addEventListener("click", function() { onVendorClick("opencode"); });
474
486
  if (vendorBtnKiro) vendorBtnKiro.addEventListener("click", function() { onVendorClick("kiro"); });
475
487
 
476
488
  // --- Reactive UI sync ---
@@ -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 });
@@ -19,21 +19,27 @@ import { getScheduledMsgEl } from './app-rate-limit.js';
19
19
  export var VENDOR_AVATARS = {
20
20
  claude: "/claude-code-avatar.png",
21
21
  codex: "/codex-avatar.png",
22
+ gemini: "/gemini-avatar.svg",
23
+ opencode: "/opencode-avatar.svg",
22
24
  kiro: "/kiro-avatar.svg",
23
25
  };
24
26
  export var VENDOR_NAMES = {
25
27
  claude: "Claude Code",
26
28
  codex: "Codex",
29
+ gemini: "Gemini CLI",
30
+ opencode: "OpenCode",
27
31
  kiro: "Kiro CLI",
28
32
  };
29
33
  // Display order for every vendor Clay knows about, installed or not. Pickers
30
34
  // render the full list so a missing CLI reads as "not installed yet" rather
31
35
  // than "Clay doesn't support it".
32
- export var VENDOR_ORDER = ["claude", "codex", "kiro"];
36
+ export var VENDOR_ORDER = ["claude", "codex", "gemini", "opencode", "kiro"];
33
37
  // Where to send the user when they pick a vendor whose CLI isn't installed.
34
38
  export var VENDOR_HOMEPAGES = {
35
39
  claude: "https://claude.com/product/claude-code",
36
40
  codex: "https://openai.com/codex/",
41
+ gemini: "https://github.com/google-gemini/gemini-cli",
42
+ opencode: "https://opencode.ai/",
37
43
  kiro: "https://kiro.dev/",
38
44
  };
39
45
  var NEW_MSG_BTN_DEFAULT = "\u2193 Latest";