agent-sh 0.9.0 → 0.10.1

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 (88) hide show
  1. package/README.md +25 -30
  2. package/dist/agent/agent-loop.d.ts +43 -6
  3. package/dist/agent/agent-loop.js +817 -157
  4. package/dist/agent/conversation-state.d.ts +72 -21
  5. package/dist/agent/conversation-state.js +364 -151
  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 +84 -3
  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 +34 -1
  15. package/dist/agent/system-prompt.js +96 -47
  16. package/dist/agent/token-budget.d.ts +10 -13
  17. package/dist/agent/token-budget.js +6 -46
  18. package/dist/agent/tool-protocol.d.ts +23 -1
  19. package/dist/agent/tool-protocol.js +169 -4
  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 +1 -2
  26. package/dist/context-manager.d.ts +16 -19
  27. package/dist/context-manager.js +48 -152
  28. package/dist/core.js +27 -6
  29. package/dist/event-bus.d.ts +59 -3
  30. package/dist/executor.d.ts +4 -3
  31. package/dist/executor.js +18 -15
  32. package/dist/extension-loader.js +75 -17
  33. package/dist/extensions/agent-backend.d.ts +8 -7
  34. package/dist/extensions/agent-backend.js +72 -50
  35. package/dist/extensions/index.js +0 -2
  36. package/dist/extensions/slash-commands.js +14 -9
  37. package/dist/extensions/tui-renderer.js +67 -80
  38. package/dist/index.js +25 -6
  39. package/dist/settings.d.ts +39 -16
  40. package/dist/settings.js +51 -11
  41. package/dist/shell/input-handler.d.ts +2 -1
  42. package/dist/shell/input-handler.js +84 -76
  43. package/dist/shell/shell.js +19 -2
  44. package/dist/types.d.ts +15 -0
  45. package/dist/utils/ansi.d.ts +7 -0
  46. package/dist/utils/ansi.js +69 -8
  47. package/dist/utils/box-frame.js +8 -2
  48. package/dist/utils/compositor.d.ts +5 -0
  49. package/dist/utils/compositor.js +31 -3
  50. package/dist/utils/diff-renderer.d.ts +9 -0
  51. package/dist/utils/diff-renderer.js +221 -143
  52. package/dist/utils/diff.d.ts +21 -2
  53. package/dist/utils/diff.js +165 -89
  54. package/dist/utils/handler-registry.d.ts +5 -0
  55. package/dist/utils/handler-registry.js +6 -0
  56. package/dist/utils/line-editor.d.ts +11 -1
  57. package/dist/utils/line-editor.js +44 -5
  58. package/dist/utils/markdown.js +23 -8
  59. package/dist/utils/package-version.d.ts +1 -0
  60. package/dist/utils/package-version.js +10 -0
  61. package/dist/utils/shell-output-spill.d.ts +2 -0
  62. package/dist/utils/shell-output-spill.js +81 -0
  63. package/dist/utils/tool-display.d.ts +1 -1
  64. package/dist/utils/tool-display.js +4 -4
  65. package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
  66. package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
  67. package/examples/extensions/claude-code-bridge/README.md +14 -0
  68. package/examples/extensions/claude-code-bridge/index.ts +204 -145
  69. package/examples/extensions/claude-code-bridge/package.json +1 -0
  70. package/examples/extensions/interactive-prompts.ts +39 -25
  71. package/examples/extensions/overlay-agent.ts +3 -3
  72. package/examples/extensions/peer-mesh.ts +115 -0
  73. package/examples/extensions/pi-bridge/README.md +16 -0
  74. package/examples/extensions/pi-bridge/index.ts +9 -155
  75. package/examples/extensions/questionnaire.ts +16 -5
  76. package/examples/extensions/subagents.ts +19 -4
  77. package/examples/extensions/terminal-buffer.ts +163 -0
  78. package/examples/extensions/user-shell.ts +136 -0
  79. package/examples/extensions/web-access.ts +8 -0
  80. package/package.json +36 -2
  81. package/dist/agent/tools/display.d.ts +0 -13
  82. package/dist/agent/tools/display.js +0 -70
  83. package/dist/agent/tools/user-shell.d.ts +0 -13
  84. package/dist/agent/tools/user-shell.js +0 -87
  85. package/dist/extensions/shell-recall.d.ts +0 -9
  86. package/dist/extensions/shell-recall.js +0 -8
  87. package/dist/extensions/terminal-buffer.d.ts +0 -14
  88. package/dist/extensions/terminal-buffer.js +0 -134
@@ -1,9 +1,85 @@
1
1
  /**
2
- * Lightweight LCS-based line diff for file modification previews.
2
+ * Line-level diff computation, powered by the `diff` npm package.
3
+ *
4
+ * Exposes a unified `DiffResult` interface consumed by the diff renderer.
5
+ * Three entry points cover the main use cases:
6
+ *
7
+ * computeDiff — full-file diff (write_file, or when edit region can't be located)
8
+ * computeEditDiff — edit_file: locates the edit region, builds the new file, full diff
9
+ * computeInputDiff — fast preview: diffs only old_text vs new_text, no file I/O
3
10
  */
11
+ import * as Diff from "diff";
12
+ // ── Helpers ──────────────────────────────────────────────────────────
13
+ /**
14
+ * Convert a `diff` library Change[] into our DiffLine[], tracking real
15
+ * old/new line numbers.
16
+ */
17
+ function changesToDiffLines(changes) {
18
+ const result = [];
19
+ let oldNo = 0;
20
+ let newNo = 0;
21
+ for (const change of changes) {
22
+ const lines = change.value.replace(/\n$/, "").split("\n");
23
+ for (const text of lines) {
24
+ if (change.added) {
25
+ newNo++;
26
+ result.push({ type: "added", oldNo: null, newNo, text });
27
+ }
28
+ else if (change.removed) {
29
+ oldNo++;
30
+ result.push({ type: "removed", oldNo, newNo: null, text });
31
+ }
32
+ else {
33
+ oldNo++;
34
+ newNo++;
35
+ result.push({ type: "context", oldNo, newNo, text });
36
+ }
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ /**
42
+ * Group raw DiffLines into hunks with `context` lines of surrounding context.
43
+ */
44
+ function groupHunks(lines, ctx) {
45
+ const changeIdx = [];
46
+ for (let i = 0; i < lines.length; i++)
47
+ if (lines[i].type !== "context")
48
+ changeIdx.push(i);
49
+ if (changeIdx.length === 0)
50
+ return [];
51
+ const hunks = [];
52
+ let start = Math.max(0, changeIdx[0] - ctx);
53
+ let end = Math.min(lines.length - 1, changeIdx[0] + ctx);
54
+ for (let k = 1; k < changeIdx.length; k++) {
55
+ const ns = Math.max(0, changeIdx[k] - ctx);
56
+ const ne = Math.min(lines.length - 1, changeIdx[k] + ctx);
57
+ if (ns <= end + 1) {
58
+ end = ne;
59
+ }
60
+ else {
61
+ hunks.push({ lines: lines.slice(start, end + 1) });
62
+ start = ns;
63
+ end = ne;
64
+ }
65
+ }
66
+ hunks.push({ lines: lines.slice(start, end + 1) });
67
+ return hunks;
68
+ }
69
+ function countChanges(lines) {
70
+ let added = 0;
71
+ let removed = 0;
72
+ for (const l of lines) {
73
+ if (l.type === "added")
74
+ added++;
75
+ else if (l.type === "removed")
76
+ removed++;
77
+ }
78
+ return { added, removed };
79
+ }
80
+ // ── Public API ───────────────────────────────────────────────────────
4
81
  /**
5
82
  * Compute a line-level diff between old and new file content.
6
- * Returns grouped hunks with 3 lines of context around each change.
7
83
  */
8
84
  export function computeDiff(oldText, newText) {
9
85
  // New file — everything is an addition
@@ -28,37 +104,11 @@ export function computeDiff(oldText, newText) {
28
104
  }
29
105
  // Identical — nothing to show
30
106
  if (oldText === newText) {
31
- return {
32
- hunks: [],
33
- added: 0,
34
- removed: 0,
35
- isIdentical: true,
36
- isNewFile: false,
37
- };
38
- }
39
- // Build LCS table and backtrack to produce diff lines
40
- const a = oldText.split("\n");
41
- const b = newText.split("\n");
42
- // Bail out if LCS table would be too large (avoids OOM / hang)
43
- if (a.length * b.length > 10_000_000) {
44
- return {
45
- hunks: [],
46
- added: b.length,
47
- removed: a.length,
48
- isIdentical: false,
49
- isNewFile: false,
50
- };
51
- }
52
- const dp = buildLcs(a, b);
53
- const raw = backtrack(dp, a, b);
54
- let added = 0;
55
- let removed = 0;
56
- for (const l of raw) {
57
- if (l.type === "added")
58
- added++;
59
- else if (l.type === "removed")
60
- removed++;
107
+ return { hunks: [], added: 0, removed: 0, isIdentical: true, isNewFile: false };
61
108
  }
109
+ const changes = Diff.diffLines(oldText, newText);
110
+ const raw = changesToDiffLines(changes);
111
+ const { added, removed } = countChanges(raw);
62
112
  return {
63
113
  hunks: groupHunks(raw, 3),
64
114
  added,
@@ -67,66 +117,92 @@ export function computeDiff(oldText, newText) {
67
117
  isNewFile: false,
68
118
  };
69
119
  }
70
- function buildLcs(a, b) {
71
- const m = a.length;
72
- const n = b.length;
73
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
74
- for (let i = 1; i <= m; i++)
75
- for (let j = 1; j <= n; j++)
76
- dp[i][j] =
77
- a[i - 1] === b[j - 1]
78
- ? dp[i - 1][j - 1] + 1
79
- : Math.max(dp[i - 1][j], dp[i][j - 1]);
80
- return dp;
81
- }
82
- function backtrack(dp, a, b) {
83
- const result = [];
84
- let i = a.length;
85
- let j = b.length;
86
- while (i > 0 || j > 0) {
87
- if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
88
- result.unshift({ type: "context", oldNo: i, newNo: j, text: a[i - 1] });
89
- i--;
90
- j--;
91
- }
92
- else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
93
- result.unshift({ type: "added", oldNo: null, newNo: j, text: b[j - 1] });
94
- j--;
95
- }
96
- else {
97
- result.unshift({
98
- type: "removed",
99
- oldNo: i,
100
- newNo: null,
101
- text: a[i - 1],
102
- });
103
- i--;
120
+ /**
121
+ * Compute a diff for an edit operation where we know the old/new text.
122
+ * Locates the edit region(s) in the file, constructs the full new file,
123
+ * then diffs the whole thing so line numbers are file-relative.
124
+ */
125
+ export function computeEditDiff(oldFileText, editOld, editNew, replaceAll = false) {
126
+ const a = oldFileText.split("\n");
127
+ const editOldLines = editOld.split("\n");
128
+ const editNewLines = editNew.split("\n");
129
+ // Find all occurrences of editOld in the file
130
+ const regions = [];
131
+ if (replaceAll) {
132
+ let i = 0;
133
+ while (i <= a.length - editOldLines.length) {
134
+ let match = true;
135
+ for (let k = 0; k < editOldLines.length; k++) {
136
+ if (a[i + k] !== editOldLines[k]) {
137
+ match = false;
138
+ break;
139
+ }
140
+ }
141
+ if (match) {
142
+ regions.push({ start: i, end: i + editOldLines.length });
143
+ i += editOldLines.length;
144
+ }
145
+ else {
146
+ i++;
147
+ }
104
148
  }
105
149
  }
106
- return result;
107
- }
108
- function groupHunks(lines, ctx) {
109
- const changeIdx = [];
110
- for (let i = 0; i < lines.length; i++)
111
- if (lines[i].type !== "context")
112
- changeIdx.push(i);
113
- if (changeIdx.length === 0)
114
- return [];
115
- const hunks = [];
116
- let start = Math.max(0, changeIdx[0] - ctx);
117
- let end = Math.min(lines.length - 1, changeIdx[0] + ctx);
118
- for (let k = 1; k < changeIdx.length; k++) {
119
- const ns = Math.max(0, changeIdx[k] - ctx);
120
- const ne = Math.min(lines.length - 1, changeIdx[k] + ctx);
121
- if (ns <= end + 1) {
122
- end = ne;
150
+ else {
151
+ for (let i = 0; i <= a.length - editOldLines.length; i++) {
152
+ let match = true;
153
+ for (let k = 0; k < editOldLines.length; k++) {
154
+ if (a[i + k] !== editOldLines[k]) {
155
+ match = false;
156
+ break;
157
+ }
158
+ }
159
+ if (match) {
160
+ regions.push({ start: i, end: i + editOldLines.length });
161
+ break;
162
+ }
123
163
  }
124
- else {
125
- hunks.push({ lines: lines.slice(start, end + 1) });
126
- start = ns;
127
- end = ne;
164
+ }
165
+ // Build the full new file
166
+ let newFile;
167
+ if (replaceAll && regions.length > 0) {
168
+ const parts = [];
169
+ let last = 0;
170
+ for (const r of regions) {
171
+ parts.push(...a.slice(last, r.start));
172
+ parts.push(...editNewLines);
173
+ last = r.end;
128
174
  }
175
+ parts.push(...a.slice(last));
176
+ newFile = parts;
129
177
  }
130
- hunks.push({ lines: lines.slice(start, end + 1) });
131
- return hunks;
178
+ else if (regions.length === 1) {
179
+ const r = regions[0];
180
+ newFile = [...a.slice(0, r.start), ...editNewLines, ...a.slice(r.end)];
181
+ }
182
+ else {
183
+ // Couldn't locate edit — fall back to string replace + full diff
184
+ const newContent = replaceAll
185
+ ? oldFileText.split(editOld).join(editNew)
186
+ : oldFileText.replace(editOld, editNew);
187
+ return computeDiff(oldFileText, newContent);
188
+ }
189
+ return computeDiff(oldFileText, newFile.join("\n"));
190
+ }
191
+ /**
192
+ * Diff two edit strings directly — no file read needed.
193
+ * Line numbers are relative to the edit region, not the file.
194
+ * Use for permission prompt previews where speed matters more than
195
+ * exact file-relative line numbers.
196
+ */
197
+ export function computeInputDiff(oldText, newText) {
198
+ const changes = Diff.diffLines(oldText, newText);
199
+ const raw = changesToDiffLines(changes);
200
+ const { added, removed } = countChanges(raw);
201
+ return {
202
+ hunks: groupHunks(raw, 3),
203
+ added,
204
+ removed,
205
+ isIdentical: false,
206
+ isNewFile: false,
207
+ };
132
208
  }
@@ -23,6 +23,7 @@ export interface HandlerFunctions {
23
23
  define(name: string, fn: (...args: any[]) => any): void;
24
24
  advise(name: string, advisor: (next: (...args: any[]) => any, ...args: any[]) => any): () => void;
25
25
  call(name: string, ...args: any[]): any;
26
+ list(): string[];
26
27
  }
27
28
  export declare class HandlerRegistry {
28
29
  private entries;
@@ -53,5 +54,9 @@ export declare class HandlerRegistry {
53
54
  * Check if a named handler exists.
54
55
  */
55
56
  has(name: string): boolean;
57
+ /**
58
+ * Names of all registered handlers. For diagnostic/introspection use.
59
+ */
60
+ list(): string[];
56
61
  }
57
62
  export {};
@@ -85,4 +85,10 @@ export class HandlerRegistry {
85
85
  has(name) {
86
86
  return this.entries.has(name);
87
87
  }
88
+ /**
89
+ * Names of all registered handlers. For diagnostic/introspection use.
90
+ */
91
+ list() {
92
+ return [...this.entries.keys()];
93
+ }
88
94
  }
@@ -44,8 +44,10 @@ export declare class LineEditor {
44
44
  get text(): string;
45
45
  /** Display text — paste placeholders replaced with labels. For rendering. */
46
46
  get displayText(): string;
47
- /** Cursor position mapped to display-text coordinates. */
47
+ /** Cursor position mapped to display-text character offset. */
48
48
  get displayCursor(): number;
49
+ /** Cursor position as visible terminal-column width (accounts for CJK etc.). */
50
+ get displayCursorWidth(): number;
49
51
  /** Number of logical positions in the buffer. */
50
52
  get length(): number;
51
53
  /** Replace buffer content. Clears paste attachments. */
@@ -73,6 +75,14 @@ export declare class LineEditor {
73
75
  private handleKittyKey;
74
76
  private insertAt;
75
77
  private moveTo;
78
+ /** Move cursor to start of the current logical line. */
79
+ private moveToLineStart;
80
+ /** Move cursor to end of the current logical line. */
81
+ private moveToLineEnd;
82
+ /** Delete from start of current logical line to cursor (Ctrl+U). */
83
+ private deleteLineStart;
84
+ /** Delete from cursor to end of current logical line (Ctrl+K). */
85
+ private deleteLineEnd;
76
86
  private deleteBackward;
77
87
  private deleteForward;
78
88
  private deleteRange;
@@ -11,6 +11,7 @@
11
11
  * - `displayCursor` — cursor column in display coordinates
12
12
  * - `setText()` — replace buffer content (clears paste attachments)
13
13
  */
14
+ import { charWidth } from "./ansi.js";
14
15
  // ── Kitty protocol keycode → readable name ──────────────────────
15
16
  const KITTY_KEY_NAMES = {
16
17
  9: "tab", 13: "enter", 27: "escape", 127: "backspace",
@@ -61,7 +62,7 @@ export class LineEditor {
61
62
  }
62
63
  return result;
63
64
  }
64
- /** Cursor position mapped to display-text coordinates. */
65
+ /** Cursor position mapped to display-text character offset. */
65
66
  get displayCursor() {
66
67
  let pos = 0;
67
68
  for (let i = 0; i < this._buf.length && i < this.cursor; i++) {
@@ -77,6 +78,22 @@ export class LineEditor {
77
78
  }
78
79
  return pos;
79
80
  }
81
+ /** Cursor position as visible terminal-column width (accounts for CJK etc.). */
82
+ get displayCursorWidth() {
83
+ let width = 0;
84
+ for (let i = 0; i < this._buf.length && i < this.cursor; i++) {
85
+ const ch = this._buf[i];
86
+ const paste = this.pastes.get(ch.charCodeAt(0) - PUA_BASE);
87
+ if (paste) {
88
+ const n = paste.split("\n").length;
89
+ width += `[paste +${n} lines]`.length; // ASCII-only, 1 col each
90
+ }
91
+ else {
92
+ width += charWidth(ch.codePointAt(0) ?? 0);
93
+ }
94
+ }
95
+ return width;
96
+ }
80
97
  /** Number of logical positions in the buffer. */
81
98
  get length() {
82
99
  return this._buf.length;
@@ -286,12 +303,12 @@ export class LineEditor {
286
303
  "tab": () => ({ action: "tab" }),
287
304
  "backspace": () => this.deleteBackward(),
288
305
  "ctrl+d": () => this._buf.length === 0 ? { action: "delete-empty" } : this.deleteForward(),
289
- "ctrl+a": () => this.moveTo(0),
290
- "ctrl+e": () => this.moveTo(this._buf.length),
306
+ "ctrl+a": () => this.moveToLineStart(),
307
+ "ctrl+e": () => this.moveToLineEnd(),
291
308
  "ctrl+b": () => this.moveTo(this.cursor - 1),
292
309
  "ctrl+f": () => this.moveTo(this.cursor + 1),
293
- "ctrl+u": () => this.deleteRange(0, this.cursor),
294
- "ctrl+k": () => this.deleteRange(this.cursor, this._buf.length),
310
+ "ctrl+u": () => this.deleteLineStart(),
311
+ "ctrl+k": () => this.deleteLineEnd(),
295
312
  "ctrl+w": () => this.deleteWordBackward() ? { action: "changed" } : null,
296
313
  "alt+f": () => this.wordForward() ? { action: "changed" } : null,
297
314
  "alt+b": () => this.wordBackward() ? { action: "changed" } : null,
@@ -353,6 +370,28 @@ export class LineEditor {
353
370
  this.cursor = clamped;
354
371
  return { action: "changed" };
355
372
  }
373
+ /** Move cursor to start of the current logical line. */
374
+ moveToLineStart() {
375
+ const lineStart = this._buf.lastIndexOf("\n", this.cursor - 1) + 1;
376
+ return this.moveTo(lineStart);
377
+ }
378
+ /** Move cursor to end of the current logical line. */
379
+ moveToLineEnd() {
380
+ const nextNewline = this._buf.indexOf("\n", this.cursor);
381
+ const lineEnd = nextNewline === -1 ? this._buf.length : nextNewline;
382
+ return this.moveTo(lineEnd);
383
+ }
384
+ /** Delete from start of current logical line to cursor (Ctrl+U). */
385
+ deleteLineStart() {
386
+ const lineStart = this._buf.lastIndexOf("\n", this.cursor - 1) + 1;
387
+ return this.deleteRange(lineStart, this.cursor);
388
+ }
389
+ /** Delete from cursor to end of current logical line (Ctrl+K). */
390
+ deleteLineEnd() {
391
+ const nextNewline = this._buf.indexOf("\n", this.cursor);
392
+ const lineEnd = nextNewline === -1 ? this._buf.length : nextNewline;
393
+ return this.deleteRange(this.cursor, lineEnd);
394
+ }
356
395
  deleteBackward() {
357
396
  if (this._buf.length === 0)
358
397
  return { action: "delete-empty" };
@@ -1,4 +1,4 @@
1
- import { visibleLen, truncateToWidth, padEndToWidth } from "./ansi.js";
1
+ import { visibleLen, truncateToWidth, padEndToWidth, charWidth } from "./ansi.js";
2
2
  import { palette as p } from "./palette.js";
3
3
  export const MAX_CONTENT_WIDTH = 90;
4
4
  /**
@@ -33,16 +33,31 @@ export function wrapLine(text, maxWidth) {
33
33
  for (const word of words) {
34
34
  if (word.length === 0)
35
35
  continue;
36
- if (currentWidth + word.length <= maxWidth) {
36
+ const wordWidth = visibleLen(word);
37
+ if (currentWidth + wordWidth <= maxWidth) {
37
38
  currentLine += word;
38
- currentWidth += word.length;
39
+ currentWidth += wordWidth;
39
40
  }
40
41
  else if (currentWidth === 0) {
41
- // Single word longer than maxWidth — hard break
42
+ // Single word longer than maxWidth — hard break by visible width
42
43
  let remaining = word;
43
44
  while (remaining.length > 0) {
44
- const chunk = remaining.slice(0, maxWidth - currentWidth || maxWidth);
45
- remaining = remaining.slice(chunk.length);
45
+ // Find the largest prefix that fits
46
+ let fitLen = 0;
47
+ let fitWidth = 0;
48
+ for (const ch of remaining) {
49
+ const cw = charWidth(ch.codePointAt(0) ?? 0);
50
+ if (fitWidth + cw > maxWidth)
51
+ break;
52
+ fitWidth += cw;
53
+ fitLen += ch.length;
54
+ }
55
+ if (fitLen === 0) {
56
+ // Even one char doesn't fit — force take one char to avoid infinite loop
57
+ fitLen = remaining[0]?.length ?? 1;
58
+ }
59
+ const chunk = remaining.slice(0, fitLen);
60
+ remaining = remaining.slice(fitLen);
46
61
  currentLine += chunk;
47
62
  if (remaining.length > 0) {
48
63
  result.push(currentLine + p.reset);
@@ -50,7 +65,7 @@ export function wrapLine(text, maxWidth) {
50
65
  currentWidth = 0;
51
66
  }
52
67
  else {
53
- currentWidth += chunk.length;
68
+ currentWidth += fitWidth;
54
69
  }
55
70
  }
56
71
  }
@@ -62,7 +77,7 @@ export function wrapLine(text, maxWidth) {
62
77
  // Skip leading spaces on new line
63
78
  const trimmed = word.replace(/^ +/, "");
64
79
  currentLine += trimmed;
65
- currentWidth = trimmed.length;
80
+ currentWidth = visibleLen(trimmed);
66
81
  }
67
82
  }
68
83
  }
@@ -0,0 +1 @@
1
+ export declare const PACKAGE_VERSION: string;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The agent-sh package version, read from package.json at load time.
3
+ * Emitted on `agent:info` so consumers (TUI, remote peers, logs) see a
4
+ * version that tracks releases instead of a hand-edited constant.
5
+ */
6
+ import { createRequire } from "module";
7
+ const require = createRequire(import.meta.url);
8
+ // dist/utils/package-version.js → ../../package.json (project root)
9
+ const pkg = require("../../package.json");
10
+ export const PACKAGE_VERSION = pkg.version ?? "0.0.0";
@@ -0,0 +1,2 @@
1
+ export declare function getSessionDir(): string;
2
+ export declare function spillOutput(id: number, text: string): string;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Spill long shell outputs to per-session tempfiles.
3
+ *
4
+ * Captured PTY output that exceeds the truncation threshold is written to
5
+ * `<tmpdir>/agent-sh-<pid>/<id>.out`. The in-memory exchange keeps only a
6
+ * head+tail stub pointing at that path, so the agent can fetch the full
7
+ * text via `read_file` on demand. The session dir is removed on process
8
+ * exit; stale dirs from dead processes are swept lazily on first use.
9
+ */
10
+ import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ const DIR_PREFIX = "agent-sh-";
14
+ let sessionDir = null;
15
+ let cleanupRegistered = false;
16
+ export function getSessionDir() {
17
+ if (sessionDir)
18
+ return sessionDir;
19
+ sessionDir = join(tmpdir(), `${DIR_PREFIX}${process.pid}`);
20
+ mkdirSync(sessionDir, { recursive: true });
21
+ sweepStaleDirs();
22
+ if (!cleanupRegistered) {
23
+ cleanupRegistered = true;
24
+ const cleanup = () => {
25
+ if (!sessionDir)
26
+ return;
27
+ try {
28
+ rmSync(sessionDir, { recursive: true, force: true });
29
+ }
30
+ catch { }
31
+ sessionDir = null;
32
+ };
33
+ process.on("exit", cleanup);
34
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
35
+ process.on(sig, () => { cleanup(); process.exit(128); });
36
+ }
37
+ }
38
+ return sessionDir;
39
+ }
40
+ export function spillOutput(id, text) {
41
+ const path = join(getSessionDir(), `${id}.out`);
42
+ writeFileSync(path, text);
43
+ return path;
44
+ }
45
+ function sweepStaleDirs() {
46
+ const base = tmpdir();
47
+ let entries;
48
+ try {
49
+ entries = readdirSync(base);
50
+ }
51
+ catch {
52
+ return;
53
+ }
54
+ for (const name of entries) {
55
+ if (!name.startsWith(DIR_PREFIX))
56
+ continue;
57
+ const pid = Number(name.slice(DIR_PREFIX.length));
58
+ if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid)
59
+ continue;
60
+ if (isProcessAlive(pid))
61
+ continue;
62
+ const full = join(base, name);
63
+ try {
64
+ // Small safety check: only remove directories.
65
+ if (statSync(full).isDirectory()) {
66
+ rmSync(full, { recursive: true, force: true });
67
+ }
68
+ }
69
+ catch { }
70
+ }
71
+ }
72
+ function isProcessAlive(pid) {
73
+ try {
74
+ process.kill(pid, 0);
75
+ return true;
76
+ }
77
+ catch (e) {
78
+ // ESRCH = no such process; EPERM = exists but we can't signal it
79
+ return e.code === "EPERM";
80
+ }
81
+ }
@@ -27,7 +27,7 @@ export interface ToolResultRender {
27
27
  }
28
28
  export declare function isQuietCommand(command: string): boolean;
29
29
  export declare function selectToolDisplayMode(width: number): ToolDisplayMode;
30
- export declare function renderToolCall(tool: ToolCallRender, width: number): string[];
30
+ export declare function renderToolCall(tool: ToolCallRender, width: number, cwd?: string): string[];
31
31
  export declare function renderToolResult(result: ToolResultRender, width: number): string[];
32
32
  export declare function formatElapsed(ms: number): string;
33
33
  export declare const SPINNER_FRAMES: string[];
@@ -39,7 +39,6 @@ const KIND_ICONS = {
39
39
  move: "↗",
40
40
  search: "⌕",
41
41
  execute: "▶",
42
- display: "◇",
43
42
  think: "◇",
44
43
  fetch: "↓",
45
44
  switch_mode: "⇄",
@@ -48,7 +47,7 @@ function kindIcon(kind) {
48
47
  return kind ? (KIND_ICONS[kind] ?? "▶") : "▶";
49
48
  }
50
49
  // ── Tool call rendering ──────────────────────────────────────────
51
- export function renderToolCall(tool, width) {
50
+ export function renderToolCall(tool, width, cwd = process.cwd()) {
52
51
  const mode = selectToolDisplayMode(width);
53
52
  const icon = tool.icon ?? kindIcon(tool.kind);
54
53
  // If the tool registered a custom icon, it's self-describing — omit the name.
@@ -61,7 +60,6 @@ export function renderToolCall(tool, width) {
61
60
  const lines = [];
62
61
  // Build a compact detail string to append after the title
63
62
  let detail = "";
64
- const cwd = process.cwd();
65
63
  if (mode === "full" && tool.displayDetail) {
66
64
  detail = tool.displayDetail;
67
65
  }
@@ -70,7 +68,7 @@ export function renderToolCall(tool, width) {
70
68
  detail = `$ ${tool.command}`;
71
69
  }
72
70
  else if (tool.locations && tool.locations.length > 0) {
73
- const loc = tool.locations[0];
71
+ const loc = tool.locations.find((l) => l?.path) ?? tool.locations[0];
74
72
  const lineInfo = loc.line ? `:${loc.line}` : "";
75
73
  detail = `${shortenPath(loc.path, cwd)}${lineInfo}`;
76
74
  }
@@ -227,6 +225,8 @@ export function renderSpinnerLine(state, label, opts) {
227
225
  * Shorten an absolute path to a relative or tilde-prefixed form.
228
226
  */
229
227
  function shortenPath(p, cwd) {
228
+ if (!p || typeof p !== "string")
229
+ return "";
230
230
  if (p.startsWith(cwd + "/"))
231
231
  return p.slice(cwd.length + 1);
232
232
  if (p.startsWith(cwd))
@@ -411,7 +411,6 @@ async function handleSessionNew(id: number | string, params: Record<string, unkn
411
411
  const headlessDisabled = [
412
412
  "tui-renderer",
413
413
  "file-autocomplete",
414
- "terminal-buffer",
415
414
  "overlay-agent",
416
415
  ...(settings.disabledBuiltins ?? []),
417
416
  ];
@@ -428,6 +427,10 @@ async function handleSessionNew(id: number | string, params: Record<string, unkn
428
427
  process.stderr.write(`Warning: ${err instanceof Error ? err.message : err}\n`);
429
428
  });
430
429
 
430
+ // Signal deferred-init listeners (agent-backend) that the provider
431
+ // registry is complete — they resolve their LLM config on this event.
432
+ core.bus.emit("core:extensions-loaded", {});
433
+
431
434
  core.activateBackend();
432
435
  }
433
436