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.
Files changed (86) hide show
  1. package/README.md +28 -33
  2. package/dist/agent/agent-loop.d.ts +31 -8
  3. package/dist/agent/agent-loop.js +277 -66
  4. package/dist/agent/conversation-state.d.ts +41 -9
  5. package/dist/agent/conversation-state.js +340 -17
  6. package/dist/agent/history-file.d.ts +36 -0
  7. package/dist/agent/history-file.js +167 -0
  8. package/dist/agent/nuclear-form.d.ts +41 -0
  9. package/dist/agent/nuclear-form.js +176 -0
  10. package/dist/agent/system-prompt.d.ts +4 -5
  11. package/dist/agent/system-prompt.js +16 -11
  12. package/dist/agent/token-budget.d.ts +13 -0
  13. package/dist/agent/token-budget.js +50 -0
  14. package/dist/agent/tool-protocol.d.ts +83 -0
  15. package/dist/agent/tool-protocol.js +386 -0
  16. package/dist/agent/tools/user-shell.js +4 -1
  17. package/dist/agent/types.d.ts +21 -1
  18. package/dist/context-manager.d.ts +0 -1
  19. package/dist/context-manager.js +5 -110
  20. package/dist/core.d.ts +7 -7
  21. package/dist/core.js +76 -180
  22. package/dist/event-bus.d.ts +40 -0
  23. package/dist/event-bus.js +20 -1
  24. package/dist/extension-loader.d.ts +5 -0
  25. package/dist/extension-loader.js +104 -17
  26. package/dist/extensions/agent-backend.d.ts +13 -0
  27. package/dist/extensions/agent-backend.js +167 -0
  28. package/dist/extensions/command-suggest.d.ts +3 -3
  29. package/dist/extensions/command-suggest.js +4 -3
  30. package/dist/extensions/index.d.ts +19 -0
  31. package/dist/extensions/index.js +25 -0
  32. package/dist/extensions/slash-commands.d.ts +1 -1
  33. package/dist/extensions/slash-commands.js +44 -1
  34. package/dist/extensions/terminal-buffer.d.ts +1 -1
  35. package/dist/extensions/terminal-buffer.js +22 -8
  36. package/dist/extensions/tui-renderer.js +177 -122
  37. package/dist/index.js +14 -20
  38. package/dist/settings.d.ts +25 -2
  39. package/dist/settings.js +25 -4
  40. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
  41. package/dist/{input-handler.js → shell/input-handler.js} +60 -43
  42. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  43. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  44. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  45. package/dist/{shell.js → shell/shell.js} +24 -6
  46. package/dist/types.d.ts +49 -32
  47. package/dist/utils/ansi.d.ts +10 -0
  48. package/dist/utils/ansi.js +27 -0
  49. package/dist/utils/compositor.d.ts +62 -0
  50. package/dist/utils/compositor.js +88 -0
  51. package/dist/utils/diff-renderer.js +92 -4
  52. package/dist/utils/floating-panel.d.ts +34 -3
  53. package/dist/utils/floating-panel.js +315 -82
  54. package/dist/utils/handler-registry.d.ts +26 -10
  55. package/dist/utils/handler-registry.js +52 -16
  56. package/dist/utils/line-editor.d.ts +32 -3
  57. package/dist/utils/line-editor.js +218 -36
  58. package/dist/utils/markdown.d.ts +1 -0
  59. package/dist/utils/markdown.js +4 -4
  60. package/dist/utils/message-utils.d.ts +35 -0
  61. package/dist/utils/message-utils.js +75 -0
  62. package/dist/utils/terminal-buffer.d.ts +9 -1
  63. package/dist/utils/terminal-buffer.js +31 -2
  64. package/dist/utils/tool-display.d.ts +1 -0
  65. package/dist/utils/tool-display.js +1 -1
  66. package/dist/utils/tool-interactive.d.ts +12 -0
  67. package/dist/utils/tool-interactive.js +53 -0
  68. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  69. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  70. package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
  71. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  72. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  73. package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
  74. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  75. package/examples/extensions/claude-code-bridge/index.ts +77 -1
  76. package/examples/extensions/interactive-prompts.ts +82 -110
  77. package/examples/extensions/overlay-agent.ts +84 -38
  78. package/examples/extensions/peer-mesh.ts +450 -0
  79. package/examples/extensions/pi-bridge/index.ts +87 -2
  80. package/examples/extensions/questionnaire.ts +249 -0
  81. package/examples/extensions/tmux-pane.ts +307 -0
  82. package/examples/extensions/web-access.ts +327 -0
  83. package/package.json +9 -1
  84. package/dist/extensions/overlay-agent.d.ts +0 -11
  85. package/dist/extensions/overlay-agent.js +0 -43
  86. 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
- handlers = new Map();
20
+ entries = new Map();
18
21
  /**
19
- * Register a named handler. If one already exists, it's replaced.
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.handlers.set(name, fn);
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
- * Wrap a named handler with advice. The wrapper receives the
26
- * previous handler as `next` and all original arguments.
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 original (around/before/after)
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
- * Multiple advisors chain: each wraps the previous one.
33
- * If no handler exists yet, `next` is a no-op.
42
+ * Advisors run outermost-first (last added = outermost).
43
+ * Returns an unadvise function that cleanly removes this advisor.
34
44
  */
35
- advise(name, wrapper) {
36
- const original = this.handlers.get(name) ?? (() => undefined);
37
- this.handlers.set(name, (...args) => wrapper(original, ...args));
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. Returns undefined if no handler is registered.
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 fn = this.handlers.get(name);
44
- return fn?.(...args);
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.handlers.has(name);
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. Buffer and
6
- * cursor state are public for rendering.
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
- buffer: string;
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. Buffer and
6
- * cursor state are public for rendering.
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
- buffer = "";
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.buffer.length) {
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.buffer.length) {
86
- this.cursor = this.buffer.length;
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.buffer = this.buffer.slice(0, this.cursor) + ch + this.buffer.slice(this.cursor);
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.buffer = "";
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.buffer }),
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.buffer.length === 0 ? { action: "delete-empty" } : this.deleteForward(),
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.buffer.length),
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.buffer.length),
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.buffer = this.buffer.slice(0, this.cursor) + ch + this.buffer.slice(this.cursor);
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.buffer.length));
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.buffer.length === 0)
357
+ if (this._buf.length === 0)
231
358
  return { action: "delete-empty" };
232
359
  if (this.cursor <= 0)
233
360
  return null;
234
- this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
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.buffer.length)
371
+ if (this.cursor >= this._buf.length)
240
372
  return null;
241
- this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
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
- this.buffer = this.buffer.slice(0, start) + this.buffer.slice(end);
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.buffer.length) {
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.buffer.length) {
311
- this.cursor = this.buffer.length;
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.buffer.length) {
328
- this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
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.buffer[pos - 1] === " ")
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.buffer[pos - 1] !== " ")
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.buffer.length)
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.buffer.length && this.buffer[pos] !== " ")
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.buffer.length && this.buffer[pos] === " ")
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
- this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(start);
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.buffer.length)
553
+ if (this.cursor >= this._buf.length)
378
554
  return false;
379
555
  const start = this.cursor;
380
556
  this.wordForward();
381
- this.buffer = this.buffer.slice(0, start) + this.buffer.slice(this.cursor);
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
  }
@@ -1,3 +1,4 @@
1
+ export declare const MAX_CONTENT_WIDTH = 90;
1
2
  /**
2
3
  * Word-wrap a string (which may contain ANSI codes) to a maximum visible width.
3
4
  * Returns an array of lines, each fitting within `maxWidth` visible characters.
@@ -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].length);
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.length > w ? cell.slice(0, w - 1) + "…" : cell.padEnd(w);
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[];