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.
- package/README.md +25 -30
- package/dist/agent/agent-loop.d.ts +43 -6
- package/dist/agent/agent-loop.js +817 -157
- package/dist/agent/conversation-state.d.ts +72 -21
- package/dist/agent/conversation-state.js +364 -151
- 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 +84 -3
- 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 +34 -1
- package/dist/agent/system-prompt.js +96 -47
- package/dist/agent/token-budget.d.ts +10 -13
- package/dist/agent/token-budget.js +6 -46
- package/dist/agent/tool-protocol.d.ts +23 -1
- package/dist/agent/tool-protocol.js +169 -4
- 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 +1 -2
- package/dist/context-manager.d.ts +16 -19
- package/dist/context-manager.js +48 -152
- package/dist/core.js +27 -6
- package/dist/event-bus.d.ts +59 -3
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.js +75 -17
- package/dist/extensions/agent-backend.d.ts +8 -7
- package/dist/extensions/agent-backend.js +72 -50
- package/dist/extensions/index.js +0 -2
- package/dist/extensions/slash-commands.js +14 -9
- package/dist/extensions/tui-renderer.js +67 -80
- package/dist/index.js +25 -6
- package/dist/settings.d.ts +39 -16
- package/dist/settings.js +51 -11
- package/dist/shell/input-handler.d.ts +2 -1
- package/dist/shell/input-handler.js +84 -76
- package/dist/shell/shell.js +19 -2
- package/dist/types.d.ts +15 -0
- package/dist/utils/ansi.d.ts +7 -0
- package/dist/utils/ansi.js +69 -8
- package/dist/utils/box-frame.js +8 -2
- package/dist/utils/compositor.d.ts +5 -0
- package/dist/utils/compositor.js +31 -3
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +221 -143
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/handler-registry.d.ts +5 -0
- package/dist/utils/handler-registry.js +6 -0
- package/dist/utils/line-editor.d.ts +11 -1
- package/dist/utils/line-editor.js +44 -5
- package/dist/utils/markdown.js +23 -8
- package/dist/utils/package-version.d.ts +1 -0
- package/dist/utils/package-version.js +10 -0
- package/dist/utils/shell-output-spill.d.ts +2 -0
- package/dist/utils/shell-output-spill.js +81 -0
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
- package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
- package/examples/extensions/claude-code-bridge/README.md +14 -0
- package/examples/extensions/claude-code-bridge/index.ts +204 -145
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +39 -25
- package/examples/extensions/overlay-agent.ts +3 -3
- package/examples/extensions/peer-mesh.ts +115 -0
- package/examples/extensions/pi-bridge/README.md +16 -0
- package/examples/extensions/pi-bridge/index.ts +9 -155
- package/examples/extensions/questionnaire.ts +16 -5
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +163 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +8 -0
- package/package.json +36 -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/shell-recall.d.ts +0 -9
- package/dist/extensions/shell-recall.js +0 -8
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -134
package/dist/utils/diff.js
CHANGED
|
@@ -1,9 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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 {};
|
|
@@ -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
|
|
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
|
|
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.
|
|
290
|
-
"ctrl+e": () => this.
|
|
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.
|
|
294
|
-
"ctrl+k": () => this.
|
|
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" };
|
package/dist/utils/markdown.js
CHANGED
|
@@ -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
|
-
|
|
36
|
+
const wordWidth = visibleLen(word);
|
|
37
|
+
if (currentWidth + wordWidth <= maxWidth) {
|
|
37
38
|
currentLine += word;
|
|
38
|
-
currentWidth +=
|
|
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
|
-
|
|
45
|
-
|
|
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 +=
|
|
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
|
|
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,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
|
|