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.
Files changed (74) hide show
  1. package/README.md +25 -34
  2. package/dist/agent/agent-loop.d.ts +29 -6
  3. package/dist/agent/agent-loop.js +177 -59
  4. package/dist/agent/conversation-state.d.ts +3 -1
  5. package/dist/agent/conversation-state.js +6 -2
  6. package/dist/agent/nuclear-form.js +5 -4
  7. package/dist/agent/system-prompt.d.ts +4 -5
  8. package/dist/agent/system-prompt.js +12 -28
  9. package/dist/{token-budget.js → agent/token-budget.js} +1 -1
  10. package/dist/agent/tool-protocol.d.ts +83 -0
  11. package/dist/agent/tool-protocol.js +386 -0
  12. package/dist/agent/types.d.ts +21 -1
  13. package/dist/core.d.ts +7 -7
  14. package/dist/core.js +76 -194
  15. package/dist/event-bus.d.ts +26 -0
  16. package/dist/event-bus.js +20 -1
  17. package/dist/extension-loader.d.ts +5 -0
  18. package/dist/extension-loader.js +104 -17
  19. package/dist/extensions/agent-backend.d.ts +13 -0
  20. package/dist/extensions/agent-backend.js +167 -0
  21. package/dist/extensions/command-suggest.d.ts +3 -3
  22. package/dist/extensions/command-suggest.js +4 -3
  23. package/dist/extensions/index.d.ts +19 -0
  24. package/dist/extensions/index.js +25 -0
  25. package/dist/extensions/slash-commands.d.ts +1 -1
  26. package/dist/extensions/slash-commands.js +16 -1
  27. package/dist/extensions/terminal-buffer.d.ts +1 -1
  28. package/dist/extensions/terminal-buffer.js +13 -4
  29. package/dist/extensions/tui-renderer.js +63 -43
  30. package/dist/index.js +14 -20
  31. package/dist/settings.d.ts +6 -0
  32. package/dist/settings.js +4 -1
  33. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
  34. package/dist/{input-handler.js → shell/input-handler.js} +60 -43
  35. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  36. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  37. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  38. package/dist/{shell.js → shell/shell.js} +20 -6
  39. package/dist/types.d.ts +49 -10
  40. package/dist/utils/compositor.d.ts +62 -0
  41. package/dist/utils/compositor.js +88 -0
  42. package/dist/utils/diff-renderer.js +92 -4
  43. package/dist/utils/floating-panel.d.ts +2 -0
  44. package/dist/utils/floating-panel.js +30 -14
  45. package/dist/utils/handler-registry.d.ts +26 -10
  46. package/dist/utils/handler-registry.js +52 -16
  47. package/dist/utils/line-editor.d.ts +23 -3
  48. package/dist/utils/line-editor.js +180 -42
  49. package/dist/utils/markdown.d.ts +1 -0
  50. package/dist/utils/markdown.js +1 -1
  51. package/dist/utils/message-utils.d.ts +35 -0
  52. package/dist/utils/message-utils.js +75 -0
  53. package/dist/utils/terminal-buffer.d.ts +5 -1
  54. package/dist/utils/terminal-buffer.js +18 -2
  55. package/dist/utils/tool-interactive.d.ts +12 -0
  56. package/dist/utils/tool-interactive.js +53 -0
  57. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  58. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  59. package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
  60. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  61. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  62. package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
  63. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  64. package/examples/extensions/interactive-prompts.ts +82 -110
  65. package/examples/extensions/overlay-agent.ts +84 -38
  66. package/examples/extensions/peer-mesh.ts +450 -0
  67. package/examples/extensions/questionnaire.ts +249 -0
  68. package/examples/extensions/tmux-pane.ts +307 -0
  69. package/examples/extensions/web-access.ts +327 -0
  70. package/package.json +9 -1
  71. package/dist/extensions/overlay-agent.d.ts +0 -14
  72. package/dist/extensions/overlay-agent.js +0 -147
  73. package/examples/extensions/terminal-buffer.ts +0 -184
  74. /package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +0 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Compositor — routes named render streams to surfaces.
3
+ *
4
+ * Components write to named streams ("agent", "query", "status").
5
+ * The compositor decides where each stream actually goes based on
6
+ * the current routing table. Extensions override routing with
7
+ * `redirect()` to capture output (e.g. overlay panels).
8
+ *
9
+ * Streams are hierarchical: "agent:diff" falls back to "agent" if
10
+ * no override or default is registered for "agent:diff" specifically.
11
+ * This enables fine-grained interception — redirect just diffs into
12
+ * a panel, or just a subagent's output ("agent:sub:abc123"), while
13
+ * everything else flows to the parent stream's surface.
14
+ *
15
+ * // tui-renderer registers default surfaces
16
+ * compositor.setDefault("agent", stdoutSurface);
17
+ *
18
+ * // overlay-agent redirects when active
19
+ * const restore = compositor.redirect("agent", panelSurface);
20
+ * // ... later ...
21
+ * restore(); // back to stdout
22
+ *
23
+ * // fine-grained: redirect only diffs to a viewer panel
24
+ * compositor.redirect("agent:diff", diffPanelSurface);
25
+ * // "agent:text", "agent:tool" etc. still go to stdout
26
+ */
27
+ /** Silent sink — drops all output. Used when no surface is registered. */
28
+ export const nullSurface = {
29
+ write() { },
30
+ writeLine() { },
31
+ get columns() { return 80; },
32
+ };
33
+ /** Surface backed by process.stdout. */
34
+ export class StdoutSurface {
35
+ write(text) {
36
+ if (process.stdout.writable) {
37
+ try {
38
+ process.stdout.write(text);
39
+ }
40
+ catch { /* ignore */ }
41
+ }
42
+ }
43
+ writeLine(line) {
44
+ this.write(line + "\n");
45
+ }
46
+ get columns() {
47
+ return process.stdout.columns || 80;
48
+ }
49
+ }
50
+ export class DefaultCompositor {
51
+ defaults = new Map();
52
+ overrides = new Map();
53
+ surface(stream) {
54
+ const stack = this.overrides.get(stream);
55
+ if (stack && stack.length > 0)
56
+ return stack[stack.length - 1];
57
+ if (this.defaults.has(stream))
58
+ return this.defaults.get(stream);
59
+ // Hierarchical fallback: "agent:diff" → "agent"
60
+ const colon = stream.lastIndexOf(":");
61
+ if (colon !== -1)
62
+ return this.surface(stream.slice(0, colon));
63
+ return nullSurface;
64
+ }
65
+ redirect(stream, target) {
66
+ let stack = this.overrides.get(stream);
67
+ if (!stack) {
68
+ stack = [];
69
+ this.overrides.set(stream, stack);
70
+ }
71
+ stack.push(target);
72
+ let restored = false;
73
+ return () => {
74
+ if (restored)
75
+ return;
76
+ restored = true;
77
+ const s = this.overrides.get(stream);
78
+ if (!s)
79
+ return;
80
+ const idx = s.indexOf(target);
81
+ if (idx !== -1)
82
+ s.splice(idx, 1);
83
+ };
84
+ }
85
+ setDefault(stream, target) {
86
+ this.defaults.set(stream, target);
87
+ }
88
+ }
@@ -254,7 +254,7 @@ function renderUnified(diff, opts) {
254
254
  const renderedAsPartOfPair = new Set();
255
255
  for (let i = 0; i < hunk.lines.length; i++) {
256
256
  const line = hunk.lines[i];
257
- const no = String(line.oldNo ?? line.newNo ?? "").padStart(noW);
257
+ const no = String(line.type === "removed" ? (line.oldNo ?? "") : (line.newNo ?? line.oldNo ?? "")).padStart(noW);
258
258
  if (line.type === "context") {
259
259
  const raw = truncateText(line.text, lineTextW);
260
260
  const text = lang ? highlightLine(raw, lang) : raw;
@@ -468,6 +468,91 @@ function truncateText(text, maxWidth) {
468
468
  }
469
469
  return text.slice(0, i) + p.reset + "…";
470
470
  }
471
+ // ── Truncation ──────────────────────────────────────────────────
472
+ /**
473
+ * Trim context lines from hunks so the rendered output fits within a budget.
474
+ * Change lines are never removed — only the surrounding context shrinks.
475
+ */
476
+ function trimHunksToFit(hunks, maxLines) {
477
+ // Count change lines across all hunks
478
+ let changeCount = 0;
479
+ for (const hunk of hunks) {
480
+ for (const line of hunk.lines) {
481
+ if (line.type !== "context")
482
+ changeCount++;
483
+ }
484
+ }
485
+ // Separators between hunks
486
+ const separators = Math.max(0, hunks.length - 1);
487
+ // How many context lines can we afford?
488
+ const contextBudget = Math.max(0, maxLines - changeCount - separators);
489
+ // Count total context to see if trimming is needed
490
+ let totalContext = 0;
491
+ for (const hunk of hunks) {
492
+ for (const line of hunk.lines) {
493
+ if (line.type === "context")
494
+ totalContext++;
495
+ }
496
+ }
497
+ if (totalContext <= contextBudget)
498
+ return hunks;
499
+ // Determine how many context lines to keep per side of each change.
500
+ // Binary-search for the largest per-side context that fits.
501
+ let lo = 0;
502
+ let hi = 3; // original context size from groupHunks
503
+ while (lo < hi) {
504
+ const mid = Math.ceil((lo + hi) / 2);
505
+ if (countContextWithLimit(hunks, mid) <= contextBudget)
506
+ lo = mid;
507
+ else
508
+ hi = mid - 1;
509
+ }
510
+ return rebuildHunks(hunks, lo);
511
+ }
512
+ /** Count how many context lines remain if we keep at most `ctx` per side of each change. */
513
+ function countContextWithLimit(hunks, ctx) {
514
+ let count = 0;
515
+ for (const hunk of hunks) {
516
+ const lines = hunk.lines;
517
+ for (let i = 0; i < lines.length; i++) {
518
+ if (lines[i].type !== "context")
519
+ continue;
520
+ // Keep this context line if it's within `ctx` of any change
521
+ let nearChange = false;
522
+ for (let d = 1; d <= ctx; d++) {
523
+ if ((i - d >= 0 && lines[i - d].type !== "context") ||
524
+ (i + d < lines.length && lines[i + d].type !== "context")) {
525
+ nearChange = true;
526
+ break;
527
+ }
528
+ }
529
+ if (nearChange)
530
+ count++;
531
+ }
532
+ }
533
+ return count;
534
+ }
535
+ /** Rebuild hunks keeping only context lines within `ctx` distance of a change. */
536
+ function rebuildHunks(hunks, ctx) {
537
+ return hunks.map((hunk) => {
538
+ const lines = hunk.lines;
539
+ const kept = [];
540
+ for (let i = 0; i < lines.length; i++) {
541
+ if (lines[i].type !== "context") {
542
+ kept.push(lines[i]);
543
+ continue;
544
+ }
545
+ for (let d = 1; d <= ctx; d++) {
546
+ if ((i - d >= 0 && lines[i - d].type !== "context") ||
547
+ (i + d < lines.length && lines[i + d].type !== "context")) {
548
+ kept.push(lines[i]);
549
+ break;
550
+ }
551
+ }
552
+ }
553
+ return { lines: kept };
554
+ });
555
+ }
471
556
  // ── Public API ───────────────────────────────────────────────────
472
557
  /** Select display mode based on available terminal width. */
473
558
  export function selectMode(width) {
@@ -487,16 +572,19 @@ export function renderDiff(diff, opts) {
487
572
  if (mode === "summary") {
488
573
  return [header, ...renderSummary(diff)];
489
574
  }
575
+ // Trim context lines from hunks if the diff would exceed the budget,
576
+ // so that actual changes are always visible.
577
+ const trimmed = { ...diff, hunks: trimHunksToFit(diff.hunks, maxLines) };
490
578
  let bodyLines;
491
579
  switch (mode) {
492
580
  case "split":
493
- bodyLines = renderSplit(diff, opts);
581
+ bodyLines = renderSplit(trimmed, opts);
494
582
  break;
495
583
  case "unified":
496
- bodyLines = renderUnified(diff, opts);
584
+ bodyLines = renderUnified(trimmed, opts);
497
585
  break;
498
586
  }
499
- // Truncation
587
+ // Final safety net — if still over budget, simple tail truncation.
500
588
  if (bodyLines.length > maxLines) {
501
589
  const overflow = bodyLines.length - maxLines;
502
590
  bodyLines = bodyLines.slice(0, maxLines);
@@ -182,6 +182,8 @@ export declare class FloatingPanel {
182
182
  private ensureBuffer;
183
183
  /** Whether the panel has an active conversation (may be hidden). */
184
184
  get active(): boolean;
185
+ /** Whether the agent is currently processing a query. */
186
+ get processing(): boolean;
185
187
  /** Whether the panel is currently visible on screen. */
186
188
  get visible(): boolean;
187
189
  get terminalBuffer(): TerminalBuffer | null;
@@ -328,6 +328,10 @@ export class FloatingPanel {
328
328
  get active() {
329
329
  return this.phase !== "idle";
330
330
  }
331
+ /** Whether the agent is currently processing a query. */
332
+ get processing() {
333
+ return this.phase === "active";
334
+ }
331
335
  /** Whether the panel is currently visible on screen. */
332
336
  get visible() {
333
337
  return this._visible;
@@ -515,7 +519,7 @@ export class FloatingPanel {
515
519
  this.render();
516
520
  }
517
521
  getInput() {
518
- return this.editor.buffer;
522
+ return this.editor.text;
519
523
  }
520
524
  requestRender() {
521
525
  this.scheduleRender();
@@ -634,7 +638,7 @@ export class FloatingPanel {
634
638
  for (const action of actions) {
635
639
  switch (action.action) {
636
640
  case "submit": {
637
- const query = this.editor.buffer.trim();
641
+ const query = this.editor.text.trim();
638
642
  if (!query) {
639
643
  this.hide();
640
644
  return;
@@ -688,8 +692,8 @@ export class FloatingPanel {
688
692
  width: geo.contentW,
689
693
  height: geo.contentH,
690
694
  phase: this.phase,
691
- inputBuffer: this.editor.buffer,
692
- inputCursor: this.editor.cursor,
695
+ inputBuffer: this.editor.displayText,
696
+ inputCursor: this.editor.displayCursor,
693
697
  scrollOffset: this.scrollOffset,
694
698
  contentLines: this.contentLines,
695
699
  partialLine: this.currentPartialLine,
@@ -752,23 +756,35 @@ export class FloatingPanel {
752
756
  this.resizeHandler = null;
753
757
  }
754
758
  this.suppressNextRedraw = true;
759
+ // Re-check alt screen state: the program we overlaid may have exited
760
+ // (e.g. agent quit vim via terminal_keys) while the panel was active.
761
+ const stillInAltScreen = !this.usedAltScreen && !!this.buffer?.altScreen;
762
+ const programExited = !this.usedAltScreen && !stillInAltScreen;
755
763
  if (this.usedAltScreen) {
756
764
  process.stdout.write("\x1b[?1049l");
757
765
  }
758
- // ncurses's curscr is stale only a real dimension change triggers
759
- // clearok + full repaint (same-size SIGWINCH is a no-op).
760
- const cols = process.stdout.columns || 80;
761
- const rows = process.stdout.rows || 24;
762
- this.bus.emit("shell:pty-resize", { cols, rows: rows - 1 });
763
- setTimeout(() => {
764
- this.bus.emit("shell:pty-resize", { cols, rows });
765
- }, 50);
766
- if (!this.buffer && this.ptyBuffer) {
766
+ // Replay PTY output that arrived while the overlay was active.
767
+ // Without this, commands run by the agent (e.g. user_shell ls)
768
+ // would vanish the alt screen exit restores the saved screen
769
+ // from before the overlay opened, losing any shell output produced
770
+ // during the session.
771
+ if (this.ptyBuffer) {
767
772
  process.stdout.write(this.ptyBuffer);
768
773
  }
769
774
  this.ptyBuffer = "";
770
- this.bus.emit("shell:stdout-hide", {});
771
775
  this.bus.emit("shell:stdout-release", {});
776
+ if (stillInAltScreen || programExited) {
777
+ // Either a TUI app is still running and needs SIGWINCH to repaint,
778
+ // or the overlaid program exited (e.g. agent quit vim) and we
779
+ // discarded its stale buffer — SIGWINCH makes the shell redraw
780
+ // its prompt cleanly.
781
+ const cols = process.stdout.columns || 80;
782
+ const rows = process.stdout.rows || 24;
783
+ this.bus.emit("shell:pty-resize", { cols, rows: rows - 1 });
784
+ setTimeout(() => {
785
+ this.bus.emit("shell:pty-resize", { cols, rows });
786
+ }, 50);
787
+ }
772
788
  }
773
789
  // ── Passthrough rendering ─────────────────────────────────
774
790
  /** Start rendering TerminalBuffer directly (no overlay box). */
@@ -11,27 +11,42 @@
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
  */
19
+ type HandlerFn = (...args: any[]) => any;
20
+ type Advisor = (next: HandlerFn, ...args: any[]) => any;
21
+ /** The subset of HandlerRegistry methods available to extensions. */
22
+ export interface HandlerFunctions {
23
+ define(name: string, fn: (...args: any[]) => any): void;
24
+ advise(name: string, advisor: (next: (...args: any[]) => any, ...args: any[]) => any): () => void;
25
+ call(name: string, ...args: any[]): any;
26
+ }
15
27
  export declare class HandlerRegistry {
16
- private handlers;
28
+ private entries;
17
29
  /**
18
- * Register a named handler. If one already exists, it's replaced.
30
+ * Register a named handler. If one already exists, its base is replaced
31
+ * but existing advisors are preserved.
19
32
  */
20
- define(name: string, fn: (...args: any[]) => any): void;
33
+ define(name: string, fn: HandlerFn): void;
21
34
  /**
22
- * Wrap a named handler with advice. The wrapper receives the
23
- * 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.
24
37
  *
25
- * - Call `next(...args)` to invoke the original (around/before/after)
38
+ * - Call `next(...args)` to invoke the rest of the chain
26
39
  * - Don't call `next` to replace entirely (override)
27
40
  * - Call `next` conditionally to wrap (around)
28
41
  *
29
- * Multiple advisors chain: each wraps the previous one.
30
- * 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.
31
44
  */
32
- advise(name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any): void;
45
+ advise(name: string, advisor: Advisor): () => void;
33
46
  /**
34
- * Call a named handler. Returns undefined if no handler is registered.
47
+ * Call a named handler. Builds the advisor chain on each call:
48
+ * outermost advisor wraps the next, down to the base handler.
49
+ * Returns undefined if no handler is registered.
35
50
  */
36
51
  call(name: string, ...args: any[]): any;
37
52
  /**
@@ -39,3 +54,4 @@ export declare class HandlerRegistry {
39
54
  */
40
55
  has(name: string): boolean;
41
56
  }
57
+ export {};
@@ -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,12 +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;
30
40
  private history;
31
41
  private historyIndex;
32
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;
33
53
  /** Process raw terminal input, return actions for the consumer. */
34
54
  feed(data: string): LineEditAction[];
35
55
  /** Check if there's a pending incomplete escape sequence. */