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
|
@@ -2,22 +2,110 @@
|
|
|
2
2
|
* Minimal line editor with readline-style keybindings.
|
|
3
3
|
*
|
|
4
4
|
* Pure logic — no I/O, no rendering, no event bus. Consumers feed raw
|
|
5
|
-
* terminal input bytes and receive high-level actions back.
|
|
6
|
-
*
|
|
5
|
+
* terminal input bytes and receive high-level actions back.
|
|
6
|
+
*
|
|
7
|
+
* The internal buffer may contain PUA placeholder characters for pasted
|
|
8
|
+
* multi-line content. Consumers should use the typed accessors:
|
|
9
|
+
* - `text` — resolved content (pastes expanded), for submit/history/logic
|
|
10
|
+
* - `displayText` — display content (pastes collapsed to labels), for rendering
|
|
11
|
+
* - `displayCursor` — cursor column in display coordinates
|
|
12
|
+
* - `setText()` — replace buffer content (clears paste attachments)
|
|
7
13
|
*/
|
|
14
|
+
import { charWidth } from "./ansi.js";
|
|
8
15
|
// ── Kitty protocol keycode → readable name ──────────────────────
|
|
9
16
|
const KITTY_KEY_NAMES = {
|
|
10
17
|
9: "tab", 13: "enter", 27: "escape", 127: "backspace",
|
|
11
18
|
};
|
|
19
|
+
// ── Paste placeholder ───────────────────────────────────────────
|
|
20
|
+
/** First Unicode Private Use Area codepoint, used as paste placeholder. */
|
|
21
|
+
const PUA_BASE = 0xE000;
|
|
22
|
+
function isPUA(ch) {
|
|
23
|
+
const code = ch.charCodeAt(0);
|
|
24
|
+
return code >= PUA_BASE && code <= 0xF8FF;
|
|
25
|
+
}
|
|
12
26
|
// ── Line editor ─────────────────────────────────────────────────
|
|
13
27
|
export class LineEditor {
|
|
14
|
-
|
|
28
|
+
_buf = "";
|
|
15
29
|
cursor = 0;
|
|
16
30
|
pendingSeq = ""; // buffered incomplete escape sequence
|
|
31
|
+
// ── Bracket paste state ─────────────────────────────────────
|
|
32
|
+
inPaste = false;
|
|
33
|
+
pasteAccum = ""; // accumulates during bracket paste
|
|
34
|
+
pastes = new Map(); // id → pasted content
|
|
35
|
+
pasteCounter = 0;
|
|
17
36
|
// ── History ──────────────────────────────────────────────────
|
|
18
37
|
history = [];
|
|
19
38
|
historyIndex = -1; // -1 = current input, 0..N = history entries (newest first)
|
|
20
39
|
savedBuffer = ""; // saves current input when browsing history
|
|
40
|
+
// ── Public accessors ────────────────────────────────────────
|
|
41
|
+
/** Resolved text — paste placeholders expanded. For submit, history, logic. */
|
|
42
|
+
get text() {
|
|
43
|
+
let result = "";
|
|
44
|
+
for (const ch of this._buf) {
|
|
45
|
+
const paste = this.pastes.get(ch.charCodeAt(0) - PUA_BASE);
|
|
46
|
+
result += paste ?? ch;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/** Display text — paste placeholders replaced with labels. For rendering. */
|
|
51
|
+
get displayText() {
|
|
52
|
+
let result = "";
|
|
53
|
+
for (const ch of this._buf) {
|
|
54
|
+
const paste = this.pastes.get(ch.charCodeAt(0) - PUA_BASE);
|
|
55
|
+
if (paste) {
|
|
56
|
+
const n = paste.split("\n").length;
|
|
57
|
+
result += `[paste +${n} lines]`;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
result += ch;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/** Cursor position mapped to display-text character offset. */
|
|
66
|
+
get displayCursor() {
|
|
67
|
+
let pos = 0;
|
|
68
|
+
for (let i = 0; i < this._buf.length && i < this.cursor; i++) {
|
|
69
|
+
const ch = this._buf[i];
|
|
70
|
+
const paste = this.pastes.get(ch.charCodeAt(0) - PUA_BASE);
|
|
71
|
+
if (paste) {
|
|
72
|
+
const n = paste.split("\n").length;
|
|
73
|
+
pos += `[paste +${n} lines]`.length;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
pos++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return pos;
|
|
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
|
+
}
|
|
97
|
+
/** Number of logical positions in the buffer. */
|
|
98
|
+
get length() {
|
|
99
|
+
return this._buf.length;
|
|
100
|
+
}
|
|
101
|
+
/** Replace buffer content. Clears paste attachments. */
|
|
102
|
+
setText(value) {
|
|
103
|
+
this._buf = value;
|
|
104
|
+
this.pastes.clear();
|
|
105
|
+
this.pasteCounter = 0;
|
|
106
|
+
this.cursor = value.length;
|
|
107
|
+
}
|
|
108
|
+
// ── Input processing ────────────────────────────────────────
|
|
21
109
|
/** Process raw terminal input, return actions for the consumer. */
|
|
22
110
|
feed(data) {
|
|
23
111
|
// If we had a pending incomplete escape sequence, prepend it
|
|
@@ -68,7 +156,7 @@ export class LineEditor {
|
|
|
68
156
|
actions.push({ action: "arrow-down" });
|
|
69
157
|
break;
|
|
70
158
|
case "C":
|
|
71
|
-
if (this.cursor < this.
|
|
159
|
+
if (this.cursor < this._buf.length) {
|
|
72
160
|
this.cursor++;
|
|
73
161
|
actions.push({ action: "changed" });
|
|
74
162
|
}
|
|
@@ -86,8 +174,8 @@ export class LineEditor {
|
|
|
86
174
|
}
|
|
87
175
|
break;
|
|
88
176
|
case "F": // End
|
|
89
|
-
if (this.cursor < this.
|
|
90
|
-
this.cursor = this.
|
|
177
|
+
if (this.cursor < this._buf.length) {
|
|
178
|
+
this.cursor = this._buf.length;
|
|
91
179
|
actions.push({ action: "changed" });
|
|
92
180
|
}
|
|
93
181
|
break;
|
|
@@ -119,6 +207,16 @@ export class LineEditor {
|
|
|
119
207
|
// Other Alt+key — ignore
|
|
120
208
|
continue;
|
|
121
209
|
}
|
|
210
|
+
// ── Bracket paste: accumulate into side buffer ─────
|
|
211
|
+
if (this.inPaste) {
|
|
212
|
+
if (ch === "\r") {
|
|
213
|
+
i++;
|
|
214
|
+
continue;
|
|
215
|
+
} // skip CR (CR+LF → just LF)
|
|
216
|
+
this.pasteAccum += ch;
|
|
217
|
+
i++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
122
220
|
// ── Control characters ──────────────────────────────
|
|
123
221
|
if (ch.charCodeAt(0) < 0x20 || ch === "\x7f") {
|
|
124
222
|
const action = this.handleControl(ch);
|
|
@@ -128,7 +226,7 @@ export class LineEditor {
|
|
|
128
226
|
continue;
|
|
129
227
|
}
|
|
130
228
|
// ── Printable character ─────────────────────────────
|
|
131
|
-
this.
|
|
229
|
+
this._buf = this._buf.slice(0, this.cursor) + ch + this._buf.slice(this.cursor);
|
|
132
230
|
this.cursor++;
|
|
133
231
|
actions.push({ action: "changed" });
|
|
134
232
|
i++;
|
|
@@ -148,9 +246,13 @@ export class LineEditor {
|
|
|
148
246
|
return wasBarEscape ? [{ action: "cancel" }] : [];
|
|
149
247
|
}
|
|
150
248
|
clear() {
|
|
151
|
-
this.
|
|
249
|
+
this._buf = "";
|
|
152
250
|
this.cursor = 0;
|
|
153
251
|
this.pendingSeq = "";
|
|
252
|
+
this.inPaste = false;
|
|
253
|
+
this.pasteAccum = "";
|
|
254
|
+
this.pastes.clear();
|
|
255
|
+
this.pasteCounter = 0;
|
|
154
256
|
this.historyIndex = -1;
|
|
155
257
|
this.savedBuffer = "";
|
|
156
258
|
}
|
|
@@ -171,11 +273,10 @@ export class LineEditor {
|
|
|
171
273
|
if (this.historyIndex + 1 >= this.history.length)
|
|
172
274
|
return null;
|
|
173
275
|
if (this.historyIndex === -1) {
|
|
174
|
-
this.savedBuffer = this.
|
|
276
|
+
this.savedBuffer = this.text; // save resolved current input
|
|
175
277
|
}
|
|
176
278
|
this.historyIndex++;
|
|
177
|
-
this.
|
|
178
|
-
this.cursor = this.buffer.length;
|
|
279
|
+
this.setText(this.history[this.historyIndex]);
|
|
179
280
|
return { action: "changed" };
|
|
180
281
|
}
|
|
181
282
|
/** Navigate to a more recent history entry. Returns changed action or null. */
|
|
@@ -184,12 +285,11 @@ export class LineEditor {
|
|
|
184
285
|
return null;
|
|
185
286
|
this.historyIndex--;
|
|
186
287
|
if (this.historyIndex === -1) {
|
|
187
|
-
this.
|
|
288
|
+
this.setText(this.savedBuffer);
|
|
188
289
|
}
|
|
189
290
|
else {
|
|
190
|
-
this.
|
|
291
|
+
this.setText(this.history[this.historyIndex]);
|
|
191
292
|
}
|
|
192
|
-
this.cursor = this.buffer.length;
|
|
193
293
|
return { action: "changed" };
|
|
194
294
|
}
|
|
195
295
|
// ── Key bindings ────────────────────────────────────────────
|
|
@@ -198,17 +298,17 @@ export class LineEditor {
|
|
|
198
298
|
// characters and kitty protocol sequences resolve to a key name
|
|
199
299
|
// and look it up here. To add a binding, add one entry.
|
|
200
300
|
bindings = {
|
|
201
|
-
"enter": () => ({ action: "submit", buffer: this.
|
|
301
|
+
"enter": () => ({ action: "submit", buffer: this.text }),
|
|
202
302
|
"ctrl+c": () => ({ action: "cancel" }),
|
|
203
303
|
"tab": () => ({ action: "tab" }),
|
|
204
304
|
"backspace": () => this.deleteBackward(),
|
|
205
|
-
"ctrl+d": () => this.
|
|
206
|
-
"ctrl+a": () => this.
|
|
207
|
-
"ctrl+e": () => this.
|
|
305
|
+
"ctrl+d": () => this._buf.length === 0 ? { action: "delete-empty" } : this.deleteForward(),
|
|
306
|
+
"ctrl+a": () => this.moveToLineStart(),
|
|
307
|
+
"ctrl+e": () => this.moveToLineEnd(),
|
|
208
308
|
"ctrl+b": () => this.moveTo(this.cursor - 1),
|
|
209
309
|
"ctrl+f": () => this.moveTo(this.cursor + 1),
|
|
210
|
-
"ctrl+u": () => this.
|
|
211
|
-
"ctrl+k": () => this.
|
|
310
|
+
"ctrl+u": () => this.deleteLineStart(),
|
|
311
|
+
"ctrl+k": () => this.deleteLineEnd(),
|
|
212
312
|
"ctrl+w": () => this.deleteWordBackward() ? { action: "changed" } : null,
|
|
213
313
|
"alt+f": () => this.wordForward() ? { action: "changed" } : null,
|
|
214
314
|
"alt+b": () => this.wordBackward() ? { action: "changed" } : null,
|
|
@@ -259,36 +359,73 @@ export class LineEditor {
|
|
|
259
359
|
}
|
|
260
360
|
// ── Editing primitives ─────────────────────────────────────
|
|
261
361
|
insertAt(ch) {
|
|
262
|
-
this.
|
|
362
|
+
this._buf = this._buf.slice(0, this.cursor) + ch + this._buf.slice(this.cursor);
|
|
263
363
|
this.cursor++;
|
|
264
364
|
return { action: "changed" };
|
|
265
365
|
}
|
|
266
366
|
moveTo(pos) {
|
|
267
|
-
const clamped = Math.max(0, Math.min(pos, this.
|
|
367
|
+
const clamped = Math.max(0, Math.min(pos, this._buf.length));
|
|
268
368
|
if (clamped === this.cursor)
|
|
269
369
|
return null;
|
|
270
370
|
this.cursor = clamped;
|
|
271
371
|
return { action: "changed" };
|
|
272
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
|
+
}
|
|
273
395
|
deleteBackward() {
|
|
274
|
-
if (this.
|
|
396
|
+
if (this._buf.length === 0)
|
|
275
397
|
return { action: "delete-empty" };
|
|
276
398
|
if (this.cursor <= 0)
|
|
277
399
|
return null;
|
|
278
|
-
|
|
400
|
+
// If deleting a paste placeholder, also remove the paste entry
|
|
401
|
+
const deleted = this._buf[this.cursor - 1];
|
|
402
|
+
if (isPUA(deleted)) {
|
|
403
|
+
this.pastes.delete(deleted.charCodeAt(0) - PUA_BASE);
|
|
404
|
+
}
|
|
405
|
+
this._buf = this._buf.slice(0, this.cursor - 1) + this._buf.slice(this.cursor);
|
|
279
406
|
this.cursor--;
|
|
280
407
|
return { action: "changed" };
|
|
281
408
|
}
|
|
282
409
|
deleteForward() {
|
|
283
|
-
if (this.cursor >= this.
|
|
410
|
+
if (this.cursor >= this._buf.length)
|
|
284
411
|
return null;
|
|
285
|
-
|
|
412
|
+
const deleted = this._buf[this.cursor];
|
|
413
|
+
if (isPUA(deleted)) {
|
|
414
|
+
this.pastes.delete(deleted.charCodeAt(0) - PUA_BASE);
|
|
415
|
+
}
|
|
416
|
+
this._buf = this._buf.slice(0, this.cursor) + this._buf.slice(this.cursor + 1);
|
|
286
417
|
return { action: "changed" };
|
|
287
418
|
}
|
|
288
419
|
deleteRange(start, end) {
|
|
289
420
|
if (start >= end)
|
|
290
421
|
return null;
|
|
291
|
-
|
|
422
|
+
// Clean up any paste entries in the deleted range
|
|
423
|
+
for (let k = start; k < end; k++) {
|
|
424
|
+
const ch = this._buf[k];
|
|
425
|
+
if (isPUA(ch))
|
|
426
|
+
this.pastes.delete(ch.charCodeAt(0) - PUA_BASE);
|
|
427
|
+
}
|
|
428
|
+
this._buf = this._buf.slice(0, start) + this._buf.slice(end);
|
|
292
429
|
this.cursor = start;
|
|
293
430
|
return { action: "changed" };
|
|
294
431
|
}
|
|
@@ -326,7 +463,7 @@ export class LineEditor {
|
|
|
326
463
|
actions.push({ action: "changed" });
|
|
327
464
|
}
|
|
328
465
|
else {
|
|
329
|
-
if (this.cursor < this.
|
|
466
|
+
if (this.cursor < this._buf.length) {
|
|
330
467
|
this.cursor++;
|
|
331
468
|
actions.push({ action: "changed" });
|
|
332
469
|
}
|
|
@@ -351,8 +488,8 @@ export class LineEditor {
|
|
|
351
488
|
}
|
|
352
489
|
break;
|
|
353
490
|
case "F": // End
|
|
354
|
-
if (this.cursor < this.
|
|
355
|
-
this.cursor = this.
|
|
491
|
+
if (this.cursor < this._buf.length) {
|
|
492
|
+
this.cursor = this._buf.length;
|
|
356
493
|
actions.push({ action: "changed" });
|
|
357
494
|
}
|
|
358
495
|
break;
|
|
@@ -365,11 +502,39 @@ export class LineEditor {
|
|
|
365
502
|
actions.push(action);
|
|
366
503
|
break;
|
|
367
504
|
}
|
|
368
|
-
case "~": // Extended keys: Delete (3~), etc.
|
|
505
|
+
case "~": // Extended keys: Delete (3~), bracket paste (200~/201~), etc.
|
|
369
506
|
if (params === "3") {
|
|
370
507
|
// Delete key: delete char under cursor
|
|
371
|
-
if (this.cursor < this.
|
|
372
|
-
|
|
508
|
+
if (this.cursor < this._buf.length) {
|
|
509
|
+
const deleted = this._buf[this.cursor];
|
|
510
|
+
if (isPUA(deleted))
|
|
511
|
+
this.pastes.delete(deleted.charCodeAt(0) - PUA_BASE);
|
|
512
|
+
this._buf = this._buf.slice(0, this.cursor) + this._buf.slice(this.cursor + 1);
|
|
513
|
+
actions.push({ action: "changed" });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else if (params === "200") {
|
|
517
|
+
this.inPaste = true;
|
|
518
|
+
this.pasteAccum = "";
|
|
519
|
+
}
|
|
520
|
+
else if (params === "201") {
|
|
521
|
+
this.inPaste = false;
|
|
522
|
+
if (this.pasteAccum) {
|
|
523
|
+
const lines = this.pasteAccum.split("\n");
|
|
524
|
+
if (lines.length <= 1) {
|
|
525
|
+
// Single-line paste — inline directly
|
|
526
|
+
this._buf = this._buf.slice(0, this.cursor) + this.pasteAccum + this._buf.slice(this.cursor);
|
|
527
|
+
this.cursor += this.pasteAccum.length;
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
// Multi-line paste — store and insert placeholder
|
|
531
|
+
const id = this.pasteCounter++;
|
|
532
|
+
this.pastes.set(id, this.pasteAccum);
|
|
533
|
+
const placeholder = String.fromCharCode(PUA_BASE + id);
|
|
534
|
+
this._buf = this._buf.slice(0, this.cursor) + placeholder + this._buf.slice(this.cursor);
|
|
535
|
+
this.cursor++;
|
|
536
|
+
}
|
|
537
|
+
this.pasteAccum = "";
|
|
373
538
|
actions.push({ action: "changed" });
|
|
374
539
|
}
|
|
375
540
|
}
|
|
@@ -383,11 +548,11 @@ export class LineEditor {
|
|
|
383
548
|
if (this.cursor === 0)
|
|
384
549
|
return false;
|
|
385
550
|
let pos = this.cursor;
|
|
386
|
-
// Skip spaces
|
|
387
|
-
while (pos > 0 && this.
|
|
551
|
+
// Skip PUA placeholders and spaces
|
|
552
|
+
while (pos > 0 && (this._buf[pos - 1] === " " || isPUA(this._buf[pos - 1])))
|
|
388
553
|
pos--;
|
|
389
554
|
// Skip word chars
|
|
390
|
-
while (pos > 0 && this.
|
|
555
|
+
while (pos > 0 && this._buf[pos - 1] !== " " && !isPUA(this._buf[pos - 1]))
|
|
391
556
|
pos--;
|
|
392
557
|
if (pos === this.cursor)
|
|
393
558
|
return false;
|
|
@@ -395,14 +560,14 @@ export class LineEditor {
|
|
|
395
560
|
return true;
|
|
396
561
|
}
|
|
397
562
|
wordForward() {
|
|
398
|
-
if (this.cursor >= this.
|
|
563
|
+
if (this.cursor >= this._buf.length)
|
|
399
564
|
return false;
|
|
400
565
|
let pos = this.cursor;
|
|
401
|
-
// Skip word chars
|
|
402
|
-
while (pos < this.
|
|
566
|
+
// Skip word chars and PUA placeholders
|
|
567
|
+
while (pos < this._buf.length && this._buf[pos] !== " " && !isPUA(this._buf[pos]))
|
|
403
568
|
pos++;
|
|
404
|
-
// Skip spaces
|
|
405
|
-
while (pos < this.
|
|
569
|
+
// Skip spaces and PUA
|
|
570
|
+
while (pos < this._buf.length && (this._buf[pos] === " " || isPUA(this._buf[pos])))
|
|
406
571
|
pos++;
|
|
407
572
|
if (pos === this.cursor)
|
|
408
573
|
return false;
|
|
@@ -414,15 +579,27 @@ export class LineEditor {
|
|
|
414
579
|
return false;
|
|
415
580
|
const start = this.cursor;
|
|
416
581
|
this.wordBackward();
|
|
417
|
-
|
|
582
|
+
// Clean up paste entries
|
|
583
|
+
for (let k = this.cursor; k < start; k++) {
|
|
584
|
+
const ch = this._buf[k];
|
|
585
|
+
if (isPUA(ch))
|
|
586
|
+
this.pastes.delete(ch.charCodeAt(0) - PUA_BASE);
|
|
587
|
+
}
|
|
588
|
+
this._buf = this._buf.slice(0, this.cursor) + this._buf.slice(start);
|
|
418
589
|
return true;
|
|
419
590
|
}
|
|
420
591
|
deleteWordForward() {
|
|
421
|
-
if (this.cursor >= this.
|
|
592
|
+
if (this.cursor >= this._buf.length)
|
|
422
593
|
return false;
|
|
423
594
|
const start = this.cursor;
|
|
424
595
|
this.wordForward();
|
|
425
|
-
|
|
596
|
+
// Clean up paste entries
|
|
597
|
+
for (let k = start; k < this.cursor; k++) {
|
|
598
|
+
const ch = this._buf[k];
|
|
599
|
+
if (isPUA(ch))
|
|
600
|
+
this.pastes.delete(ch.charCodeAt(0) - PUA_BASE);
|
|
601
|
+
}
|
|
602
|
+
this._buf = this._buf.slice(0, start) + this._buf.slice(this.cursor);
|
|
426
603
|
this.cursor = start;
|
|
427
604
|
return true;
|
|
428
605
|
}
|
package/dist/utils/markdown.d.ts
CHANGED
package/dist/utils/markdown.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { visibleLen, truncateToWidth, padEndToWidth } from "./ansi.js";
|
|
2
2
|
import { palette as p } from "./palette.js";
|
|
3
|
-
const MAX_CONTENT_WIDTH = 90;
|
|
3
|
+
export const MAX_CONTENT_WIDTH = 90;
|
|
4
4
|
/**
|
|
5
5
|
* Word-wrap a string (which may contain ANSI codes) to a maximum visible width.
|
|
6
6
|
* Returns an array of lines, each fitting within `maxWidth` visible characters.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for manipulating OpenAI-format message arrays.
|
|
3
|
+
*
|
|
4
|
+
* Used by extensions advising `conversation:prepare` to transform
|
|
5
|
+
* the message array before it's sent to the LLM.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Find tool call IDs matching a tool name and optional argument filter.
|
|
9
|
+
*
|
|
10
|
+
* Scans assistant messages for tool_calls where `function.name` matches
|
|
11
|
+
* and parsed arguments satisfy the filter (shallow key/value match).
|
|
12
|
+
*
|
|
13
|
+
* Returns call IDs in message order (earliest first).
|
|
14
|
+
*/
|
|
15
|
+
export declare function findToolCallIds(messages: any[], toolName: string, argFilter?: Record<string, unknown>): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Replace tool result content for specific call IDs.
|
|
18
|
+
*
|
|
19
|
+
* Returns a new array (shallow copy) with matching tool messages
|
|
20
|
+
* replaced. Non-matching messages are passed through by reference.
|
|
21
|
+
*/
|
|
22
|
+
export declare function stubToolResults(messages: any[], callIds: Set<string>, stub: string): any[];
|
|
23
|
+
/**
|
|
24
|
+
* Deduplicate tool results: keep only the latest result for a given
|
|
25
|
+
* tool name + argument filter, replace all older results with a stub.
|
|
26
|
+
*
|
|
27
|
+
* Common use case: a file that's read repeatedly (e.g. a live transcript)
|
|
28
|
+
* — only the most recent read matters.
|
|
29
|
+
*
|
|
30
|
+
* Example:
|
|
31
|
+
* dedupeToolResults(messages, "read_file",
|
|
32
|
+
* { path: "/path/to/transcript.txt" },
|
|
33
|
+
* "[stale — superseded by later read]")
|
|
34
|
+
*/
|
|
35
|
+
export declare function dedupeToolResults(messages: any[], toolName: string, argFilter?: Record<string, unknown>, stub?: string): any[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for manipulating OpenAI-format message arrays.
|
|
3
|
+
*
|
|
4
|
+
* Used by extensions advising `conversation:prepare` to transform
|
|
5
|
+
* the message array before it's sent to the LLM.
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
/**
|
|
9
|
+
* Find tool call IDs matching a tool name and optional argument filter.
|
|
10
|
+
*
|
|
11
|
+
* Scans assistant messages for tool_calls where `function.name` matches
|
|
12
|
+
* and parsed arguments satisfy the filter (shallow key/value match).
|
|
13
|
+
*
|
|
14
|
+
* Returns call IDs in message order (earliest first).
|
|
15
|
+
*/
|
|
16
|
+
export function findToolCallIds(messages, toolName, argFilter) {
|
|
17
|
+
const ids = [];
|
|
18
|
+
for (const msg of messages) {
|
|
19
|
+
if (msg.role !== "assistant" || !msg.tool_calls)
|
|
20
|
+
continue;
|
|
21
|
+
for (const tc of msg.tool_calls) {
|
|
22
|
+
const fn = tc.function ?? tc.fn;
|
|
23
|
+
if (!fn || fn.name !== toolName)
|
|
24
|
+
continue;
|
|
25
|
+
if (argFilter) {
|
|
26
|
+
let args;
|
|
27
|
+
try {
|
|
28
|
+
args = JSON.parse(fn.arguments);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const match = Object.entries(argFilter).every(([k, v]) => args[k] === v);
|
|
34
|
+
if (!match)
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
ids.push(tc.id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return ids;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Replace tool result content for specific call IDs.
|
|
44
|
+
*
|
|
45
|
+
* Returns a new array (shallow copy) with matching tool messages
|
|
46
|
+
* replaced. Non-matching messages are passed through by reference.
|
|
47
|
+
*/
|
|
48
|
+
export function stubToolResults(messages, callIds, stub) {
|
|
49
|
+
return messages.map((msg) => {
|
|
50
|
+
if (msg.role === "tool" && callIds.has(msg.tool_call_id)) {
|
|
51
|
+
return { ...msg, content: stub };
|
|
52
|
+
}
|
|
53
|
+
return msg;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Deduplicate tool results: keep only the latest result for a given
|
|
58
|
+
* tool name + argument filter, replace all older results with a stub.
|
|
59
|
+
*
|
|
60
|
+
* Common use case: a file that's read repeatedly (e.g. a live transcript)
|
|
61
|
+
* — only the most recent read matters.
|
|
62
|
+
*
|
|
63
|
+
* Example:
|
|
64
|
+
* dedupeToolResults(messages, "read_file",
|
|
65
|
+
* { path: "/path/to/transcript.txt" },
|
|
66
|
+
* "[stale — superseded by later read]")
|
|
67
|
+
*/
|
|
68
|
+
export function dedupeToolResults(messages, toolName, argFilter, stub = "[superseded by later call]") {
|
|
69
|
+
const callIds = findToolCallIds(messages, toolName, argFilter);
|
|
70
|
+
if (callIds.length <= 1)
|
|
71
|
+
return messages;
|
|
72
|
+
// Keep the last one, stub the rest
|
|
73
|
+
const staleIds = new Set(callIds.slice(0, -1));
|
|
74
|
+
return stubToolResults(messages, staleIds, stub);
|
|
75
|
+
}
|
|
@@ -47,7 +47,9 @@ export declare class TerminalBuffer {
|
|
|
47
47
|
/** Get the raw serialized terminal output (includes ANSI sequences). */
|
|
48
48
|
serialize(): string;
|
|
49
49
|
/** Read clean screen text with metadata. */
|
|
50
|
-
readScreen(
|
|
50
|
+
readScreen(opts?: {
|
|
51
|
+
includeScrollback?: boolean;
|
|
52
|
+
}): ScreenSnapshot;
|
|
51
53
|
/**
|
|
52
54
|
* Get terminal screen as lines, padded/trimmed to exactly `rows` lines.
|
|
53
55
|
* Clean text only (ANSI stripped). Reads from the active buffer's
|
|
@@ -57,6 +59,8 @@ export declare class TerminalBuffer {
|
|
|
57
59
|
getScreenLines(rows?: number): string[];
|
|
58
60
|
/** Read visible viewport lines from a buffer. */
|
|
59
61
|
private readViewportLines;
|
|
62
|
+
/** Read all lines including scrollback from a buffer. */
|
|
63
|
+
private readAllLines;
|
|
60
64
|
/** Get cursor position. */
|
|
61
65
|
getCursor(): {
|
|
62
66
|
x: number;
|
|
@@ -130,9 +130,11 @@ export class TerminalBuffer {
|
|
|
130
130
|
return this.serializeAddon.serialize();
|
|
131
131
|
}
|
|
132
132
|
/** Read clean screen text with metadata. */
|
|
133
|
-
readScreen() {
|
|
133
|
+
readScreen(opts) {
|
|
134
134
|
const buf = this.term.buffer.active;
|
|
135
|
-
const lines =
|
|
135
|
+
const lines = opts?.includeScrollback
|
|
136
|
+
? this.readAllLines(buf)
|
|
137
|
+
: this.readViewportLines(buf);
|
|
136
138
|
return {
|
|
137
139
|
text: lines.join("\n"),
|
|
138
140
|
altScreen: buf.type === "alternate",
|
|
@@ -161,6 +163,20 @@ export class TerminalBuffer {
|
|
|
161
163
|
}
|
|
162
164
|
return lines;
|
|
163
165
|
}
|
|
166
|
+
/** Read all lines including scrollback from a buffer. */
|
|
167
|
+
readAllLines(buf) {
|
|
168
|
+
const total = (buf.baseY ?? 0) + buf.length;
|
|
169
|
+
const lines = [];
|
|
170
|
+
for (let y = 0; y < total; y++) {
|
|
171
|
+
const line = buf.getLine(y);
|
|
172
|
+
lines.push(line ? line.translateToString(true) : "");
|
|
173
|
+
}
|
|
174
|
+
// Trim trailing empty lines
|
|
175
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
176
|
+
lines.pop();
|
|
177
|
+
}
|
|
178
|
+
return lines;
|
|
179
|
+
}
|
|
164
180
|
/** Get cursor position. */
|
|
165
181
|
getCursor() {
|
|
166
182
|
return {
|
|
@@ -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))
|