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.
@@ -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 user_shell tool
29
- bus.onPipe("session:configure", (payload) => {
30
- return {
31
- ...payload,
32
- mcpServers: [
33
- ...payload.mcpServers,
34
- {
35
- name: "agent-sh",
36
- command: process.execPath,
37
- args: [path.join(__dirname, "..", "mcp-server.js")],
38
- env: [{ name: "AGENT_SH_SOCKET", value: socketPath }],
39
- },
40
- ],
41
- };
42
- });
43
- // Also set AGENT_SH_SOCKET for pi extensions that connect directly
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
- // Show the command output in the TUI
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
- return { result: contextManager.expand(ids.map(Number)) };
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, renderToolResult, startSpinner, stopSpinner as stopToolSpinner, } from "../utils/tool-display.js";
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
- const MAX_COMMAND_OUTPUT_LINES = 30;
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}${p.bold}💭 Thinking${p.reset}`);
47
+ renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
43
48
  }
44
49
  else {
45
- startThinkingSpinner("Thinking");
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
- showToolCall(e.title, lastCommand, e);
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) => showToolComplete(e.exitCode));
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
- process.stdout.write("\n");
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
- for (const line of lines) {
186
- renderer.writeLine(line);
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 termW = process.stdout.columns || 80;
196
- const lines = renderToolResult({ exitCode }, termW);
197
- for (const line of lines) {
198
- renderer.writeLine(line);
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 startThinkingSpinner(label = "Thinking") {
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
- spinner = startSpinner(label);
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 < MAX_COMMAND_OUTPUT_LINES) {
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 < MAX_COMMAND_OUTPUT_LINES) {
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: DIFF_MAX_LINES,
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: DIFF_MAX_LINES,
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
- const state = showThinkingText ? "on" : "off";
352
- process.stdout.write(`\n${p.dim}Thinking display: ${state}${p.reset}\n`);
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`);