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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/dist/agent/agent-loop.js +2 -5
  4. package/dist/agent/extensions/rolling-history/index.js +20 -8
  5. package/dist/agent/extensions/rolling-history/recall.d.ts +2 -2
  6. package/dist/agent/extensions/rolling-history/recall.js +17 -7
  7. package/dist/agent/providers/openai-compatible.d.ts +8 -0
  8. package/dist/agent/providers/openai-compatible.js +9 -2
  9. package/dist/agent/store.js +6 -1
  10. package/dist/agent/token-budget.d.ts +2 -1
  11. package/dist/agent/token-budget.js +6 -1
  12. package/dist/agent/types.d.ts +4 -1
  13. package/dist/cli/index.js +1 -1
  14. package/dist/core/event-bus.d.ts +16 -1
  15. package/dist/core/event-bus.js +73 -11
  16. package/dist/core/index.js +18 -0
  17. package/dist/shell/tui-renderer.js +116 -174
  18. package/dist/utils/diff-renderer.js +65 -30
  19. package/dist/utils/executor.js +19 -11
  20. package/dist/utils/floating-panel.d.ts +1 -0
  21. package/dist/utils/floating-panel.js +28 -26
  22. package/dist/utils/markdown.js +56 -44
  23. package/dist/utils/palette.d.ts +11 -0
  24. package/dist/utils/palette.js +11 -0
  25. package/docs/agent.md +13 -11
  26. package/docs/architecture.md +3 -5
  27. package/docs/extensions.md +21 -20
  28. package/docs/library.md +6 -3
  29. package/docs/troubleshooting.md +2 -2
  30. package/docs/tui-composition.md +11 -3
  31. package/docs/usage.md +70 -50
  32. package/examples/extensions/ashi/src/chat/assistant.ts +6 -4
  33. package/examples/extensions/ashi/src/compaction.ts +4 -7
  34. package/examples/extensions/ashi/src/frontend.ts +2 -0
  35. package/examples/extensions/ashi/src/schema.ts +8 -2
  36. package/examples/extensions/command-suggest.ts +90 -0
  37. package/examples/extensions/solarized-theme.ts +11 -0
  38. package/package.json +5 -5
  39. package/src/agent/agent-loop.ts +2 -5
  40. package/src/agent/extensions/rolling-history/index.ts +20 -8
  41. package/src/agent/extensions/rolling-history/recall.ts +28 -7
  42. package/src/agent/providers/openai-compatible.ts +19 -4
  43. package/src/agent/store.ts +5 -1
  44. package/src/agent/token-budget.ts +10 -1
  45. package/src/agent/types.ts +4 -1
  46. package/src/cli/index.ts +1 -1
  47. package/src/core/event-bus.ts +67 -12
  48. package/src/core/index.ts +18 -0
  49. package/src/shell/tui-renderer.ts +131 -207
  50. package/src/utils/diff-renderer.ts +62 -29
  51. package/src/utils/executor.ts +17 -14
  52. package/src/utils/floating-panel.ts +24 -22
  53. package/src/utils/markdown.ts +49 -40
  54. package/src/utils/palette.ts +30 -5
@@ -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 handleData = (data: Buffer) => {
110
- const raw = data.toString("utf-8");
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
- child.stdout?.on("data", handleData);
127
- child.stderr?.on("data", handleData);
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 handleData = (data: Buffer) => {
222
- const raw = data.toString("utf-8");
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
- child.stdout?.on("data", handleData);
233
- child.stderr?.on("data", handleData);
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
- this.autocompleteIndex = this.autocompleteIndex === 0
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
- case "arrow-down": {
961
- if (this.autocompleteActive) {
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();
@@ -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
- let remaining = token;
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
- lineTokens.push(token);
151
- lineWidth += tokenWidth;
152
- lastVisibleIdx = lineTokens.length - 1;
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 h1 = line.match(/^# (.+)/);
345
- if (h1) return `${p.bold}${p.warning}${h1[1]}${p.reset}`;
346
-
347
- const h2 = line.match(/^## (.+)/);
348
- if (h2) return `${p.bold}${p.accent}${h2[1]}${p.reset}`;
349
-
350
- const h3 = line.match(/^### (.+)/);
351
- if (h3) return `${p.bold}${h3[1]}${p.reset}`;
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 — subtle short separator, not full-width
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.muted}│${p.reset} ${p.dim}${p.italic}${this.renderInline(bq[1] || "")}${p.reset}`;
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.accent}*${p.reset} ${this.renderInline(ul[2] || "")}`;
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.accent}${ol[2]}.${p.reset} ${this.renderInline(ol[3] || "")}`;
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 ${p.muted}${p.underline}($2)${p.reset}`
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.accent}$1${p.reset}`);
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.dim}$1${p.reset}`);
419
+ text = text.replace(/~~(.+?)~~/g, `${p.strikethrough}$1${p.reset}`);
411
420
  return text;
412
421
  }
413
422
 
@@ -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: "\x1b[1m",
49
- dim: "\x1b[2m",
50
- italic: "\x1b[3m",
51
- underline: "\x1b[4m",
52
- reset: "\x1b[0m",
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. */