agent-sh 0.3.0 → 0.4.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.
@@ -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;
36
- showUserQuery(e.query);
77
+ s.spinnerStartTime = 0;
78
+ showUserQuery(e.query, e.modeLabel);
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
- 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
240
+ function showUserQuery(query, modeLabel) {
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);
@@ -172,82 +258,138 @@ export default function activate({ bus, getAcpClient }) {
172
258
  lines.push(`${p.accent}${remaining}${p.reset}`);
173
259
  }
174
260
  }
261
+ // Mode-specific border color and title
262
+ const isExecute = modeLabel === "Execute";
263
+ const borderColor = isExecute ? p.success : p.accent;
264
+ const title = modeLabel
265
+ ? `${borderColor}${p.bold} ${modeLabel} ${p.reset}`
266
+ : `${p.accent}${p.bold}❯${p.reset}`;
175
267
  const framed = renderBoxFrame(lines, {
176
268
  width: boxW,
177
269
  style: "rounded",
178
- borderColor: p.accent,
179
- title: `${p.accent}${p.bold}❯${p.reset}`,
270
+ borderColor,
271
+ title,
180
272
  });
181
- process.stdout.write("\n");
273
+ writer.write("\n");
182
274
  for (const line of framed) {
183
- process.stdout.write(line + "\n");
275
+ writer.write(line + "\n");
184
276
  }
185
277
  }
186
278
  function writeAgentText(text) {
187
279
  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}`);
280
+ const needsGap = s.hadToolCalls;
281
+ s.hadToolCalls = false;
282
+ if (s.isThinking) {
283
+ s.isThinking = false;
284
+ if (s.showThinkingText && s.renderer) {
285
+ s.renderer.flush();
286
+ const w = Math.min(80, writer.columns);
287
+ s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
288
+ drain();
197
289
  }
198
290
  }
291
+ showCollapsedThinking();
199
292
  stopCurrentSpinner();
200
- if (!renderer)
293
+ if (!s.renderer)
201
294
  startAgentResponse();
202
295
  if (needsGap)
203
- process.stdout.write("\n");
204
- renderer.push(text);
205
- flushOutput();
296
+ writer.write("\n");
297
+ s.renderer.push(text);
298
+ drain();
299
+ }
300
+ define("render:code-block", (language, code, width) => {
301
+ flushForRaw();
302
+ if (language) {
303
+ s.renderer.writeLine(`${p.dim}${language}${p.reset}`);
304
+ }
305
+ let highlighted;
306
+ if (!language) {
307
+ // No language specified — render as plain text to avoid false syntax detection
308
+ highlighted = code;
309
+ }
310
+ else {
311
+ try {
312
+ highlighted = highlight(code, { language });
313
+ }
314
+ catch {
315
+ highlighted = `${p.success}${code}${p.reset}`;
316
+ }
317
+ }
318
+ const contentWidth = Math.min(90, width - 2);
319
+ for (const line of highlighted.split("\n")) {
320
+ const indented = ` ${line}`;
321
+ const wrapped = wrapLine(indented, contentWidth);
322
+ for (const wl of wrapped) {
323
+ s.renderer.writeLine(wl);
324
+ }
325
+ }
326
+ drain();
327
+ });
328
+ function writeCodeBlock(language, code) {
329
+ ctx.call("render:code-block", language, code, writer.columns);
330
+ }
331
+ function flushForRaw() {
332
+ closeToolLine();
333
+ stopCurrentSpinner();
334
+ if (!s.renderer)
335
+ startAgentResponse();
336
+ s.renderer.flush();
337
+ drain();
338
+ }
339
+ define("render:image", (data) => {
340
+ flushForRaw();
341
+ const escape = encodeImageForTerminal(data);
342
+ if (escape) {
343
+ writer.write(" " + escape + "\n");
344
+ }
345
+ });
346
+ function writeInlineImage(data) {
347
+ ctx.call("render:image", data);
206
348
  }
207
349
  function showToolCall(title, command, extra) {
208
350
  closeToolLine();
209
351
  stopCurrentSpinner();
210
- if (!renderer)
352
+ if (!s.renderer)
211
353
  startAgentResponse();
212
- renderer.flush();
213
- const termW = process.stdout.columns || 80;
354
+ showCollapsedThinking();
355
+ s.renderer.flush();
356
+ drain();
214
357
  const lines = renderToolCall({
215
358
  title,
216
359
  command: command || undefined,
217
360
  kind: extra?.kind,
218
361
  locations: extra?.locations,
219
362
  rawInput: extra?.rawInput,
220
- }, termW);
221
- // Write all lines except the last normally, write last without \n
363
+ }, writer.columns);
222
364
  for (let i = 0; i < lines.length - 1; i++) {
223
- renderer.writeLine(lines[i]);
365
+ s.renderer.writeLine(lines[i]);
224
366
  }
367
+ drain();
225
368
  if (lines.length > 0) {
226
- process.stdout.write(` ${lines[lines.length - 1]}`);
227
- toolLineOpen = true;
369
+ writer.write(` ${lines[lines.length - 1]}`);
370
+ s.toolLineOpen = true;
228
371
  }
229
- hadToolCalls = true;
230
- // Reset output tracking for the new tool
231
- commandOutputLineCount = 0;
232
- commandOutputOverflow = 0;
372
+ s.hadToolCalls = true;
373
+ s.commandOutputLineCount = 0;
374
+ s.commandOutputOverflow = 0;
233
375
  }
234
376
  function showToolComplete(exitCode) {
235
- if (!renderer)
377
+ if (!s.renderer)
236
378
  return;
237
379
  const mark = exitCode === null
238
380
  ? `${p.muted}(timed out)${p.reset}`
239
381
  : exitCode === 0
240
382
  ? `${p.success}✓${p.reset}`
241
383
  : `${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;
384
+ if (s.toolLineOpen && s.commandOutputLineCount === 0) {
385
+ writer.write(` ${mark}\n`);
386
+ s.toolLineOpen = false;
246
387
  }
247
388
  else {
248
389
  closeToolLine();
249
390
  flushCommandOutput();
250
- renderer.writeLine(` ${mark}`);
391
+ s.renderer.writeLine(` ${mark}`);
392
+ drain();
251
393
  }
252
394
  }
253
395
  function hasThinkingMode() {
@@ -255,69 +397,81 @@ export default function activate({ bus, getAcpClient }) {
255
397
  return !mode || mode.id !== "off";
256
398
  }
257
399
  function startThinkingSpinner() {
258
- // Preserve start time if restarting (e.g. toggle), otherwise reset
259
- if (!spinnerStartTime)
260
- spinnerStartTime = Date.now();
400
+ if (!s.spinnerStartTime)
401
+ s.spinnerStartTime = Date.now();
261
402
  stopCurrentSpinner();
262
403
  const thinking = hasThinkingMode();
263
- const label = thinking ? "Thinking" : "Working";
404
+ s.spinnerLabel = thinking ? "Thinking" : "Working";
264
405
  const hint = thinking
265
- ? (showThinkingText ? "(ctrl+t to collapse)" : "(ctrl+t to expand)")
406
+ ? (s.showThinkingText ? "(ctrl+t to collapse)" : "(ctrl+t to expand)")
266
407
  : "";
267
- spinner = startSpinner(label, { hint: hint || undefined, startTime: spinnerStartTime });
408
+ s.spinnerOpts = { hint: hint || undefined, startTime: s.spinnerStartTime };
409
+ s.spinner = createSpinner({ startTime: s.spinnerStartTime });
410
+ s.spinnerInterval = setInterval(() => {
411
+ if (s.spinner) {
412
+ const line = renderSpinnerLine(s.spinner, s.spinnerLabel, s.spinnerOpts);
413
+ writer.write(`\r ${line}\x1b[K`);
414
+ }
415
+ }, 80);
268
416
  }
269
417
  function stopCurrentSpinner() {
270
- if (spinner) {
271
- stopToolSpinner(spinner);
272
- spinner = null;
418
+ if (s.spinnerInterval) {
419
+ clearInterval(s.spinnerInterval);
420
+ s.spinnerInterval = null;
421
+ }
422
+ if (s.spinner) {
423
+ writer.write("\r\x1b[2K");
424
+ s.spinner = null;
273
425
  }
274
426
  }
275
427
  function closeToolLine() {
276
- if (toolLineOpen) {
277
- process.stdout.write("\n");
278
- toolLineOpen = false;
428
+ if (s.toolLineOpen) {
429
+ writer.write("\n");
430
+ s.toolLineOpen = false;
279
431
  }
280
432
  }
281
433
  function writeCommandOutput(chunk) {
282
- if (!renderer)
434
+ if (!s.renderer)
283
435
  return;
284
436
  closeToolLine();
285
- const maxLines = currentToolKind === "read"
437
+ const maxLines = s.currentToolKind === "read"
286
438
  ? getSettings().readOutputMaxLines
287
439
  : getSettings().maxCommandOutputLines;
288
- commandOutputBuffer += chunk;
289
- const lines = commandOutputBuffer.split("\n");
290
- commandOutputBuffer = lines.pop();
440
+ s.commandOutputBuffer += chunk;
441
+ const lines = s.commandOutputBuffer.split("\n");
442
+ s.commandOutputBuffer = lines.pop();
291
443
  for (const line of lines) {
292
- if (commandOutputLineCount < maxLines) {
293
- renderer.writeLine(`${p.dim} ${line}${p.reset}`);
294
- commandOutputLineCount++;
444
+ if (s.commandOutputLineCount < maxLines) {
445
+ s.renderer.writeLine(`${p.dim} ${line}${p.reset}`);
446
+ s.commandOutputLineCount++;
295
447
  }
296
448
  else {
297
- commandOutputOverflow++;
449
+ s.commandOutputOverflow++;
298
450
  }
299
451
  }
452
+ drain();
300
453
  }
301
454
  function flushCommandOutput() {
302
- if (!renderer)
455
+ if (!s.renderer)
303
456
  return;
304
- const maxLines = currentToolKind === "read"
457
+ const maxLines = s.currentToolKind === "read"
305
458
  ? getSettings().readOutputMaxLines
306
459
  : getSettings().maxCommandOutputLines;
307
- if (commandOutputBuffer) {
308
- if (commandOutputLineCount < maxLines) {
309
- renderer.writeLine(`${p.dim} ${commandOutputBuffer}${p.reset}`);
310
- commandOutputLineCount++;
460
+ if (s.commandOutputBuffer) {
461
+ if (s.commandOutputLineCount < maxLines) {
462
+ s.renderer.writeLine(`${p.dim} ${s.commandOutputBuffer}${p.reset}`);
463
+ s.commandOutputLineCount++;
311
464
  }
312
465
  else {
313
- commandOutputOverflow++;
466
+ s.commandOutputOverflow++;
314
467
  }
315
- commandOutputBuffer = "";
468
+ s.commandOutputBuffer = "";
316
469
  }
317
- if (commandOutputOverflow > 0 && maxLines > 0) {
318
- renderer.writeLine(`${p.dim} … ${commandOutputOverflow} more lines${p.reset}`);
470
+ if (s.commandOutputOverflow > 0 && maxLines > 0) {
471
+ s.renderer.writeLine(`${p.dim} … ${s.commandOutputOverflow} more lines${p.reset}`);
319
472
  }
320
- commandOutputOverflow = 0;
473
+ s.commandOutputOverflow = 0;
474
+ drain();
321
475
  }
322
476
  function diffTitle(filePath, diff) {
323
477
  const stats = diff.isNewFile
@@ -328,8 +482,7 @@ export default function activate({ bus, getAcpClient }) {
328
482
  function showFileDiff(filePath, diff) {
329
483
  if (diff.isIdentical)
330
484
  return;
331
- const termW = process.stdout.columns || 80;
332
- const boxW = Math.min(84, termW);
485
+ const boxW = Math.min(84, writer.columns);
333
486
  const contentW = boxW - 4;
334
487
  const diffLines = renderDiff(diff, {
335
488
  width: contentW,
@@ -341,10 +494,10 @@ export default function activate({ bus, getAcpClient }) {
341
494
  const lastLine = diffLines[diffLines.length - 1] ?? "";
342
495
  const isTruncated = lastLine.includes("… ");
343
496
  if (isTruncated) {
344
- lastTruncatedDiff = { filePath, diff, expanded: false };
497
+ s.lastTruncatedDiff = { filePath, diff, expanded: false };
345
498
  }
346
499
  else {
347
- lastTruncatedDiff = null;
500
+ s.lastTruncatedDiff = null;
348
501
  }
349
502
  const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
350
503
  const footer = isTruncated
@@ -357,16 +510,17 @@ export default function activate({ bus, getAcpClient }) {
357
510
  title: diffTitle(filePath, diff),
358
511
  footer,
359
512
  });
360
- if (!renderer)
513
+ if (!s.renderer)
361
514
  startAgentResponse();
362
515
  for (const line of framed) {
363
- renderer.writeLine(line);
516
+ s.renderer.writeLine(line);
364
517
  }
518
+ drain();
365
519
  }
366
520
  function expandLastDiff() {
367
- if (!lastTruncatedDiff)
521
+ if (!s.lastTruncatedDiff)
368
522
  return;
369
- const entry = lastTruncatedDiff;
523
+ const entry = s.lastTruncatedDiff;
370
524
  entry.expanded = !entry.expanded;
371
525
  if (!entry.expanded) {
372
526
  showFileDiffCached(entry);
@@ -374,8 +528,7 @@ export default function activate({ bus, getAcpClient }) {
374
528
  }
375
529
  if (!entry.expandedLines) {
376
530
  const { filePath, diff } = entry;
377
- const termW = process.stdout.columns || 80;
378
- const boxW = Math.min(120, termW);
531
+ const boxW = Math.min(120, writer.columns);
379
532
  const contentW = boxW - 4;
380
533
  const diffLines = renderDiff(diff, {
381
534
  width: contentW,
@@ -392,15 +545,14 @@ export default function activate({ bus, getAcpClient }) {
392
545
  footer: [` ${p.dim}ctrl+o to collapse${p.reset}`],
393
546
  });
394
547
  }
395
- process.stdout.write("\n");
548
+ writer.write("\n");
396
549
  for (const line of entry.expandedLines) {
397
- process.stdout.write(line + "\n");
550
+ writer.write(line + "\n");
398
551
  }
399
552
  }
400
553
  function showFileDiffCached(entry) {
401
554
  const { filePath, diff } = entry;
402
- const termW = process.stdout.columns || 80;
403
- const boxW = Math.min(84, termW);
555
+ const boxW = Math.min(84, writer.columns);
404
556
  const contentW = boxW - 4;
405
557
  const diffLines = renderDiff(diff, {
406
558
  width: contentW,
@@ -417,43 +569,51 @@ export default function activate({ bus, getAcpClient }) {
417
569
  title: diffTitle(filePath, diff),
418
570
  footer: [` ${p.dim}ctrl+o to expand${p.reset}`],
419
571
  });
420
- process.stdout.write("\n");
572
+ writer.write("\n");
421
573
  for (const line of framed) {
422
- process.stdout.write(line + "\n");
574
+ writer.write(line + "\n");
423
575
  }
424
576
  }
425
577
  function toggleThinkingDisplay() {
426
- showThinkingText = !showThinkingText;
427
- // Update spinner hint to reflect new state, even if not actively thinking
428
- if (spinner) {
578
+ s.showThinkingText = !s.showThinkingText;
579
+ if (s.spinner) {
429
580
  stopCurrentSpinner();
430
- startThinkingSpinner();
581
+ if (s.showThinkingText) {
582
+ // Expanding: replace spinner with thinking text header
583
+ if (!s.renderer)
584
+ startAgentResponse();
585
+ s.renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
586
+ drain();
587
+ }
588
+ else {
589
+ // Collapsing: restart spinner with updated hint
590
+ startThinkingSpinner();
591
+ }
431
592
  return;
432
593
  }
433
- if (!isThinking)
594
+ if (!s.isThinking)
434
595
  return;
435
- if (showThinkingText) {
436
- // Switch from spinner to streaming text
596
+ if (s.showThinkingText) {
437
597
  stopCurrentSpinner();
438
- if (!renderer)
598
+ if (!s.renderer)
439
599
  startAgentResponse();
440
- renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
600
+ s.renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
601
+ drain();
441
602
  }
442
603
  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}`);
604
+ if (s.renderer) {
605
+ s.renderer.flush();
606
+ const w = Math.min(80, writer.columns);
607
+ s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
608
+ drain();
449
609
  }
450
610
  startThinkingSpinner();
451
611
  }
452
612
  }
453
613
  function showError(message) {
454
- process.stdout.write(`\n${p.error}Error: ${message}${p.reset}\n`);
614
+ writer.write(`\n${p.error}Error: ${message}${p.reset}\n`);
455
615
  }
456
616
  function showInfo(message) {
457
- process.stdout.write(`${p.muted}${message}${p.reset}\n`);
617
+ writer.write(`${p.muted}${message}${p.reset}\n`);
458
618
  }
459
619
  }