agent-sh 0.15.5 → 0.15.7
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/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/agent/agent-loop.js +2 -5
- package/dist/agent/extensions/rolling-history/index.js +20 -8
- package/dist/agent/extensions/rolling-history/recall.d.ts +2 -2
- package/dist/agent/extensions/rolling-history/recall.js +17 -7
- package/dist/agent/providers/openai-compatible.d.ts +8 -0
- package/dist/agent/providers/openai-compatible.js +9 -2
- package/dist/agent/store.js +6 -1
- package/dist/agent/token-budget.d.ts +2 -1
- package/dist/agent/token-budget.js +6 -1
- package/dist/agent/types.d.ts +4 -1
- package/dist/cli/index.js +1 -1
- package/dist/core/event-bus.d.ts +16 -1
- package/dist/core/event-bus.js +73 -11
- package/dist/core/index.js +18 -0
- package/dist/shell/tui-renderer.js +116 -174
- package/dist/utils/diff-renderer.js +65 -30
- package/dist/utils/executor.js +19 -11
- package/dist/utils/floating-panel.d.ts +1 -0
- package/dist/utils/floating-panel.js +28 -26
- package/dist/utils/markdown.js +56 -44
- package/dist/utils/palette.d.ts +11 -0
- package/dist/utils/palette.js +11 -0
- package/docs/agent.md +13 -11
- package/docs/architecture.md +3 -5
- package/docs/extensions.md +21 -20
- package/docs/library.md +6 -3
- package/docs/troubleshooting.md +2 -2
- package/docs/tui-composition.md +11 -3
- package/docs/usage.md +70 -50
- package/examples/extensions/ashi/src/chat/assistant.ts +6 -4
- package/examples/extensions/ashi/src/compaction.ts +4 -7
- package/examples/extensions/ashi/src/frontend.ts +2 -0
- package/examples/extensions/ashi/src/schema.ts +8 -2
- package/examples/extensions/command-suggest.ts +90 -0
- package/examples/extensions/solarized-theme.ts +11 -0
- package/package.json +5 -5
- package/src/agent/agent-loop.ts +2 -5
- package/src/agent/extensions/rolling-history/index.ts +20 -8
- package/src/agent/extensions/rolling-history/recall.ts +28 -7
- package/src/agent/providers/openai-compatible.ts +19 -4
- package/src/agent/store.ts +5 -1
- package/src/agent/token-budget.ts +10 -1
- package/src/agent/types.ts +4 -1
- package/src/cli/index.ts +1 -1
- package/src/core/event-bus.ts +67 -12
- package/src/core/index.ts +18 -0
- package/src/shell/tui-renderer.ts +131 -207
- package/src/utils/diff-renderer.ts +62 -29
- package/src/utils/executor.ts +17 -14
- package/src/utils/floating-panel.ts +24 -22
- package/src/utils/markdown.ts +49 -40
- package/src/utils/palette.ts +30 -5
package/src/utils/executor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
4
|
import { stripAnsi } from "./ansi.js";
|
|
4
5
|
|
|
5
6
|
// Node reports a missing cwd as `spawn <binary> ENOENT` — disambiguate.
|
|
@@ -106,25 +107,23 @@ export function executeCommand(opts: {
|
|
|
106
107
|
|
|
107
108
|
session.process = child;
|
|
108
109
|
|
|
109
|
-
const
|
|
110
|
-
|
|
110
|
+
const handleText = (raw: string) => {
|
|
111
|
+
if (!raw) return;
|
|
111
112
|
const clean = stripAnsi(raw);
|
|
112
|
-
|
|
113
|
-
// Accumulate cleaned output for the agent
|
|
114
113
|
session.output += clean;
|
|
115
|
-
|
|
116
|
-
// Enforce output cap — truncate from beginning, keep tail
|
|
117
114
|
if (session.output.length > maxOutput) {
|
|
118
115
|
session.output = session.output.slice(-maxOutput);
|
|
119
116
|
session.truncated = true;
|
|
120
117
|
}
|
|
121
|
-
|
|
122
|
-
// Real-time streaming callback
|
|
123
118
|
opts.onOutput?.(raw);
|
|
124
119
|
};
|
|
125
120
|
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
const outDecoder = new StringDecoder("utf-8");
|
|
122
|
+
const errDecoder = new StringDecoder("utf-8");
|
|
123
|
+
child.stdout?.on("data", (d: Buffer) => handleText(outDecoder.write(d)));
|
|
124
|
+
child.stderr?.on("data", (d: Buffer) => handleText(errDecoder.write(d)));
|
|
125
|
+
child.stdout?.on("end", () => handleText(outDecoder.end()));
|
|
126
|
+
child.stderr?.on("end", () => handleText(errDecoder.end()));
|
|
128
127
|
|
|
129
128
|
let cancelKill: (() => void) | undefined;
|
|
130
129
|
const timer = setTimeout(() => {
|
|
@@ -218,8 +217,8 @@ export function executeArgv(opts: {
|
|
|
218
217
|
|
|
219
218
|
session.process = child;
|
|
220
219
|
|
|
221
|
-
const
|
|
222
|
-
|
|
220
|
+
const handleText = (raw: string) => {
|
|
221
|
+
if (!raw) return;
|
|
223
222
|
const clean = stripAnsi(raw);
|
|
224
223
|
session.output += clean;
|
|
225
224
|
if (session.output.length > maxOutput) {
|
|
@@ -229,8 +228,12 @@ export function executeArgv(opts: {
|
|
|
229
228
|
opts.onOutput?.(raw);
|
|
230
229
|
};
|
|
231
230
|
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
const outDecoder = new StringDecoder("utf-8");
|
|
232
|
+
const errDecoder = new StringDecoder("utf-8");
|
|
233
|
+
child.stdout?.on("data", (d: Buffer) => handleText(outDecoder.write(d)));
|
|
234
|
+
child.stderr?.on("data", (d: Buffer) => handleText(errDecoder.write(d)));
|
|
235
|
+
child.stdout?.on("end", () => handleText(outDecoder.end()));
|
|
236
|
+
child.stderr?.on("end", () => handleText(errDecoder.end()));
|
|
234
237
|
|
|
235
238
|
const timer = setTimeout(() => {
|
|
236
239
|
if (!session.done && session.process) {
|
|
@@ -823,6 +823,13 @@ export class FloatingPanel {
|
|
|
823
823
|
this.autocompleteIndex = 0;
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
+
private moveAutocomplete(delta: number): void {
|
|
827
|
+
const n = this.autocompleteItems.length;
|
|
828
|
+
if (n === 0) return;
|
|
829
|
+
this.autocompleteIndex = (this.autocompleteIndex + delta + n) % n;
|
|
830
|
+
this.render();
|
|
831
|
+
}
|
|
832
|
+
|
|
826
833
|
// ── Input handling ──────────────────────────────────────────
|
|
827
834
|
|
|
828
835
|
private handleIntercept(payload: { data: string; consumed: boolean }): { data: string; consumed: boolean } {
|
|
@@ -913,6 +920,16 @@ export class FloatingPanel {
|
|
|
913
920
|
|
|
914
921
|
if (this.handleScroll(data, false)) return;
|
|
915
922
|
|
|
923
|
+
if (data === "\x10" || data === "\x0e") {
|
|
924
|
+
const forward = data === "\x0e";
|
|
925
|
+
if (this.autocompleteActive) {
|
|
926
|
+
this.moveAutocomplete(forward ? 1 : -1);
|
|
927
|
+
} else if (forward ? this.editor.historyForward() : this.editor.historyBack()) {
|
|
928
|
+
this.render();
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
916
933
|
const actions = this.editor.feed(data);
|
|
917
934
|
for (const action of actions) {
|
|
918
935
|
switch (action.action) {
|
|
@@ -924,6 +941,7 @@ export class FloatingPanel {
|
|
|
924
941
|
this.editor.pushHistory(query);
|
|
925
942
|
this.editor.clear();
|
|
926
943
|
this.clearAutocomplete();
|
|
944
|
+
this.userScrolled = false;
|
|
927
945
|
// Phase change is the submit handler's call — sync slash commands
|
|
928
946
|
// (e.g. /model, /help) keep the user in input mode.
|
|
929
947
|
this.handlers.call(`${this.prefix}:submit`, query);
|
|
@@ -945,30 +963,14 @@ export class FloatingPanel {
|
|
|
945
963
|
case "shift+tab":
|
|
946
964
|
this.render();
|
|
947
965
|
break;
|
|
948
|
-
case "arrow-up":
|
|
949
|
-
if (this.autocompleteActive)
|
|
950
|
-
|
|
951
|
-
? this.autocompleteItems.length - 1
|
|
952
|
-
: this.autocompleteIndex - 1;
|
|
953
|
-
this.render();
|
|
954
|
-
} else {
|
|
955
|
-
const hist = this.editor.historyBack();
|
|
956
|
-
if (hist) this.render();
|
|
957
|
-
}
|
|
966
|
+
case "arrow-up":
|
|
967
|
+
if (this.autocompleteActive) this.moveAutocomplete(-1);
|
|
968
|
+
else this.scrollUp(1);
|
|
958
969
|
break;
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
this.autocompleteIndex = this.autocompleteIndex === this.autocompleteItems.length - 1
|
|
963
|
-
? 0
|
|
964
|
-
: this.autocompleteIndex + 1;
|
|
965
|
-
this.render();
|
|
966
|
-
} else {
|
|
967
|
-
const hist = this.editor.historyForward();
|
|
968
|
-
if (hist) this.render();
|
|
969
|
-
}
|
|
970
|
+
case "arrow-down":
|
|
971
|
+
if (this.autocompleteActive) this.moveAutocomplete(1);
|
|
972
|
+
else this.scrollDown(1);
|
|
970
973
|
break;
|
|
971
|
-
}
|
|
972
974
|
case "changed":
|
|
973
975
|
case "delete-empty":
|
|
974
976
|
this.updateAutocomplete();
|
package/src/utils/markdown.ts
CHANGED
|
@@ -79,6 +79,30 @@ export function wrapLine(text: string, maxWidth: number): string[] {
|
|
|
79
79
|
lastVisibleIdx = -1;
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
const hardBreak = (token: string): void => {
|
|
83
|
+
let remaining = token;
|
|
84
|
+
while (remaining.length > 0) {
|
|
85
|
+
let fitLen = 0, fitWidth = 0;
|
|
86
|
+
for (const ch of remaining) {
|
|
87
|
+
const cw = charWidth(ch.codePointAt(0) ?? 0);
|
|
88
|
+
if (fitWidth + cw > maxWidth - lineWidth) break;
|
|
89
|
+
fitWidth += cw;
|
|
90
|
+
fitLen += ch.length;
|
|
91
|
+
}
|
|
92
|
+
if (fitLen === 0) {
|
|
93
|
+
// Force one char on an empty line so an over-wide char can't loop forever.
|
|
94
|
+
if (lineWidth > 0) { commit(); continue; }
|
|
95
|
+
fitLen = remaining[0]?.length ?? 1;
|
|
96
|
+
}
|
|
97
|
+
const chunk = remaining.slice(0, fitLen);
|
|
98
|
+
remaining = remaining.slice(fitLen);
|
|
99
|
+
lineTokens.push(chunk);
|
|
100
|
+
lineWidth += visibleLen(chunk);
|
|
101
|
+
lastVisibleIdx = lineTokens.length - 1;
|
|
102
|
+
if (remaining.length > 0) commit();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
82
106
|
for (const seg of segments) {
|
|
83
107
|
if (seg.startsWith("\x1b[")) {
|
|
84
108
|
lineTokens.push(seg);
|
|
@@ -103,23 +127,7 @@ export function wrapLine(text: string, maxWidth: number): string[] {
|
|
|
103
127
|
|
|
104
128
|
if (lineWidth === 0) {
|
|
105
129
|
// Token longer than the entire line — hard-break by char width.
|
|
106
|
-
|
|
107
|
-
while (remaining.length > 0) {
|
|
108
|
-
let fitLen = 0, fitWidth = 0;
|
|
109
|
-
for (const ch of remaining) {
|
|
110
|
-
const cw = charWidth(ch.codePointAt(0) ?? 0);
|
|
111
|
-
if (fitWidth + cw > maxWidth) break;
|
|
112
|
-
fitWidth += cw;
|
|
113
|
-
fitLen += ch.length;
|
|
114
|
-
}
|
|
115
|
-
if (fitLen === 0) fitLen = remaining[0]?.length ?? 1;
|
|
116
|
-
const chunk = remaining.slice(0, fitLen);
|
|
117
|
-
remaining = remaining.slice(fitLen);
|
|
118
|
-
lineTokens.push(chunk);
|
|
119
|
-
lineWidth += visibleLen(chunk);
|
|
120
|
-
lastVisibleIdx = lineTokens.length - 1;
|
|
121
|
-
if (remaining.length > 0) commit();
|
|
122
|
-
}
|
|
130
|
+
hardBreak(token);
|
|
123
131
|
continue;
|
|
124
132
|
}
|
|
125
133
|
|
|
@@ -147,9 +155,13 @@ export function wrapLine(text: string, maxWidth: number): string[] {
|
|
|
147
155
|
lineTokens.push(t);
|
|
148
156
|
lineWidth += visibleLen(t);
|
|
149
157
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
if (lineWidth + tokenWidth <= maxWidth) {
|
|
159
|
+
lineTokens.push(token);
|
|
160
|
+
lineWidth += tokenWidth;
|
|
161
|
+
lastVisibleIdx = lineTokens.length - 1;
|
|
162
|
+
} else {
|
|
163
|
+
hardBreak(token);
|
|
164
|
+
}
|
|
153
165
|
}
|
|
154
166
|
}
|
|
155
167
|
|
|
@@ -340,27 +352,24 @@ export class MarkdownRenderer {
|
|
|
340
352
|
private renderLine(line: string): string {
|
|
341
353
|
if (line.trim() === "") return "";
|
|
342
354
|
|
|
343
|
-
// Headings
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const h4 = line.match(/^#{4,} (.+)/);
|
|
354
|
-
if (h4) return `${p.bold}${h4[1]}${p.reset}`;
|
|
355
|
+
// Headings — H3+ keep the `###` marker; H1/H2 don't
|
|
356
|
+
const heading = line.match(/^(#{1,6}) (.+)/);
|
|
357
|
+
if (heading) {
|
|
358
|
+
const level = heading[1]!.length;
|
|
359
|
+
const text = heading[2]!;
|
|
360
|
+
if (level === 1) return `${p.bold}${p.underline}${p.mdHeading}${text}${p.reset}`;
|
|
361
|
+
if (level === 2) return `${p.bold}${p.mdHeading}${text}${p.reset}`;
|
|
362
|
+
return `${p.bold}${p.mdHeading}${"#".repeat(level)} ${text}${p.reset}`;
|
|
363
|
+
}
|
|
355
364
|
|
|
356
|
-
// Horizontal rule
|
|
365
|
+
// Horizontal rule
|
|
357
366
|
if (/^(-{3,}|_{3,}|\*{3,})\s*$/.test(line)) {
|
|
358
|
-
return ""
|
|
367
|
+
return `${p.mdHr}${"─".repeat(Math.min(this.contentWidth, 80))}${p.reset}`;
|
|
359
368
|
}
|
|
360
369
|
|
|
361
370
|
// Blockquote
|
|
362
371
|
const bq = line.match(/^>\s?(.*)/);
|
|
363
|
-
if (bq) return `${p.
|
|
372
|
+
if (bq) return `${p.mdQuoteBorder}│${p.reset} ${p.mdQuote}${p.italic}${this.renderInline(bq[1] || "")}${p.reset}`;
|
|
364
373
|
|
|
365
374
|
// Task list (checkbox items) — must come before generic unordered list
|
|
366
375
|
const task = line.match(/^(\s*)[*\-+]\s+\[([ xX])\]\s+(.*)/);
|
|
@@ -377,14 +386,14 @@ export class MarkdownRenderer {
|
|
|
377
386
|
const ul = line.match(/^(\s*)[*\-+]\s+(.*)/);
|
|
378
387
|
if (ul) {
|
|
379
388
|
const indent = ul[1] || "";
|
|
380
|
-
return `${indent} ${p.
|
|
389
|
+
return `${indent} ${p.mdListBullet}-${p.reset} ${this.renderInline(ul[2] || "")}`;
|
|
381
390
|
}
|
|
382
391
|
|
|
383
392
|
// Ordered list
|
|
384
393
|
const ol = line.match(/^(\s*)(\d+)[.)]\s+(.*)/);
|
|
385
394
|
if (ol) {
|
|
386
395
|
const indent = ol[1] || "";
|
|
387
|
-
return `${indent} ${p.
|
|
396
|
+
return `${indent} ${p.mdListBullet}${ol[2]}.${p.reset} ${this.renderInline(ol[3] || "")}`;
|
|
388
397
|
}
|
|
389
398
|
|
|
390
399
|
return this.renderInline(line);
|
|
@@ -394,10 +403,10 @@ export class MarkdownRenderer {
|
|
|
394
403
|
// Links first — later subs inject `\x1b[…m` whose `[` would be eaten here.
|
|
395
404
|
text = text.replace(
|
|
396
405
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
397
|
-
`$1
|
|
406
|
+
`${p.mdLink}${p.underline}$1${p.reset} ${p.mdLinkUrl}($2)${p.reset}`
|
|
398
407
|
);
|
|
399
408
|
// Inline code
|
|
400
|
-
text = text.replace(/`([^`]+)`/g, `${p.
|
|
409
|
+
text = text.replace(/`([^`]+)`/g, `${p.mdCode}$1${p.reset}`);
|
|
401
410
|
// Bold + italic
|
|
402
411
|
text = text.replace(/\*\*\*(.+?)\*\*\*/g, `${p.bold}${p.italic}$1${p.reset}`);
|
|
403
412
|
// Bold
|
|
@@ -407,7 +416,7 @@ export class MarkdownRenderer {
|
|
|
407
416
|
text = text.replace(/\*(.+?)\*/g, `${p.italic}$1${p.reset}`);
|
|
408
417
|
text = text.replace(/(?<!\w)_(.+?)_(?!\w)/g, `${p.italic}$1${p.reset}`);
|
|
409
418
|
// Strikethrough
|
|
410
|
-
text = text.replace(/~~(.+?)~~/g, `${p.
|
|
419
|
+
text = text.replace(/~~(.+?)~~/g, `${p.strikethrough}$1${p.reset}`);
|
|
411
420
|
return text;
|
|
412
421
|
}
|
|
413
422
|
|
package/src/utils/palette.ts
CHANGED
|
@@ -29,7 +29,20 @@ export interface ColorPalette {
|
|
|
29
29
|
dim: string;
|
|
30
30
|
italic: string;
|
|
31
31
|
underline: string;
|
|
32
|
+
strikethrough: string;
|
|
32
33
|
reset: string;
|
|
34
|
+
|
|
35
|
+
// ── Markdown element colors ───────────────────────────────
|
|
36
|
+
mdHeading: string; // headings (all levels)
|
|
37
|
+
mdLink: string; // link text
|
|
38
|
+
mdLinkUrl: string; // link URL
|
|
39
|
+
mdCode: string; // inline code span
|
|
40
|
+
mdCodeBlock: string; // fenced code fallback (no highlight)
|
|
41
|
+
mdCodeBlockBorder: string; // code fence / language label
|
|
42
|
+
mdQuote: string; // blockquote text
|
|
43
|
+
mdQuoteBorder: string; // blockquote left bar
|
|
44
|
+
mdHr: string; // horizontal rule
|
|
45
|
+
mdListBullet: string; // list bullet / ordinal
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
const defaultPalette: ColorPalette = {
|
|
@@ -45,11 +58,23 @@ const defaultPalette: ColorPalette = {
|
|
|
45
58
|
errorBgEmph: "\x1b[48;2;124;50;64m",
|
|
46
59
|
diffText: "\x1b[97m", // bright white — readable on the red/green tints
|
|
47
60
|
|
|
48
|
-
bold:
|
|
49
|
-
dim:
|
|
50
|
-
italic:
|
|
51
|
-
underline:
|
|
52
|
-
|
|
61
|
+
bold: "\x1b[1m",
|
|
62
|
+
dim: "\x1b[2m",
|
|
63
|
+
italic: "\x1b[3m",
|
|
64
|
+
underline: "\x1b[4m",
|
|
65
|
+
strikethrough: "\x1b[9m",
|
|
66
|
+
reset: "\x1b[0m",
|
|
67
|
+
|
|
68
|
+
mdHeading: "\x1b[38;2;240;198;116m", // #f0c674 gold
|
|
69
|
+
mdLink: "\x1b[38;2;129;162;190m", // #81a2be blue
|
|
70
|
+
mdLinkUrl: "\x1b[38;2;102;102;102m", // #666666 dim gray
|
|
71
|
+
mdCode: "\x1b[38;2;138;190;183m", // #8abeb7 teal
|
|
72
|
+
mdCodeBlock: "\x1b[38;2;181;189;104m", // #b5bd68 green
|
|
73
|
+
mdCodeBlockBorder: "\x1b[38;2;128;128;128m", // #808080 gray
|
|
74
|
+
mdQuote: "\x1b[38;2;128;128;128m", // #808080 gray
|
|
75
|
+
mdQuoteBorder: "\x1b[38;2;128;128;128m", // #808080 gray
|
|
76
|
+
mdHr: "\x1b[38;2;128;128;128m", // #808080 gray
|
|
77
|
+
mdListBullet: "\x1b[38;2;138;190;183m", // #8abeb7 teal
|
|
53
78
|
};
|
|
54
79
|
|
|
55
80
|
/** Active palette — import and use directly in components. */
|