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.
- package/README.md +27 -43
- package/dist/agent/agent-loop.d.ts +69 -6
- package/dist/agent/agent-loop.js +954 -153
- package/dist/agent/conversation-state.d.ts +74 -21
- package/dist/agent/conversation-state.js +361 -150
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +88 -6
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +37 -5
- package/dist/agent/system-prompt.js +100 -67
- package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
- package/dist/{token-budget.js → agent/token-budget.js} +15 -20
- package/dist/agent/tool-protocol.d.ts +105 -0
- package/dist/agent/tool-protocol.js +551 -0
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +22 -2
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.d.ts +7 -7
- package/dist/core.js +99 -196
- package/dist/event-bus.d.ts +85 -2
- package/dist/event-bus.js +20 -1
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +143 -19
- package/dist/extensions/agent-backend.d.ts +14 -0
- package/dist/extensions/agent-backend.js +188 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +24 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +30 -10
- package/dist/extensions/tui-renderer.js +117 -113
- package/dist/index.js +39 -26
- package/dist/settings.d.ts +40 -3
- package/dist/settings.js +57 -10
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
- package/dist/{input-handler.js → shell/input-handler.js} +111 -85
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +39 -8
- package/dist/types.d.ts +61 -10
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +67 -0
- package/dist/utils/compositor.js +116 -0
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +312 -146
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +31 -10
- package/dist/utils/handler-registry.js +58 -16
- package/dist/utils/line-editor.d.ts +33 -3
- package/dist/utils/line-editor.js +221 -44
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +5 -1
- package/dist/utils/terminal-buffer.js +18 -2
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +574 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +164 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -51
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +98 -112
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +565 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +260 -0
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +32 -53
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +335 -0
- package/package.json +44 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- 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,
|
|
72
|
-
const writer = new StdoutWriter();
|
|
70
|
+
const { bus, define, compositor } = ctx;
|
|
73
71
|
const s = createRenderState();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
bus.on("shell:
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
444
|
+
s.renderer = new MarkdownRenderer(cappedW());
|
|
418
445
|
s.hadToolCalls = false;
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
+
const border = ctx.call("tui:response-border", "bottom", cappedW());
|
|
486
|
+
if (border)
|
|
487
|
+
s.renderer.writeLine(border);
|
|
457
488
|
drain();
|
|
458
|
-
|
|
489
|
+
out().write("\n");
|
|
459
490
|
s.renderer = null;
|
|
460
491
|
}
|
|
461
492
|
}
|
|
462
493
|
function showUserQuery(query) {
|
|
463
|
-
const model = backendInfo?.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
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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,
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
621
|
-
fp = fp.slice(
|
|
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(
|
|
638
|
-
fp = fp.slice(
|
|
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
|
-
},
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
done(env);
|
|
50
52
|
});
|
|
51
53
|
child.on("error", () => {
|
|
52
|
-
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
done({});
|
|
53
56
|
});
|
|
54
|
-
setTimeout(() => {
|
|
57
|
+
const timer = setTimeout(() => {
|
|
55
58
|
child.kill("SIGTERM");
|
|
56
|
-
|
|
59
|
+
done({});
|
|
57
60
|
}, 5000);
|
|
58
61
|
}
|
|
59
62
|
catch {
|
|
60
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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 ?? "
|
|
268
|
-
const model = info?.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" +
|