agent-sh 0.1.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.
@@ -18,6 +18,8 @@ export interface ShellEvents {
18
18
  "shell:foreground-busy": {
19
19
  busy: boolean;
20
20
  };
21
+ "shell:agent-exec-start": Record<string, never>;
22
+ "shell:agent-exec-done": Record<string, never>;
21
23
  "agent:submit": {
22
24
  query: string;
23
25
  };
@@ -52,10 +54,17 @@ export interface ShellEvents {
52
54
  "agent:tool-started": {
53
55
  title: string;
54
56
  toolCallId?: string;
57
+ kind?: string;
58
+ locations?: {
59
+ path: string;
60
+ line?: number | null;
61
+ }[];
62
+ rawInput?: unknown;
55
63
  };
56
64
  "agent:tool-completed": {
57
65
  toolCallId?: string;
58
66
  exitCode: number | null;
67
+ rawOutput?: unknown;
59
68
  };
60
69
  "agent:tool-output-chunk": {
61
70
  chunk: string;
@@ -89,6 +98,26 @@ export interface ShellEvents {
89
98
  cwd: string;
90
99
  handled: boolean;
91
100
  };
101
+ "shell:exec-request": {
102
+ command: string;
103
+ output: string;
104
+ cwd: string;
105
+ done: boolean;
106
+ };
107
+ "session:configure": {
108
+ cwd: string;
109
+ mcpServers: {
110
+ name: string;
111
+ command: string;
112
+ args: string[];
113
+ env: {
114
+ name: string;
115
+ value: string;
116
+ }[];
117
+ }[];
118
+ };
119
+ "config:changed": Record<string, never>;
120
+ "config:cycle": Record<string, never>;
92
121
  "autocomplete:request": {
93
122
  buffer: string;
94
123
  items: {
@@ -1,9 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import * as os from "node:os";
4
- const CONFIG_DIR = path.join(os.homedir(), ".agent-sh");
3
+ import { CONFIG_DIR, getSettings } from "./settings.js";
5
4
  const EXT_DIR = path.join(CONFIG_DIR, "extensions");
6
- const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
7
5
  const TS_EXTS = [".ts", ".tsx", ".mts"];
8
6
  const SCRIPT_EXTS = [".js", ".mjs", ".ts", ".tsx", ".mts"];
9
7
  let tsRegistered = false;
@@ -19,15 +17,6 @@ async function ensureTsSupport() {
19
17
  // tsx not available — TS extensions will fail with a clear error
20
18
  }
21
19
  }
22
- async function loadSettings() {
23
- try {
24
- const raw = await fs.readFile(SETTINGS_PATH, "utf-8");
25
- return JSON.parse(raw);
26
- }
27
- catch {
28
- return {};
29
- }
30
- }
31
20
  /**
32
21
  * Load extensions from three sources (merged, deduplicated):
33
22
  *
@@ -49,8 +38,8 @@ export async function loadExtensions(ctx, cliExtensions) {
49
38
  specifiers.push(...cliExtensions);
50
39
  }
51
40
  // 2. settings.json
52
- const settings = await loadSettings();
53
- if (settings.extensions) {
41
+ const settings = getSettings();
42
+ if (settings.extensions.length > 0) {
54
43
  specifiers.push(...settings.extensions);
55
44
  }
56
45
  // 3. ~/.agent-sh/extensions/ directory
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shell exec extension.
3
+ *
4
+ * Runs a Unix domain socket server speaking JSON-RPC 2.0 that external
5
+ * tools (MCP server, pi extensions, etc.) connect to for interacting
6
+ * with the user's live PTY shell.
7
+ *
8
+ * Also registers the MCP server via the `session:configure` pipe so
9
+ * ACP agents discover the `user_shell` tool automatically.
10
+ *
11
+ * This extension has no direct PTY or Shell knowledge — it communicates
12
+ * exclusively through the bus, following the headless-core philosophy.
13
+ *
14
+ * ## Socket protocol (JSON-RPC 2.0, newline-delimited)
15
+ *
16
+ * shell/exec { command: string } → { output, cwd }
17
+ * shell/cwd {} → { cwd }
18
+ * shell/info {} → { busy, shell }
19
+ * shell/recall { operation, ... } → { result }
20
+ */
21
+ import type { ExtensionContext } from "../types.js";
22
+ export default function activate({ bus, contextManager }: ExtensionContext, opts: {
23
+ socketPath: string;
24
+ }): void;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Shell exec extension.
3
+ *
4
+ * Runs a Unix domain socket server speaking JSON-RPC 2.0 that external
5
+ * tools (MCP server, pi extensions, etc.) connect to for interacting
6
+ * with the user's live PTY shell.
7
+ *
8
+ * Also registers the MCP server via the `session:configure` pipe so
9
+ * ACP agents discover the `user_shell` tool automatically.
10
+ *
11
+ * This extension has no direct PTY or Shell knowledge — it communicates
12
+ * exclusively through the bus, following the headless-core philosophy.
13
+ *
14
+ * ## Socket protocol (JSON-RPC 2.0, newline-delimited)
15
+ *
16
+ * shell/exec { command: string } → { output, cwd }
17
+ * shell/cwd {} → { cwd }
18
+ * shell/info {} → { busy, shell }
19
+ * shell/recall { operation, ... } → { result }
20
+ */
21
+ import * as net from "node:net";
22
+ import * as fs from "node:fs";
23
+ import * as path from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { getSettings } from "../settings.js";
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
+ export default function activate({ bus, contextManager }, opts) {
28
+ const { socketPath } = opts;
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
49
+ process.env.AGENT_SH_SOCKET = socketPath;
50
+ // Serialize shell/exec requests — only one PTY command at a time
51
+ let execPending = Promise.resolve();
52
+ // ── JSON-RPC handler ────────────────────────────────────────────
53
+ async function handleRequest(method, params) {
54
+ switch (method) {
55
+ case "shell/exec": {
56
+ const command = params?.command;
57
+ if (typeof command !== "string" || !command) {
58
+ throw rpcError(-32602, "Missing required parameter: command");
59
+ }
60
+ // Serialize — one PTY command at a time
61
+ return new Promise((resolve, reject) => {
62
+ execPending = execPending.then(async () => {
63
+ try {
64
+ bus.emit("shell:agent-exec-start", {});
65
+ const result = await bus.emitPipeAsync("shell:exec-request", {
66
+ command,
67
+ output: "",
68
+ cwd: "",
69
+ done: false,
70
+ });
71
+ bus.emit("shell:agent-exec-done", {});
72
+ resolve({ output: result.output, cwd: result.cwd });
73
+ }
74
+ catch (err) {
75
+ bus.emit("shell:agent-exec-done", {});
76
+ const message = err instanceof Error ? err.message : String(err);
77
+ reject(rpcError(-32000, message));
78
+ }
79
+ });
80
+ });
81
+ }
82
+ case "shell/cwd":
83
+ return { cwd: contextManager.getCwd() };
84
+ case "shell/info":
85
+ return {
86
+ shell: process.env.SHELL || "unknown",
87
+ agentSh: true,
88
+ };
89
+ case "shell/recall": {
90
+ const operation = params?.operation || "browse";
91
+ switch (operation) {
92
+ case "search": {
93
+ const query = params?.query;
94
+ if (typeof query !== "string" || !query) {
95
+ throw rpcError(-32602, "Missing required parameter: query");
96
+ }
97
+ return { result: contextManager.search(query) };
98
+ }
99
+ case "expand": {
100
+ const ids = params?.ids;
101
+ if (!Array.isArray(ids) || ids.length === 0) {
102
+ throw rpcError(-32602, "Missing required parameter: ids (array of numbers)");
103
+ }
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) };
107
+ }
108
+ case "browse":
109
+ return { result: contextManager.getRecentSummary() };
110
+ default:
111
+ throw rpcError(-32602, `Unknown recall operation: ${operation}`);
112
+ }
113
+ }
114
+ default:
115
+ throw rpcError(-32601, `Method not found: ${method}`);
116
+ }
117
+ }
118
+ // ── Socket server ───────────────────────────────────────────────
119
+ const server = net.createServer((conn) => {
120
+ let buffer = "";
121
+ conn.on("data", (chunk) => {
122
+ buffer += chunk.toString();
123
+ // Process complete lines (newline-delimited JSON-RPC)
124
+ let newlineIdx;
125
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
126
+ const line = buffer.slice(0, newlineIdx).trim();
127
+ buffer = buffer.slice(newlineIdx + 1);
128
+ if (!line)
129
+ continue;
130
+ processMessage(conn, line);
131
+ }
132
+ });
133
+ });
134
+ function processMessage(conn, line) {
135
+ let id = null;
136
+ try {
137
+ const msg = JSON.parse(line);
138
+ id = msg.id ?? null;
139
+ const method = msg.method;
140
+ if (!method) {
141
+ sendError(conn, id, -32600, "Invalid request: missing method");
142
+ return;
143
+ }
144
+ handleRequest(method, msg.params)
145
+ .then((result) => sendResult(conn, id, result))
146
+ .catch((err) => {
147
+ if (err && typeof err === "object" && "rpcCode" in err) {
148
+ sendError(conn, id, err.rpcCode, err.message);
149
+ }
150
+ else {
151
+ sendError(conn, id, -32603, String(err));
152
+ }
153
+ });
154
+ }
155
+ catch {
156
+ sendError(conn, id, -32700, "Parse error");
157
+ }
158
+ }
159
+ // Clean up stale socket file
160
+ try {
161
+ fs.unlinkSync(socketPath);
162
+ }
163
+ catch {
164
+ // Doesn't exist — fine
165
+ }
166
+ server.listen(socketPath);
167
+ // Cleanup on exit
168
+ const cleanup = () => {
169
+ server.close();
170
+ try {
171
+ fs.unlinkSync(socketPath);
172
+ }
173
+ catch { }
174
+ };
175
+ process.on("exit", cleanup);
176
+ }
177
+ // ── JSON-RPC helpers ──────────────────────────────────────────────
178
+ function sendResult(conn, id, result) {
179
+ conn.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
180
+ }
181
+ function sendError(conn, id, code, message) {
182
+ conn.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
183
+ }
184
+ function rpcError(code, message) {
185
+ const err = new Error(message);
186
+ err.rpcCode = code;
187
+ return err;
188
+ }
@@ -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);
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,19 +199,34 @@ 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
- function showToolCall(title, command) {
207
+ function showToolCall(title, command, extra) {
208
+ closeToolLine();
173
209
  stopCurrentSpinner();
174
210
  if (!renderer)
175
211
  startAgentResponse();
176
212
  renderer.flush();
177
213
  const termW = process.stdout.columns || 80;
178
- const lines = renderToolCall({ title, command: command || undefined }, termW);
179
- for (const line of lines) {
180
- renderer.writeLine(line);
214
+ const lines = renderToolCall({
215
+ title,
216
+ command: command || undefined,
217
+ kind: extra?.kind,
218
+ locations: extra?.locations,
219
+ rawInput: extra?.rawInput,
220
+ }, termW);
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]);
181
224
  }
225
+ if (lines.length > 0) {
226
+ process.stdout.write(` ${lines[lines.length - 1]}`);
227
+ toolLineOpen = true;
228
+ }
229
+ hadToolCalls = true;
182
230
  // Reset output tracking for the new tool
183
231
  commandOutputLineCount = 0;
184
232
  commandOutputOverflow = 0;
@@ -186,15 +234,37 @@ export default function activate({ bus }) {
186
234
  function showToolComplete(exitCode) {
187
235
  if (!renderer)
188
236
  return;
189
- const termW = process.stdout.columns || 80;
190
- const lines = renderToolResult({ exitCode }, termW);
191
- for (const line of lines) {
192
- 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}`);
193
251
  }
194
252
  }
195
- 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();
196
261
  stopCurrentSpinner();
197
- 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 });
198
268
  }
199
269
  function stopCurrentSpinner() {
200
270
  if (spinner) {
@@ -202,14 +272,24 @@ export default function activate({ bus }) {
202
272
  spinner = null;
203
273
  }
204
274
  }
275
+ function closeToolLine() {
276
+ if (toolLineOpen) {
277
+ process.stdout.write("\n");
278
+ toolLineOpen = false;
279
+ }
280
+ }
205
281
  function writeCommandOutput(chunk) {
206
282
  if (!renderer)
207
283
  return;
284
+ closeToolLine();
285
+ const maxLines = currentToolKind === "read"
286
+ ? getSettings().readOutputMaxLines
287
+ : getSettings().maxCommandOutputLines;
208
288
  commandOutputBuffer += chunk;
209
289
  const lines = commandOutputBuffer.split("\n");
210
290
  commandOutputBuffer = lines.pop();
211
291
  for (const line of lines) {
212
- if (commandOutputLineCount < MAX_COMMAND_OUTPUT_LINES) {
292
+ if (commandOutputLineCount < maxLines) {
213
293
  renderer.writeLine(`${p.dim} ${line}${p.reset}`);
214
294
  commandOutputLineCount++;
215
295
  }
@@ -221,8 +301,11 @@ export default function activate({ bus }) {
221
301
  function flushCommandOutput() {
222
302
  if (!renderer)
223
303
  return;
304
+ const maxLines = currentToolKind === "read"
305
+ ? getSettings().readOutputMaxLines
306
+ : getSettings().maxCommandOutputLines;
224
307
  if (commandOutputBuffer) {
225
- if (commandOutputLineCount < MAX_COMMAND_OUTPUT_LINES) {
308
+ if (commandOutputLineCount < maxLines) {
226
309
  renderer.writeLine(`${p.dim} ${commandOutputBuffer}${p.reset}`);
227
310
  commandOutputLineCount++;
228
311
  }
@@ -231,12 +314,11 @@ export default function activate({ bus }) {
231
314
  }
232
315
  commandOutputBuffer = "";
233
316
  }
234
- if (commandOutputOverflow > 0) {
317
+ if (commandOutputOverflow > 0 && maxLines > 0) {
235
318
  renderer.writeLine(`${p.dim} … ${commandOutputOverflow} more lines${p.reset}`);
236
- commandOutputOverflow = 0;
237
319
  }
320
+ commandOutputOverflow = 0;
238
321
  }
239
- const DIFF_MAX_LINES = 20;
240
322
  function diffTitle(filePath, diff) {
241
323
  const stats = diff.isNewFile
242
324
  ? `${p.success}+${diff.added}${p.reset}`
@@ -252,7 +334,7 @@ export default function activate({ bus }) {
252
334
  const diffLines = renderDiff(diff, {
253
335
  width: contentW,
254
336
  filePath,
255
- maxLines: DIFF_MAX_LINES,
337
+ maxLines: getSettings().diffMaxLines,
256
338
  trueColor: true,
257
339
  mode: "unified",
258
340
  });
@@ -323,7 +405,7 @@ export default function activate({ bus }) {
323
405
  const diffLines = renderDiff(diff, {
324
406
  width: contentW,
325
407
  filePath,
326
- maxLines: DIFF_MAX_LINES,
408
+ maxLines: getSettings().diffMaxLines,
327
409
  trueColor: true,
328
410
  mode: "unified",
329
411
  });
@@ -342,8 +424,31 @@ export default function activate({ bus }) {
342
424
  }
343
425
  function toggleThinkingDisplay() {
344
426
  showThinkingText = !showThinkingText;
345
- const state = showThinkingText ? "on" : "off";
346
- 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
+ }
347
452
  }
348
453
  function showError(message) {
349
454
  process.stdout.write(`\n${p.error}Error: ${message}${p.reset}\n`);