agent-sh 0.8.0 → 0.9.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 +25 -34
- package/dist/agent/agent-loop.d.ts +29 -6
- package/dist/agent/agent-loop.js +177 -59
- package/dist/agent/conversation-state.d.ts +3 -1
- package/dist/agent/conversation-state.js +6 -2
- package/dist/agent/nuclear-form.js +5 -4
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +12 -28
- package/dist/{token-budget.js → agent/token-budget.js} +1 -1
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/types.d.ts +21 -1
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -194
- package/dist/event-bus.d.ts +26 -0
- package/dist/event-bus.js +20 -1
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +104 -17
- package/dist/extensions/agent-backend.d.ts +13 -0
- package/dist/extensions/agent-backend.js +167 -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 +25 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +16 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +13 -4
- package/dist/extensions/tui-renderer.js +63 -43
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +4 -1
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
- package/dist/{input-handler.js → shell/input-handler.js} +60 -43
- 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} +20 -6
- package/dist/types.d.ts +49 -10
- package/dist/utils/compositor.d.ts +62 -0
- package/dist/utils/compositor.js +88 -0
- package/dist/utils/diff-renderer.js +92 -4
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +23 -3
- package/dist/utils/line-editor.js +180 -42
- 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-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 +571 -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 +154 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/interactive-prompts.ts +82 -110
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +450 -0
- package/examples/extensions/questionnaire.ts +249 -0
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/web-access.ts +327 -0
- package/package.json +9 -1
- package/dist/extensions/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/examples/extensions/terminal-buffer.ts +0 -184
- /package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +0 -0
|
@@ -2,22 +2,93 @@
|
|
|
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
|
*/
|
|
8
14
|
// ── Kitty protocol keycode → readable name ──────────────────────
|
|
9
15
|
const KITTY_KEY_NAMES = {
|
|
10
16
|
9: "tab", 13: "enter", 27: "escape", 127: "backspace",
|
|
11
17
|
};
|
|
18
|
+
// ── Paste placeholder ───────────────────────────────────────────
|
|
19
|
+
/** First Unicode Private Use Area codepoint, used as paste placeholder. */
|
|
20
|
+
const PUA_BASE = 0xE000;
|
|
21
|
+
function isPUA(ch) {
|
|
22
|
+
const code = ch.charCodeAt(0);
|
|
23
|
+
return code >= PUA_BASE && code <= 0xF8FF;
|
|
24
|
+
}
|
|
12
25
|
// ── Line editor ─────────────────────────────────────────────────
|
|
13
26
|
export class LineEditor {
|
|
14
|
-
|
|
27
|
+
_buf = "";
|
|
15
28
|
cursor = 0;
|
|
16
29
|
pendingSeq = ""; // buffered incomplete escape sequence
|
|
30
|
+
// ── Bracket paste state ─────────────────────────────────────
|
|
31
|
+
inPaste = false;
|
|
32
|
+
pasteAccum = ""; // accumulates during bracket paste
|
|
33
|
+
pastes = new Map(); // id → pasted content
|
|
34
|
+
pasteCounter = 0;
|
|
17
35
|
// ── History ──────────────────────────────────────────────────
|
|
18
36
|
history = [];
|
|
19
37
|
historyIndex = -1; // -1 = current input, 0..N = history entries (newest first)
|
|
20
38
|
savedBuffer = ""; // saves current input when browsing history
|
|
39
|
+
// ── Public accessors ────────────────────────────────────────
|
|
40
|
+
/** Resolved text — paste placeholders expanded. For submit, history, logic. */
|
|
41
|
+
get text() {
|
|
42
|
+
let result = "";
|
|
43
|
+
for (const ch of this._buf) {
|
|
44
|
+
const paste = this.pastes.get(ch.charCodeAt(0) - PUA_BASE);
|
|
45
|
+
result += paste ?? ch;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/** Display text — paste placeholders replaced with labels. For rendering. */
|
|
50
|
+
get displayText() {
|
|
51
|
+
let result = "";
|
|
52
|
+
for (const ch of this._buf) {
|
|
53
|
+
const paste = this.pastes.get(ch.charCodeAt(0) - PUA_BASE);
|
|
54
|
+
if (paste) {
|
|
55
|
+
const n = paste.split("\n").length;
|
|
56
|
+
result += `[paste +${n} lines]`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
result += ch;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
/** Cursor position mapped to display-text coordinates. */
|
|
65
|
+
get displayCursor() {
|
|
66
|
+
let pos = 0;
|
|
67
|
+
for (let i = 0; i < this._buf.length && i < this.cursor; i++) {
|
|
68
|
+
const ch = this._buf[i];
|
|
69
|
+
const paste = this.pastes.get(ch.charCodeAt(0) - PUA_BASE);
|
|
70
|
+
if (paste) {
|
|
71
|
+
const n = paste.split("\n").length;
|
|
72
|
+
pos += `[paste +${n} lines]`.length;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
pos++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return pos;
|
|
79
|
+
}
|
|
80
|
+
/** Number of logical positions in the buffer. */
|
|
81
|
+
get length() {
|
|
82
|
+
return this._buf.length;
|
|
83
|
+
}
|
|
84
|
+
/** Replace buffer content. Clears paste attachments. */
|
|
85
|
+
setText(value) {
|
|
86
|
+
this._buf = value;
|
|
87
|
+
this.pastes.clear();
|
|
88
|
+
this.pasteCounter = 0;
|
|
89
|
+
this.cursor = value.length;
|
|
90
|
+
}
|
|
91
|
+
// ── Input processing ────────────────────────────────────────
|
|
21
92
|
/** Process raw terminal input, return actions for the consumer. */
|
|
22
93
|
feed(data) {
|
|
23
94
|
// If we had a pending incomplete escape sequence, prepend it
|
|
@@ -68,7 +139,7 @@ export class LineEditor {
|
|
|
68
139
|
actions.push({ action: "arrow-down" });
|
|
69
140
|
break;
|
|
70
141
|
case "C":
|
|
71
|
-
if (this.cursor < this.
|
|
142
|
+
if (this.cursor < this._buf.length) {
|
|
72
143
|
this.cursor++;
|
|
73
144
|
actions.push({ action: "changed" });
|
|
74
145
|
}
|
|
@@ -86,8 +157,8 @@ export class LineEditor {
|
|
|
86
157
|
}
|
|
87
158
|
break;
|
|
88
159
|
case "F": // End
|
|
89
|
-
if (this.cursor < this.
|
|
90
|
-
this.cursor = this.
|
|
160
|
+
if (this.cursor < this._buf.length) {
|
|
161
|
+
this.cursor = this._buf.length;
|
|
91
162
|
actions.push({ action: "changed" });
|
|
92
163
|
}
|
|
93
164
|
break;
|
|
@@ -119,6 +190,16 @@ export class LineEditor {
|
|
|
119
190
|
// Other Alt+key — ignore
|
|
120
191
|
continue;
|
|
121
192
|
}
|
|
193
|
+
// ── Bracket paste: accumulate into side buffer ─────
|
|
194
|
+
if (this.inPaste) {
|
|
195
|
+
if (ch === "\r") {
|
|
196
|
+
i++;
|
|
197
|
+
continue;
|
|
198
|
+
} // skip CR (CR+LF → just LF)
|
|
199
|
+
this.pasteAccum += ch;
|
|
200
|
+
i++;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
122
203
|
// ── Control characters ──────────────────────────────
|
|
123
204
|
if (ch.charCodeAt(0) < 0x20 || ch === "\x7f") {
|
|
124
205
|
const action = this.handleControl(ch);
|
|
@@ -128,7 +209,7 @@ export class LineEditor {
|
|
|
128
209
|
continue;
|
|
129
210
|
}
|
|
130
211
|
// ── Printable character ─────────────────────────────
|
|
131
|
-
this.
|
|
212
|
+
this._buf = this._buf.slice(0, this.cursor) + ch + this._buf.slice(this.cursor);
|
|
132
213
|
this.cursor++;
|
|
133
214
|
actions.push({ action: "changed" });
|
|
134
215
|
i++;
|
|
@@ -148,9 +229,13 @@ export class LineEditor {
|
|
|
148
229
|
return wasBarEscape ? [{ action: "cancel" }] : [];
|
|
149
230
|
}
|
|
150
231
|
clear() {
|
|
151
|
-
this.
|
|
232
|
+
this._buf = "";
|
|
152
233
|
this.cursor = 0;
|
|
153
234
|
this.pendingSeq = "";
|
|
235
|
+
this.inPaste = false;
|
|
236
|
+
this.pasteAccum = "";
|
|
237
|
+
this.pastes.clear();
|
|
238
|
+
this.pasteCounter = 0;
|
|
154
239
|
this.historyIndex = -1;
|
|
155
240
|
this.savedBuffer = "";
|
|
156
241
|
}
|
|
@@ -171,11 +256,10 @@ export class LineEditor {
|
|
|
171
256
|
if (this.historyIndex + 1 >= this.history.length)
|
|
172
257
|
return null;
|
|
173
258
|
if (this.historyIndex === -1) {
|
|
174
|
-
this.savedBuffer = this.
|
|
259
|
+
this.savedBuffer = this.text; // save resolved current input
|
|
175
260
|
}
|
|
176
261
|
this.historyIndex++;
|
|
177
|
-
this.
|
|
178
|
-
this.cursor = this.buffer.length;
|
|
262
|
+
this.setText(this.history[this.historyIndex]);
|
|
179
263
|
return { action: "changed" };
|
|
180
264
|
}
|
|
181
265
|
/** Navigate to a more recent history entry. Returns changed action or null. */
|
|
@@ -184,12 +268,11 @@ export class LineEditor {
|
|
|
184
268
|
return null;
|
|
185
269
|
this.historyIndex--;
|
|
186
270
|
if (this.historyIndex === -1) {
|
|
187
|
-
this.
|
|
271
|
+
this.setText(this.savedBuffer);
|
|
188
272
|
}
|
|
189
273
|
else {
|
|
190
|
-
this.
|
|
274
|
+
this.setText(this.history[this.historyIndex]);
|
|
191
275
|
}
|
|
192
|
-
this.cursor = this.buffer.length;
|
|
193
276
|
return { action: "changed" };
|
|
194
277
|
}
|
|
195
278
|
// ── Key bindings ────────────────────────────────────────────
|
|
@@ -198,17 +281,17 @@ export class LineEditor {
|
|
|
198
281
|
// characters and kitty protocol sequences resolve to a key name
|
|
199
282
|
// and look it up here. To add a binding, add one entry.
|
|
200
283
|
bindings = {
|
|
201
|
-
"enter": () => ({ action: "submit", buffer: this.
|
|
284
|
+
"enter": () => ({ action: "submit", buffer: this.text }),
|
|
202
285
|
"ctrl+c": () => ({ action: "cancel" }),
|
|
203
286
|
"tab": () => ({ action: "tab" }),
|
|
204
287
|
"backspace": () => this.deleteBackward(),
|
|
205
|
-
"ctrl+d": () => this.
|
|
288
|
+
"ctrl+d": () => this._buf.length === 0 ? { action: "delete-empty" } : this.deleteForward(),
|
|
206
289
|
"ctrl+a": () => this.moveTo(0),
|
|
207
|
-
"ctrl+e": () => this.moveTo(this.
|
|
290
|
+
"ctrl+e": () => this.moveTo(this._buf.length),
|
|
208
291
|
"ctrl+b": () => this.moveTo(this.cursor - 1),
|
|
209
292
|
"ctrl+f": () => this.moveTo(this.cursor + 1),
|
|
210
293
|
"ctrl+u": () => this.deleteRange(0, this.cursor),
|
|
211
|
-
"ctrl+k": () => this.deleteRange(this.cursor, this.
|
|
294
|
+
"ctrl+k": () => this.deleteRange(this.cursor, this._buf.length),
|
|
212
295
|
"ctrl+w": () => this.deleteWordBackward() ? { action: "changed" } : null,
|
|
213
296
|
"alt+f": () => this.wordForward() ? { action: "changed" } : null,
|
|
214
297
|
"alt+b": () => this.wordBackward() ? { action: "changed" } : null,
|
|
@@ -259,36 +342,51 @@ export class LineEditor {
|
|
|
259
342
|
}
|
|
260
343
|
// ── Editing primitives ─────────────────────────────────────
|
|
261
344
|
insertAt(ch) {
|
|
262
|
-
this.
|
|
345
|
+
this._buf = this._buf.slice(0, this.cursor) + ch + this._buf.slice(this.cursor);
|
|
263
346
|
this.cursor++;
|
|
264
347
|
return { action: "changed" };
|
|
265
348
|
}
|
|
266
349
|
moveTo(pos) {
|
|
267
|
-
const clamped = Math.max(0, Math.min(pos, this.
|
|
350
|
+
const clamped = Math.max(0, Math.min(pos, this._buf.length));
|
|
268
351
|
if (clamped === this.cursor)
|
|
269
352
|
return null;
|
|
270
353
|
this.cursor = clamped;
|
|
271
354
|
return { action: "changed" };
|
|
272
355
|
}
|
|
273
356
|
deleteBackward() {
|
|
274
|
-
if (this.
|
|
357
|
+
if (this._buf.length === 0)
|
|
275
358
|
return { action: "delete-empty" };
|
|
276
359
|
if (this.cursor <= 0)
|
|
277
360
|
return null;
|
|
278
|
-
|
|
361
|
+
// If deleting a paste placeholder, also remove the paste entry
|
|
362
|
+
const deleted = this._buf[this.cursor - 1];
|
|
363
|
+
if (isPUA(deleted)) {
|
|
364
|
+
this.pastes.delete(deleted.charCodeAt(0) - PUA_BASE);
|
|
365
|
+
}
|
|
366
|
+
this._buf = this._buf.slice(0, this.cursor - 1) + this._buf.slice(this.cursor);
|
|
279
367
|
this.cursor--;
|
|
280
368
|
return { action: "changed" };
|
|
281
369
|
}
|
|
282
370
|
deleteForward() {
|
|
283
|
-
if (this.cursor >= this.
|
|
371
|
+
if (this.cursor >= this._buf.length)
|
|
284
372
|
return null;
|
|
285
|
-
|
|
373
|
+
const deleted = this._buf[this.cursor];
|
|
374
|
+
if (isPUA(deleted)) {
|
|
375
|
+
this.pastes.delete(deleted.charCodeAt(0) - PUA_BASE);
|
|
376
|
+
}
|
|
377
|
+
this._buf = this._buf.slice(0, this.cursor) + this._buf.slice(this.cursor + 1);
|
|
286
378
|
return { action: "changed" };
|
|
287
379
|
}
|
|
288
380
|
deleteRange(start, end) {
|
|
289
381
|
if (start >= end)
|
|
290
382
|
return null;
|
|
291
|
-
|
|
383
|
+
// Clean up any paste entries in the deleted range
|
|
384
|
+
for (let k = start; k < end; k++) {
|
|
385
|
+
const ch = this._buf[k];
|
|
386
|
+
if (isPUA(ch))
|
|
387
|
+
this.pastes.delete(ch.charCodeAt(0) - PUA_BASE);
|
|
388
|
+
}
|
|
389
|
+
this._buf = this._buf.slice(0, start) + this._buf.slice(end);
|
|
292
390
|
this.cursor = start;
|
|
293
391
|
return { action: "changed" };
|
|
294
392
|
}
|
|
@@ -326,7 +424,7 @@ export class LineEditor {
|
|
|
326
424
|
actions.push({ action: "changed" });
|
|
327
425
|
}
|
|
328
426
|
else {
|
|
329
|
-
if (this.cursor < this.
|
|
427
|
+
if (this.cursor < this._buf.length) {
|
|
330
428
|
this.cursor++;
|
|
331
429
|
actions.push({ action: "changed" });
|
|
332
430
|
}
|
|
@@ -351,8 +449,8 @@ export class LineEditor {
|
|
|
351
449
|
}
|
|
352
450
|
break;
|
|
353
451
|
case "F": // End
|
|
354
|
-
if (this.cursor < this.
|
|
355
|
-
this.cursor = this.
|
|
452
|
+
if (this.cursor < this._buf.length) {
|
|
453
|
+
this.cursor = this._buf.length;
|
|
356
454
|
actions.push({ action: "changed" });
|
|
357
455
|
}
|
|
358
456
|
break;
|
|
@@ -365,11 +463,39 @@ export class LineEditor {
|
|
|
365
463
|
actions.push(action);
|
|
366
464
|
break;
|
|
367
465
|
}
|
|
368
|
-
case "~": // Extended keys: Delete (3~), etc.
|
|
466
|
+
case "~": // Extended keys: Delete (3~), bracket paste (200~/201~), etc.
|
|
369
467
|
if (params === "3") {
|
|
370
468
|
// Delete key: delete char under cursor
|
|
371
|
-
if (this.cursor < this.
|
|
372
|
-
|
|
469
|
+
if (this.cursor < this._buf.length) {
|
|
470
|
+
const deleted = this._buf[this.cursor];
|
|
471
|
+
if (isPUA(deleted))
|
|
472
|
+
this.pastes.delete(deleted.charCodeAt(0) - PUA_BASE);
|
|
473
|
+
this._buf = this._buf.slice(0, this.cursor) + this._buf.slice(this.cursor + 1);
|
|
474
|
+
actions.push({ action: "changed" });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else if (params === "200") {
|
|
478
|
+
this.inPaste = true;
|
|
479
|
+
this.pasteAccum = "";
|
|
480
|
+
}
|
|
481
|
+
else if (params === "201") {
|
|
482
|
+
this.inPaste = false;
|
|
483
|
+
if (this.pasteAccum) {
|
|
484
|
+
const lines = this.pasteAccum.split("\n");
|
|
485
|
+
if (lines.length <= 1) {
|
|
486
|
+
// Single-line paste — inline directly
|
|
487
|
+
this._buf = this._buf.slice(0, this.cursor) + this.pasteAccum + this._buf.slice(this.cursor);
|
|
488
|
+
this.cursor += this.pasteAccum.length;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Multi-line paste — store and insert placeholder
|
|
492
|
+
const id = this.pasteCounter++;
|
|
493
|
+
this.pastes.set(id, this.pasteAccum);
|
|
494
|
+
const placeholder = String.fromCharCode(PUA_BASE + id);
|
|
495
|
+
this._buf = this._buf.slice(0, this.cursor) + placeholder + this._buf.slice(this.cursor);
|
|
496
|
+
this.cursor++;
|
|
497
|
+
}
|
|
498
|
+
this.pasteAccum = "";
|
|
373
499
|
actions.push({ action: "changed" });
|
|
374
500
|
}
|
|
375
501
|
}
|
|
@@ -383,11 +509,11 @@ export class LineEditor {
|
|
|
383
509
|
if (this.cursor === 0)
|
|
384
510
|
return false;
|
|
385
511
|
let pos = this.cursor;
|
|
386
|
-
// Skip spaces
|
|
387
|
-
while (pos > 0 && this.
|
|
512
|
+
// Skip PUA placeholders and spaces
|
|
513
|
+
while (pos > 0 && (this._buf[pos - 1] === " " || isPUA(this._buf[pos - 1])))
|
|
388
514
|
pos--;
|
|
389
515
|
// Skip word chars
|
|
390
|
-
while (pos > 0 && this.
|
|
516
|
+
while (pos > 0 && this._buf[pos - 1] !== " " && !isPUA(this._buf[pos - 1]))
|
|
391
517
|
pos--;
|
|
392
518
|
if (pos === this.cursor)
|
|
393
519
|
return false;
|
|
@@ -395,14 +521,14 @@ export class LineEditor {
|
|
|
395
521
|
return true;
|
|
396
522
|
}
|
|
397
523
|
wordForward() {
|
|
398
|
-
if (this.cursor >= this.
|
|
524
|
+
if (this.cursor >= this._buf.length)
|
|
399
525
|
return false;
|
|
400
526
|
let pos = this.cursor;
|
|
401
|
-
// Skip word chars
|
|
402
|
-
while (pos < this.
|
|
527
|
+
// Skip word chars and PUA placeholders
|
|
528
|
+
while (pos < this._buf.length && this._buf[pos] !== " " && !isPUA(this._buf[pos]))
|
|
403
529
|
pos++;
|
|
404
|
-
// Skip spaces
|
|
405
|
-
while (pos < this.
|
|
530
|
+
// Skip spaces and PUA
|
|
531
|
+
while (pos < this._buf.length && (this._buf[pos] === " " || isPUA(this._buf[pos])))
|
|
406
532
|
pos++;
|
|
407
533
|
if (pos === this.cursor)
|
|
408
534
|
return false;
|
|
@@ -414,15 +540,27 @@ export class LineEditor {
|
|
|
414
540
|
return false;
|
|
415
541
|
const start = this.cursor;
|
|
416
542
|
this.wordBackward();
|
|
417
|
-
|
|
543
|
+
// Clean up paste entries
|
|
544
|
+
for (let k = this.cursor; k < start; k++) {
|
|
545
|
+
const ch = this._buf[k];
|
|
546
|
+
if (isPUA(ch))
|
|
547
|
+
this.pastes.delete(ch.charCodeAt(0) - PUA_BASE);
|
|
548
|
+
}
|
|
549
|
+
this._buf = this._buf.slice(0, this.cursor) + this._buf.slice(start);
|
|
418
550
|
return true;
|
|
419
551
|
}
|
|
420
552
|
deleteWordForward() {
|
|
421
|
-
if (this.cursor >= this.
|
|
553
|
+
if (this.cursor >= this._buf.length)
|
|
422
554
|
return false;
|
|
423
555
|
const start = this.cursor;
|
|
424
556
|
this.wordForward();
|
|
425
|
-
|
|
557
|
+
// Clean up paste entries
|
|
558
|
+
for (let k = start; k < this.cursor; k++) {
|
|
559
|
+
const ch = this._buf[k];
|
|
560
|
+
if (isPUA(ch))
|
|
561
|
+
this.pastes.delete(ch.charCodeAt(0) - PUA_BASE);
|
|
562
|
+
}
|
|
563
|
+
this._buf = this._buf.slice(0, start) + this._buf.slice(this.cursor);
|
|
426
564
|
this.cursor = start;
|
|
427
565
|
return true;
|
|
428
566
|
}
|
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 {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive UI primitive for tools.
|
|
3
|
+
*
|
|
4
|
+
* Gives a tool imperative control over rendering and input on the active
|
|
5
|
+
* surface. The tool provides render() + handleInput(), the primitive
|
|
6
|
+
* handles surface writing, input interception, shell pause/unpause,
|
|
7
|
+
* and cleanup.
|
|
8
|
+
*/
|
|
9
|
+
import type { EventBus } from "../event-bus.js";
|
|
10
|
+
import type { RenderSurface } from "./compositor.js";
|
|
11
|
+
import type { ToolUI } from "../agent/types.js";
|
|
12
|
+
export declare function createToolUI(bus: EventBus, surface: RenderSurface): ToolUI;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Clear N lines above the cursor. */
|
|
2
|
+
function clearLines(surface, count) {
|
|
3
|
+
for (let i = 0; i < count; i++) {
|
|
4
|
+
surface.write("\x1b[A\x1b[2K");
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function createToolUI(bus, surface) {
|
|
8
|
+
return {
|
|
9
|
+
custom(session) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
let prevLineCount = 0;
|
|
12
|
+
let finished = false;
|
|
13
|
+
const done = (result) => {
|
|
14
|
+
if (finished)
|
|
15
|
+
return;
|
|
16
|
+
finished = true;
|
|
17
|
+
clearLines(surface, prevLineCount);
|
|
18
|
+
bus.offPipe("input:intercept", interceptor);
|
|
19
|
+
bus.emit("shell:stdout-hide", {});
|
|
20
|
+
bus.emit("tool:interactive-end", {});
|
|
21
|
+
session.onUnmount?.();
|
|
22
|
+
resolve(result);
|
|
23
|
+
};
|
|
24
|
+
const render = () => {
|
|
25
|
+
if (finished)
|
|
26
|
+
return;
|
|
27
|
+
clearLines(surface, prevLineCount);
|
|
28
|
+
const lines = session.render(surface.columns);
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
surface.writeLine(line);
|
|
31
|
+
}
|
|
32
|
+
prevLineCount = lines.length;
|
|
33
|
+
};
|
|
34
|
+
const interceptor = (payload) => {
|
|
35
|
+
if (finished)
|
|
36
|
+
return payload;
|
|
37
|
+
// Let Ctrl+C through for agent cancellation
|
|
38
|
+
if (payload.data === "\x03")
|
|
39
|
+
return payload;
|
|
40
|
+
session.handleInput(payload.data, done);
|
|
41
|
+
render();
|
|
42
|
+
return { ...payload, consumed: true };
|
|
43
|
+
};
|
|
44
|
+
// Setup
|
|
45
|
+
bus.emit("tool:interactive-start", {});
|
|
46
|
+
bus.emit("shell:stdout-show", {});
|
|
47
|
+
bus.onPipe("input:intercept", interceptor);
|
|
48
|
+
session.onMount?.(() => render());
|
|
49
|
+
render();
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|