agent-sh 0.2.0 → 0.3.1

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 (42) hide show
  1. package/README.md +21 -0
  2. package/dist/acp-client.d.ts +24 -0
  3. package/dist/acp-client.js +155 -33
  4. package/dist/context-manager.d.ts +5 -3
  5. package/dist/context-manager.js +62 -31
  6. package/dist/core.js +10 -0
  7. package/dist/event-bus.d.ts +26 -0
  8. package/dist/event-bus.js +10 -0
  9. package/dist/extension-loader.js +3 -14
  10. package/dist/extensions/shell-exec.js +27 -22
  11. package/dist/extensions/tui-renderer.d.ts +1 -1
  12. package/dist/extensions/tui-renderer.js +369 -126
  13. package/dist/index.js +184 -37
  14. package/dist/input-handler.d.ts +10 -0
  15. package/dist/input-handler.js +169 -10
  16. package/dist/mcp-server.js +37 -8
  17. package/dist/settings.d.ts +44 -0
  18. package/dist/settings.js +61 -0
  19. package/dist/shell.d.ts +1 -0
  20. package/dist/shell.js +44 -4
  21. package/dist/types.d.ts +17 -0
  22. package/dist/utils/ansi.d.ts +4 -1
  23. package/dist/utils/ansi.js +60 -2
  24. package/dist/utils/box-frame.js +2 -1
  25. package/dist/utils/diff-renderer.js +1 -1
  26. package/dist/utils/frame-renderer.d.ts +26 -0
  27. package/dist/utils/frame-renderer.js +76 -0
  28. package/dist/utils/handler-registry.d.ts +41 -0
  29. package/dist/utils/handler-registry.js +52 -0
  30. package/dist/utils/line-editor.d.ts +21 -1
  31. package/dist/utils/line-editor.js +193 -99
  32. package/dist/utils/markdown.d.ts +15 -6
  33. package/dist/utils/markdown.js +106 -67
  34. package/dist/utils/output-writer.d.ts +22 -0
  35. package/dist/utils/output-writer.js +29 -0
  36. package/dist/utils/stream-transform.d.ts +70 -0
  37. package/dist/utils/stream-transform.js +229 -0
  38. package/dist/utils/tool-display.d.ts +11 -8
  39. package/dist/utils/tool-display.js +69 -46
  40. package/examples/extensions/latex-images.ts +142 -0
  41. package/examples/pi-agent-sh.ts +166 -0
  42. package/package.json +10 -2
@@ -10,82 +10,190 @@
10
10
  * silently dropped. Alternative renderers (web UI, logging, minimal)
11
11
  * can subscribe to the same events.
12
12
  */
13
- import { MarkdownRenderer } from "../utils/markdown.js";
13
+ import { highlight } from "cli-highlight";
14
+ import { MarkdownRenderer, wrapLine } from "../utils/markdown.js";
15
+ import { createFencedBlockTransform } from "../utils/stream-transform.js";
14
16
  import { palette as p } from "../utils/palette.js";
15
- import { renderToolCall, renderToolResult, startSpinner, stopSpinner as stopToolSpinner, } from "../utils/tool-display.js";
17
+ import { renderToolCall, createSpinner, renderSpinnerLine, } from "../utils/tool-display.js";
16
18
  import { renderDiff } from "../utils/diff-renderer.js";
17
19
  import { renderBoxFrame } from "../utils/box-frame.js";
18
- const MAX_COMMAND_OUTPUT_LINES = 30;
19
- export default function activate({ bus }) {
20
- let spinner = null;
21
- let renderer = null;
22
- let commandOutputBuffer = "";
23
- let commandOutputLineCount = 0;
24
- let commandOutputOverflow = 0;
25
- let lastCommand = "";
26
- let isThinking = false;
27
- let showThinkingText = false;
28
- let lastTruncatedDiff = null;
20
+ import { getSettings } from "../settings.js";
21
+ import { StdoutWriter } from "../utils/output-writer.js";
22
+ /** Encode a PNG buffer as a terminal inline image escape sequence. */
23
+ function encodeImageForTerminal(data) {
24
+ const b64 = data.toString("base64");
25
+ if (process.env.TERM_PROGRAM === "iTerm.app" || process.env.TERM_PROGRAM === "WezTerm") {
26
+ return `\x1b]1337;File=inline=1;size=${data.length};preserveAspectRatio=1:${b64}\x07`;
27
+ }
28
+ if (process.env.KITTY_WINDOW_ID || process.env.TERM_PROGRAM === "ghostty") {
29
+ const chunks = [];
30
+ for (let i = 0; i < b64.length; i += 4096) {
31
+ const chunk = b64.slice(i, i + 4096);
32
+ const isLast = i + 4096 >= b64.length;
33
+ chunks.push(i === 0
34
+ ? `\x1b_Gf=100,t=d,a=T,m=${isLast ? 0 : 1};${chunk}\x1b\\`
35
+ : `\x1b_Gm=${isLast ? 0 : 1};${chunk}\x1b\\`);
36
+ }
37
+ return chunks.join("");
38
+ }
39
+ return null;
40
+ }
41
+ function createRenderState() {
42
+ return {
43
+ renderer: null,
44
+ hadToolCalls: false,
45
+ spinner: null,
46
+ spinnerLabel: "",
47
+ spinnerOpts: {},
48
+ spinnerInterval: null,
49
+ spinnerStartTime: 0,
50
+ lastCommand: "",
51
+ toolLineOpen: false,
52
+ currentToolKind: undefined,
53
+ commandOutputBuffer: "",
54
+ commandOutputLineCount: 0,
55
+ commandOutputOverflow: 0,
56
+ isThinking: false,
57
+ showThinkingText: false,
58
+ thinkingPending: false,
59
+ lastTruncatedDiff: null,
60
+ };
61
+ }
62
+ export default function activate(ctx) {
63
+ const { bus, getAcpClient, define } = ctx;
64
+ const writer = new StdoutWriter();
65
+ const s = createRenderState();
66
+ // ── Register fenced block transform (code blocks → ContentBlock) ──
67
+ // Nobody is special — tui-renderer uses the same primitive as any extension.
68
+ const fencedTransform = createFencedBlockTransform(bus, {
69
+ open: /^```(\w*)\s*$/,
70
+ close: /^```\s*$/,
71
+ transform(match, content) {
72
+ return { type: "code-block", language: match[1] || "", code: content };
73
+ },
74
+ });
29
75
  // ── Event subscriptions ─────────────────────────────────────
30
76
  bus.on("agent:query", (e) => {
77
+ s.spinnerStartTime = 0;
31
78
  showUserQuery(e.query);
32
79
  startAgentResponse();
33
80
  startThinkingSpinner();
34
81
  });
35
82
  bus.on("agent:thinking-chunk", (e) => {
36
- if (!isThinking) {
37
- isThinking = true;
38
- stopCurrentSpinner();
39
- if (showThinkingText) {
40
- if (!renderer)
83
+ s.thinkingPending = true;
84
+ if (!s.isThinking) {
85
+ s.isThinking = true;
86
+ if (s.showThinkingText) {
87
+ stopCurrentSpinner();
88
+ if (!s.renderer)
41
89
  startAgentResponse();
42
- renderer.writeLine(`${p.dim}${p.bold}💭 Thinking${p.reset}`);
90
+ s.renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
91
+ drain();
43
92
  }
44
93
  else {
45
- startThinkingSpinner("Thinking");
94
+ // Restart spinner with ctrl+t hint now that we know thinking is available
95
+ startThinkingSpinner();
46
96
  }
47
97
  }
48
- if (showThinkingText && e.text) {
49
- if (!renderer)
98
+ if (s.showThinkingText && e.text) {
99
+ s.thinkingPending = false;
100
+ if (!s.renderer)
50
101
  startAgentResponse();
51
- renderer.push(`${p.dim}${e.text}${p.reset}`);
52
- flushOutput();
102
+ s.renderer.push(`${p.dim}${e.text}${p.reset}`);
103
+ drain();
104
+ }
105
+ });
106
+ bus.on("agent:response-chunk", (e) => {
107
+ if (e.blocks) {
108
+ // Inject spacing: append \n to text blocks that precede non-text blocks
109
+ const blocks = e.blocks;
110
+ for (let i = 0; i < blocks.length; i++) {
111
+ const block = blocks[i];
112
+ const next = blocks[i + 1];
113
+ if (block.type === "text" && next && next.type !== "text") {
114
+ block.text += "\n";
115
+ }
116
+ }
117
+ for (const block of blocks) {
118
+ switch (block.type) {
119
+ case "text":
120
+ if (block.text)
121
+ writeAgentText(block.text);
122
+ break;
123
+ case "code-block":
124
+ writeCodeBlock(block.language, block.code);
125
+ break;
126
+ case "image":
127
+ writeInlineImage(block.data);
128
+ break;
129
+ case "raw":
130
+ flushForRaw();
131
+ writer.write(block.escape);
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ else {
137
+ writeAgentText(e.text);
53
138
  }
54
139
  });
55
- bus.on("agent:response-chunk", (e) => writeAgentText(e.text));
56
140
  bus.on("agent:response-done", () => {
57
- isThinking = false;
141
+ s.isThinking = false;
58
142
  endAgentResponse();
59
143
  });
60
144
  bus.on("agent:tool-call", (e) => {
61
- lastCommand = e.tool;
145
+ s.lastCommand = e.tool;
62
146
  });
63
147
  bus.on("agent:tool-started", (e) => {
148
+ fencedTransform.flush();
64
149
  stopCurrentSpinner();
65
- showToolCall(e.title, lastCommand, e);
66
- lastCommand = "";
150
+ s.currentToolKind = e.kind;
151
+ if (e.title === "user_shell") {
152
+ closeToolLine();
153
+ if (!s.renderer)
154
+ startAgentResponse();
155
+ s.renderer.flush();
156
+ const cmd = e.rawInput?.command || "";
157
+ s.renderer.writeLine(`${p.dim}▶ user_shell: ${cmd}${p.reset}`);
158
+ drain();
159
+ s.hadToolCalls = true;
160
+ }
161
+ else {
162
+ showToolCall(e.title, s.lastCommand, e);
163
+ }
164
+ s.lastCommand = "";
165
+ });
166
+ bus.on("agent:tool-completed", (e) => {
167
+ showToolComplete(e.exitCode);
168
+ s.currentToolKind = undefined;
169
+ s.spinnerStartTime = 0;
170
+ startThinkingSpinner();
67
171
  });
68
- bus.on("agent:tool-completed", (e) => showToolComplete(e.exitCode));
69
172
  bus.on("agent:tool-output-chunk", (e) => writeCommandOutput(e.chunk));
70
173
  bus.on("agent:tool-output", () => flushCommandOutput());
71
174
  bus.on("agent:cancelled", () => {
72
- isThinking = false;
175
+ s.isThinking = false;
73
176
  stopCurrentSpinner();
74
177
  showInfo("(cancelled)");
75
178
  endAgentResponse();
76
179
  });
180
+ bus.on("agent:processing-done", () => {
181
+ s.isThinking = false;
182
+ stopCurrentSpinner();
183
+ endAgentResponse();
184
+ });
77
185
  bus.on("agent:error", (e) => showError(e.message));
78
- // Flush rendering state and show inline diff for file writes
79
186
  bus.on("permission:request", (e) => {
80
187
  stopCurrentSpinner();
81
188
  flushCommandOutput();
82
- renderer?.flush();
189
+ if (s.renderer) {
190
+ s.renderer.flush();
191
+ drain();
192
+ }
83
193
  if (e.kind === "file-write" && e.metadata?.diff) {
84
194
  showFileDiff(e.title, e.metadata.diff);
85
195
  }
86
196
  else {
87
- // Non-file permission (e.g. tool-call) — end response box
88
- // so interactive extensions can render their own UI
89
197
  endAgentResponse();
90
198
  }
91
199
  });
@@ -98,38 +206,46 @@ export default function activate({ bus }) {
98
206
  bus.on("ui:info", (e) => showInfo(e.message));
99
207
  bus.on("ui:error", (e) => showError(e.message));
100
208
  // ── Rendering functions ─────────────────────────────────────
101
- function flushOutput() {
102
- if (process.stdout.writable) {
103
- try {
104
- process.stdout.write("");
105
- }
106
- catch { }
209
+ function drain() {
210
+ if (!s.renderer)
211
+ return;
212
+ for (const line of s.renderer.drainLines()) {
213
+ writer.write(line + "\n");
107
214
  }
108
215
  }
109
216
  function startAgentResponse() {
110
- renderer = new MarkdownRenderer();
111
- process.stdout.write("\n");
112
- renderer.printTopBorder();
217
+ s.renderer = new MarkdownRenderer(writer.columns);
218
+ s.hadToolCalls = false;
219
+ s.renderer.printTopBorder();
220
+ drain();
221
+ }
222
+ function showCollapsedThinking() {
223
+ if (s.thinkingPending && !s.showThinkingText) {
224
+ if (!s.renderer)
225
+ startAgentResponse();
226
+ s.renderer.writeLine(`${p.muted}… thinking${p.reset}`);
227
+ s.thinkingPending = false;
228
+ }
113
229
  }
114
230
  function endAgentResponse() {
115
- if (renderer) {
116
- renderer.flush();
117
- renderer.printBottomBorder();
118
- renderer = null;
231
+ closeToolLine();
232
+ stopCurrentSpinner();
233
+ if (s.renderer) {
234
+ s.renderer.flush();
235
+ s.renderer.printBottomBorder();
236
+ drain();
237
+ s.renderer = null;
119
238
  }
120
239
  }
121
240
  function showUserQuery(query) {
122
- const termW = process.stdout.columns || 80;
123
- const boxW = Math.min(84, termW);
124
- const contentW = boxW - 4; // inside box padding
125
- // Wrap long queries to fit within box
241
+ const boxW = Math.min(84, writer.columns);
242
+ const contentW = boxW - 4;
126
243
  const lines = [];
127
244
  for (const raw of query.split("\n")) {
128
245
  if (raw.length <= contentW) {
129
246
  lines.push(`${p.accent}${raw}${p.reset}`);
130
247
  }
131
248
  else {
132
- // Simple word wrap
133
249
  let remaining = raw;
134
250
  while (remaining.length > contentW) {
135
251
  let breakAt = remaining.lastIndexOf(" ", contentW);
@@ -148,101 +264,209 @@ export default function activate({ bus }) {
148
264
  borderColor: p.accent,
149
265
  title: `${p.accent}${p.bold}❯${p.reset}`,
150
266
  });
151
- process.stdout.write("\n");
267
+ writer.write("\n");
152
268
  for (const line of framed) {
153
- process.stdout.write(line + "\n");
269
+ writer.write(line + "\n");
154
270
  }
155
271
  }
156
272
  function writeAgentText(text) {
157
- if (isThinking) {
158
- isThinking = false;
159
- if (showThinkingText && renderer) {
160
- renderer.flush();
161
- const termW = process.stdout.columns || 80;
162
- const w = Math.min(80, termW);
163
- renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
273
+ closeToolLine();
274
+ const needsGap = s.hadToolCalls;
275
+ s.hadToolCalls = false;
276
+ if (s.isThinking) {
277
+ s.isThinking = false;
278
+ if (s.showThinkingText && s.renderer) {
279
+ s.renderer.flush();
280
+ const w = Math.min(80, writer.columns);
281
+ s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
282
+ drain();
283
+ }
284
+ }
285
+ showCollapsedThinking();
286
+ stopCurrentSpinner();
287
+ if (!s.renderer)
288
+ startAgentResponse();
289
+ if (needsGap)
290
+ writer.write("\n");
291
+ s.renderer.push(text);
292
+ drain();
293
+ }
294
+ define("render:code-block", (language, code, width) => {
295
+ flushForRaw();
296
+ if (language) {
297
+ s.renderer.writeLine(`${p.dim}${language}${p.reset}`);
298
+ }
299
+ let highlighted;
300
+ if (!language) {
301
+ // No language specified — render as plain text to avoid false syntax detection
302
+ highlighted = code;
303
+ }
304
+ else {
305
+ try {
306
+ highlighted = highlight(code, { language });
307
+ }
308
+ catch {
309
+ highlighted = `${p.success}${code}${p.reset}`;
310
+ }
311
+ }
312
+ const contentWidth = Math.min(90, width - 2);
313
+ for (const line of highlighted.split("\n")) {
314
+ const indented = ` ${line}`;
315
+ const wrapped = wrapLine(indented, contentWidth);
316
+ for (const wl of wrapped) {
317
+ s.renderer.writeLine(wl);
164
318
  }
165
319
  }
320
+ drain();
321
+ });
322
+ function writeCodeBlock(language, code) {
323
+ ctx.call("render:code-block", language, code, writer.columns);
324
+ }
325
+ function flushForRaw() {
326
+ closeToolLine();
166
327
  stopCurrentSpinner();
167
- if (!renderer)
328
+ if (!s.renderer)
168
329
  startAgentResponse();
169
- renderer.push(text);
170
- flushOutput();
330
+ s.renderer.flush();
331
+ drain();
332
+ }
333
+ define("render:image", (data) => {
334
+ flushForRaw();
335
+ const escape = encodeImageForTerminal(data);
336
+ if (escape) {
337
+ writer.write(" " + escape + "\n");
338
+ }
339
+ });
340
+ function writeInlineImage(data) {
341
+ ctx.call("render:image", data);
171
342
  }
172
343
  function showToolCall(title, command, extra) {
344
+ closeToolLine();
173
345
  stopCurrentSpinner();
174
- if (!renderer)
346
+ if (!s.renderer)
175
347
  startAgentResponse();
176
- renderer.flush();
177
- const termW = process.stdout.columns || 80;
348
+ showCollapsedThinking();
349
+ s.renderer.flush();
350
+ drain();
178
351
  const lines = renderToolCall({
179
352
  title,
180
353
  command: command || undefined,
181
354
  kind: extra?.kind,
182
355
  locations: extra?.locations,
183
356
  rawInput: extra?.rawInput,
184
- }, termW);
185
- for (const line of lines) {
186
- renderer.writeLine(line);
357
+ }, writer.columns);
358
+ for (let i = 0; i < lines.length - 1; i++) {
359
+ s.renderer.writeLine(lines[i]);
187
360
  }
188
- // Reset output tracking for the new tool
189
- commandOutputLineCount = 0;
190
- commandOutputOverflow = 0;
361
+ drain();
362
+ if (lines.length > 0) {
363
+ writer.write(` ${lines[lines.length - 1]}`);
364
+ s.toolLineOpen = true;
365
+ }
366
+ s.hadToolCalls = true;
367
+ s.commandOutputLineCount = 0;
368
+ s.commandOutputOverflow = 0;
191
369
  }
192
370
  function showToolComplete(exitCode) {
193
- if (!renderer)
371
+ if (!s.renderer)
194
372
  return;
195
- const termW = process.stdout.columns || 80;
196
- const lines = renderToolResult({ exitCode }, termW);
197
- for (const line of lines) {
198
- renderer.writeLine(line);
373
+ const mark = exitCode === null
374
+ ? `${p.muted}(timed out)${p.reset}`
375
+ : exitCode === 0
376
+ ? `${p.success}✓${p.reset}`
377
+ : `${p.error}✗ exit ${exitCode}${p.reset}`;
378
+ if (s.toolLineOpen && s.commandOutputLineCount === 0) {
379
+ writer.write(` ${mark}\n`);
380
+ s.toolLineOpen = false;
199
381
  }
382
+ else {
383
+ closeToolLine();
384
+ flushCommandOutput();
385
+ s.renderer.writeLine(` ${mark}`);
386
+ drain();
387
+ }
388
+ }
389
+ function hasThinkingMode() {
390
+ const mode = getAcpClient().getCurrentMode();
391
+ return !mode || mode.id !== "off";
200
392
  }
201
- function startThinkingSpinner(label = "Thinking") {
393
+ function startThinkingSpinner() {
394
+ if (!s.spinnerStartTime)
395
+ s.spinnerStartTime = Date.now();
202
396
  stopCurrentSpinner();
203
- spinner = startSpinner(label);
397
+ const thinking = hasThinkingMode();
398
+ s.spinnerLabel = thinking ? "Thinking" : "Working";
399
+ const hint = thinking
400
+ ? (s.showThinkingText ? "(ctrl+t to collapse)" : "(ctrl+t to expand)")
401
+ : "";
402
+ s.spinnerOpts = { hint: hint || undefined, startTime: s.spinnerStartTime };
403
+ s.spinner = createSpinner({ startTime: s.spinnerStartTime });
404
+ s.spinnerInterval = setInterval(() => {
405
+ if (s.spinner) {
406
+ const line = renderSpinnerLine(s.spinner, s.spinnerLabel, s.spinnerOpts);
407
+ writer.write(`\r ${line}\x1b[K`);
408
+ }
409
+ }, 80);
204
410
  }
205
411
  function stopCurrentSpinner() {
206
- if (spinner) {
207
- stopToolSpinner(spinner);
208
- spinner = null;
412
+ if (s.spinnerInterval) {
413
+ clearInterval(s.spinnerInterval);
414
+ s.spinnerInterval = null;
415
+ }
416
+ if (s.spinner) {
417
+ writer.write("\r\x1b[2K");
418
+ s.spinner = null;
419
+ }
420
+ }
421
+ function closeToolLine() {
422
+ if (s.toolLineOpen) {
423
+ writer.write("\n");
424
+ s.toolLineOpen = false;
209
425
  }
210
426
  }
211
427
  function writeCommandOutput(chunk) {
212
- if (!renderer)
428
+ if (!s.renderer)
213
429
  return;
214
- commandOutputBuffer += chunk;
215
- const lines = commandOutputBuffer.split("\n");
216
- commandOutputBuffer = lines.pop();
430
+ closeToolLine();
431
+ const maxLines = s.currentToolKind === "read"
432
+ ? getSettings().readOutputMaxLines
433
+ : getSettings().maxCommandOutputLines;
434
+ s.commandOutputBuffer += chunk;
435
+ const lines = s.commandOutputBuffer.split("\n");
436
+ s.commandOutputBuffer = lines.pop();
217
437
  for (const line of lines) {
218
- if (commandOutputLineCount < MAX_COMMAND_OUTPUT_LINES) {
219
- renderer.writeLine(`${p.dim} ${line}${p.reset}`);
220
- commandOutputLineCount++;
438
+ if (s.commandOutputLineCount < maxLines) {
439
+ s.renderer.writeLine(`${p.dim} ${line}${p.reset}`);
440
+ s.commandOutputLineCount++;
221
441
  }
222
442
  else {
223
- commandOutputOverflow++;
443
+ s.commandOutputOverflow++;
224
444
  }
225
445
  }
446
+ drain();
226
447
  }
227
448
  function flushCommandOutput() {
228
- if (!renderer)
449
+ if (!s.renderer)
229
450
  return;
230
- if (commandOutputBuffer) {
231
- if (commandOutputLineCount < MAX_COMMAND_OUTPUT_LINES) {
232
- renderer.writeLine(`${p.dim} ${commandOutputBuffer}${p.reset}`);
233
- commandOutputLineCount++;
451
+ const maxLines = s.currentToolKind === "read"
452
+ ? getSettings().readOutputMaxLines
453
+ : getSettings().maxCommandOutputLines;
454
+ if (s.commandOutputBuffer) {
455
+ if (s.commandOutputLineCount < maxLines) {
456
+ s.renderer.writeLine(`${p.dim} ${s.commandOutputBuffer}${p.reset}`);
457
+ s.commandOutputLineCount++;
234
458
  }
235
459
  else {
236
- commandOutputOverflow++;
460
+ s.commandOutputOverflow++;
237
461
  }
238
- commandOutputBuffer = "";
462
+ s.commandOutputBuffer = "";
239
463
  }
240
- if (commandOutputOverflow > 0) {
241
- renderer.writeLine(`${p.dim} … ${commandOutputOverflow} more lines${p.reset}`);
242
- commandOutputOverflow = 0;
464
+ if (s.commandOutputOverflow > 0 && maxLines > 0) {
465
+ s.renderer.writeLine(`${p.dim} … ${s.commandOutputOverflow} more lines${p.reset}`);
243
466
  }
467
+ s.commandOutputOverflow = 0;
468
+ drain();
244
469
  }
245
- const DIFF_MAX_LINES = 20;
246
470
  function diffTitle(filePath, diff) {
247
471
  const stats = diff.isNewFile
248
472
  ? `${p.success}+${diff.added}${p.reset}`
@@ -252,23 +476,22 @@ export default function activate({ bus }) {
252
476
  function showFileDiff(filePath, diff) {
253
477
  if (diff.isIdentical)
254
478
  return;
255
- const termW = process.stdout.columns || 80;
256
- const boxW = Math.min(84, termW);
479
+ const boxW = Math.min(84, writer.columns);
257
480
  const contentW = boxW - 4;
258
481
  const diffLines = renderDiff(diff, {
259
482
  width: contentW,
260
483
  filePath,
261
- maxLines: DIFF_MAX_LINES,
484
+ maxLines: getSettings().diffMaxLines,
262
485
  trueColor: true,
263
486
  mode: "unified",
264
487
  });
265
488
  const lastLine = diffLines[diffLines.length - 1] ?? "";
266
489
  const isTruncated = lastLine.includes("… ");
267
490
  if (isTruncated) {
268
- lastTruncatedDiff = { filePath, diff, expanded: false };
491
+ s.lastTruncatedDiff = { filePath, diff, expanded: false };
269
492
  }
270
493
  else {
271
- lastTruncatedDiff = null;
494
+ s.lastTruncatedDiff = null;
272
495
  }
273
496
  const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
274
497
  const footer = isTruncated
@@ -281,16 +504,17 @@ export default function activate({ bus }) {
281
504
  title: diffTitle(filePath, diff),
282
505
  footer,
283
506
  });
284
- if (!renderer)
507
+ if (!s.renderer)
285
508
  startAgentResponse();
286
509
  for (const line of framed) {
287
- renderer.writeLine(line);
510
+ s.renderer.writeLine(line);
288
511
  }
512
+ drain();
289
513
  }
290
514
  function expandLastDiff() {
291
- if (!lastTruncatedDiff)
515
+ if (!s.lastTruncatedDiff)
292
516
  return;
293
- const entry = lastTruncatedDiff;
517
+ const entry = s.lastTruncatedDiff;
294
518
  entry.expanded = !entry.expanded;
295
519
  if (!entry.expanded) {
296
520
  showFileDiffCached(entry);
@@ -298,8 +522,7 @@ export default function activate({ bus }) {
298
522
  }
299
523
  if (!entry.expandedLines) {
300
524
  const { filePath, diff } = entry;
301
- const termW = process.stdout.columns || 80;
302
- const boxW = Math.min(120, termW);
525
+ const boxW = Math.min(120, writer.columns);
303
526
  const contentW = boxW - 4;
304
527
  const diffLines = renderDiff(diff, {
305
528
  width: contentW,
@@ -316,20 +539,19 @@ export default function activate({ bus }) {
316
539
  footer: [` ${p.dim}ctrl+o to collapse${p.reset}`],
317
540
  });
318
541
  }
319
- process.stdout.write("\n");
542
+ writer.write("\n");
320
543
  for (const line of entry.expandedLines) {
321
- process.stdout.write(line + "\n");
544
+ writer.write(line + "\n");
322
545
  }
323
546
  }
324
547
  function showFileDiffCached(entry) {
325
548
  const { filePath, diff } = entry;
326
- const termW = process.stdout.columns || 80;
327
- const boxW = Math.min(84, termW);
549
+ const boxW = Math.min(84, writer.columns);
328
550
  const contentW = boxW - 4;
329
551
  const diffLines = renderDiff(diff, {
330
552
  width: contentW,
331
553
  filePath,
332
- maxLines: DIFF_MAX_LINES,
554
+ maxLines: getSettings().diffMaxLines,
333
555
  trueColor: true,
334
556
  mode: "unified",
335
557
  });
@@ -341,20 +563,41 @@ export default function activate({ bus }) {
341
563
  title: diffTitle(filePath, diff),
342
564
  footer: [` ${p.dim}ctrl+o to expand${p.reset}`],
343
565
  });
344
- process.stdout.write("\n");
566
+ writer.write("\n");
345
567
  for (const line of framed) {
346
- process.stdout.write(line + "\n");
568
+ writer.write(line + "\n");
347
569
  }
348
570
  }
349
571
  function toggleThinkingDisplay() {
350
- showThinkingText = !showThinkingText;
351
- const state = showThinkingText ? "on" : "off";
352
- process.stdout.write(`\n${p.dim}Thinking display: ${state}${p.reset}\n`);
572
+ s.showThinkingText = !s.showThinkingText;
573
+ if (s.spinner) {
574
+ stopCurrentSpinner();
575
+ startThinkingSpinner();
576
+ return;
577
+ }
578
+ if (!s.isThinking)
579
+ return;
580
+ if (s.showThinkingText) {
581
+ stopCurrentSpinner();
582
+ if (!s.renderer)
583
+ startAgentResponse();
584
+ s.renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
585
+ drain();
586
+ }
587
+ else {
588
+ if (s.renderer) {
589
+ s.renderer.flush();
590
+ const w = Math.min(80, writer.columns);
591
+ s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
592
+ drain();
593
+ }
594
+ startThinkingSpinner();
595
+ }
353
596
  }
354
597
  function showError(message) {
355
- process.stdout.write(`\n${p.error}Error: ${message}${p.reset}\n`);
598
+ writer.write(`\n${p.error}Error: ${message}${p.reset}\n`);
356
599
  }
357
600
  function showInfo(message) {
358
- process.stdout.write(`${p.muted}${message}${p.reset}\n`);
601
+ writer.write(`${p.muted}${message}${p.reset}\n`);
359
602
  }
360
603
  }