agent-sh 0.7.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 +28 -33
- package/dist/agent/agent-loop.d.ts +31 -8
- package/dist/agent/agent-loop.js +277 -66
- package/dist/agent/conversation-state.d.ts +41 -9
- package/dist/agent/conversation-state.js +340 -17
- package/dist/agent/history-file.d.ts +36 -0
- package/dist/agent/history-file.js +167 -0
- package/dist/agent/nuclear-form.d.ts +41 -0
- package/dist/agent/nuclear-form.js +176 -0
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +16 -11
- package/dist/agent/token-budget.d.ts +13 -0
- package/dist/agent/token-budget.js +50 -0
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/agent/types.d.ts +21 -1
- package/dist/context-manager.d.ts +0 -1
- package/dist/context-manager.js +5 -110
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -180
- package/dist/event-bus.d.ts +40 -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 +44 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +22 -8
- package/dist/extensions/tui-renderer.js +177 -122
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +25 -2
- package/dist/settings.js +25 -4
- 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} +24 -6
- package/dist/types.d.ts +49 -32
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- 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 +34 -3
- package/dist/utils/floating-panel.js +315 -82
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +32 -3
- package/dist/utils/line-editor.js +218 -36
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +9 -1
- package/dist/utils/terminal-buffer.js +31 -2
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- 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/claude-code-bridge/index.ts +77 -1
- 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/pi-bridge/index.ts +87 -2
- 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 -11
- package/dist/extensions/overlay-agent.js +0 -43
- package/examples/extensions/terminal-buffer.ts +0 -184
|
@@ -11,42 +11,78 @@
|
|
|
11
11
|
* if (lang === "latex") return renderLatex(code);
|
|
12
12
|
* return next(lang, code); // call original
|
|
13
13
|
* });
|
|
14
|
+
*
|
|
15
|
+
* Internally, each handler is stored as a base function plus an ordered
|
|
16
|
+
* list of advisors. `call` builds the chain on invocation, so advisors
|
|
17
|
+
* can be added or removed at any time without closure entanglement.
|
|
14
18
|
*/
|
|
15
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
16
19
|
export class HandlerRegistry {
|
|
17
|
-
|
|
20
|
+
entries = new Map();
|
|
18
21
|
/**
|
|
19
|
-
* Register a named handler. If one already exists,
|
|
22
|
+
* Register a named handler. If one already exists, its base is replaced
|
|
23
|
+
* but existing advisors are preserved.
|
|
20
24
|
*/
|
|
21
25
|
define(name, fn) {
|
|
22
|
-
this.
|
|
26
|
+
const existing = this.entries.get(name);
|
|
27
|
+
if (existing) {
|
|
28
|
+
existing.base = fn;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.entries.set(name, { base: fn, advisors: [] });
|
|
32
|
+
}
|
|
23
33
|
}
|
|
24
34
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
35
|
+
* Add an advisor to a named handler. The advisor receives `next`
|
|
36
|
+
* (the rest of the chain) and all original arguments.
|
|
27
37
|
*
|
|
28
|
-
* - Call `next(...args)` to invoke the
|
|
38
|
+
* - Call `next(...args)` to invoke the rest of the chain
|
|
29
39
|
* - Don't call `next` to replace entirely (override)
|
|
30
40
|
* - Call `next` conditionally to wrap (around)
|
|
31
41
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
42
|
+
* Advisors run outermost-first (last added = outermost).
|
|
43
|
+
* Returns an unadvise function that cleanly removes this advisor.
|
|
34
44
|
*/
|
|
35
|
-
advise(name,
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
advise(name, advisor) {
|
|
46
|
+
let entry = this.entries.get(name);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
entry = { base: (() => undefined), advisors: [] };
|
|
49
|
+
this.entries.set(name, entry);
|
|
50
|
+
}
|
|
51
|
+
entry.advisors.push(advisor);
|
|
52
|
+
let removed = false;
|
|
53
|
+
return () => {
|
|
54
|
+
if (removed)
|
|
55
|
+
return;
|
|
56
|
+
removed = true;
|
|
57
|
+
const e = this.entries.get(name);
|
|
58
|
+
if (!e)
|
|
59
|
+
return;
|
|
60
|
+
const idx = e.advisors.indexOf(advisor);
|
|
61
|
+
if (idx !== -1)
|
|
62
|
+
e.advisors.splice(idx, 1);
|
|
63
|
+
};
|
|
38
64
|
}
|
|
39
65
|
/**
|
|
40
|
-
* Call a named handler.
|
|
66
|
+
* Call a named handler. Builds the advisor chain on each call:
|
|
67
|
+
* outermost advisor wraps the next, down to the base handler.
|
|
68
|
+
* Returns undefined if no handler is registered.
|
|
41
69
|
*/
|
|
42
70
|
call(name, ...args) {
|
|
43
|
-
const
|
|
44
|
-
|
|
71
|
+
const entry = this.entries.get(name);
|
|
72
|
+
if (!entry)
|
|
73
|
+
return undefined;
|
|
74
|
+
// Build chain: base ← advisor[0] ← advisor[1] ← ... ← advisor[n-1]
|
|
75
|
+
let fn = entry.base;
|
|
76
|
+
for (const advisor of entry.advisors) {
|
|
77
|
+
const next = fn;
|
|
78
|
+
fn = (...a) => advisor(next, ...a);
|
|
79
|
+
}
|
|
80
|
+
return fn(...args);
|
|
45
81
|
}
|
|
46
82
|
/**
|
|
47
83
|
* Check if a named handler exists.
|
|
48
84
|
*/
|
|
49
85
|
has(name) {
|
|
50
|
-
return this.
|
|
86
|
+
return this.entries.has(name);
|
|
51
87
|
}
|
|
52
88
|
}
|
|
@@ -2,8 +2,14 @@
|
|
|
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
|
export type LineEditAction = {
|
|
9
15
|
action: "changed";
|
|
@@ -24,9 +30,26 @@ export type LineEditAction = {
|
|
|
24
30
|
action: "arrow-down";
|
|
25
31
|
};
|
|
26
32
|
export declare class LineEditor {
|
|
27
|
-
|
|
33
|
+
private _buf;
|
|
28
34
|
cursor: number;
|
|
29
35
|
private pendingSeq;
|
|
36
|
+
private inPaste;
|
|
37
|
+
private pasteAccum;
|
|
38
|
+
private pastes;
|
|
39
|
+
private pasteCounter;
|
|
40
|
+
private history;
|
|
41
|
+
private historyIndex;
|
|
42
|
+
private savedBuffer;
|
|
43
|
+
/** Resolved text — paste placeholders expanded. For submit, history, logic. */
|
|
44
|
+
get text(): string;
|
|
45
|
+
/** Display text — paste placeholders replaced with labels. For rendering. */
|
|
46
|
+
get displayText(): string;
|
|
47
|
+
/** Cursor position mapped to display-text coordinates. */
|
|
48
|
+
get displayCursor(): number;
|
|
49
|
+
/** Number of logical positions in the buffer. */
|
|
50
|
+
get length(): number;
|
|
51
|
+
/** Replace buffer content. Clears paste attachments. */
|
|
52
|
+
setText(value: string): void;
|
|
30
53
|
/** Process raw terminal input, return actions for the consumer. */
|
|
31
54
|
feed(data: string): LineEditAction[];
|
|
32
55
|
/** Check if there's a pending incomplete escape sequence. */
|
|
@@ -34,6 +57,12 @@ export declare class LineEditor {
|
|
|
34
57
|
/** Flush a pending sequence — treat bare \x1b as cancel, discard incomplete CSI. */
|
|
35
58
|
flushPendingEscape(): LineEditAction[];
|
|
36
59
|
clear(): void;
|
|
60
|
+
/** Add a line to history (most recent first). */
|
|
61
|
+
pushHistory(line: string): void;
|
|
62
|
+
/** Navigate to a previous history entry. Returns changed action or null. */
|
|
63
|
+
historyBack(): LineEditAction | null;
|
|
64
|
+
/** Navigate to a more recent history entry. Returns changed action or null. */
|
|
65
|
+
historyForward(): LineEditAction | null;
|
|
37
66
|
private readonly bindings;
|
|
38
67
|
/** Resolve a key name from the bindings table and execute it. */
|
|
39
68
|
private dispatch;
|
|
@@ -2,18 +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;
|
|
35
|
+
// ── History ──────────────────────────────────────────────────
|
|
36
|
+
history = [];
|
|
37
|
+
historyIndex = -1; // -1 = current input, 0..N = history entries (newest first)
|
|
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 ────────────────────────────────────────
|
|
17
92
|
/** Process raw terminal input, return actions for the consumer. */
|
|
18
93
|
feed(data) {
|
|
19
94
|
// If we had a pending incomplete escape sequence, prepend it
|
|
@@ -64,7 +139,7 @@ export class LineEditor {
|
|
|
64
139
|
actions.push({ action: "arrow-down" });
|
|
65
140
|
break;
|
|
66
141
|
case "C":
|
|
67
|
-
if (this.cursor < this.
|
|
142
|
+
if (this.cursor < this._buf.length) {
|
|
68
143
|
this.cursor++;
|
|
69
144
|
actions.push({ action: "changed" });
|
|
70
145
|
}
|
|
@@ -82,8 +157,8 @@ export class LineEditor {
|
|
|
82
157
|
}
|
|
83
158
|
break;
|
|
84
159
|
case "F": // End
|
|
85
|
-
if (this.cursor < this.
|
|
86
|
-
this.cursor = this.
|
|
160
|
+
if (this.cursor < this._buf.length) {
|
|
161
|
+
this.cursor = this._buf.length;
|
|
87
162
|
actions.push({ action: "changed" });
|
|
88
163
|
}
|
|
89
164
|
break;
|
|
@@ -115,6 +190,16 @@ export class LineEditor {
|
|
|
115
190
|
// Other Alt+key — ignore
|
|
116
191
|
continue;
|
|
117
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
|
+
}
|
|
118
203
|
// ── Control characters ──────────────────────────────
|
|
119
204
|
if (ch.charCodeAt(0) < 0x20 || ch === "\x7f") {
|
|
120
205
|
const action = this.handleControl(ch);
|
|
@@ -124,7 +209,7 @@ export class LineEditor {
|
|
|
124
209
|
continue;
|
|
125
210
|
}
|
|
126
211
|
// ── Printable character ─────────────────────────────
|
|
127
|
-
this.
|
|
212
|
+
this._buf = this._buf.slice(0, this.cursor) + ch + this._buf.slice(this.cursor);
|
|
128
213
|
this.cursor++;
|
|
129
214
|
actions.push({ action: "changed" });
|
|
130
215
|
i++;
|
|
@@ -144,9 +229,51 @@ export class LineEditor {
|
|
|
144
229
|
return wasBarEscape ? [{ action: "cancel" }] : [];
|
|
145
230
|
}
|
|
146
231
|
clear() {
|
|
147
|
-
this.
|
|
232
|
+
this._buf = "";
|
|
148
233
|
this.cursor = 0;
|
|
149
234
|
this.pendingSeq = "";
|
|
235
|
+
this.inPaste = false;
|
|
236
|
+
this.pasteAccum = "";
|
|
237
|
+
this.pastes.clear();
|
|
238
|
+
this.pasteCounter = 0;
|
|
239
|
+
this.historyIndex = -1;
|
|
240
|
+
this.savedBuffer = "";
|
|
241
|
+
}
|
|
242
|
+
/** Add a line to history (most recent first). */
|
|
243
|
+
pushHistory(line) {
|
|
244
|
+
if (!line.trim())
|
|
245
|
+
return;
|
|
246
|
+
// Deduplicate: remove if already at top
|
|
247
|
+
if (this.history.length > 0 && this.history[0] === line)
|
|
248
|
+
return;
|
|
249
|
+
this.history.unshift(line);
|
|
250
|
+
// Cap history size
|
|
251
|
+
if (this.history.length > 100)
|
|
252
|
+
this.history.pop();
|
|
253
|
+
}
|
|
254
|
+
/** Navigate to a previous history entry. Returns changed action or null. */
|
|
255
|
+
historyBack() {
|
|
256
|
+
if (this.historyIndex + 1 >= this.history.length)
|
|
257
|
+
return null;
|
|
258
|
+
if (this.historyIndex === -1) {
|
|
259
|
+
this.savedBuffer = this.text; // save resolved current input
|
|
260
|
+
}
|
|
261
|
+
this.historyIndex++;
|
|
262
|
+
this.setText(this.history[this.historyIndex]);
|
|
263
|
+
return { action: "changed" };
|
|
264
|
+
}
|
|
265
|
+
/** Navigate to a more recent history entry. Returns changed action or null. */
|
|
266
|
+
historyForward() {
|
|
267
|
+
if (this.historyIndex <= -1)
|
|
268
|
+
return null;
|
|
269
|
+
this.historyIndex--;
|
|
270
|
+
if (this.historyIndex === -1) {
|
|
271
|
+
this.setText(this.savedBuffer);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this.setText(this.history[this.historyIndex]);
|
|
275
|
+
}
|
|
276
|
+
return { action: "changed" };
|
|
150
277
|
}
|
|
151
278
|
// ── Key bindings ────────────────────────────────────────────
|
|
152
279
|
//
|
|
@@ -154,17 +281,17 @@ export class LineEditor {
|
|
|
154
281
|
// characters and kitty protocol sequences resolve to a key name
|
|
155
282
|
// and look it up here. To add a binding, add one entry.
|
|
156
283
|
bindings = {
|
|
157
|
-
"enter": () => ({ action: "submit", buffer: this.
|
|
284
|
+
"enter": () => ({ action: "submit", buffer: this.text }),
|
|
158
285
|
"ctrl+c": () => ({ action: "cancel" }),
|
|
159
286
|
"tab": () => ({ action: "tab" }),
|
|
160
287
|
"backspace": () => this.deleteBackward(),
|
|
161
|
-
"ctrl+d": () => this.
|
|
288
|
+
"ctrl+d": () => this._buf.length === 0 ? { action: "delete-empty" } : this.deleteForward(),
|
|
162
289
|
"ctrl+a": () => this.moveTo(0),
|
|
163
|
-
"ctrl+e": () => this.moveTo(this.
|
|
290
|
+
"ctrl+e": () => this.moveTo(this._buf.length),
|
|
164
291
|
"ctrl+b": () => this.moveTo(this.cursor - 1),
|
|
165
292
|
"ctrl+f": () => this.moveTo(this.cursor + 1),
|
|
166
293
|
"ctrl+u": () => this.deleteRange(0, this.cursor),
|
|
167
|
-
"ctrl+k": () => this.deleteRange(this.cursor, this.
|
|
294
|
+
"ctrl+k": () => this.deleteRange(this.cursor, this._buf.length),
|
|
168
295
|
"ctrl+w": () => this.deleteWordBackward() ? { action: "changed" } : null,
|
|
169
296
|
"alt+f": () => this.wordForward() ? { action: "changed" } : null,
|
|
170
297
|
"alt+b": () => this.wordBackward() ? { action: "changed" } : null,
|
|
@@ -215,36 +342,51 @@ export class LineEditor {
|
|
|
215
342
|
}
|
|
216
343
|
// ── Editing primitives ─────────────────────────────────────
|
|
217
344
|
insertAt(ch) {
|
|
218
|
-
this.
|
|
345
|
+
this._buf = this._buf.slice(0, this.cursor) + ch + this._buf.slice(this.cursor);
|
|
219
346
|
this.cursor++;
|
|
220
347
|
return { action: "changed" };
|
|
221
348
|
}
|
|
222
349
|
moveTo(pos) {
|
|
223
|
-
const clamped = Math.max(0, Math.min(pos, this.
|
|
350
|
+
const clamped = Math.max(0, Math.min(pos, this._buf.length));
|
|
224
351
|
if (clamped === this.cursor)
|
|
225
352
|
return null;
|
|
226
353
|
this.cursor = clamped;
|
|
227
354
|
return { action: "changed" };
|
|
228
355
|
}
|
|
229
356
|
deleteBackward() {
|
|
230
|
-
if (this.
|
|
357
|
+
if (this._buf.length === 0)
|
|
231
358
|
return { action: "delete-empty" };
|
|
232
359
|
if (this.cursor <= 0)
|
|
233
360
|
return null;
|
|
234
|
-
|
|
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);
|
|
235
367
|
this.cursor--;
|
|
236
368
|
return { action: "changed" };
|
|
237
369
|
}
|
|
238
370
|
deleteForward() {
|
|
239
|
-
if (this.cursor >= this.
|
|
371
|
+
if (this.cursor >= this._buf.length)
|
|
240
372
|
return null;
|
|
241
|
-
|
|
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);
|
|
242
378
|
return { action: "changed" };
|
|
243
379
|
}
|
|
244
380
|
deleteRange(start, end) {
|
|
245
381
|
if (start >= end)
|
|
246
382
|
return null;
|
|
247
|
-
|
|
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);
|
|
248
390
|
this.cursor = start;
|
|
249
391
|
return { action: "changed" };
|
|
250
392
|
}
|
|
@@ -282,7 +424,7 @@ export class LineEditor {
|
|
|
282
424
|
actions.push({ action: "changed" });
|
|
283
425
|
}
|
|
284
426
|
else {
|
|
285
|
-
if (this.cursor < this.
|
|
427
|
+
if (this.cursor < this._buf.length) {
|
|
286
428
|
this.cursor++;
|
|
287
429
|
actions.push({ action: "changed" });
|
|
288
430
|
}
|
|
@@ -307,8 +449,8 @@ export class LineEditor {
|
|
|
307
449
|
}
|
|
308
450
|
break;
|
|
309
451
|
case "F": // End
|
|
310
|
-
if (this.cursor < this.
|
|
311
|
-
this.cursor = this.
|
|
452
|
+
if (this.cursor < this._buf.length) {
|
|
453
|
+
this.cursor = this._buf.length;
|
|
312
454
|
actions.push({ action: "changed" });
|
|
313
455
|
}
|
|
314
456
|
break;
|
|
@@ -321,11 +463,39 @@ export class LineEditor {
|
|
|
321
463
|
actions.push(action);
|
|
322
464
|
break;
|
|
323
465
|
}
|
|
324
|
-
case "~": // Extended keys: Delete (3~), etc.
|
|
466
|
+
case "~": // Extended keys: Delete (3~), bracket paste (200~/201~), etc.
|
|
325
467
|
if (params === "3") {
|
|
326
468
|
// Delete key: delete char under cursor
|
|
327
|
-
if (this.cursor < this.
|
|
328
|
-
|
|
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 = "";
|
|
329
499
|
actions.push({ action: "changed" });
|
|
330
500
|
}
|
|
331
501
|
}
|
|
@@ -339,11 +509,11 @@ export class LineEditor {
|
|
|
339
509
|
if (this.cursor === 0)
|
|
340
510
|
return false;
|
|
341
511
|
let pos = this.cursor;
|
|
342
|
-
// Skip spaces
|
|
343
|
-
while (pos > 0 && this.
|
|
512
|
+
// Skip PUA placeholders and spaces
|
|
513
|
+
while (pos > 0 && (this._buf[pos - 1] === " " || isPUA(this._buf[pos - 1])))
|
|
344
514
|
pos--;
|
|
345
515
|
// Skip word chars
|
|
346
|
-
while (pos > 0 && this.
|
|
516
|
+
while (pos > 0 && this._buf[pos - 1] !== " " && !isPUA(this._buf[pos - 1]))
|
|
347
517
|
pos--;
|
|
348
518
|
if (pos === this.cursor)
|
|
349
519
|
return false;
|
|
@@ -351,14 +521,14 @@ export class LineEditor {
|
|
|
351
521
|
return true;
|
|
352
522
|
}
|
|
353
523
|
wordForward() {
|
|
354
|
-
if (this.cursor >= this.
|
|
524
|
+
if (this.cursor >= this._buf.length)
|
|
355
525
|
return false;
|
|
356
526
|
let pos = this.cursor;
|
|
357
|
-
// Skip word chars
|
|
358
|
-
while (pos < this.
|
|
527
|
+
// Skip word chars and PUA placeholders
|
|
528
|
+
while (pos < this._buf.length && this._buf[pos] !== " " && !isPUA(this._buf[pos]))
|
|
359
529
|
pos++;
|
|
360
|
-
// Skip spaces
|
|
361
|
-
while (pos < this.
|
|
530
|
+
// Skip spaces and PUA
|
|
531
|
+
while (pos < this._buf.length && (this._buf[pos] === " " || isPUA(this._buf[pos])))
|
|
362
532
|
pos++;
|
|
363
533
|
if (pos === this.cursor)
|
|
364
534
|
return false;
|
|
@@ -370,15 +540,27 @@ export class LineEditor {
|
|
|
370
540
|
return false;
|
|
371
541
|
const start = this.cursor;
|
|
372
542
|
this.wordBackward();
|
|
373
|
-
|
|
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);
|
|
374
550
|
return true;
|
|
375
551
|
}
|
|
376
552
|
deleteWordForward() {
|
|
377
|
-
if (this.cursor >= this.
|
|
553
|
+
if (this.cursor >= this._buf.length)
|
|
378
554
|
return false;
|
|
379
555
|
const start = this.cursor;
|
|
380
556
|
this.wordForward();
|
|
381
|
-
|
|
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);
|
|
382
564
|
this.cursor = start;
|
|
383
565
|
return true;
|
|
384
566
|
}
|
package/dist/utils/markdown.d.ts
CHANGED
package/dist/utils/markdown.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { visibleLen } from "./ansi.js";
|
|
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.
|
|
@@ -177,7 +177,7 @@ export class MarkdownRenderer {
|
|
|
177
177
|
const colWidths = new Array(numCols).fill(0);
|
|
178
178
|
for (const row of dataRows) {
|
|
179
179
|
for (let c = 0; c < numCols; c++) {
|
|
180
|
-
colWidths[c] = Math.max(colWidths[c], row[c]
|
|
180
|
+
colWidths[c] = Math.max(colWidths[c], visibleLen(row[c]));
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
// Shrink columns proportionally if total exceeds content width
|
|
@@ -201,7 +201,7 @@ export class MarkdownRenderer {
|
|
|
201
201
|
const isHeader = hasHeader && i === 0;
|
|
202
202
|
const cells = row.map((cell, c) => {
|
|
203
203
|
const w = colWidths[c];
|
|
204
|
-
const text = cell
|
|
204
|
+
const text = visibleLen(cell) > w ? truncateToWidth(cell, w) : padEndToWidth(cell, w);
|
|
205
205
|
return isHeader ? `${p.bold}${text}${p.reset}` : text;
|
|
206
206
|
});
|
|
207
207
|
this.writeLine(`${p.dim}│${p.reset} ${cells.join(` ${p.dim}│${p.reset} `)} ${p.dim}│${p.reset}`);
|
|
@@ -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[];
|