agent-sh 0.2.0 → 0.3.0
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/README.md +21 -0
- package/dist/acp-client.d.ts +24 -0
- package/dist/acp-client.js +144 -38
- package/dist/context-manager.d.ts +5 -3
- package/dist/context-manager.js +62 -31
- package/dist/event-bus.d.ts +4 -0
- package/dist/extension-loader.js +3 -14
- package/dist/extensions/shell-exec.js +27 -22
- package/dist/extensions/tui-renderer.d.ts +1 -1
- package/dist/extensions/tui-renderer.js +125 -26
- package/dist/index.js +184 -37
- package/dist/input-handler.d.ts +10 -0
- package/dist/input-handler.js +169 -10
- package/dist/mcp-server.js +37 -8
- package/dist/settings.d.ts +33 -0
- package/dist/settings.js +43 -0
- package/dist/shell.d.ts +1 -0
- package/dist/shell.js +44 -4
- package/dist/types.d.ts +2 -0
- package/dist/utils/ansi.d.ts +4 -1
- package/dist/utils/ansi.js +60 -2
- package/dist/utils/line-editor.d.ts +21 -1
- package/dist/utils/line-editor.js +193 -99
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/tool-display.d.ts +2 -0
- package/dist/utils/tool-display.js +48 -20
- package/examples/pi-agent-sh.ts +166 -0
- package/package.json +1 -1
|
@@ -22,25 +22,30 @@ import * as net from "node:net";
|
|
|
22
22
|
import * as fs from "node:fs";
|
|
23
23
|
import * as path from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { getSettings } from "../settings.js";
|
|
25
26
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
27
|
export default function activate({ bus, contextManager }, opts) {
|
|
27
28
|
const { socketPath } = opts;
|
|
28
|
-
// Register MCP server so ACP agents discover the
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
// Register MCP server so ACP agents discover the bridge tools.
|
|
30
|
+
// Agents that don't support MCP (e.g. pi-acp) simply ignore it.
|
|
31
|
+
// Can be disabled via settings.json if not needed.
|
|
32
|
+
if (getSettings().enableMcp) {
|
|
33
|
+
bus.onPipe("session:configure", (payload) => {
|
|
34
|
+
return {
|
|
35
|
+
...payload,
|
|
36
|
+
mcpServers: [
|
|
37
|
+
...payload.mcpServers,
|
|
38
|
+
{
|
|
39
|
+
name: "agent-sh",
|
|
40
|
+
command: process.execPath,
|
|
41
|
+
args: [path.join(__dirname, "..", "mcp-server.js")],
|
|
42
|
+
env: [{ name: "AGENT_SH_SOCKET", value: socketPath }],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// Set AGENT_SH_SOCKET for agent extensions that connect directly
|
|
44
49
|
process.env.AGENT_SH_SOCKET = socketPath;
|
|
45
50
|
// Serialize shell/exec requests — only one PTY command at a time
|
|
46
51
|
let execPending = Promise.resolve();
|
|
@@ -56,21 +61,19 @@ export default function activate({ bus, contextManager }, opts) {
|
|
|
56
61
|
return new Promise((resolve, reject) => {
|
|
57
62
|
execPending = execPending.then(async () => {
|
|
58
63
|
try {
|
|
64
|
+
bus.emit("shell:agent-exec-start", {});
|
|
59
65
|
const result = await bus.emitPipeAsync("shell:exec-request", {
|
|
60
66
|
command,
|
|
61
67
|
output: "",
|
|
62
68
|
cwd: "",
|
|
63
69
|
done: false,
|
|
64
70
|
});
|
|
65
|
-
|
|
66
|
-
if (result.output) {
|
|
67
|
-
bus.emit("agent:tool-output-chunk", { chunk: result.output });
|
|
68
|
-
}
|
|
71
|
+
bus.emit("shell:agent-exec-done", {});
|
|
69
72
|
resolve({ output: result.output, cwd: result.cwd });
|
|
70
73
|
}
|
|
71
74
|
catch (err) {
|
|
75
|
+
bus.emit("shell:agent-exec-done", {});
|
|
72
76
|
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
-
bus.emit("agent:tool-output-chunk", { chunk: `Error: ${message}` });
|
|
74
77
|
reject(rpcError(-32000, message));
|
|
75
78
|
}
|
|
76
79
|
});
|
|
@@ -98,7 +101,9 @@ export default function activate({ bus, contextManager }, opts) {
|
|
|
98
101
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
99
102
|
throw rpcError(-32602, "Missing required parameter: ids (array of numbers)");
|
|
100
103
|
}
|
|
101
|
-
|
|
104
|
+
const start = typeof params?.start === "number" ? params.start : undefined;
|
|
105
|
+
const end = typeof params?.end === "number" ? params.end : undefined;
|
|
106
|
+
return { result: contextManager.expand(ids.map(Number), start, end) };
|
|
102
107
|
}
|
|
103
108
|
case "browse":
|
|
104
109
|
return { result: contextManager.getRecentSummary() };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ExtensionContext } from "../types.js";
|
|
2
|
-
export default function activate({ bus }: ExtensionContext): void;
|
|
2
|
+
export default function activate({ bus, getAcpClient }: ExtensionContext): void;
|
|
@@ -12,22 +12,27 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { MarkdownRenderer } from "../utils/markdown.js";
|
|
14
14
|
import { palette as p } from "../utils/palette.js";
|
|
15
|
-
import { renderToolCall,
|
|
15
|
+
import { renderToolCall, startSpinner, stopSpinner as stopToolSpinner, } from "../utils/tool-display.js";
|
|
16
16
|
import { renderDiff } from "../utils/diff-renderer.js";
|
|
17
17
|
import { renderBoxFrame } from "../utils/box-frame.js";
|
|
18
|
-
|
|
19
|
-
export default function activate({ bus }) {
|
|
18
|
+
import { getSettings } from "../settings.js";
|
|
19
|
+
export default function activate({ bus, getAcpClient }) {
|
|
20
20
|
let spinner = null;
|
|
21
21
|
let renderer = null;
|
|
22
22
|
let commandOutputBuffer = "";
|
|
23
23
|
let commandOutputLineCount = 0;
|
|
24
24
|
let commandOutputOverflow = 0;
|
|
25
25
|
let lastCommand = "";
|
|
26
|
+
let toolLineOpen = false; // true when tool header was written without \n
|
|
27
|
+
let hadToolCalls = false; // true after any tool call in current response
|
|
28
|
+
let currentToolKind; // kind of the currently executing tool
|
|
26
29
|
let isThinking = false;
|
|
27
30
|
let showThinkingText = false;
|
|
31
|
+
let spinnerStartTime = 0; // preserved across spinner restarts
|
|
28
32
|
let lastTruncatedDiff = null;
|
|
29
33
|
// ── Event subscriptions ─────────────────────────────────────
|
|
30
34
|
bus.on("agent:query", (e) => {
|
|
35
|
+
spinnerStartTime = 0;
|
|
31
36
|
showUserQuery(e.query);
|
|
32
37
|
startAgentResponse();
|
|
33
38
|
startThinkingSpinner();
|
|
@@ -35,14 +40,15 @@ export default function activate({ bus }) {
|
|
|
35
40
|
bus.on("agent:thinking-chunk", (e) => {
|
|
36
41
|
if (!isThinking) {
|
|
37
42
|
isThinking = true;
|
|
38
|
-
stopCurrentSpinner();
|
|
39
43
|
if (showThinkingText) {
|
|
44
|
+
stopCurrentSpinner();
|
|
40
45
|
if (!renderer)
|
|
41
46
|
startAgentResponse();
|
|
42
|
-
renderer.writeLine(`${p.dim}
|
|
47
|
+
renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
|
|
43
48
|
}
|
|
44
49
|
else {
|
|
45
|
-
|
|
50
|
+
// Restart spinner with ctrl+t hint now that we know thinking is available
|
|
51
|
+
startThinkingSpinner();
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
if (showThinkingText && e.text) {
|
|
@@ -62,10 +68,28 @@ export default function activate({ bus }) {
|
|
|
62
68
|
});
|
|
63
69
|
bus.on("agent:tool-started", (e) => {
|
|
64
70
|
stopCurrentSpinner();
|
|
65
|
-
|
|
71
|
+
currentToolKind = e.kind;
|
|
72
|
+
if (e.title === "user_shell") {
|
|
73
|
+
// Minimal annotation — PTY echo will show the output
|
|
74
|
+
closeToolLine();
|
|
75
|
+
if (!renderer)
|
|
76
|
+
startAgentResponse();
|
|
77
|
+
renderer.flush();
|
|
78
|
+
const cmd = e.rawInput?.command || "";
|
|
79
|
+
renderer.writeLine(`${p.dim}▶ user_shell: ${cmd}${p.reset}`);
|
|
80
|
+
hadToolCalls = true;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
showToolCall(e.title, lastCommand, e);
|
|
84
|
+
}
|
|
66
85
|
lastCommand = "";
|
|
67
86
|
});
|
|
68
|
-
bus.on("agent:tool-completed", (e) =>
|
|
87
|
+
bus.on("agent:tool-completed", (e) => {
|
|
88
|
+
showToolComplete(e.exitCode);
|
|
89
|
+
currentToolKind = undefined;
|
|
90
|
+
spinnerStartTime = 0;
|
|
91
|
+
startThinkingSpinner();
|
|
92
|
+
});
|
|
69
93
|
bus.on("agent:tool-output-chunk", (e) => writeCommandOutput(e.chunk));
|
|
70
94
|
bus.on("agent:tool-output", () => flushCommandOutput());
|
|
71
95
|
bus.on("agent:cancelled", () => {
|
|
@@ -74,6 +98,11 @@ export default function activate({ bus }) {
|
|
|
74
98
|
showInfo("(cancelled)");
|
|
75
99
|
endAgentResponse();
|
|
76
100
|
});
|
|
101
|
+
bus.on("agent:processing-done", () => {
|
|
102
|
+
isThinking = false;
|
|
103
|
+
stopCurrentSpinner();
|
|
104
|
+
endAgentResponse();
|
|
105
|
+
});
|
|
77
106
|
bus.on("agent:error", (e) => showError(e.message));
|
|
78
107
|
// Flush rendering state and show inline diff for file writes
|
|
79
108
|
bus.on("permission:request", (e) => {
|
|
@@ -108,10 +137,11 @@ export default function activate({ bus }) {
|
|
|
108
137
|
}
|
|
109
138
|
function startAgentResponse() {
|
|
110
139
|
renderer = new MarkdownRenderer();
|
|
111
|
-
|
|
140
|
+
hadToolCalls = false;
|
|
112
141
|
renderer.printTopBorder();
|
|
113
142
|
}
|
|
114
143
|
function endAgentResponse() {
|
|
144
|
+
closeToolLine();
|
|
115
145
|
if (renderer) {
|
|
116
146
|
renderer.flush();
|
|
117
147
|
renderer.printBottomBorder();
|
|
@@ -154,6 +184,9 @@ export default function activate({ bus }) {
|
|
|
154
184
|
}
|
|
155
185
|
}
|
|
156
186
|
function writeAgentText(text) {
|
|
187
|
+
closeToolLine();
|
|
188
|
+
const needsGap = hadToolCalls;
|
|
189
|
+
hadToolCalls = false;
|
|
157
190
|
if (isThinking) {
|
|
158
191
|
isThinking = false;
|
|
159
192
|
if (showThinkingText && renderer) {
|
|
@@ -166,10 +199,13 @@ export default function activate({ bus }) {
|
|
|
166
199
|
stopCurrentSpinner();
|
|
167
200
|
if (!renderer)
|
|
168
201
|
startAgentResponse();
|
|
202
|
+
if (needsGap)
|
|
203
|
+
process.stdout.write("\n");
|
|
169
204
|
renderer.push(text);
|
|
170
205
|
flushOutput();
|
|
171
206
|
}
|
|
172
207
|
function showToolCall(title, command, extra) {
|
|
208
|
+
closeToolLine();
|
|
173
209
|
stopCurrentSpinner();
|
|
174
210
|
if (!renderer)
|
|
175
211
|
startAgentResponse();
|
|
@@ -182,9 +218,15 @@ export default function activate({ bus }) {
|
|
|
182
218
|
locations: extra?.locations,
|
|
183
219
|
rawInput: extra?.rawInput,
|
|
184
220
|
}, termW);
|
|
185
|
-
|
|
186
|
-
|
|
221
|
+
// Write all lines except the last normally, write last without \n
|
|
222
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
223
|
+
renderer.writeLine(lines[i]);
|
|
187
224
|
}
|
|
225
|
+
if (lines.length > 0) {
|
|
226
|
+
process.stdout.write(` ${lines[lines.length - 1]}`);
|
|
227
|
+
toolLineOpen = true;
|
|
228
|
+
}
|
|
229
|
+
hadToolCalls = true;
|
|
188
230
|
// Reset output tracking for the new tool
|
|
189
231
|
commandOutputLineCount = 0;
|
|
190
232
|
commandOutputOverflow = 0;
|
|
@@ -192,15 +234,37 @@ export default function activate({ bus }) {
|
|
|
192
234
|
function showToolComplete(exitCode) {
|
|
193
235
|
if (!renderer)
|
|
194
236
|
return;
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
237
|
+
const mark = exitCode === null
|
|
238
|
+
? `${p.muted}(timed out)${p.reset}`
|
|
239
|
+
: exitCode === 0
|
|
240
|
+
? `${p.success}✓${p.reset}`
|
|
241
|
+
: `${p.error}✗ exit ${exitCode}${p.reset}`;
|
|
242
|
+
if (toolLineOpen && commandOutputLineCount === 0) {
|
|
243
|
+
// No output written — append mark on same line as tool header
|
|
244
|
+
process.stdout.write(` ${mark}\n`);
|
|
245
|
+
toolLineOpen = false;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
closeToolLine();
|
|
249
|
+
flushCommandOutput();
|
|
250
|
+
renderer.writeLine(` ${mark}`);
|
|
199
251
|
}
|
|
200
252
|
}
|
|
201
|
-
function
|
|
253
|
+
function hasThinkingMode() {
|
|
254
|
+
const mode = getAcpClient().getCurrentMode();
|
|
255
|
+
return !mode || mode.id !== "off";
|
|
256
|
+
}
|
|
257
|
+
function startThinkingSpinner() {
|
|
258
|
+
// Preserve start time if restarting (e.g. toggle), otherwise reset
|
|
259
|
+
if (!spinnerStartTime)
|
|
260
|
+
spinnerStartTime = Date.now();
|
|
202
261
|
stopCurrentSpinner();
|
|
203
|
-
|
|
262
|
+
const thinking = hasThinkingMode();
|
|
263
|
+
const label = thinking ? "Thinking" : "Working";
|
|
264
|
+
const hint = thinking
|
|
265
|
+
? (showThinkingText ? "(ctrl+t to collapse)" : "(ctrl+t to expand)")
|
|
266
|
+
: "";
|
|
267
|
+
spinner = startSpinner(label, { hint: hint || undefined, startTime: spinnerStartTime });
|
|
204
268
|
}
|
|
205
269
|
function stopCurrentSpinner() {
|
|
206
270
|
if (spinner) {
|
|
@@ -208,14 +272,24 @@ export default function activate({ bus }) {
|
|
|
208
272
|
spinner = null;
|
|
209
273
|
}
|
|
210
274
|
}
|
|
275
|
+
function closeToolLine() {
|
|
276
|
+
if (toolLineOpen) {
|
|
277
|
+
process.stdout.write("\n");
|
|
278
|
+
toolLineOpen = false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
211
281
|
function writeCommandOutput(chunk) {
|
|
212
282
|
if (!renderer)
|
|
213
283
|
return;
|
|
284
|
+
closeToolLine();
|
|
285
|
+
const maxLines = currentToolKind === "read"
|
|
286
|
+
? getSettings().readOutputMaxLines
|
|
287
|
+
: getSettings().maxCommandOutputLines;
|
|
214
288
|
commandOutputBuffer += chunk;
|
|
215
289
|
const lines = commandOutputBuffer.split("\n");
|
|
216
290
|
commandOutputBuffer = lines.pop();
|
|
217
291
|
for (const line of lines) {
|
|
218
|
-
if (commandOutputLineCount <
|
|
292
|
+
if (commandOutputLineCount < maxLines) {
|
|
219
293
|
renderer.writeLine(`${p.dim} ${line}${p.reset}`);
|
|
220
294
|
commandOutputLineCount++;
|
|
221
295
|
}
|
|
@@ -227,8 +301,11 @@ export default function activate({ bus }) {
|
|
|
227
301
|
function flushCommandOutput() {
|
|
228
302
|
if (!renderer)
|
|
229
303
|
return;
|
|
304
|
+
const maxLines = currentToolKind === "read"
|
|
305
|
+
? getSettings().readOutputMaxLines
|
|
306
|
+
: getSettings().maxCommandOutputLines;
|
|
230
307
|
if (commandOutputBuffer) {
|
|
231
|
-
if (commandOutputLineCount <
|
|
308
|
+
if (commandOutputLineCount < maxLines) {
|
|
232
309
|
renderer.writeLine(`${p.dim} ${commandOutputBuffer}${p.reset}`);
|
|
233
310
|
commandOutputLineCount++;
|
|
234
311
|
}
|
|
@@ -237,12 +314,11 @@ export default function activate({ bus }) {
|
|
|
237
314
|
}
|
|
238
315
|
commandOutputBuffer = "";
|
|
239
316
|
}
|
|
240
|
-
if (commandOutputOverflow > 0) {
|
|
317
|
+
if (commandOutputOverflow > 0 && maxLines > 0) {
|
|
241
318
|
renderer.writeLine(`${p.dim} … ${commandOutputOverflow} more lines${p.reset}`);
|
|
242
|
-
commandOutputOverflow = 0;
|
|
243
319
|
}
|
|
320
|
+
commandOutputOverflow = 0;
|
|
244
321
|
}
|
|
245
|
-
const DIFF_MAX_LINES = 20;
|
|
246
322
|
function diffTitle(filePath, diff) {
|
|
247
323
|
const stats = diff.isNewFile
|
|
248
324
|
? `${p.success}+${diff.added}${p.reset}`
|
|
@@ -258,7 +334,7 @@ export default function activate({ bus }) {
|
|
|
258
334
|
const diffLines = renderDiff(diff, {
|
|
259
335
|
width: contentW,
|
|
260
336
|
filePath,
|
|
261
|
-
maxLines:
|
|
337
|
+
maxLines: getSettings().diffMaxLines,
|
|
262
338
|
trueColor: true,
|
|
263
339
|
mode: "unified",
|
|
264
340
|
});
|
|
@@ -329,7 +405,7 @@ export default function activate({ bus }) {
|
|
|
329
405
|
const diffLines = renderDiff(diff, {
|
|
330
406
|
width: contentW,
|
|
331
407
|
filePath,
|
|
332
|
-
maxLines:
|
|
408
|
+
maxLines: getSettings().diffMaxLines,
|
|
333
409
|
trueColor: true,
|
|
334
410
|
mode: "unified",
|
|
335
411
|
});
|
|
@@ -348,8 +424,31 @@ export default function activate({ bus }) {
|
|
|
348
424
|
}
|
|
349
425
|
function toggleThinkingDisplay() {
|
|
350
426
|
showThinkingText = !showThinkingText;
|
|
351
|
-
|
|
352
|
-
|
|
427
|
+
// Update spinner hint to reflect new state, even if not actively thinking
|
|
428
|
+
if (spinner) {
|
|
429
|
+
stopCurrentSpinner();
|
|
430
|
+
startThinkingSpinner();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (!isThinking)
|
|
434
|
+
return;
|
|
435
|
+
if (showThinkingText) {
|
|
436
|
+
// Switch from spinner to streaming text
|
|
437
|
+
stopCurrentSpinner();
|
|
438
|
+
if (!renderer)
|
|
439
|
+
startAgentResponse();
|
|
440
|
+
renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
// Switch from streaming text to spinner
|
|
444
|
+
if (renderer) {
|
|
445
|
+
renderer.flush();
|
|
446
|
+
const termW = process.stdout.columns || 80;
|
|
447
|
+
const w = Math.min(80, termW);
|
|
448
|
+
renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
|
|
449
|
+
}
|
|
450
|
+
startThinkingSpinner();
|
|
451
|
+
}
|
|
353
452
|
}
|
|
354
453
|
function showError(message) {
|
|
355
454
|
process.stdout.write(`\n${p.error}Error: ${message}${p.reset}\n`);
|