agent-sh 0.7.0 → 0.9.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.
Files changed (86) hide show
  1. package/README.md +28 -33
  2. package/dist/agent/agent-loop.d.ts +31 -8
  3. package/dist/agent/agent-loop.js +277 -66
  4. package/dist/agent/conversation-state.d.ts +41 -9
  5. package/dist/agent/conversation-state.js +340 -17
  6. package/dist/agent/history-file.d.ts +36 -0
  7. package/dist/agent/history-file.js +167 -0
  8. package/dist/agent/nuclear-form.d.ts +41 -0
  9. package/dist/agent/nuclear-form.js +176 -0
  10. package/dist/agent/system-prompt.d.ts +4 -5
  11. package/dist/agent/system-prompt.js +16 -11
  12. package/dist/agent/token-budget.d.ts +13 -0
  13. package/dist/agent/token-budget.js +50 -0
  14. package/dist/agent/tool-protocol.d.ts +83 -0
  15. package/dist/agent/tool-protocol.js +386 -0
  16. package/dist/agent/tools/user-shell.js +4 -1
  17. package/dist/agent/types.d.ts +21 -1
  18. package/dist/context-manager.d.ts +0 -1
  19. package/dist/context-manager.js +5 -110
  20. package/dist/core.d.ts +7 -7
  21. package/dist/core.js +76 -180
  22. package/dist/event-bus.d.ts +40 -0
  23. package/dist/event-bus.js +20 -1
  24. package/dist/extension-loader.d.ts +5 -0
  25. package/dist/extension-loader.js +104 -17
  26. package/dist/extensions/agent-backend.d.ts +13 -0
  27. package/dist/extensions/agent-backend.js +167 -0
  28. package/dist/extensions/command-suggest.d.ts +3 -3
  29. package/dist/extensions/command-suggest.js +4 -3
  30. package/dist/extensions/index.d.ts +19 -0
  31. package/dist/extensions/index.js +25 -0
  32. package/dist/extensions/slash-commands.d.ts +1 -1
  33. package/dist/extensions/slash-commands.js +44 -1
  34. package/dist/extensions/terminal-buffer.d.ts +1 -1
  35. package/dist/extensions/terminal-buffer.js +22 -8
  36. package/dist/extensions/tui-renderer.js +177 -122
  37. package/dist/index.js +14 -20
  38. package/dist/settings.d.ts +25 -2
  39. package/dist/settings.js +25 -4
  40. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
  41. package/dist/{input-handler.js → shell/input-handler.js} +60 -43
  42. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  43. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  44. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  45. package/dist/{shell.js → shell/shell.js} +24 -6
  46. package/dist/types.d.ts +49 -32
  47. package/dist/utils/ansi.d.ts +10 -0
  48. package/dist/utils/ansi.js +27 -0
  49. package/dist/utils/compositor.d.ts +62 -0
  50. package/dist/utils/compositor.js +88 -0
  51. package/dist/utils/diff-renderer.js +92 -4
  52. package/dist/utils/floating-panel.d.ts +34 -3
  53. package/dist/utils/floating-panel.js +315 -82
  54. package/dist/utils/handler-registry.d.ts +26 -10
  55. package/dist/utils/handler-registry.js +52 -16
  56. package/dist/utils/line-editor.d.ts +32 -3
  57. package/dist/utils/line-editor.js +218 -36
  58. package/dist/utils/markdown.d.ts +1 -0
  59. package/dist/utils/markdown.js +4 -4
  60. package/dist/utils/message-utils.d.ts +35 -0
  61. package/dist/utils/message-utils.js +75 -0
  62. package/dist/utils/terminal-buffer.d.ts +9 -1
  63. package/dist/utils/terminal-buffer.js +31 -2
  64. package/dist/utils/tool-display.d.ts +1 -0
  65. package/dist/utils/tool-display.js +1 -1
  66. package/dist/utils/tool-interactive.d.ts +12 -0
  67. package/dist/utils/tool-interactive.js +53 -0
  68. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  69. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  70. package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
  71. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  72. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  73. package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
  74. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  75. package/examples/extensions/claude-code-bridge/index.ts +77 -1
  76. package/examples/extensions/interactive-prompts.ts +82 -110
  77. package/examples/extensions/overlay-agent.ts +84 -38
  78. package/examples/extensions/peer-mesh.ts +450 -0
  79. package/examples/extensions/pi-bridge/index.ts +87 -2
  80. package/examples/extensions/questionnaire.ts +249 -0
  81. package/examples/extensions/tmux-pane.ts +307 -0
  82. package/examples/extensions/web-access.ts +327 -0
  83. package/package.json +9 -1
  84. package/dist/extensions/overlay-agent.d.ts +0 -11
  85. package/dist/extensions/overlay-agent.js +0 -43
  86. package/examples/extensions/terminal-buffer.ts +0 -184
@@ -11,14 +11,13 @@
11
11
  * can subscribe to the same events.
12
12
  */
13
13
  import { highlight } from "cli-highlight";
14
- import { MarkdownRenderer, wrapLine } from "../utils/markdown.js";
14
+ import { MarkdownRenderer, wrapLine, MAX_CONTENT_WIDTH } from "../utils/markdown.js";
15
15
  import { createFencedBlockTransform } from "../utils/stream-transform.js";
16
16
  import { palette as p } from "../utils/palette.js";
17
- import { renderToolCall, createSpinner, renderSpinnerLine, formatElapsed, } from "../utils/tool-display.js";
17
+ import { renderToolCall, createSpinner, formatElapsed, SPINNER_FRAMES, } from "../utils/tool-display.js";
18
18
  import { renderDiff } from "../utils/diff-renderer.js";
19
19
  import { renderBoxFrame } from "../utils/box-frame.js";
20
20
  import { getSettings } from "../settings.js";
21
- import { StdoutWriter } from "../utils/output-writer.js";
22
21
  /** Encode a PNG buffer as a terminal inline image escape sequence. */
23
22
  function encodeImageForTerminal(data) {
24
23
  const b64 = data.toString("base64");
@@ -68,12 +67,78 @@ function createRenderState() {
68
67
  };
69
68
  }
70
69
  export default function activate(ctx) {
71
- const { bus, llmClient, define } = ctx;
72
- const writer = new StdoutWriter();
70
+ const { bus, define, compositor } = ctx;
73
71
  const s = createRenderState();
74
- // Suppress all TUI output while stdout is held (overlay extensions)
75
- bus.on("shell:stdout-hold", () => { writer.hold(); });
76
- bus.on("shell:stdout-release", () => { writer.release(); });
72
+ /** Shorthand get the current agent surface. */
73
+ function out() { return compositor.surface("agent"); }
74
+ /** Capped width for borders, tool lines, and content — keeps everything aligned. */
75
+ function cappedW() { return Math.min(MAX_CONTENT_WIDTH + 2, out().columns); }
76
+ // Gate: other extensions (e.g. overlay) can advise this to suppress
77
+ // TUI rendering of agent output while they own the display.
78
+ define("tui:should-render-agent", () => true);
79
+ function shouldRender() { return ctx.call("tui:should-render-agent"); }
80
+ // ── Advisable rendering handlers ───────────────────────────────
81
+ // Extensions advise these to customize how the TUI renders content.
82
+ // Each handler receives data and returns rendered strings.
83
+ define("tui:response-border", (position, width) => {
84
+ return `${p.dim}${p.accent}${"─".repeat(width)}${p.reset}`;
85
+ });
86
+ define("tui:response-start", () => { });
87
+ define("tui:response-end", (_hadToolCalls) => { });
88
+ define("tui:render-info", (message) => `${p.muted}${message}${p.reset}`);
89
+ define("tui:render-error", (message) => `${p.error}Error: ${message}${p.reset}`);
90
+ define("tui:render-usage", (promptTokens, completionTokens, maxTokens) => {
91
+ const ctxK = (promptTokens / 1000).toFixed(1);
92
+ const maxK = (maxTokens / 1000).toFixed(0);
93
+ const pct = Math.min(100, (promptTokens / maxTokens) * 100).toFixed(0);
94
+ return `${p.dim}⬆ ${promptTokens} ⬇ ${completionTokens} ctx: ${ctxK}k/${maxK}k (${pct}%)${p.reset}`;
95
+ });
96
+ define("tui:render-content-gap", (fromKind, toKind) => fromKind !== toKind ? "\n" : null);
97
+ define("tui:render-tool-complete", (exitCode, elapsed, summary) => {
98
+ const timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
99
+ const summaryStr = summary ? ` ${p.dim}${summary}${p.reset}` : "";
100
+ if (exitCode === null)
101
+ return `${p.muted}(timed out)${p.reset}`;
102
+ if (exitCode === 0)
103
+ return `${p.success}✓${p.reset}${summaryStr}${timer}`;
104
+ return `${p.error}✗ exit ${exitCode}${p.reset}${summaryStr}${timer}`;
105
+ });
106
+ define("tui:render-tool-group-summary", (count, rendered, allOk, summaries) => {
107
+ const mark = allOk ? `${p.success}✓${p.reset}` : `${p.error}✗${p.reset}`;
108
+ const summaryStr = summaries.length > 0 ? ` ${p.dim}${summaries.join(", ")}${p.reset}` : "";
109
+ const collapsed = count - rendered;
110
+ if (collapsed > 0) {
111
+ return ` ${p.muted}└${p.reset} ${p.dim}+${collapsed} more${p.reset} ${mark}${summaryStr}`;
112
+ }
113
+ return ` ${p.muted}└${p.reset} ${mark}${summaryStr}`;
114
+ });
115
+ define("tui:render-command-output", (line, _kind) => `${p.dim} ${line}${p.reset}`);
116
+ define("tui:render-spinner", (label, frame, elapsed, hint) => {
117
+ const timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
118
+ const hintStr = hint ? ` ${p.dim}${hint}${p.reset}` : "";
119
+ return `${p.accent}${frame} ${label}...${p.reset}${timer}${hintStr}`;
120
+ });
121
+ define("tui:render-user-query", (query, width, modelLabel) => {
122
+ const contentW = width - 4;
123
+ let lines = [];
124
+ for (const raw of query.split("\n")) {
125
+ for (const wrapped of wrapLine(`${p.accent}${raw}${p.reset}`, contentW)) {
126
+ lines.push(wrapped);
127
+ }
128
+ }
129
+ const MAX_QUERY_LINES = 20;
130
+ if (lines.length > MAX_QUERY_LINES) {
131
+ const overflow = lines.length - MAX_QUERY_LINES;
132
+ lines = [...lines.slice(0, MAX_QUERY_LINES), `${p.dim}… ${overflow} more lines${p.reset}`];
133
+ }
134
+ return renderBoxFrame(lines, {
135
+ width,
136
+ style: "rounded",
137
+ borderColor: p.accent,
138
+ title: `${p.accent}${p.bold}❯${p.reset}`,
139
+ titleRight: modelLabel,
140
+ });
141
+ });
77
142
  // Track backend/model info for display on response border
78
143
  let backendInfo = null;
79
144
  bus.on("agent:info", (info) => { backendInfo = info; });
@@ -88,12 +153,16 @@ export default function activate(ctx) {
88
153
  });
89
154
  // ── Event subscriptions ─────────────────────────────────────
90
155
  bus.on("agent:query", (e) => {
156
+ if (!shouldRender())
157
+ return;
91
158
  s.spinnerStartTime = 0;
92
159
  showUserQuery(e.query);
93
160
  startAgentResponse();
94
161
  startThinkingSpinner();
95
162
  });
96
163
  bus.on("agent:thinking-chunk", (e) => {
164
+ if (!shouldRender())
165
+ return;
97
166
  s.thinkingPending = true;
98
167
  if (!s.isThinking) {
99
168
  s.isThinking = true;
@@ -118,6 +187,8 @@ export default function activate(ctx) {
118
187
  }
119
188
  });
120
189
  bus.on("agent:response-chunk", (e) => {
190
+ if (!shouldRender())
191
+ return;
121
192
  const { blocks } = e;
122
193
  // Inject spacing: append \n to text blocks that precede non-text blocks
123
194
  for (let i = 0; i < blocks.length; i++) {
@@ -141,7 +212,7 @@ export default function activate(ctx) {
141
212
  break;
142
213
  case "raw":
143
214
  flushForRaw();
144
- writer.write(block.escape);
215
+ out().write(block.escape);
145
216
  break;
146
217
  }
147
218
  }
@@ -150,17 +221,14 @@ export default function activate(ctx) {
150
221
  let pendingUsage = null;
151
222
  bus.on("agent:usage", (e) => { pendingUsage = e; });
152
223
  bus.on("agent:response-done", () => {
224
+ if (!shouldRender())
225
+ return;
153
226
  s.isThinking = false;
154
227
  if (pendingUsage && s.renderer) {
155
228
  const { prompt_tokens, completion_tokens } = pendingUsage;
156
229
  const maxTokens = backendInfo?.contextWindow ?? 128_000;
157
- // prompt_tokens of the latest call = current context usage
158
- // (it includes the full conversation history)
159
- const ctxK = (prompt_tokens / 1000).toFixed(1);
160
- const maxK = (maxTokens / 1000).toFixed(0);
161
- const pct = Math.min(100, (prompt_tokens / maxTokens) * 100).toFixed(0);
162
230
  s.renderer.writeLine("");
163
- s.renderer.writeLine(`${p.dim}⬆ ${prompt_tokens} ⬇ ${completion_tokens} ctx: ${ctxK}k/${maxK}k (${pct}%)${p.reset}`);
231
+ s.renderer.writeLine(ctx.call("tui:render-usage", prompt_tokens, completion_tokens, maxTokens));
164
232
  drain();
165
233
  pendingUsage = null;
166
234
  }
@@ -173,6 +241,8 @@ export default function activate(ctx) {
173
241
  // Batch groups: kind → { total, rendered, headerShown }
174
242
  let batchGroups = new Map();
175
243
  bus.on("agent:tool-batch", (e) => {
244
+ if (!shouldRender())
245
+ return;
176
246
  fencedTransform.flush();
177
247
  finalizeToolGroup();
178
248
  batchGroups = new Map();
@@ -185,6 +255,8 @@ export default function activate(ctx) {
185
255
  }
186
256
  });
187
257
  bus.on("agent:tool-started", (e) => {
258
+ if (!shouldRender())
259
+ return;
188
260
  fencedTransform.flush();
189
261
  stopCurrentSpinner();
190
262
  s.currentToolKind = e.kind;
@@ -248,6 +320,8 @@ export default function activate(ctx) {
248
320
  }
249
321
  });
250
322
  bus.on("agent:tool-completed", (e) => {
323
+ if (!shouldRender())
324
+ return;
251
325
  s.toolExitCode = e.exitCode;
252
326
  if (e.exitCode !== 0)
253
327
  s.toolGroupAllOk = false;
@@ -265,20 +339,28 @@ export default function activate(ctx) {
265
339
  startThinkingSpinner();
266
340
  }
267
341
  });
268
- bus.on("agent:tool-output-chunk", (e) => writeCommandOutput(e.chunk));
269
- bus.on("agent:tool-output", () => flushCommandOutput());
342
+ bus.on("agent:tool-output-chunk", (e) => { if (shouldRender())
343
+ writeCommandOutput(e.chunk); });
344
+ bus.on("agent:tool-output", () => { if (shouldRender())
345
+ flushCommandOutput(); });
270
346
  bus.on("agent:cancelled", () => {
347
+ if (!shouldRender())
348
+ return;
271
349
  s.isThinking = false;
272
350
  stopCurrentSpinner();
273
351
  showInfo("(cancelled)");
274
352
  endAgentResponse();
275
353
  });
276
354
  bus.on("agent:processing-done", () => {
355
+ if (!shouldRender())
356
+ return;
277
357
  s.isThinking = false;
278
358
  stopCurrentSpinner();
279
359
  endAgentResponse();
280
360
  });
281
361
  bus.on("agent:error", (e) => {
362
+ if (!shouldRender())
363
+ return;
282
364
  stopCurrentSpinner();
283
365
  showCollapsedThinking();
284
366
  if (!s.renderer)
@@ -289,6 +371,8 @@ export default function activate(ctx) {
289
371
  drain();
290
372
  });
291
373
  bus.on("permission:request", (e) => {
374
+ if (!shouldRender())
375
+ return;
292
376
  stopCurrentSpinner();
293
377
  flushCommandOutput();
294
378
  if (s.renderer) {
@@ -309,6 +393,8 @@ export default function activate(ctx) {
309
393
  if (e.key === "\x14")
310
394
  toggleThinkingDisplay(); // Ctrl+T
311
395
  });
396
+ // Interactive tool UI — stop spinner while tool has control
397
+ bus.on("tool:interactive-start", () => { stopCurrentSpinner(); });
312
398
  bus.on("ui:info", (e) => {
313
399
  stopCurrentSpinner();
314
400
  showInfo(e.message);
@@ -318,25 +404,27 @@ export default function activate(ctx) {
318
404
  });
319
405
  bus.on("ui:error", (e) => showError(e.message));
320
406
  bus.on("ui:suggestion", (e) => {
321
- writer.write(`${p.dim}💡 ${e.text}${p.reset}\n`);
407
+ compositor.surface("status").writeLine(`${p.dim}💡 ${e.text}${p.reset}`);
322
408
  });
323
409
  // ── Rendering functions ─────────────────────────────────────
324
410
  function drain() {
325
411
  if (!s.renderer)
326
412
  return;
327
413
  for (const line of s.renderer.drainLines()) {
328
- writer.write(line + "\n");
414
+ out().write(line + "\n");
329
415
  // Track whether we just emitted a blank line (for contentGap dedup).
330
416
  // Lines from the renderer are indented (" "), so a blank line is " " or empty.
331
417
  lastEmittedLineBlank = line.trimEnd() === "" || line.trimEnd().replace(/\x1b\[[^m]*m/g, "").trim() === "";
332
418
  }
333
419
  }
334
420
  function startAgentResponse() {
335
- s.renderer = new MarkdownRenderer(writer.columns);
421
+ s.renderer = new MarkdownRenderer(cappedW());
336
422
  s.hadToolCalls = false;
337
- // Preserve lastContentKind across responses so text→tool gaps work
338
- s.renderer.printTopBorder();
423
+ const border = ctx.call("tui:response-border", "top", cappedW());
424
+ if (border)
425
+ s.renderer.writeLine(border);
339
426
  drain();
427
+ ctx.call("tui:response-start");
340
428
  }
341
429
  /**
342
430
  * Insert an empty line when transitioning between different content kinds
@@ -345,12 +433,15 @@ export default function activate(ctx) {
345
433
  */
346
434
  let lastEmittedLineBlank = false;
347
435
  function contentGap(kind) {
348
- if (s.lastContentKind && s.lastContentKind !== kind) {
349
- if (s.renderer) {
350
- s.renderer.flush();
351
- drain();
436
+ if (s.lastContentKind) {
437
+ const gap = ctx.call("tui:render-content-gap", s.lastContentKind, kind);
438
+ if (gap) {
439
+ if (s.renderer) {
440
+ s.renderer.flush();
441
+ drain();
442
+ }
443
+ out().write(gap);
352
444
  }
353
- writer.write("\n");
354
445
  }
355
446
  s.lastContentKind = kind;
356
447
  }
@@ -366,48 +457,18 @@ export default function activate(ctx) {
366
457
  closeToolLine();
367
458
  stopCurrentSpinner();
368
459
  if (s.renderer) {
460
+ ctx.call("tui:response-end", s.hadToolCalls);
369
461
  s.renderer.flush();
370
- s.renderer.printBottomBorder();
462
+ const border = ctx.call("tui:response-border", "bottom", cappedW());
463
+ if (border)
464
+ s.renderer.writeLine(border);
371
465
  drain();
372
- writer.write("\n");
466
+ out().write("\n");
373
467
  s.renderer = null;
374
468
  }
375
469
  }
376
470
  function showUserQuery(query) {
377
- const boxW = writer.columns;
378
- const contentW = boxW - 4;
379
- let lines = [];
380
- for (const raw of query.split("\n")) {
381
- if (raw.length <= contentW) {
382
- lines.push(`${p.accent}${raw}${p.reset}`);
383
- }
384
- else {
385
- let remaining = raw;
386
- while (remaining.length > contentW) {
387
- let breakAt = remaining.lastIndexOf(" ", contentW);
388
- if (breakAt <= 0)
389
- breakAt = contentW;
390
- lines.push(`${p.accent}${remaining.slice(0, breakAt)}${p.reset}`);
391
- remaining = remaining.slice(breakAt).trimStart();
392
- }
393
- if (remaining)
394
- lines.push(`${p.accent}${remaining}${p.reset}`);
395
- }
396
- }
397
- // Truncate very long queries to keep the response visible
398
- const MAX_QUERY_LINES = 20;
399
- if (lines.length > MAX_QUERY_LINES) {
400
- const overflow = lines.length - MAX_QUERY_LINES;
401
- lines = [
402
- ...lines.slice(0, MAX_QUERY_LINES),
403
- `${p.dim}… ${overflow} more lines${p.reset}`,
404
- ];
405
- }
406
- // Mode-specific border color and title
407
- const borderColor = p.accent;
408
- const title = `${p.accent}${p.bold}❯${p.reset}`;
409
- // Backend/model label on the right (backend/model, highlighted)
410
- const model = backendInfo?.model ?? llmClient?.model;
471
+ const model = backendInfo?.model;
411
472
  const backend = backendInfo?.name;
412
473
  let modelLabel;
413
474
  if (backend && model) {
@@ -419,16 +480,13 @@ export default function activate(ctx) {
419
480
  else if (backend) {
420
481
  modelLabel = `${p.bold}${backend}${p.reset}`;
421
482
  }
422
- const framed = renderBoxFrame(lines, {
423
- width: boxW,
424
- style: "rounded",
425
- borderColor,
426
- title,
427
- titleRight: modelLabel,
428
- });
429
- writer.write("\n");
430
- for (const line of framed) {
431
- writer.write(line + "\n");
483
+ const querySurface = compositor.surface("query");
484
+ const framed = ctx.call("tui:render-user-query", query, querySurface.columns, modelLabel);
485
+ if (framed.length > 0) {
486
+ querySurface.write("\n");
487
+ for (const line of framed) {
488
+ querySurface.writeLine(line);
489
+ }
432
490
  }
433
491
  }
434
492
  function writeAgentText(text) {
@@ -439,7 +497,7 @@ export default function activate(ctx) {
439
497
  s.isThinking = false;
440
498
  if (s.showThinkingText && s.renderer) {
441
499
  s.renderer.flush();
442
- const w = Math.min(80, writer.columns);
500
+ const w = Math.min(80, out().columns);
443
501
  s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
444
502
  drain();
445
503
  }
@@ -478,7 +536,7 @@ export default function activate(ctx) {
478
536
  drain();
479
537
  });
480
538
  function writeCodeBlock(language, code) {
481
- ctx.call("render:code-block", language, code, writer.columns);
539
+ ctx.call("render:code-block", language, code, cappedW());
482
540
  }
483
541
  function flushForRaw() {
484
542
  closeToolLine();
@@ -492,7 +550,7 @@ export default function activate(ctx) {
492
550
  flushForRaw();
493
551
  const escape = encodeImageForTerminal(data);
494
552
  if (escape) {
495
- writer.write(" " + escape + "\n");
553
+ out().write(" " + escape + "\n");
496
554
  }
497
555
  });
498
556
  function writeInlineImage(data) {
@@ -520,7 +578,7 @@ export default function activate(ctx) {
520
578
  function renderDiffBody(diff, filePath, width) {
521
579
  if (diff.isIdentical)
522
580
  return [];
523
- const boxW = Math.min(120, width);
581
+ const boxW = Math.min(120, width - 2); // -2 for writeLine indent
524
582
  const contentW = boxW - 4;
525
583
  const diffLines = renderDiff(diff, {
526
584
  width: contentW,
@@ -616,12 +674,12 @@ export default function activate(ctx) {
616
674
  locations: extra?.locations,
617
675
  rawInput: extra?.rawInput,
618
676
  displayDetail: extra?.displayDetail,
619
- }, writer.columns);
677
+ }, cappedW());
620
678
  if (extra?.groupContinuation && lines.length > 0) {
621
679
  // Swap the colored kind icon for a muted tree connector,
622
680
  // and strip the tool name prefix — show detail only.
623
681
  const detail = extra.displayDetail || extractDetail(extra);
624
- const maxW = Math.max(1, writer.columns - 6);
682
+ const maxW = Math.max(1, cappedW() - 6);
625
683
  const text = detail.length > maxW ? detail.slice(0, maxW - 1) + "…" : detail;
626
684
  lines[0] = detail
627
685
  ? `${p.muted}├${p.reset} ${p.dim}${text}${p.reset}`
@@ -640,7 +698,7 @@ export default function activate(ctx) {
640
698
  s.toolLineOpen = false;
641
699
  }
642
700
  else {
643
- writer.write(` ${batchPrefix}${lines[lines.length - 1]}`);
701
+ out().write(` ${batchPrefix}${lines[lines.length - 1]}`);
644
702
  s.toolLineOpen = true;
645
703
  }
646
704
  }
@@ -653,15 +711,9 @@ export default function activate(ctx) {
653
711
  return;
654
712
  stopCurrentSpinner();
655
713
  const elapsed = s.toolStartTime ? formatElapsed(Date.now() - s.toolStartTime) : "";
656
- const timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
657
- const summary = resultDisplay?.summary ? ` ${p.dim}${resultDisplay.summary}${p.reset}` : "";
658
- const mark = exitCode === null
659
- ? `${p.muted}(timed out)${p.reset}`
660
- : exitCode === 0
661
- ? `${p.success}✓${p.reset}${summary}${timer}`
662
- : `${p.error}✗ exit ${exitCode}${p.reset}${summary}${timer}`;
714
+ const mark = ctx.call("tui:render-tool-complete", exitCode, elapsed, resultDisplay?.summary);
663
715
  if (s.toolLineOpen && s.commandOutputLineCount === 0) {
664
- writer.write(` ${mark}\n`);
716
+ out().write(` ${mark}\n`);
665
717
  s.toolLineOpen = false;
666
718
  }
667
719
  else {
@@ -678,7 +730,7 @@ export default function activate(ctx) {
678
730
  function renderResultBody(body) {
679
731
  if (!s.renderer)
680
732
  return;
681
- const lines = ctx.call("render:result-body", body, writer.columns) ?? [];
733
+ const lines = ctx.call("render:result-body", body, cappedW()) ?? [];
682
734
  for (const line of lines) {
683
735
  s.renderer.writeLine(line);
684
736
  }
@@ -703,8 +755,11 @@ export default function activate(ctx) {
703
755
  s.spinner = createSpinner({ startTime: s.spinnerStartTime });
704
756
  s.spinnerInterval = setInterval(() => {
705
757
  if (s.spinner) {
706
- const line = renderSpinnerLine(s.spinner, s.spinnerLabel, s.spinnerOpts);
707
- writer.write(`\r ${line}\x1b[K`);
758
+ const frame = SPINNER_FRAMES[s.spinner.frame % SPINNER_FRAMES.length];
759
+ s.spinner.frame++;
760
+ const elapsed = formatElapsed(Date.now() - s.spinner.startTime);
761
+ const line = ctx.call("tui:render-spinner", s.spinnerLabel, frame, elapsed, s.spinnerOpts.hint);
762
+ out().write(`\r ${line}\x1b[K`);
708
763
  }
709
764
  }, 80);
710
765
  }
@@ -714,13 +769,13 @@ export default function activate(ctx) {
714
769
  s.spinnerInterval = null;
715
770
  }
716
771
  if (s.spinner) {
717
- writer.write("\r\x1b[2K");
772
+ out().write("\r\x1b[2K");
718
773
  s.spinner = null;
719
774
  }
720
775
  }
721
776
  function closeToolLine() {
722
777
  if (s.toolLineOpen) {
723
- writer.write("\n");
778
+ out().write("\n");
724
779
  s.toolLineOpen = false;
725
780
  }
726
781
  }
@@ -737,20 +792,8 @@ export default function activate(ctx) {
737
792
  closeToolLine();
738
793
  if (!s.renderer)
739
794
  startAgentResponse();
740
- const mark = s.toolGroupAllOk
741
- ? `${p.success}✓${p.reset}`
742
- : `${p.error}✗${p.reset}`;
743
- const summary = s.toolGroupSummaries.length > 0
744
- ? ` ${p.dim}${s.toolGroupSummaries.join(", ")}${p.reset}`
745
- : "";
746
- const collapsed = s.toolGroupCount - s.toolGroupRendered;
747
- if (collapsed > 0) {
748
- s.renderer.writeLine(` ${p.muted}└${p.reset} ${p.dim}+${collapsed} more${p.reset} ${mark}${summary}`);
749
- }
750
- else {
751
- // All items visible — close the tree with └ mark + summary
752
- s.renderer.writeLine(` ${p.muted}└${p.reset} ${mark}${summary}`);
753
- }
795
+ const groupLine = ctx.call("tui:render-tool-group-summary", s.toolGroupCount, s.toolGroupRendered, s.toolGroupAllOk, s.toolGroupSummaries);
796
+ s.renderer.writeLine(groupLine);
754
797
  drain();
755
798
  s.toolGroupKind = undefined;
756
799
  s.toolGroupCount = 0;
@@ -758,6 +801,9 @@ export default function activate(ctx) {
758
801
  s.toolGroupRendered = 0;
759
802
  s.toolGroupSummaries = [];
760
803
  }
804
+ function renderCommandLine(line) {
805
+ return ctx.call("tui:render-command-output", line, s.currentToolKind);
806
+ }
761
807
  function writeCommandOutput(chunk) {
762
808
  if (!s.renderer)
763
809
  return;
@@ -770,7 +816,7 @@ export default function activate(ctx) {
770
816
  s.commandOutputBuffer = lines.pop();
771
817
  for (const line of lines) {
772
818
  if (s.commandOutputLineCount < maxLines) {
773
- s.renderer.writeLine(`${p.dim} ${line}${p.reset}`);
819
+ s.renderer.writeLine(renderCommandLine(line));
774
820
  s.commandOutputLineCount++;
775
821
  }
776
822
  else {
@@ -790,7 +836,7 @@ export default function activate(ctx) {
790
836
  : getSettings().maxCommandOutputLines;
791
837
  if (s.commandOutputBuffer) {
792
838
  if (s.commandOutputLineCount < maxLines) {
793
- s.renderer.writeLine(`${p.dim} ${s.commandOutputBuffer}${p.reset}`);
839
+ s.renderer.writeLine(renderCommandLine(s.commandOutputBuffer));
794
840
  s.commandOutputLineCount++;
795
841
  }
796
842
  else {
@@ -805,14 +851,22 @@ export default function activate(ctx) {
805
851
  const tail = s.commandOverflowLines.slice(-FAIL_OVERFLOW_MAX);
806
852
  const skipped = s.commandOverflowLines.length - tail.length;
807
853
  if (skipped > 0) {
808
- s.renderer.writeLine(`${p.dim} … ${skipped} lines hidden${p.reset}`);
854
+ s.renderer.writeLine(renderCommandLine(`… ${skipped} lines hidden`));
809
855
  }
810
856
  for (const line of tail) {
811
- s.renderer.writeLine(`${p.dim} ${line}${p.reset}`);
857
+ s.renderer.writeLine(renderCommandLine(line));
812
858
  }
813
859
  }
814
860
  else if (s.commandOutputOverflow > 0 && maxLines > 0) {
815
- s.renderer.writeLine(`${p.dim} … ${s.commandOutputOverflow} more lines${p.reset}`);
861
+ // Show last line of output so the user sees the tail (often the most useful part)
862
+ const tail = s.commandOverflowLines[s.commandOverflowLines.length - 1];
863
+ const hidden = tail ? s.commandOutputOverflow - 1 : s.commandOutputOverflow;
864
+ if (hidden > 0) {
865
+ s.renderer.writeLine(renderCommandLine(`… ${hidden} more lines`));
866
+ }
867
+ if (tail) {
868
+ s.renderer.writeLine(renderCommandLine(tail));
869
+ }
816
870
  }
817
871
  s.commandOutputOverflow = 0;
818
872
  s.commandOverflowLines = [];
@@ -829,7 +883,7 @@ export default function activate(ctx) {
829
883
  if (diff.isIdentical)
830
884
  return;
831
885
  contentGap("diff");
832
- const lines = ctx.call("render:result-body", { kind: "diff", diff, filePath }, writer.columns) ?? [];
886
+ const lines = ctx.call("render:result-body", { kind: "diff", diff, filePath }, cappedW()) ?? [];
833
887
  if (!s.renderer)
834
888
  startAgentResponse();
835
889
  for (const line of lines) {
@@ -848,7 +902,7 @@ export default function activate(ctx) {
848
902
  }
849
903
  if (!entry.expandedLines) {
850
904
  const { filePath, diff } = entry;
851
- const boxW = Math.min(120, writer.columns);
905
+ const boxW = Math.min(cappedW() - 2, out().columns - 2); // -2 for writeLine indent
852
906
  const contentW = boxW - 4;
853
907
  const diffLines = renderDiff(diff, {
854
908
  width: contentW,
@@ -865,16 +919,16 @@ export default function activate(ctx) {
865
919
  footer: [` ${p.dim}ctrl+o to collapse${p.reset}`],
866
920
  });
867
921
  }
868
- writer.write("\n");
922
+ out().write("\n");
869
923
  for (const line of entry.expandedLines) {
870
- writer.write(line + "\n");
924
+ out().write(line + "\n");
871
925
  }
872
926
  }
873
927
  function showFileDiffCached(entry) {
874
- const lines = ctx.call("render:result-body", { kind: "diff", diff: entry.diff, filePath: entry.filePath }, writer.columns) ?? [];
875
- writer.write("\n");
928
+ const lines = ctx.call("render:result-body", { kind: "diff", diff: entry.diff, filePath: entry.filePath }, cappedW()) ?? [];
929
+ out().write("\n");
876
930
  for (const line of lines) {
877
- writer.write(line + "\n");
931
+ out().write(line + "\n");
878
932
  }
879
933
  }
880
934
  function toggleThinkingDisplay() {
@@ -906,7 +960,7 @@ export default function activate(ctx) {
906
960
  else {
907
961
  if (s.renderer) {
908
962
  s.renderer.flush();
909
- const w = Math.min(80, writer.columns);
963
+ const w = Math.min(80, out().columns);
910
964
  s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
911
965
  drain();
912
966
  }
@@ -914,9 +968,10 @@ export default function activate(ctx) {
914
968
  }
915
969
  }
916
970
  function showError(message) {
917
- writer.write(`\n${p.error}Error: ${message}${p.reset}\n`);
971
+ const s = compositor.surface("status");
972
+ s.write("\n" + ctx.call("tui:render-error", message) + "\n");
918
973
  }
919
974
  function showInfo(message) {
920
- writer.write(`${p.muted}${message}${p.reset}\n`);
975
+ compositor.surface("status").writeLine(ctx.call("tui:render-info", message));
921
976
  }
922
977
  }