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
package/dist/types.d.ts CHANGED
@@ -1,13 +1,37 @@
1
1
  import type { EventBus } from "./event-bus.js";
2
2
  import type { ContextManager } from "./context-manager.js";
3
- import type { LlmClient } from "./utils/llm-client.js";
4
3
  import type { ColorPalette } from "./utils/palette.js";
5
4
  import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
6
5
  import type { ToolDefinition } from "./agent/types.js";
7
6
  import type { TerminalBuffer } from "./utils/terminal-buffer.js";
8
- import type { FloatingPanel, FloatingPanelConfig } from "./utils/floating-panel.js";
7
+ import type { Compositor } from "./utils/compositor.js";
9
8
  export type { ContentBlock } from "./event-bus.js";
10
9
  export type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
10
+ export type { RenderSurface } from "./utils/compositor.js";
11
+ export interface RemoteSessionOptions {
12
+ /** The surface to render agent output to. */
13
+ surface: import("./utils/compositor.js").RenderSurface;
14
+ /** Suppress response borders (default: true). */
15
+ suppressBorders?: boolean;
16
+ /** Suppress user query box (default: false).
17
+ * True for sessions with their own input (rsplit, overlay).
18
+ * False for sessions where input comes from the main shell (split). */
19
+ suppressQueryBox?: boolean;
20
+ /** Suppress usage stats line (default: true). */
21
+ suppressUsage?: boolean;
22
+ /** Set interactive-session dynamic context (default: false). */
23
+ interactive?: boolean;
24
+ }
25
+ export interface RemoteSession {
26
+ /** Submit a query to the agent from this session. */
27
+ submit(query: string): void;
28
+ /** The surface this session renders to. */
29
+ readonly surface: import("./utils/compositor.js").RenderSurface;
30
+ /** Whether this session is currently active. */
31
+ readonly active: boolean;
32
+ /** Tear down — restores all routing and advisors. */
33
+ close(): void;
34
+ }
11
35
  /** A model entry in the cycling list, optionally tied to a provider. */
12
36
  export interface AgentMode {
13
37
  model: string;
@@ -45,8 +69,8 @@ export interface AgentShellConfig {
45
69
  export interface ExtensionContext {
46
70
  bus: EventBus;
47
71
  contextManager: ContextManager;
48
- /** LLM client for fast-path features (null in ACP mode). */
49
- llmClient: LlmClient | null;
72
+ /** Stable per-instance identifier (4-char hex). */
73
+ readonly instanceId: string;
50
74
  quit: () => void;
51
75
  /** Override color palette slots for theming. */
52
76
  setPalette: (overrides: Partial<ColorPalette>) => void;
@@ -60,12 +84,18 @@ export interface ExtensionContext {
60
84
  registerCommand: (name: string, description: string, handler: (args: string) => Promise<void> | void) => void;
61
85
  /** Register a tool for the built-in agent. No-op when using bridge backends. */
62
86
  registerTool: (tool: ToolDefinition) => void;
87
+ /** Unregister a tool by name. */
88
+ unregisterTool: (name: string) => void;
63
89
  /** Get all registered tools (for subagent tool subsets). Returns [] when using bridge backends. */
64
90
  getTools: () => ToolDefinition[];
91
+ /** Register a named instruction block for the agent's system prompt. */
92
+ registerInstruction: (name: string, text: string) => void;
93
+ /** Remove a named instruction block from the system prompt. */
94
+ removeInstruction: (name: string) => void;
65
95
  /** Register a named handler. */
66
96
  define: (name: string, fn: (...args: any[]) => any) => void;
67
- /** Wrap a named handler. Receives `next` (original) + args. */
68
- advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => void;
97
+ /** Wrap a named handler. Receives `next` (original) + args. Returns an unadvise function. */
98
+ advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => () => void;
69
99
  /** Call a named handler. */
70
100
  call: (name: string, ...args: any[]) => any;
71
101
  /**
@@ -74,11 +104,20 @@ export interface ExtensionContext {
74
104
  */
75
105
  terminalBuffer: TerminalBuffer | null;
76
106
  /**
77
- * Create a floating panel overlay. The panel composites a bordered box
78
- * over the terminal with input routing, dimmed background, and
79
- * handler-based customization.
107
+ * Routes named render streams ("agent", "query", "status") to surfaces.
108
+ * Extensions use `compositor.redirect()` to capture output (e.g. overlay panels).
80
109
  */
81
- createFloatingPanel: (config: FloatingPanelConfig) => FloatingPanel;
110
+ compositor: Compositor;
111
+ /**
112
+ * Create a remote session that routes agent output to a surface and
113
+ * optionally accepts queries. Handles all compositor routing, shell
114
+ * lifecycle advisors, and chrome suppression.
115
+ *
116
+ * const session = ctx.createRemoteSession({ surface, interactive: true });
117
+ * session.submit("what's on screen?");
118
+ * session.close(); // restores everything
119
+ */
120
+ createRemoteSession: (opts: RemoteSessionOptions) => RemoteSession;
82
121
  }
83
122
  /**
84
123
  * Configuration for a registered input mode.
@@ -101,12 +140,6 @@ export interface TerminalSession {
101
140
  done: boolean;
102
141
  resolve?: (value: void) => void;
103
142
  }
104
- export interface ToolCallRecord {
105
- tool: string;
106
- args: Record<string, unknown>;
107
- output: string;
108
- exitCode: number | null;
109
- }
110
143
  export type Exchange = {
111
144
  type: "shell_command";
112
145
  id: number;
@@ -124,20 +157,4 @@ export type Exchange = {
124
157
  id: number;
125
158
  timestamp: number;
126
159
  query: string;
127
- } | {
128
- type: "agent_response";
129
- id: number;
130
- timestamp: number;
131
- response: string;
132
- toolCalls: ToolCallRecord[];
133
- } | {
134
- type: "tool_execution";
135
- id: number;
136
- timestamp: number;
137
- tool: string;
138
- args: Record<string, unknown>;
139
- output: string;
140
- exitCode: number | null;
141
- outputLines: number;
142
- outputBytes: number;
143
160
  };
@@ -11,5 +11,15 @@ export declare const RESET = "\u001B[0m";
11
11
  * Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
12
12
  */
13
13
  export declare function visibleLen(str: string): number;
14
+ /**
15
+ * Truncate a string to fit within `maxWidth` visible columns.
16
+ * Accounts for CJK double-width characters. Appends `…` if truncated.
17
+ */
18
+ export declare function truncateToWidth(str: string, maxWidth: number): string;
19
+ /**
20
+ * Pad a string with spaces to fill `targetWidth` visible columns.
21
+ * Accounts for CJK double-width characters.
22
+ */
23
+ export declare function padEndToWidth(str: string, targetWidth: number): string;
14
24
  /** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
15
25
  export declare function stripAnsi(str: string): string;
@@ -70,6 +70,33 @@ export function visibleLen(str) {
70
70
  }
71
71
  return width;
72
72
  }
73
+ /**
74
+ * Truncate a string to fit within `maxWidth` visible columns.
75
+ * Accounts for CJK double-width characters. Appends `…` if truncated.
76
+ */
77
+ export function truncateToWidth(str, maxWidth) {
78
+ const clean = str.replace(/\x1b\[[^m]*m/g, "");
79
+ let width = 0;
80
+ let i = 0;
81
+ for (const char of clean) {
82
+ const cw = charWidth(char.codePointAt(0) ?? 0);
83
+ if (width + cw > maxWidth - 1) {
84
+ // Need room for the "…" (1 column wide)
85
+ return clean.slice(0, i) + "…";
86
+ }
87
+ width += cw;
88
+ i += char.length;
89
+ }
90
+ return clean;
91
+ }
92
+ /**
93
+ * Pad a string with spaces to fill `targetWidth` visible columns.
94
+ * Accounts for CJK double-width characters.
95
+ */
96
+ export function padEndToWidth(str, targetWidth) {
97
+ const gap = targetWidth - visibleLen(str);
98
+ return gap > 0 ? str + " ".repeat(gap) : str;
99
+ }
73
100
  /** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
74
101
  export function stripAnsi(str) {
75
102
  return str
@@ -0,0 +1,62 @@
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
+ /**
28
+ * A surface accepts rendered output. Stdout is a surface.
29
+ * A floating panel's content area is a surface. A test buffer is a surface.
30
+ */
31
+ export interface RenderSurface {
32
+ /** Raw write — supports \r, partial lines, escape codes. */
33
+ write(text: string): void;
34
+ /** Convenience: write + newline. */
35
+ writeLine(line: string): void;
36
+ /** Available width in columns. */
37
+ readonly columns: number;
38
+ }
39
+ export interface Compositor {
40
+ /** Get the currently active surface for a stream. */
41
+ surface(stream: string): RenderSurface;
42
+ /** Override routing: redirect a stream to a different surface.
43
+ * Returns a restore function that undoes the redirect. */
44
+ redirect(stream: string, target: RenderSurface): () => void;
45
+ /** Register the default surface for a stream. */
46
+ setDefault(stream: string, target: RenderSurface): void;
47
+ }
48
+ /** Silent sink — drops all output. Used when no surface is registered. */
49
+ export declare const nullSurface: RenderSurface;
50
+ /** Surface backed by process.stdout. */
51
+ export declare class StdoutSurface implements RenderSurface {
52
+ write(text: string): void;
53
+ writeLine(line: string): void;
54
+ get columns(): number;
55
+ }
56
+ export declare class DefaultCompositor implements Compositor {
57
+ private defaults;
58
+ private overrides;
59
+ surface(stream: string): RenderSurface;
60
+ redirect(stream: string, target: RenderSurface): () => void;
61
+ setDefault(stream: string, target: RenderSurface): void;
62
+ }
@@ -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);
@@ -20,7 +20,7 @@ export interface FloatingPanelConfig {
20
20
  * Requires @xterm/headless — falls back to blank background if unavailable.
21
21
  */
22
22
  dimBackground?: boolean;
23
- /** Auto-dismiss delay in ms when done (0 = disabled). Default: 0. */
23
+ /** Auto-dismiss delay in ms when done (0 = auto-prompt for follow-up). Default: 0. */
24
24
  autoDismissMs?: number;
25
25
  /** Icon shown before the input cursor. Default: "\u276f". */
26
26
  promptIcon?: string;
@@ -144,6 +144,7 @@ export declare class FloatingPanel {
144
144
  * - `{prefix}:composite-row(content: string, bgLine: string|null, boxLeft: number, boxW: number, cols: number) -> string`
145
145
  * - `{prefix}:submit(query: string) -> void`
146
146
  * - `{prefix}:dismiss() -> void`
147
+ * - `{prefix}:show() -> void`
147
148
  * - `{prefix}:input(data: string) -> boolean`
148
149
  * - `{prefix}:build-row(content: string, width: number) -> string`
149
150
  */
@@ -153,29 +154,49 @@ export declare class FloatingPanel {
153
154
  /** All byte sequences that should be recognized as the trigger key. */
154
155
  private readonly triggerSeqs;
155
156
  private phase;
157
+ private _visible;
158
+ private _passthrough;
156
159
  private editor;
157
160
  private contentLines;
158
161
  private currentPartialLine;
159
162
  private scrollOffset;
163
+ private userScrolled;
160
164
  private title;
161
165
  private footer;
162
166
  private renderTimer;
163
- private autoDismissTimer;
164
167
  private resizeHandler;
165
168
  private prevFrame;
166
169
  private suppressNextRedraw;
170
+ private autoDismissTimer;
167
171
  private ptyBuffer;
168
172
  private usedAltScreen;
173
+ private wrapCache;
174
+ private wrapCacheWidth;
175
+ private passthroughTimer;
176
+ private prevSerialized;
169
177
  constructor(bus: EventBus, config: FloatingPanelConfig, handlers?: HandlerRegistry);
170
178
  private registerDefaultHandlers;
171
179
  private wireEvents;
172
180
  /** Check whether data matches any encoding of the trigger key. */
173
181
  private isTrigger;
174
182
  private ensureBuffer;
183
+ /** Whether the panel has an active conversation (may be hidden). */
175
184
  get active(): boolean;
185
+ /** Whether the agent is currently processing a query. */
186
+ get processing(): boolean;
187
+ /** Whether the panel is currently visible on screen. */
188
+ get visible(): boolean;
176
189
  get terminalBuffer(): TerminalBuffer | null;
190
+ /** Open a fresh panel with a new conversation. */
177
191
  open(): void;
192
+ /** Hide the panel without destroying conversation state. */
193
+ hide(): void;
194
+ /** Show the panel again after hide(), preserving conversation. */
195
+ show(): void;
196
+ /** Fully destroy the panel, resetting all state. */
178
197
  dismiss(): void;
198
+ /** Common screen enter logic shared by open() and show(). */
199
+ private enterScreen;
179
200
  appendText(text: string): void;
180
201
  appendLine(line: string): void;
181
202
  updateLastLine(fn: (line: string) => string): void;
@@ -184,15 +205,25 @@ export declare class FloatingPanel {
184
205
  setFooter(footer: string): void;
185
206
  setActive(): void;
186
207
  setDone(): void;
208
+ scrollUp(lines?: number): void;
209
+ scrollDown(lines?: number): void;
187
210
  getInput(): string;
188
211
  requestRender(): void;
189
212
  private handleIntercept;
213
+ /** Handle scroll input. Returns true if consumed. */
214
+ private handleScroll;
190
215
  private handleInputKey;
191
216
  /** Compute box geometry from config + current terminal size. */
192
217
  computeGeometry(): BoxGeometry;
193
218
  private buildFrame;
194
219
  private scheduleRender;
195
220
  private render;
196
- private restoreScreen;
221
+ /** Full screen teardown: exit alt screen, release stdout, force redraw. */
222
+ private teardownScreen;
223
+ /** Start rendering TerminalBuffer directly (no overlay box). */
224
+ private startPassthrough;
225
+ private stopPassthrough;
226
+ /** Render the TerminalBuffer's screen content directly (no overlay). */
227
+ private renderPassthrough;
197
228
  private resolveSize;
198
229
  }