agent-sh 0.8.0 → 0.10.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 (106) hide show
  1. package/README.md +27 -43
  2. package/dist/agent/agent-loop.d.ts +69 -6
  3. package/dist/agent/agent-loop.js +954 -153
  4. package/dist/agent/conversation-state.d.ts +74 -21
  5. package/dist/agent/conversation-state.js +361 -150
  6. package/dist/agent/history-file.d.ts +13 -4
  7. package/dist/agent/history-file.js +110 -36
  8. package/dist/agent/nuclear-form.d.ts +28 -3
  9. package/dist/agent/nuclear-form.js +88 -6
  10. package/dist/agent/skills.d.ts +2 -4
  11. package/dist/agent/skills.js +10 -4
  12. package/dist/agent/subagent.d.ts +23 -0
  13. package/dist/agent/subagent.js +53 -11
  14. package/dist/agent/system-prompt.d.ts +37 -5
  15. package/dist/agent/system-prompt.js +100 -67
  16. package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
  17. package/dist/{token-budget.js → agent/token-budget.js} +15 -20
  18. package/dist/agent/tool-protocol.d.ts +105 -0
  19. package/dist/agent/tool-protocol.js +551 -0
  20. package/dist/agent/tools/bash.js +3 -3
  21. package/dist/agent/tools/edit-file.js +9 -6
  22. package/dist/agent/tools/glob.js +4 -2
  23. package/dist/agent/tools/grep.js +27 -3
  24. package/dist/agent/tools/ls.js +5 -6
  25. package/dist/agent/types.d.ts +22 -2
  26. package/dist/context-manager.d.ts +17 -0
  27. package/dist/context-manager.js +37 -4
  28. package/dist/core.d.ts +7 -7
  29. package/dist/core.js +99 -196
  30. package/dist/event-bus.d.ts +85 -2
  31. package/dist/event-bus.js +20 -1
  32. package/dist/executor.d.ts +4 -3
  33. package/dist/executor.js +18 -15
  34. package/dist/extension-loader.d.ts +5 -0
  35. package/dist/extension-loader.js +143 -19
  36. package/dist/extensions/agent-backend.d.ts +14 -0
  37. package/dist/extensions/agent-backend.js +188 -0
  38. package/dist/extensions/command-suggest.d.ts +3 -3
  39. package/dist/extensions/command-suggest.js +4 -3
  40. package/dist/extensions/index.d.ts +19 -0
  41. package/dist/extensions/index.js +24 -0
  42. package/dist/extensions/slash-commands.d.ts +1 -1
  43. package/dist/extensions/slash-commands.js +30 -10
  44. package/dist/extensions/tui-renderer.js +117 -113
  45. package/dist/index.js +39 -26
  46. package/dist/settings.d.ts +40 -3
  47. package/dist/settings.js +57 -10
  48. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
  49. package/dist/{input-handler.js → shell/input-handler.js} +111 -85
  50. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  51. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  52. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  53. package/dist/{shell.js → shell/shell.js} +39 -8
  54. package/dist/types.d.ts +61 -10
  55. package/dist/utils/ansi.d.ts +5 -0
  56. package/dist/utils/ansi.js +1 -1
  57. package/dist/utils/compositor.d.ts +67 -0
  58. package/dist/utils/compositor.js +116 -0
  59. package/dist/utils/diff-renderer.d.ts +9 -0
  60. package/dist/utils/diff-renderer.js +312 -146
  61. package/dist/utils/diff.d.ts +21 -2
  62. package/dist/utils/diff.js +165 -89
  63. package/dist/utils/floating-panel.d.ts +2 -0
  64. package/dist/utils/floating-panel.js +30 -14
  65. package/dist/utils/handler-registry.d.ts +31 -10
  66. package/dist/utils/handler-registry.js +58 -16
  67. package/dist/utils/line-editor.d.ts +33 -3
  68. package/dist/utils/line-editor.js +221 -44
  69. package/dist/utils/markdown.d.ts +1 -0
  70. package/dist/utils/markdown.js +1 -1
  71. package/dist/utils/message-utils.d.ts +35 -0
  72. package/dist/utils/message-utils.js +75 -0
  73. package/dist/utils/terminal-buffer.d.ts +5 -1
  74. package/dist/utils/terminal-buffer.js +18 -2
  75. package/dist/utils/tool-display.d.ts +1 -1
  76. package/dist/utils/tool-display.js +4 -4
  77. package/dist/utils/tool-interactive.d.ts +12 -0
  78. package/dist/utils/tool-interactive.js +53 -0
  79. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  80. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  81. package/examples/extensions/ash-acp-bridge/src/index.ts +574 -0
  82. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  83. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  84. package/examples/extensions/ash-mcp-bridge/index.ts +164 -0
  85. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  86. package/examples/extensions/claude-code-bridge/index.ts +198 -51
  87. package/examples/extensions/claude-code-bridge/package.json +1 -0
  88. package/examples/extensions/interactive-prompts.ts +98 -112
  89. package/examples/extensions/overlay-agent.ts +84 -38
  90. package/examples/extensions/peer-mesh.ts +565 -0
  91. package/examples/extensions/pi-bridge/index.ts +2 -2
  92. package/examples/extensions/questionnaire.ts +260 -0
  93. package/examples/extensions/subagents.ts +19 -4
  94. package/examples/extensions/terminal-buffer.ts +32 -53
  95. package/examples/extensions/tmux-pane.ts +307 -0
  96. package/examples/extensions/user-shell.ts +136 -0
  97. package/examples/extensions/web-access.ts +335 -0
  98. package/package.json +44 -2
  99. package/dist/agent/tools/display.d.ts +0 -13
  100. package/dist/agent/tools/display.js +0 -70
  101. package/dist/agent/tools/user-shell.d.ts +0 -13
  102. package/dist/agent/tools/user-shell.js +0 -87
  103. package/dist/extensions/overlay-agent.d.ts +0 -14
  104. package/dist/extensions/overlay-agent.js +0 -147
  105. package/dist/extensions/terminal-buffer.d.ts +0 -14
  106. package/dist/extensions/terminal-buffer.js +0 -125
@@ -11,14 +11,14 @@
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
+ import { DEFAULT_CONTEXT_WINDOW } from "../agent/token-budget.js";
15
16
  import { createFencedBlockTransform } from "../utils/stream-transform.js";
16
17
  import { palette as p } from "../utils/palette.js";
17
18
  import { renderToolCall, createSpinner, formatElapsed, SPINNER_FRAMES, } from "../utils/tool-display.js";
18
19
  import { renderDiff } from "../utils/diff-renderer.js";
19
20
  import { renderBoxFrame } from "../utils/box-frame.js";
20
21
  import { getSettings } from "../settings.js";
21
- import { StdoutWriter } from "../utils/output-writer.js";
22
22
  /** Encode a PNG buffer as a terminal inline image escape sequence. */
23
23
  function encodeImageForTerminal(data) {
24
24
  const b64 = data.toString("base64");
@@ -64,16 +64,18 @@ function createRenderState() {
64
64
  isThinking: false,
65
65
  showThinkingText: false,
66
66
  thinkingPending: false,
67
- lastTruncatedDiff: null,
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
+ /** Track the shell's cwd so path shortening is relative to where the user actually is. */
73
+ let shellCwd = process.cwd();
74
+ bus.on("shell:cwd-change", (e) => { shellCwd = e.cwd; });
75
+ /** Shorthand — get the current agent surface. */
76
+ function out() { return compositor.surface("agent"); }
77
+ /** Capped width for borders, tool lines, and content — keeps everything aligned. */
78
+ function cappedW() { return Math.min(MAX_CONTENT_WIDTH + 2, out().columns); }
77
79
  // Gate: other extensions (e.g. overlay) can advise this to suppress
78
80
  // TUI rendering of agent output while they own the display.
79
81
  define("tui:should-render-agent", () => true);
@@ -81,6 +83,9 @@ export default function activate(ctx) {
81
83
  // ── Advisable rendering handlers ───────────────────────────────
82
84
  // Extensions advise these to customize how the TUI renders content.
83
85
  // Each handler receives data and returns rendered strings.
86
+ define("tui:response-border", (position, width) => {
87
+ return `${p.dim}${p.accent}${"─".repeat(width)}${p.reset}`;
88
+ });
84
89
  define("tui:response-start", () => { });
85
90
  define("tui:response-end", (_hadToolCalls) => { });
86
91
  define("tui:render-info", (message) => `${p.muted}${message}${p.reset}`);
@@ -210,7 +215,7 @@ export default function activate(ctx) {
210
215
  break;
211
216
  case "raw":
212
217
  flushForRaw();
213
- writer.write(block.escape);
218
+ out().write(block.escape);
214
219
  break;
215
220
  }
216
221
  }
@@ -224,7 +229,7 @@ export default function activate(ctx) {
224
229
  s.isThinking = false;
225
230
  if (pendingUsage && s.renderer) {
226
231
  const { prompt_tokens, completion_tokens } = pendingUsage;
227
- const maxTokens = backendInfo?.contextWindow ?? 128_000;
232
+ const maxTokens = backendInfo?.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
228
233
  s.renderer.writeLine("");
229
234
  s.renderer.writeLine(ctx.call("tui:render-usage", prompt_tokens, completion_tokens, maxTokens));
230
235
  drain();
@@ -377,20 +382,42 @@ export default function activate(ctx) {
377
382
  s.renderer.flush();
378
383
  drain();
379
384
  }
385
+ // Diff rendering is handled in the async pipe below so it can yield
386
+ // to the event loop between hunks (keeping the spinner responsive).
387
+ });
388
+ // Async pipe: render diffs via the tui:render-diff handler (extensions can
389
+ // advise to customize). Runs after the sync `on` handler above (which
390
+ // flushes state) and before shell.ts's pipe (which pauses stdout).
391
+ bus.onPipeAsync("permission:request", async (e) => {
392
+ if (!shouldRender())
393
+ return e;
380
394
  if (e.kind === "file-write" && e.metadata?.diff) {
381
395
  showCollapsedThinking();
382
- showFileDiff(e.title, e.metadata.diff);
396
+ const lines = ctx.call("tui:render-diff", e.title, e.metadata.diff, cappedW());
397
+ if (lines.length > 0) {
398
+ if (!s.renderer)
399
+ startAgentResponse();
400
+ contentGap("diff");
401
+ for (const line of lines)
402
+ s.renderer.writeLine(line);
403
+ drain();
404
+ }
405
+ // The diff box IS the visual representation of the upcoming tool call.
406
+ // Mark lastContentKind as "tool" so the tool call line that follows
407
+ // doesn't inject an extra gap between the diff box and the checkmark.
408
+ s.lastContentKind = "tool";
383
409
  }
384
410
  // Don't endAgentResponse() here — permission requests that aren't
385
411
  // file-write diffs are handled inline (auto-approved or by extensions).
386
412
  // Closing the response prematurely causes double separator borders.
413
+ return e;
387
414
  });
388
415
  bus.on("input:keypress", (e) => {
389
- if (e.key === "\x0f")
390
- expandLastDiff(); // Ctrl+O
391
416
  if (e.key === "\x14")
392
417
  toggleThinkingDisplay(); // Ctrl+T
393
418
  });
419
+ // Interactive tool UI — stop spinner while tool has control
420
+ bus.on("tool:interactive-start", () => { stopCurrentSpinner(); });
394
421
  bus.on("ui:info", (e) => {
395
422
  stopCurrentSpinner();
396
423
  showInfo(e.message);
@@ -400,23 +427,25 @@ export default function activate(ctx) {
400
427
  });
401
428
  bus.on("ui:error", (e) => showError(e.message));
402
429
  bus.on("ui:suggestion", (e) => {
403
- writer.write(`${p.dim}💡 ${e.text}${p.reset}\n`);
430
+ compositor.surface("status").writeLine(`${p.dim}💡 ${e.text}${p.reset}`);
404
431
  });
405
432
  // ── Rendering functions ─────────────────────────────────────
406
433
  function drain() {
407
434
  if (!s.renderer)
408
435
  return;
409
436
  for (const line of s.renderer.drainLines()) {
410
- writer.write(line + "\n");
437
+ out().write(line + "\n");
411
438
  // Track whether we just emitted a blank line (for contentGap dedup).
412
439
  // Lines from the renderer are indented (" "), so a blank line is " " or empty.
413
440
  lastEmittedLineBlank = line.trimEnd() === "" || line.trimEnd().replace(/\x1b\[[^m]*m/g, "").trim() === "";
414
441
  }
415
442
  }
416
443
  function startAgentResponse() {
417
- s.renderer = new MarkdownRenderer(writer.columns);
444
+ s.renderer = new MarkdownRenderer(cappedW());
418
445
  s.hadToolCalls = false;
419
- s.renderer.printTopBorder();
446
+ const border = ctx.call("tui:response-border", "top", cappedW());
447
+ if (border)
448
+ s.renderer.writeLine(border);
420
449
  drain();
421
450
  ctx.call("tui:response-start");
422
451
  }
@@ -434,7 +463,7 @@ export default function activate(ctx) {
434
463
  s.renderer.flush();
435
464
  drain();
436
465
  }
437
- writer.write(gap);
466
+ out().write(gap);
438
467
  }
439
468
  }
440
469
  s.lastContentKind = kind;
@@ -453,14 +482,16 @@ export default function activate(ctx) {
453
482
  if (s.renderer) {
454
483
  ctx.call("tui:response-end", s.hadToolCalls);
455
484
  s.renderer.flush();
456
- s.renderer.printBottomBorder();
485
+ const border = ctx.call("tui:response-border", "bottom", cappedW());
486
+ if (border)
487
+ s.renderer.writeLine(border);
457
488
  drain();
458
- writer.write("\n");
489
+ out().write("\n");
459
490
  s.renderer = null;
460
491
  }
461
492
  }
462
493
  function showUserQuery(query) {
463
- const model = backendInfo?.model ?? llmClient?.model;
494
+ const model = backendInfo?.model;
464
495
  const backend = backendInfo?.name;
465
496
  let modelLabel;
466
497
  if (backend && model) {
@@ -472,10 +503,13 @@ export default function activate(ctx) {
472
503
  else if (backend) {
473
504
  modelLabel = `${p.bold}${backend}${p.reset}`;
474
505
  }
475
- const framed = ctx.call("tui:render-user-query", query, writer.columns, modelLabel);
476
- writer.write("\n");
477
- for (const line of framed) {
478
- writer.write(line + "\n");
506
+ const querySurface = compositor.surface("query");
507
+ const framed = ctx.call("tui:render-user-query", query, querySurface.columns, modelLabel);
508
+ if (framed.length > 0) {
509
+ querySurface.write("\n");
510
+ for (const line of framed) {
511
+ querySurface.writeLine(line);
512
+ }
479
513
  }
480
514
  }
481
515
  function writeAgentText(text) {
@@ -486,7 +520,7 @@ export default function activate(ctx) {
486
520
  s.isThinking = false;
487
521
  if (s.showThinkingText && s.renderer) {
488
522
  s.renderer.flush();
489
- const w = Math.min(80, writer.columns);
523
+ const w = Math.min(80, out().columns);
490
524
  s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
491
525
  drain();
492
526
  }
@@ -507,9 +541,23 @@ export default function activate(ctx) {
507
541
  }
508
542
  let highlighted;
509
543
  try {
510
- highlighted = language
511
- ? highlight(code, { language })
512
- : highlight(code); // auto-detect
544
+ // highlight.js warns to console.error for unsupported languages (elisp, org, etc).
545
+ // Suppress so it doesn't leak into the terminal.
546
+ const origError = console.error;
547
+ console.error = (...args) => {
548
+ const msg = args.join(" ");
549
+ if (msg.includes("Could not find the language"))
550
+ return;
551
+ origError.apply(console, args);
552
+ };
553
+ try {
554
+ highlighted = language
555
+ ? highlight(code, { language })
556
+ : highlight(code); // auto-detect
557
+ }
558
+ finally {
559
+ console.error = origError;
560
+ }
513
561
  }
514
562
  catch {
515
563
  highlighted = code;
@@ -525,7 +573,7 @@ export default function activate(ctx) {
525
573
  drain();
526
574
  });
527
575
  function writeCodeBlock(language, code) {
528
- ctx.call("render:code-block", language, code, writer.columns);
576
+ ctx.call("render:code-block", language, code, cappedW());
529
577
  }
530
578
  function flushForRaw() {
531
579
  closeToolLine();
@@ -539,7 +587,7 @@ export default function activate(ctx) {
539
587
  flushForRaw();
540
588
  const escape = encodeImageForTerminal(data);
541
589
  if (escape) {
542
- writer.write(" " + escape + "\n");
590
+ out().write(" " + escape + "\n");
543
591
  }
544
592
  });
545
593
  function writeInlineImage(data) {
@@ -563,11 +611,22 @@ export default function activate(ctx) {
563
611
  }
564
612
  return [];
565
613
  });
614
+ /**
615
+ * Default renderer for standalone diffs (e.g. permission prompts).
616
+ * Extensions can advise this to customize diff rendering:
617
+ *
618
+ * ctx.advise("tui:render-diff", (next, filePath, diff, width) => {
619
+ * return myCustomDiffBox(filePath, diff, width);
620
+ * });
621
+ */
622
+ define("tui:render-diff", (filePath, diff, width) => {
623
+ return renderDiffBody(diff, filePath, width);
624
+ });
566
625
  /** Render a diff as framed box lines (pure — no TUI state side effects). */
567
626
  function renderDiffBody(diff, filePath, width) {
568
627
  if (diff.isIdentical)
569
628
  return [];
570
- const boxW = Math.min(120, width);
629
+ const boxW = Math.min(120, width - 2); // -2 for writeLine indent
571
630
  const contentW = boxW - 4;
572
631
  const diffLines = renderDiff(diff, {
573
632
  width: contentW,
@@ -575,18 +634,8 @@ export default function activate(ctx) {
575
634
  maxLines: getSettings().diffMaxLines,
576
635
  trueColor: true,
577
636
  });
578
- const lastLine = diffLines[diffLines.length - 1] ?? "";
579
- const isTruncated = lastLine.includes("… ");
580
- if (isTruncated) {
581
- s.lastTruncatedDiff = { filePath, diff, expanded: false };
582
- }
583
- else {
584
- s.lastTruncatedDiff = null;
585
- }
586
637
  const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
587
- const footer = isTruncated
588
- ? [` ${p.dim}ctrl+o to expand${p.reset}`]
589
- : undefined;
638
+ const footer = undefined;
590
639
  return renderBoxFrame(body, {
591
640
  width: boxW,
592
641
  style: "rounded",
@@ -614,11 +663,10 @@ export default function activate(ctx) {
614
663
  function extractDetail(extra) {
615
664
  if (extra.locations && extra.locations.length > 0) {
616
665
  const loc = extra.locations[0];
617
- const cwd = process.cwd();
618
666
  const home = process.env.HOME;
619
667
  let fp = loc.path;
620
- if (fp.startsWith(cwd + "/"))
621
- fp = fp.slice(cwd.length + 1);
668
+ if (fp.startsWith(shellCwd + "/"))
669
+ fp = fp.slice(shellCwd.length + 1);
622
670
  else if (home && fp.startsWith(home + "/"))
623
671
  fp = "~/" + fp.slice(home.length + 1);
624
672
  return loc.line ? `${fp}:${loc.line}` : fp;
@@ -631,11 +679,10 @@ export default function activate(ctx) {
631
679
  if (typeof raw.pattern === "string")
632
680
  return raw.pattern;
633
681
  if (typeof raw.path === "string") {
634
- const cwd = process.cwd();
635
682
  const home = process.env.HOME;
636
683
  let fp = raw.path;
637
- if (fp.startsWith(cwd + "/"))
638
- fp = fp.slice(cwd.length + 1);
684
+ if (fp.startsWith(shellCwd + "/"))
685
+ fp = fp.slice(shellCwd.length + 1);
639
686
  else if (home && fp.startsWith(home + "/"))
640
687
  fp = "~/" + fp.slice(home.length + 1);
641
688
  return fp;
@@ -663,12 +710,12 @@ export default function activate(ctx) {
663
710
  locations: extra?.locations,
664
711
  rawInput: extra?.rawInput,
665
712
  displayDetail: extra?.displayDetail,
666
- }, writer.columns);
713
+ }, cappedW(), shellCwd);
667
714
  if (extra?.groupContinuation && lines.length > 0) {
668
715
  // Swap the colored kind icon for a muted tree connector,
669
716
  // and strip the tool name prefix — show detail only.
670
717
  const detail = extra.displayDetail || extractDetail(extra);
671
- const maxW = Math.max(1, writer.columns - 6);
718
+ const maxW = Math.max(1, cappedW() - 6);
672
719
  const text = detail.length > maxW ? detail.slice(0, maxW - 1) + "…" : detail;
673
720
  lines[0] = detail
674
721
  ? `${p.muted}├${p.reset} ${p.dim}${text}${p.reset}`
@@ -687,7 +734,7 @@ export default function activate(ctx) {
687
734
  s.toolLineOpen = false;
688
735
  }
689
736
  else {
690
- writer.write(` ${batchPrefix}${lines[lines.length - 1]}`);
737
+ out().write(` ${batchPrefix}${lines[lines.length - 1]}`);
691
738
  s.toolLineOpen = true;
692
739
  }
693
740
  }
@@ -702,7 +749,7 @@ export default function activate(ctx) {
702
749
  const elapsed = s.toolStartTime ? formatElapsed(Date.now() - s.toolStartTime) : "";
703
750
  const mark = ctx.call("tui:render-tool-complete", exitCode, elapsed, resultDisplay?.summary);
704
751
  if (s.toolLineOpen && s.commandOutputLineCount === 0) {
705
- writer.write(` ${mark}\n`);
752
+ out().write(` ${mark}\n`);
706
753
  s.toolLineOpen = false;
707
754
  }
708
755
  else {
@@ -719,7 +766,7 @@ export default function activate(ctx) {
719
766
  function renderResultBody(body) {
720
767
  if (!s.renderer)
721
768
  return;
722
- const lines = ctx.call("render:result-body", body, writer.columns) ?? [];
769
+ const lines = ctx.call("render:result-body", body, cappedW()) ?? [];
723
770
  for (const line of lines) {
724
771
  s.renderer.writeLine(line);
725
772
  }
@@ -748,7 +795,7 @@ export default function activate(ctx) {
748
795
  s.spinner.frame++;
749
796
  const elapsed = formatElapsed(Date.now() - s.spinner.startTime);
750
797
  const line = ctx.call("tui:render-spinner", s.spinnerLabel, frame, elapsed, s.spinnerOpts.hint);
751
- writer.write(`\r ${line}\x1b[K`);
798
+ out().write(`\r ${line}\x1b[K`);
752
799
  }
753
800
  }, 80);
754
801
  }
@@ -758,13 +805,13 @@ export default function activate(ctx) {
758
805
  s.spinnerInterval = null;
759
806
  }
760
807
  if (s.spinner) {
761
- writer.write("\r\x1b[2K");
808
+ out().write("\r\x1b[2K");
762
809
  s.spinner = null;
763
810
  }
764
811
  }
765
812
  function closeToolLine() {
766
813
  if (s.toolLineOpen) {
767
- writer.write("\n");
814
+ out().write("\n");
768
815
  s.toolLineOpen = false;
769
816
  }
770
817
  }
@@ -847,7 +894,15 @@ export default function activate(ctx) {
847
894
  }
848
895
  }
849
896
  else if (s.commandOutputOverflow > 0 && maxLines > 0) {
850
- s.renderer.writeLine(renderCommandLine(`… ${s.commandOutputOverflow} more lines`));
897
+ // Show last line of output so the user sees the tail (often the most useful part)
898
+ const tail = s.commandOverflowLines[s.commandOverflowLines.length - 1];
899
+ const hidden = tail ? s.commandOutputOverflow - 1 : s.commandOutputOverflow;
900
+ if (hidden > 0) {
901
+ s.renderer.writeLine(renderCommandLine(`… ${hidden} more lines`));
902
+ }
903
+ if (tail) {
904
+ s.renderer.writeLine(renderCommandLine(tail));
905
+ }
851
906
  }
852
907
  s.commandOutputOverflow = 0;
853
908
  s.commandOverflowLines = [];
@@ -860,58 +915,6 @@ export default function activate(ctx) {
860
915
  : `${p.success}+${diff.added}${p.reset} ${p.error}-${diff.removed}${p.reset}`;
861
916
  return `${p.dim}${filePath}${p.reset} ${stats}`;
862
917
  }
863
- function showFileDiff(filePath, diff) {
864
- if (diff.isIdentical)
865
- return;
866
- contentGap("diff");
867
- const lines = ctx.call("render:result-body", { kind: "diff", diff, filePath }, writer.columns) ?? [];
868
- if (!s.renderer)
869
- startAgentResponse();
870
- for (const line of lines) {
871
- s.renderer.writeLine(line);
872
- }
873
- drain();
874
- }
875
- function expandLastDiff() {
876
- if (!s.lastTruncatedDiff)
877
- return;
878
- const entry = s.lastTruncatedDiff;
879
- entry.expanded = !entry.expanded;
880
- if (!entry.expanded) {
881
- showFileDiffCached(entry);
882
- return;
883
- }
884
- if (!entry.expandedLines) {
885
- const { filePath, diff } = entry;
886
- const boxW = Math.min(120, writer.columns);
887
- const contentW = boxW - 4;
888
- const diffLines = renderDiff(diff, {
889
- width: contentW,
890
- filePath,
891
- maxLines: 500,
892
- trueColor: true,
893
- });
894
- const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
895
- entry.expandedLines = renderBoxFrame(body, {
896
- width: boxW,
897
- style: "rounded",
898
- borderColor: p.dim,
899
- title: diffTitle(filePath, diff),
900
- footer: [` ${p.dim}ctrl+o to collapse${p.reset}`],
901
- });
902
- }
903
- writer.write("\n");
904
- for (const line of entry.expandedLines) {
905
- writer.write(line + "\n");
906
- }
907
- }
908
- function showFileDiffCached(entry) {
909
- const lines = ctx.call("render:result-body", { kind: "diff", diff: entry.diff, filePath: entry.filePath }, writer.columns) ?? [];
910
- writer.write("\n");
911
- for (const line of lines) {
912
- writer.write(line + "\n");
913
- }
914
- }
915
918
  function toggleThinkingDisplay() {
916
919
  s.showThinkingText = !s.showThinkingText;
917
920
  if (s.spinner) {
@@ -941,7 +944,7 @@ export default function activate(ctx) {
941
944
  else {
942
945
  if (s.renderer) {
943
946
  s.renderer.flush();
944
- const w = Math.min(80, writer.columns);
947
+ const w = Math.min(80, out().columns);
945
948
  s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
946
949
  drain();
947
950
  }
@@ -949,9 +952,10 @@ export default function activate(ctx) {
949
952
  }
950
953
  }
951
954
  function showError(message) {
952
- writer.write("\n" + ctx.call("tui:render-error", message) + "\n");
955
+ const s = compositor.surface("status");
956
+ s.write("\n" + ctx.call("tui:render-error", message) + "\n");
953
957
  }
954
958
  function showInfo(message) {
955
- writer.write(ctx.call("tui:render-info", message) + "\n");
959
+ compositor.surface("status").writeLine(ctx.call("tui:render-info", message));
956
960
  }
957
961
  }
package/dist/index.js CHANGED
@@ -1,16 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import * as path from "node:path";
4
- import { Shell } from "./shell.js";
4
+ import { Shell } from "./shell/shell.js";
5
5
  import { createCore } from "./core.js";
6
6
  import { palette as p } from "./utils/palette.js";
7
- import tuiRenderer from "./extensions/tui-renderer.js";
8
- import slashCommands from "./extensions/slash-commands.js";
9
- import fileAutocomplete from "./extensions/file-autocomplete.js";
10
- import shellRecall from "./extensions/shell-recall.js";
11
- import commandSuggest from "./extensions/command-suggest.js";
12
- import terminalBuffer from "./extensions/terminal-buffer.js";
13
- import overlayAgent from "./extensions/overlay-agent.js";
7
+ import { loadBuiltinExtensions } from "./extensions/index.js";
14
8
  import { loadExtensions } from "./extension-loader.js";
15
9
  import { getSettings } from "./settings.js";
16
10
  import { discoverSkills } from "./agent/skills.js";
@@ -21,6 +15,13 @@ import { discoverSkills } from "./agent/skills.js";
21
15
  */
22
16
  async function captureShellEnvAsync(shell) {
23
17
  return new Promise((resolve) => {
18
+ let settled = false;
19
+ const done = (result) => {
20
+ if (settled)
21
+ return;
22
+ settled = true;
23
+ resolve(result);
24
+ };
24
25
  try {
25
26
  const shellName = path.basename(shell);
26
27
  const isZsh = shellName.includes("zsh");
@@ -36,8 +37,9 @@ async function captureShellEnvAsync(shell) {
36
37
  output += data.toString("utf-8");
37
38
  });
38
39
  child.on("close", (code) => {
40
+ clearTimeout(timer);
39
41
  if (code !== 0 || !output) {
40
- resolve({});
42
+ done({});
41
43
  return;
42
44
  }
43
45
  const env = {};
@@ -46,18 +48,19 @@ async function captureShellEnvAsync(shell) {
46
48
  if (eq > 0)
47
49
  env[entry.slice(0, eq)] = entry.slice(eq + 1);
48
50
  }
49
- resolve(env);
51
+ done(env);
50
52
  });
51
53
  child.on("error", () => {
52
- resolve({});
54
+ clearTimeout(timer);
55
+ done({});
53
56
  });
54
- setTimeout(() => {
57
+ const timer = setTimeout(() => {
55
58
  child.kill("SIGTERM");
56
- resolve({});
59
+ done({});
57
60
  }, 5000);
58
61
  }
59
62
  catch {
60
- resolve({});
63
+ done({});
61
64
  }
62
65
  });
63
66
  }
@@ -160,6 +163,13 @@ async function main() {
160
163
  const shellEnv = await captureShellEnvAsync(shellPath);
161
164
  if (Object.keys(shellEnv).length > 0) {
162
165
  Object.assign(baseEnv, mergeShellEnv(baseEnv, shellEnv));
166
+ // Expose captured env vars to process.env so extensions can read them.
167
+ // Only add vars not already present to avoid clobbering runtime state.
168
+ for (const [k, v] of Object.entries(baseEnv)) {
169
+ if (process.env[k] === undefined) {
170
+ process.env[k] = v;
171
+ }
172
+ }
163
173
  if (process.env.DEBUG) {
164
174
  console.error('[agent-sh] Shell environment captured');
165
175
  }
@@ -195,6 +205,7 @@ async function main() {
195
205
  await new Promise(resolve => setTimeout(resolve, 100));
196
206
  const shell = new Shell({
197
207
  bus,
208
+ handlers: core.handlers,
198
209
  cols,
199
210
  rows,
200
211
  shell: config.shell || process.env.SHELL || "/bin/bash",
@@ -203,9 +214,6 @@ async function main() {
203
214
  if (agentInfo) {
204
215
  return { info: `${p.dim}${agentInfo.name}${agentInfo.model ? ` (${agentInfo.model})` : ""}${p.reset}` };
205
216
  }
206
- if (core.llmClient) {
207
- return { info: `${p.dim}agent-sh (${core.llmClient.model})${p.reset}` };
208
- }
209
217
  return { info: "" };
210
218
  },
211
219
  });
@@ -229,13 +237,8 @@ async function main() {
229
237
  console.error('[agent-sh] Setting up extensions...');
230
238
  }
231
239
  const extCtx = core.extensionContext({ quit: cleanup });
232
- tuiRenderer(extCtx);
233
- slashCommands(extCtx);
234
- fileAutocomplete(extCtx);
235
- shellRecall(extCtx);
236
- commandSuggest(extCtx);
237
- terminalBuffer(extCtx);
238
- overlayAgent(extCtx);
240
+ // Load built-in extensions (individually disableable via settings.disabledBuiltins)
241
+ await loadBuiltinExtensions(extCtx, getSettings().disabledBuiltins);
239
242
  // Load user extensions (may register alternative agent backends)
240
243
  if (process.env.DEBUG) {
241
244
  console.error('[agent-sh] Loading extensions...');
@@ -251,6 +254,9 @@ async function main() {
251
254
  if (process.env.DEBUG) {
252
255
  console.error('[agent-sh] Extensions loaded');
253
256
  }
257
+ // Tell deferred-init listeners (agent-backend) that the provider
258
+ // registry is now complete.
259
+ core.bus.emit("core:extensions-loaded", {});
254
260
  // ── Discover skills ───────────────────────────────────────────
255
261
  const skills = discoverSkills(process.cwd());
256
262
  // ── Activate agent backend ────────────────────────────────────
@@ -264,8 +270,8 @@ async function main() {
264
270
  const bannerW = Math.min(termW, 60);
265
271
  const productName = `${p.accent}${p.bold}agent-sh${p.reset}`;
266
272
  const info = agentInfo;
267
- const backendName = info?.name ?? "agent-sh";
268
- const model = info?.model ?? core.llmClient?.model;
273
+ const backendName = info?.name ?? "ash";
274
+ const model = info?.model;
269
275
  const provider = info?.provider;
270
276
  const modelValue = model
271
277
  ? provider ? `${model} [${provider}]` : model
@@ -287,6 +293,13 @@ async function main() {
287
293
  sections += `\n ${p.dim}${s.name}${p.reset}`;
288
294
  }
289
295
  }
296
+ const extSections = bus.emitPipe("banner:collect", { sections: [] }).sections;
297
+ for (const sec of extSections) {
298
+ sections += `\n\n ${p.muted}${sec.label}:${p.reset}`;
299
+ for (const item of sec.items) {
300
+ sections += `\n ${p.dim}${item}${p.reset}`;
301
+ }
302
+ }
290
303
  const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}>/help${p.muted} for commands${p.reset}`;
291
304
  const borderLine = `${p.muted}${"─".repeat(bannerW)}${p.reset}`;
292
305
  process.stdout.write("\n" + borderLine + "\n" +