agent-sh 0.3.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.
@@ -10,111 +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, 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
20
  import { getSettings } from "../settings.js";
19
- export default function activate({ bus, getAcpClient }) {
20
- let spinner = null;
21
- let renderer = null;
22
- let commandOutputBuffer = "";
23
- let commandOutputLineCount = 0;
24
- let commandOutputOverflow = 0;
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
29
- let isThinking = false;
30
- let showThinkingText = false;
31
- let spinnerStartTime = 0; // preserved across spinner restarts
32
- let lastTruncatedDiff = null;
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
+ });
33
75
  // ── Event subscriptions ─────────────────────────────────────
34
76
  bus.on("agent:query", (e) => {
35
- spinnerStartTime = 0;
77
+ s.spinnerStartTime = 0;
36
78
  showUserQuery(e.query);
37
79
  startAgentResponse();
38
80
  startThinkingSpinner();
39
81
  });
40
82
  bus.on("agent:thinking-chunk", (e) => {
41
- if (!isThinking) {
42
- isThinking = true;
43
- if (showThinkingText) {
83
+ s.thinkingPending = true;
84
+ if (!s.isThinking) {
85
+ s.isThinking = true;
86
+ if (s.showThinkingText) {
44
87
  stopCurrentSpinner();
45
- if (!renderer)
88
+ if (!s.renderer)
46
89
  startAgentResponse();
47
- renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
90
+ s.renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
91
+ drain();
48
92
  }
49
93
  else {
50
94
  // Restart spinner with ctrl+t hint now that we know thinking is available
51
95
  startThinkingSpinner();
52
96
  }
53
97
  }
54
- if (showThinkingText && e.text) {
55
- if (!renderer)
98
+ if (s.showThinkingText && e.text) {
99
+ s.thinkingPending = false;
100
+ if (!s.renderer)
56
101
  startAgentResponse();
57
- renderer.push(`${p.dim}${e.text}${p.reset}`);
58
- 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);
59
138
  }
60
139
  });
61
- bus.on("agent:response-chunk", (e) => writeAgentText(e.text));
62
140
  bus.on("agent:response-done", () => {
63
- isThinking = false;
141
+ s.isThinking = false;
64
142
  endAgentResponse();
65
143
  });
66
144
  bus.on("agent:tool-call", (e) => {
67
- lastCommand = e.tool;
145
+ s.lastCommand = e.tool;
68
146
  });
69
147
  bus.on("agent:tool-started", (e) => {
148
+ fencedTransform.flush();
70
149
  stopCurrentSpinner();
71
- currentToolKind = e.kind;
150
+ s.currentToolKind = e.kind;
72
151
  if (e.title === "user_shell") {
73
- // Minimal annotation — PTY echo will show the output
74
152
  closeToolLine();
75
- if (!renderer)
153
+ if (!s.renderer)
76
154
  startAgentResponse();
77
- renderer.flush();
155
+ s.renderer.flush();
78
156
  const cmd = e.rawInput?.command || "";
79
- renderer.writeLine(`${p.dim}▶ user_shell: ${cmd}${p.reset}`);
80
- hadToolCalls = true;
157
+ s.renderer.writeLine(`${p.dim}▶ user_shell: ${cmd}${p.reset}`);
158
+ drain();
159
+ s.hadToolCalls = true;
81
160
  }
82
161
  else {
83
- showToolCall(e.title, lastCommand, e);
162
+ showToolCall(e.title, s.lastCommand, e);
84
163
  }
85
- lastCommand = "";
164
+ s.lastCommand = "";
86
165
  });
87
166
  bus.on("agent:tool-completed", (e) => {
88
167
  showToolComplete(e.exitCode);
89
- currentToolKind = undefined;
90
- spinnerStartTime = 0;
168
+ s.currentToolKind = undefined;
169
+ s.spinnerStartTime = 0;
91
170
  startThinkingSpinner();
92
171
  });
93
172
  bus.on("agent:tool-output-chunk", (e) => writeCommandOutput(e.chunk));
94
173
  bus.on("agent:tool-output", () => flushCommandOutput());
95
174
  bus.on("agent:cancelled", () => {
96
- isThinking = false;
175
+ s.isThinking = false;
97
176
  stopCurrentSpinner();
98
177
  showInfo("(cancelled)");
99
178
  endAgentResponse();
100
179
  });
101
180
  bus.on("agent:processing-done", () => {
102
- isThinking = false;
181
+ s.isThinking = false;
103
182
  stopCurrentSpinner();
104
183
  endAgentResponse();
105
184
  });
106
185
  bus.on("agent:error", (e) => showError(e.message));
107
- // Flush rendering state and show inline diff for file writes
108
186
  bus.on("permission:request", (e) => {
109
187
  stopCurrentSpinner();
110
188
  flushCommandOutput();
111
- renderer?.flush();
189
+ if (s.renderer) {
190
+ s.renderer.flush();
191
+ drain();
192
+ }
112
193
  if (e.kind === "file-write" && e.metadata?.diff) {
113
194
  showFileDiff(e.title, e.metadata.diff);
114
195
  }
115
196
  else {
116
- // Non-file permission (e.g. tool-call) — end response box
117
- // so interactive extensions can render their own UI
118
197
  endAgentResponse();
119
198
  }
120
199
  });
@@ -127,39 +206,46 @@ export default function activate({ bus, getAcpClient }) {
127
206
  bus.on("ui:info", (e) => showInfo(e.message));
128
207
  bus.on("ui:error", (e) => showError(e.message));
129
208
  // ── Rendering functions ─────────────────────────────────────
130
- function flushOutput() {
131
- if (process.stdout.writable) {
132
- try {
133
- process.stdout.write("");
134
- }
135
- catch { }
209
+ function drain() {
210
+ if (!s.renderer)
211
+ return;
212
+ for (const line of s.renderer.drainLines()) {
213
+ writer.write(line + "\n");
136
214
  }
137
215
  }
138
216
  function startAgentResponse() {
139
- renderer = new MarkdownRenderer();
140
- hadToolCalls = false;
141
- 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
+ }
142
229
  }
143
230
  function endAgentResponse() {
144
231
  closeToolLine();
145
- if (renderer) {
146
- renderer.flush();
147
- renderer.printBottomBorder();
148
- renderer = null;
232
+ stopCurrentSpinner();
233
+ if (s.renderer) {
234
+ s.renderer.flush();
235
+ s.renderer.printBottomBorder();
236
+ drain();
237
+ s.renderer = null;
149
238
  }
150
239
  }
151
240
  function showUserQuery(query) {
152
- const termW = process.stdout.columns || 80;
153
- const boxW = Math.min(84, termW);
154
- const contentW = boxW - 4; // inside box padding
155
- // Wrap long queries to fit within box
241
+ const boxW = Math.min(84, writer.columns);
242
+ const contentW = boxW - 4;
156
243
  const lines = [];
157
244
  for (const raw of query.split("\n")) {
158
245
  if (raw.length <= contentW) {
159
246
  lines.push(`${p.accent}${raw}${p.reset}`);
160
247
  }
161
248
  else {
162
- // Simple word wrap
163
249
  let remaining = raw;
164
250
  while (remaining.length > contentW) {
165
251
  let breakAt = remaining.lastIndexOf(" ", contentW);
@@ -178,76 +264,126 @@ export default function activate({ bus, getAcpClient }) {
178
264
  borderColor: p.accent,
179
265
  title: `${p.accent}${p.bold}❯${p.reset}`,
180
266
  });
181
- process.stdout.write("\n");
267
+ writer.write("\n");
182
268
  for (const line of framed) {
183
- process.stdout.write(line + "\n");
269
+ writer.write(line + "\n");
184
270
  }
185
271
  }
186
272
  function writeAgentText(text) {
187
273
  closeToolLine();
188
- const needsGap = hadToolCalls;
189
- hadToolCalls = false;
190
- if (isThinking) {
191
- isThinking = false;
192
- if (showThinkingText && renderer) {
193
- renderer.flush();
194
- const termW = process.stdout.columns || 80;
195
- const w = Math.min(80, termW);
196
- renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
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();
197
283
  }
198
284
  }
285
+ showCollapsedThinking();
199
286
  stopCurrentSpinner();
200
- if (!renderer)
287
+ if (!s.renderer)
201
288
  startAgentResponse();
202
289
  if (needsGap)
203
- process.stdout.write("\n");
204
- renderer.push(text);
205
- flushOutput();
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);
318
+ }
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();
327
+ stopCurrentSpinner();
328
+ if (!s.renderer)
329
+ startAgentResponse();
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);
206
342
  }
207
343
  function showToolCall(title, command, extra) {
208
344
  closeToolLine();
209
345
  stopCurrentSpinner();
210
- if (!renderer)
346
+ if (!s.renderer)
211
347
  startAgentResponse();
212
- renderer.flush();
213
- const termW = process.stdout.columns || 80;
348
+ showCollapsedThinking();
349
+ s.renderer.flush();
350
+ drain();
214
351
  const lines = renderToolCall({
215
352
  title,
216
353
  command: command || undefined,
217
354
  kind: extra?.kind,
218
355
  locations: extra?.locations,
219
356
  rawInput: extra?.rawInput,
220
- }, termW);
221
- // Write all lines except the last normally, write last without \n
357
+ }, writer.columns);
222
358
  for (let i = 0; i < lines.length - 1; i++) {
223
- renderer.writeLine(lines[i]);
359
+ s.renderer.writeLine(lines[i]);
224
360
  }
361
+ drain();
225
362
  if (lines.length > 0) {
226
- process.stdout.write(` ${lines[lines.length - 1]}`);
227
- toolLineOpen = true;
363
+ writer.write(` ${lines[lines.length - 1]}`);
364
+ s.toolLineOpen = true;
228
365
  }
229
- hadToolCalls = true;
230
- // Reset output tracking for the new tool
231
- commandOutputLineCount = 0;
232
- commandOutputOverflow = 0;
366
+ s.hadToolCalls = true;
367
+ s.commandOutputLineCount = 0;
368
+ s.commandOutputOverflow = 0;
233
369
  }
234
370
  function showToolComplete(exitCode) {
235
- if (!renderer)
371
+ if (!s.renderer)
236
372
  return;
237
373
  const mark = exitCode === null
238
374
  ? `${p.muted}(timed out)${p.reset}`
239
375
  : exitCode === 0
240
376
  ? `${p.success}✓${p.reset}`
241
377
  : `${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;
378
+ if (s.toolLineOpen && s.commandOutputLineCount === 0) {
379
+ writer.write(` ${mark}\n`);
380
+ s.toolLineOpen = false;
246
381
  }
247
382
  else {
248
383
  closeToolLine();
249
384
  flushCommandOutput();
250
- renderer.writeLine(` ${mark}`);
385
+ s.renderer.writeLine(` ${mark}`);
386
+ drain();
251
387
  }
252
388
  }
253
389
  function hasThinkingMode() {
@@ -255,69 +391,81 @@ export default function activate({ bus, getAcpClient }) {
255
391
  return !mode || mode.id !== "off";
256
392
  }
257
393
  function startThinkingSpinner() {
258
- // Preserve start time if restarting (e.g. toggle), otherwise reset
259
- if (!spinnerStartTime)
260
- spinnerStartTime = Date.now();
394
+ if (!s.spinnerStartTime)
395
+ s.spinnerStartTime = Date.now();
261
396
  stopCurrentSpinner();
262
397
  const thinking = hasThinkingMode();
263
- const label = thinking ? "Thinking" : "Working";
398
+ s.spinnerLabel = thinking ? "Thinking" : "Working";
264
399
  const hint = thinking
265
- ? (showThinkingText ? "(ctrl+t to collapse)" : "(ctrl+t to expand)")
400
+ ? (s.showThinkingText ? "(ctrl+t to collapse)" : "(ctrl+t to expand)")
266
401
  : "";
267
- spinner = startSpinner(label, { hint: hint || undefined, startTime: spinnerStartTime });
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);
268
410
  }
269
411
  function stopCurrentSpinner() {
270
- if (spinner) {
271
- stopToolSpinner(spinner);
272
- 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;
273
419
  }
274
420
  }
275
421
  function closeToolLine() {
276
- if (toolLineOpen) {
277
- process.stdout.write("\n");
278
- toolLineOpen = false;
422
+ if (s.toolLineOpen) {
423
+ writer.write("\n");
424
+ s.toolLineOpen = false;
279
425
  }
280
426
  }
281
427
  function writeCommandOutput(chunk) {
282
- if (!renderer)
428
+ if (!s.renderer)
283
429
  return;
284
430
  closeToolLine();
285
- const maxLines = currentToolKind === "read"
431
+ const maxLines = s.currentToolKind === "read"
286
432
  ? getSettings().readOutputMaxLines
287
433
  : getSettings().maxCommandOutputLines;
288
- commandOutputBuffer += chunk;
289
- const lines = commandOutputBuffer.split("\n");
290
- commandOutputBuffer = lines.pop();
434
+ s.commandOutputBuffer += chunk;
435
+ const lines = s.commandOutputBuffer.split("\n");
436
+ s.commandOutputBuffer = lines.pop();
291
437
  for (const line of lines) {
292
- if (commandOutputLineCount < maxLines) {
293
- renderer.writeLine(`${p.dim} ${line}${p.reset}`);
294
- commandOutputLineCount++;
438
+ if (s.commandOutputLineCount < maxLines) {
439
+ s.renderer.writeLine(`${p.dim} ${line}${p.reset}`);
440
+ s.commandOutputLineCount++;
295
441
  }
296
442
  else {
297
- commandOutputOverflow++;
443
+ s.commandOutputOverflow++;
298
444
  }
299
445
  }
446
+ drain();
300
447
  }
301
448
  function flushCommandOutput() {
302
- if (!renderer)
449
+ if (!s.renderer)
303
450
  return;
304
- const maxLines = currentToolKind === "read"
451
+ const maxLines = s.currentToolKind === "read"
305
452
  ? getSettings().readOutputMaxLines
306
453
  : getSettings().maxCommandOutputLines;
307
- if (commandOutputBuffer) {
308
- if (commandOutputLineCount < maxLines) {
309
- renderer.writeLine(`${p.dim} ${commandOutputBuffer}${p.reset}`);
310
- commandOutputLineCount++;
454
+ if (s.commandOutputBuffer) {
455
+ if (s.commandOutputLineCount < maxLines) {
456
+ s.renderer.writeLine(`${p.dim} ${s.commandOutputBuffer}${p.reset}`);
457
+ s.commandOutputLineCount++;
311
458
  }
312
459
  else {
313
- commandOutputOverflow++;
460
+ s.commandOutputOverflow++;
314
461
  }
315
- commandOutputBuffer = "";
462
+ s.commandOutputBuffer = "";
316
463
  }
317
- if (commandOutputOverflow > 0 && maxLines > 0) {
318
- renderer.writeLine(`${p.dim} … ${commandOutputOverflow} more lines${p.reset}`);
464
+ if (s.commandOutputOverflow > 0 && maxLines > 0) {
465
+ s.renderer.writeLine(`${p.dim} … ${s.commandOutputOverflow} more lines${p.reset}`);
319
466
  }
320
- commandOutputOverflow = 0;
467
+ s.commandOutputOverflow = 0;
468
+ drain();
321
469
  }
322
470
  function diffTitle(filePath, diff) {
323
471
  const stats = diff.isNewFile
@@ -328,8 +476,7 @@ export default function activate({ bus, getAcpClient }) {
328
476
  function showFileDiff(filePath, diff) {
329
477
  if (diff.isIdentical)
330
478
  return;
331
- const termW = process.stdout.columns || 80;
332
- const boxW = Math.min(84, termW);
479
+ const boxW = Math.min(84, writer.columns);
333
480
  const contentW = boxW - 4;
334
481
  const diffLines = renderDiff(diff, {
335
482
  width: contentW,
@@ -341,10 +488,10 @@ export default function activate({ bus, getAcpClient }) {
341
488
  const lastLine = diffLines[diffLines.length - 1] ?? "";
342
489
  const isTruncated = lastLine.includes("… ");
343
490
  if (isTruncated) {
344
- lastTruncatedDiff = { filePath, diff, expanded: false };
491
+ s.lastTruncatedDiff = { filePath, diff, expanded: false };
345
492
  }
346
493
  else {
347
- lastTruncatedDiff = null;
494
+ s.lastTruncatedDiff = null;
348
495
  }
349
496
  const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
350
497
  const footer = isTruncated
@@ -357,16 +504,17 @@ export default function activate({ bus, getAcpClient }) {
357
504
  title: diffTitle(filePath, diff),
358
505
  footer,
359
506
  });
360
- if (!renderer)
507
+ if (!s.renderer)
361
508
  startAgentResponse();
362
509
  for (const line of framed) {
363
- renderer.writeLine(line);
510
+ s.renderer.writeLine(line);
364
511
  }
512
+ drain();
365
513
  }
366
514
  function expandLastDiff() {
367
- if (!lastTruncatedDiff)
515
+ if (!s.lastTruncatedDiff)
368
516
  return;
369
- const entry = lastTruncatedDiff;
517
+ const entry = s.lastTruncatedDiff;
370
518
  entry.expanded = !entry.expanded;
371
519
  if (!entry.expanded) {
372
520
  showFileDiffCached(entry);
@@ -374,8 +522,7 @@ export default function activate({ bus, getAcpClient }) {
374
522
  }
375
523
  if (!entry.expandedLines) {
376
524
  const { filePath, diff } = entry;
377
- const termW = process.stdout.columns || 80;
378
- const boxW = Math.min(120, termW);
525
+ const boxW = Math.min(120, writer.columns);
379
526
  const contentW = boxW - 4;
380
527
  const diffLines = renderDiff(diff, {
381
528
  width: contentW,
@@ -392,15 +539,14 @@ export default function activate({ bus, getAcpClient }) {
392
539
  footer: [` ${p.dim}ctrl+o to collapse${p.reset}`],
393
540
  });
394
541
  }
395
- process.stdout.write("\n");
542
+ writer.write("\n");
396
543
  for (const line of entry.expandedLines) {
397
- process.stdout.write(line + "\n");
544
+ writer.write(line + "\n");
398
545
  }
399
546
  }
400
547
  function showFileDiffCached(entry) {
401
548
  const { filePath, diff } = entry;
402
- const termW = process.stdout.columns || 80;
403
- const boxW = Math.min(84, termW);
549
+ const boxW = Math.min(84, writer.columns);
404
550
  const contentW = boxW - 4;
405
551
  const diffLines = renderDiff(diff, {
406
552
  width: contentW,
@@ -417,43 +563,41 @@ export default function activate({ bus, getAcpClient }) {
417
563
  title: diffTitle(filePath, diff),
418
564
  footer: [` ${p.dim}ctrl+o to expand${p.reset}`],
419
565
  });
420
- process.stdout.write("\n");
566
+ writer.write("\n");
421
567
  for (const line of framed) {
422
- process.stdout.write(line + "\n");
568
+ writer.write(line + "\n");
423
569
  }
424
570
  }
425
571
  function toggleThinkingDisplay() {
426
- showThinkingText = !showThinkingText;
427
- // Update spinner hint to reflect new state, even if not actively thinking
428
- if (spinner) {
572
+ s.showThinkingText = !s.showThinkingText;
573
+ if (s.spinner) {
429
574
  stopCurrentSpinner();
430
575
  startThinkingSpinner();
431
576
  return;
432
577
  }
433
- if (!isThinking)
578
+ if (!s.isThinking)
434
579
  return;
435
- if (showThinkingText) {
436
- // Switch from spinner to streaming text
580
+ if (s.showThinkingText) {
437
581
  stopCurrentSpinner();
438
- if (!renderer)
582
+ if (!s.renderer)
439
583
  startAgentResponse();
440
- renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
584
+ s.renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
585
+ drain();
441
586
  }
442
587
  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}`);
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();
449
593
  }
450
594
  startThinkingSpinner();
451
595
  }
452
596
  }
453
597
  function showError(message) {
454
- process.stdout.write(`\n${p.error}Error: ${message}${p.reset}\n`);
598
+ writer.write(`\n${p.error}Error: ${message}${p.reset}\n`);
455
599
  }
456
600
  function showInfo(message) {
457
- process.stdout.write(`${p.muted}${message}${p.reset}\n`);
601
+ writer.write(`${p.muted}${message}${p.reset}\n`);
458
602
  }
459
603
  }