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.
- package/lib/project-file-watch.js +77 -16
- 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 +67 -0
- package/lib/public/css/mates.css +1 -0
- package/lib/public/gemini-avatar.svg +11 -0
- package/lib/public/index.html +13 -0
- package/lib/public/modules/app-messages.js +16 -1
- package/lib/public/modules/app-panels.js +13 -1
- package/lib/public/modules/app-projects.js +2 -0
- package/lib/public/modules/app-rendering.js +7 -1
- package/lib/public/modules/input.js +58 -28
- package/lib/public/modules/mate-sidebar.js +9 -3
- package/lib/public/modules/shell-command.js +148 -0
- package/lib/public/modules/sidebar-mates.js +7 -1
- package/lib/public/modules/tools.js +7 -1
- package/lib/public/opencode-avatar.svg +4 -0
- package/lib/sdk-bridge.js +18 -2
- package/lib/sdk-message-processor.js +10 -1
- package/lib/ws-schema.js +3 -0
- package/lib/yoke/acp-agent-profiles.js +188 -0
- package/lib/yoke/acp-driver-runtime.js +50 -0
- package/lib/yoke/acp-event-normalizer.js +179 -0
- package/lib/yoke/acp-process-manager.js +264 -0
- package/lib/yoke/acp-query-handle.js +487 -0
- package/lib/yoke/adapters/acp.js +317 -0
- package/lib/yoke/adapters/gemini.js +7 -0
- package/lib/yoke/adapters/kiro.js +4 -4
- package/lib/yoke/adapters/opencode.js +7 -0
- package/lib/yoke/index.js +45 -11
- package/lib/yoke/interface.js +2 -0
- package/lib/yoke/kiro-acp-server.js +30 -276
- package/lib/yoke/vendor-registry.js +22 -0
- 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
|
-
|
|
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
|
-
|
|
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");
|
package/lib/public/css/input.css
CHANGED
|
@@ -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;
|
package/lib/public/css/mates.css
CHANGED
|
@@ -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>
|
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>
|
|
@@ -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 = {
|
|
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";
|