agent-sh 0.5.0 → 0.7.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 (54) hide show
  1. package/README.md +12 -43
  2. package/dist/agent/agent-loop.d.ts +1 -0
  3. package/dist/agent/agent-loop.js +119 -26
  4. package/dist/agent/subagent.js +3 -1
  5. package/dist/agent/system-prompt.d.ts +1 -1
  6. package/dist/agent/system-prompt.js +21 -16
  7. package/dist/agent/tools/bash.js +10 -1
  8. package/dist/agent/tools/display.d.ts +13 -0
  9. package/dist/agent/tools/display.js +70 -0
  10. package/dist/agent/tools/edit-file.js +60 -7
  11. package/dist/agent/tools/glob.js +39 -7
  12. package/dist/agent/tools/grep.js +111 -20
  13. package/dist/agent/tools/ls.js +31 -2
  14. package/dist/agent/tools/read-file.d.ts +9 -1
  15. package/dist/agent/tools/read-file.js +50 -4
  16. package/dist/agent/tools/user-shell.js +40 -13
  17. package/dist/agent/tools/write-file.js +9 -1
  18. package/dist/agent/types.d.ts +35 -1
  19. package/dist/context-manager.d.ts +3 -1
  20. package/dist/context-manager.js +11 -1
  21. package/dist/core.d.ts +1 -3
  22. package/dist/core.js +23 -12
  23. package/dist/event-bus.d.ts +41 -3
  24. package/dist/extension-loader.d.ts +1 -1
  25. package/dist/extension-loader.js +1 -3
  26. package/dist/extensions/overlay-agent.d.ts +11 -0
  27. package/dist/extensions/overlay-agent.js +43 -0
  28. package/dist/extensions/terminal-buffer.d.ts +14 -0
  29. package/dist/extensions/terminal-buffer.js +120 -0
  30. package/dist/extensions/tui-renderer.js +344 -83
  31. package/dist/index.js +45 -36
  32. package/dist/input-handler.js +10 -3
  33. package/dist/output-parser.js +8 -0
  34. package/dist/settings.js +1 -1
  35. package/dist/shell.d.ts +5 -0
  36. package/dist/shell.js +29 -4
  37. package/dist/types.d.ts +13 -0
  38. package/dist/utils/diff.js +10 -0
  39. package/dist/utils/floating-panel.d.ts +198 -0
  40. package/dist/utils/floating-panel.js +590 -0
  41. package/dist/utils/markdown.d.ts +1 -0
  42. package/dist/utils/markdown.js +23 -1
  43. package/dist/utils/output-writer.d.ts +14 -0
  44. package/dist/utils/output-writer.js +16 -0
  45. package/dist/utils/terminal-buffer.d.ts +65 -0
  46. package/dist/utils/terminal-buffer.js +166 -0
  47. package/dist/utils/tool-display.d.ts +4 -0
  48. package/dist/utils/tool-display.js +22 -5
  49. package/examples/extensions/claude-code-bridge/index.ts +8 -12
  50. package/examples/extensions/overlay-agent.ts +70 -0
  51. package/examples/extensions/pi-bridge/index.ts +10 -12
  52. package/examples/extensions/secret-guard.ts +100 -0
  53. package/examples/extensions/terminal-buffer.ts +184 -0
  54. package/package.json +5 -1
package/dist/index.js CHANGED
@@ -9,8 +9,11 @@ import slashCommands from "./extensions/slash-commands.js";
9
9
  import fileAutocomplete from "./extensions/file-autocomplete.js";
10
10
  import shellRecall from "./extensions/shell-recall.js";
11
11
  import commandSuggest from "./extensions/command-suggest.js";
12
+ import terminalBuffer from "./extensions/terminal-buffer.js";
13
+ import overlayAgent from "./extensions/overlay-agent.js";
12
14
  import { loadExtensions } from "./extension-loader.js";
13
15
  import { getSettings } from "./settings.js";
16
+ import { discoverSkills } from "./agent/skills.js";
14
17
  /**
15
18
  * Capture the user's full shell environment.
16
19
  * This picks up env vars exported in .zshrc/.bashrc that the
@@ -129,8 +132,7 @@ Examples:
129
132
 
130
133
  Inside the shell:
131
134
  Type normally Commands run in your real shell
132
- > <query> Ask the AI agent a question (execute mode)
133
- ? <command> Have the agent run a command in your shell (help mode)
135
+ > <query> Ask the AI agent (it decides how to help)
134
136
  > /help Show available slash commands
135
137
  Ctrl-C Cancel agent response (or signal shell as usual)
136
138
  `);
@@ -210,40 +212,18 @@ async function main() {
210
212
  if (process.env.DEBUG) {
211
213
  console.error('[agent-sh] Shell created');
212
214
  }
213
- // ── Input modes ──────────────────────────────────────────────
215
+ // ── Input mode ───────────────────────────────────────────────
214
216
  bus.emit("input-mode:register", {
215
- id: "execute",
217
+ id: "agent",
216
218
  trigger: ">",
217
- label: "execute",
219
+ label: "agent",
218
220
  promptIcon: "❯",
219
221
  indicator: "●",
220
222
  onSubmit(query, b) {
221
- b.emit("agent:submit", { query, modeLabel: "Execute", modeInstruction: "[mode: execute]" });
223
+ b.emit("agent:submit", { query });
222
224
  },
223
225
  returnToSelf: true,
224
226
  });
225
- bus.emit("input-mode:register", {
226
- id: "help",
227
- trigger: "?",
228
- label: "help",
229
- promptIcon: "❯",
230
- indicator: "❓",
231
- onSubmit(query, b) {
232
- const onToolDone = (e) => {
233
- if (e.kind === "execute") {
234
- b.emit("agent:cancel-request", { silent: true });
235
- }
236
- };
237
- const cleanup = () => {
238
- b.off("agent:tool-completed", onToolDone);
239
- b.off("agent:processing-done", cleanup);
240
- };
241
- b.on("agent:tool-completed", onToolDone);
242
- b.on("agent:processing-done", cleanup);
243
- b.emit("agent:submit", { query, modeLabel: "Help", modeInstruction: "[mode: help]" });
244
- },
245
- returnToSelf: false,
246
- });
247
227
  // ── Extensions ────────────────────────────────────────────────
248
228
  if (process.env.DEBUG) {
249
229
  console.error('[agent-sh] Setting up extensions...');
@@ -254,13 +234,16 @@ async function main() {
254
234
  fileAutocomplete(extCtx);
255
235
  shellRecall(extCtx);
256
236
  commandSuggest(extCtx);
237
+ terminalBuffer(extCtx);
238
+ overlayAgent(extCtx);
257
239
  // Load user extensions (may register alternative agent backends)
258
240
  if (process.env.DEBUG) {
259
241
  console.error('[agent-sh] Loading extensions...');
260
242
  }
261
243
  const loadExtensionsTimeoutMs = 10000;
244
+ let loadedExtensions = [];
262
245
  await Promise.race([
263
- loadExtensions(extCtx, config.extensions),
246
+ loadExtensions(extCtx, config.extensions).then((names) => { loadedExtensions = names; }),
264
247
  new Promise((_, reject) => setTimeout(() => reject(new Error(`Extension loading timeout after ${loadExtensionsTimeoutMs}ms`)), loadExtensionsTimeoutMs)),
265
248
  ]).catch((err) => {
266
249
  console.error(`Warning: ${err.message}`);
@@ -268,6 +251,8 @@ async function main() {
268
251
  if (process.env.DEBUG) {
269
252
  console.error('[agent-sh] Extensions loaded');
270
253
  }
254
+ // ── Discover skills ───────────────────────────────────────────
255
+ const skills = discoverSkills(process.cwd());
271
256
  // ── Activate agent backend ────────────────────────────────────
272
257
  // Extensions had their chance to register via agent:register-backend.
273
258
  // If none did, the built-in AgentLoop gets wired to bus events.
@@ -275,15 +260,39 @@ async function main() {
275
260
  // ── Startup banner ───────────────────────────────────────────
276
261
  const settings = getSettings();
277
262
  if (settings.startupBanner !== false) {
278
- const title = core.llmClient
279
- ? `${p.accent}${p.bold}agent-sh${p.reset}${p.dim} · ${core.llmClient.model}${p.reset}`
280
- : `${p.accent}${p.bold}agent-sh${p.reset}`;
281
- const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}?${p.muted} to run in shell · ${p.warning}/help${p.muted} for commands${p.reset}`;
282
263
  const termW = process.stdout.columns || 80;
283
- const borderLine = `${p.muted}${"─".repeat(Math.min(termW, 60))}${p.reset}`;
264
+ const bannerW = Math.min(termW, 60);
265
+ const productName = `${p.accent}${p.bold}agent-sh${p.reset}`;
266
+ const info = agentInfo;
267
+ const backendName = info?.name ?? "agent-sh";
268
+ const model = info?.model ?? core.llmClient?.model;
269
+ const provider = info?.provider;
270
+ const modelValue = model
271
+ ? provider ? `${model} [${provider}]` : model
272
+ : null;
273
+ let sections = "";
274
+ sections += `\n\n ${p.muted}Backend:${p.reset} ${p.dim}${backendName}${p.reset}`;
275
+ if (modelValue) {
276
+ sections += `\n ${p.muted}Model:${p.reset} ${p.dim}${modelValue}${p.reset}`;
277
+ }
278
+ if (loadedExtensions.length > 0) {
279
+ sections += `\n\n ${p.muted}Extensions:${p.reset}`;
280
+ for (const name of loadedExtensions) {
281
+ sections += `\n ${p.dim}${name}${p.reset}`;
282
+ }
283
+ }
284
+ if (skills.length > 0) {
285
+ sections += `\n\n ${p.muted}Skills:${p.reset}`;
286
+ for (const s of skills) {
287
+ sections += `\n ${p.dim}${s.name}${p.reset}`;
288
+ }
289
+ }
290
+ const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}>/help${p.muted} for commands${p.reset}`;
291
+ const borderLine = `${p.muted}${"─".repeat(bannerW)}${p.reset}`;
284
292
  process.stdout.write("\n" + borderLine + "\n" +
285
- " " + title + "\n" +
286
- " " + hint + "\n" +
293
+ " " + productName +
294
+ sections + "\n" +
295
+ "\n " + hint + "\n" +
287
296
  borderLine + "\n\n");
288
297
  }
289
298
  // ── Terminal lifecycle ────────────────────────────────────────
@@ -137,6 +137,10 @@ export class InputHandler {
137
137
  }
138
138
  }
139
139
  handleInput(data) {
140
+ // Allow extensions to capture raw input (e.g. overlay prompt during vim)
141
+ const intercepted = this.bus.emitPipe("input:intercept", { data, consumed: false });
142
+ if (intercepted.consumed)
143
+ return;
140
144
  // If agent is running (processing a query), only Ctrl-C and control keys
141
145
  if (this.ctx.isAgentActive()) {
142
146
  if (data === "\x03") {
@@ -235,7 +239,8 @@ export class InputHandler {
235
239
  this.enterMode(mode);
236
240
  return; // don't process remaining chars
237
241
  }
238
- this.lineBuffer += ch;
242
+ if (!this.ctx.isForegroundBusy())
243
+ this.lineBuffer += ch;
239
244
  this.ctx.writeToPty(ch);
240
245
  }
241
246
  }
@@ -509,7 +514,8 @@ export class InputHandler {
509
514
  }
510
515
  this.editor.buffer = this.history[this.historyIndex];
511
516
  this.editor.cursor = this.editor.buffer.length;
512
- this.renderModeInput();
517
+ this.clearAutocompleteLines();
518
+ this.writeModePromptLine();
513
519
  }
514
520
  break;
515
521
  case "arrow-down":
@@ -532,7 +538,8 @@ export class InputHandler {
532
538
  this.editor.buffer = this.savedBuffer;
533
539
  }
534
540
  this.editor.cursor = this.editor.buffer.length;
535
- this.renderModeInput();
541
+ this.clearAutocompleteLines();
542
+ this.writeModePromptLine();
536
543
  }
537
544
  break;
538
545
  }
@@ -109,7 +109,15 @@ export class OutputParser {
109
109
  this.currentOutputCapture = "";
110
110
  }
111
111
  else {
112
+ // Cap capture buffer to avoid unbounded growth when a foreground
113
+ // program (tmux, vim, etc.) produces output without prompt markers.
114
+ // Keep only the tail — the final output is what matters for
115
+ // command-done context.
116
+ const MAX_CAPTURE = 128 * 1024; // 128 KB
112
117
  this.currentOutputCapture += data;
118
+ if (this.currentOutputCapture.length > MAX_CAPTURE) {
119
+ this.currentOutputCapture = this.currentOutputCapture.slice(-MAX_CAPTURE);
120
+ }
113
121
  }
114
122
  }
115
123
  /**
package/dist/settings.js CHANGED
@@ -22,7 +22,7 @@ const DEFAULTS = {
22
22
  shellTailLines: 5,
23
23
  recallExpandMaxLines: 100,
24
24
  maxCommandOutputLines: 3,
25
- readOutputMaxLines: 0,
25
+ readOutputMaxLines: 10,
26
26
  diffMaxLines: 20,
27
27
  skillPaths: [],
28
28
  startupBanner: true,
package/dist/shell.d.ts CHANGED
@@ -6,6 +6,8 @@ export declare class Shell implements InputContext {
6
6
  private inputHandler;
7
7
  private outputParser;
8
8
  private paused;
9
+ private stdoutHold;
10
+ private stdoutShow;
9
11
  private echoSkip;
10
12
  private agentActive;
11
13
  private isZsh;
@@ -37,6 +39,9 @@ export declare class Shell implements InputContext {
37
39
  * Heavy redraw: send \n to PTY to trigger a full precmd → prompt cycle.
38
40
  * Use this after agent responses where stdout has moved far from where
39
41
  * zle expects the cursor. The blank line is acceptable as a separator.
42
+ *
43
+ * Routed through shell:redraw-prompt pipe so extensions (e.g. overlay)
44
+ * can suppress it by setting `handled: true`.
40
45
  */
41
46
  freshPrompt(): void;
42
47
  onCommandEntered(command: string, cwd: string): void;
package/dist/shell.js CHANGED
@@ -5,12 +5,15 @@ import * as pty from "node-pty";
5
5
  import { InputHandler } from "./input-handler.js";
6
6
  import { OutputParser } from "./output-parser.js";
7
7
  import { getSettings } from "./settings.js";
8
+ import { RefCounter } from "./utils/output-writer.js";
8
9
  export class Shell {
9
10
  ptyProcess;
10
11
  bus;
11
12
  inputHandler;
12
13
  outputParser;
13
14
  paused = false;
15
+ stdoutHold = new RefCounter();
16
+ stdoutShow = new RefCounter();
14
17
  echoSkip = false;
15
18
  agentActive = false;
16
19
  isZsh = false;
@@ -156,6 +159,16 @@ export class Shell {
156
159
  this.setupOutput();
157
160
  this.setupInput();
158
161
  this.setupAgentLifecycle();
162
+ // Allow extensions to inject raw keystrokes into the PTY
163
+ this.bus.on("shell:pty-write", ({ data }) => {
164
+ this.ptyProcess.write(data);
165
+ });
166
+ // Ref-counted stdout hold — overlay extensions suppress PTY output
167
+ this.bus.on("shell:stdout-hold", () => { this.stdoutHold.increment(); });
168
+ this.bus.on("shell:stdout-release", () => { this.stdoutHold.decrement(); });
169
+ // Ref-counted stdout show — tools temporarily force output visible during agent processing
170
+ this.bus.on("shell:stdout-show", () => { this.stdoutShow.increment(); });
171
+ this.bus.on("shell:stdout-hide", () => { this.stdoutShow.decrement(); });
159
172
  }
160
173
  // ── InputContext implementation (delegates to OutputParser) ──
161
174
  isForegroundBusy() {
@@ -197,9 +210,18 @@ export class Shell {
197
210
  * Heavy redraw: send \n to PTY to trigger a full precmd → prompt cycle.
198
211
  * Use this after agent responses where stdout has moved far from where
199
212
  * zle expects the cursor. The blank line is acceptable as a separator.
213
+ *
214
+ * Routed through shell:redraw-prompt pipe so extensions (e.g. overlay)
215
+ * can suppress it by setting `handled: true`.
200
216
  */
201
217
  freshPrompt() {
202
- this.ptyProcess.write("\n");
218
+ const result = this.bus.emitPipe("shell:redraw-prompt", {
219
+ cwd: this.outputParser.getCwd(),
220
+ handled: false,
221
+ });
222
+ if (!result.handled) {
223
+ this.ptyProcess.write("\n");
224
+ }
203
225
  }
204
226
  onCommandEntered(command, cwd) {
205
227
  this.outputParser.onCommandEntered(command, cwd);
@@ -207,8 +229,11 @@ export class Shell {
207
229
  // ── PTY I/O wiring ─────────────────────────────────────────
208
230
  setupOutput() {
209
231
  this.ptyProcess.onData((data) => {
232
+ this.bus.emit("shell:pty-data", { raw: data });
210
233
  this.outputParser.processData(data);
211
- if (this.paused)
234
+ if (this.stdoutHold.active)
235
+ return;
236
+ if (this.paused && !this.stdoutShow.active)
212
237
  return;
213
238
  // During user_shell exec, skip the command echo (first line)
214
239
  if (this.echoSkip) {
@@ -274,7 +299,7 @@ export class Shell {
274
299
  const handler = (e) => {
275
300
  clearTimeout(timeout);
276
301
  this.bus.off("shell:command-done", handler);
277
- resolve({ output: e.output, cwd: e.cwd });
302
+ resolve({ output: e.output, cwd: e.cwd, exitCode: e.exitCode });
278
303
  };
279
304
  this.bus.on("shell:command-done", handler);
280
305
  this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
@@ -283,7 +308,7 @@ export class Shell {
283
308
  this.paused = true;
284
309
  this.echoSkip = false;
285
310
  this.bus.emit("shell:agent-exec-done", {});
286
- return { ...payload, output: output.output, cwd: output.cwd, done: true };
311
+ return { ...payload, output: output.output, cwd: output.cwd, exitCode: output.exitCode, done: true };
287
312
  });
288
313
  }
289
314
  // ── Public API (used by index.ts) ──
package/dist/types.d.ts CHANGED
@@ -4,6 +4,8 @@ import type { LlmClient } from "./utils/llm-client.js";
4
4
  import type { ColorPalette } from "./utils/palette.js";
5
5
  import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
6
6
  import type { ToolDefinition } from "./agent/types.js";
7
+ import type { TerminalBuffer } from "./utils/terminal-buffer.js";
8
+ import type { FloatingPanel, FloatingPanelConfig } from "./utils/floating-panel.js";
7
9
  export type { ContentBlock } from "./event-bus.js";
8
10
  export type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
9
11
  /** A model entry in the cycling list, optionally tied to a provider. */
@@ -66,6 +68,17 @@ export interface ExtensionContext {
66
68
  advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => void;
67
69
  /** Call a named handler. */
68
70
  call: (name: string, ...args: any[]) => any;
71
+ /**
72
+ * Shared headless terminal buffer mirroring PTY output.
73
+ * Lazily created on first access. Returns null if @xterm/headless is not installed.
74
+ */
75
+ terminalBuffer: TerminalBuffer | null;
76
+ /**
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.
80
+ */
81
+ createFloatingPanel: (config: FloatingPanelConfig) => FloatingPanel;
69
82
  }
70
83
  /**
71
84
  * Configuration for a registered input mode.
@@ -39,6 +39,16 @@ export function computeDiff(oldText, newText) {
39
39
  // Build LCS table and backtrack to produce diff lines
40
40
  const a = oldText.split("\n");
41
41
  const b = newText.split("\n");
42
+ // Bail out if LCS table would be too large (avoids OOM / hang)
43
+ if (a.length * b.length > 10_000_000) {
44
+ return {
45
+ hunks: [],
46
+ added: b.length,
47
+ removed: a.length,
48
+ isIdentical: false,
49
+ isNewFile: false,
50
+ };
51
+ }
42
52
  const dp = buildLcs(a, b);
43
53
  const raw = backtrack(dp, a, b);
44
54
  let added = 0;
@@ -0,0 +1,198 @@
1
+ import { TerminalBuffer } from "./terminal-buffer.js";
2
+ import { HandlerRegistry } from "./handler-registry.js";
3
+ import type { EventBus } from "../event-bus.js";
4
+ import type { BorderStyle } from "./box-frame.js";
5
+ export interface FloatingPanelConfig {
6
+ /** Key sequence that toggles the panel (e.g. "\x1c" for Ctrl+\). */
7
+ trigger: string;
8
+ /** Panel width. Number = columns, string with % = percentage. Default: "80%". */
9
+ width?: number | string;
10
+ /** Max width in columns. Default: 100. */
11
+ maxWidth?: number;
12
+ /** Panel height. Number = rows, string with % = percentage. Default: "60%". */
13
+ height?: number | string;
14
+ /** Min content rows inside the panel. Default: 6. */
15
+ minHeight?: number;
16
+ /** Border style. Default: "rounded". */
17
+ borderStyle?: BorderStyle;
18
+ /**
19
+ * Show dimmed terminal content behind the panel. Default: true.
20
+ * Requires @xterm/headless — falls back to blank background if unavailable.
21
+ */
22
+ dimBackground?: boolean;
23
+ /** Auto-dismiss delay in ms when done (0 = disabled). Default: 0. */
24
+ autoDismissMs?: number;
25
+ /** Icon shown before the input cursor. Default: "\u276f". */
26
+ promptIcon?: string;
27
+ /**
28
+ * Pre-existing TerminalBuffer to reuse. If provided, the panel will
29
+ * not create its own headless terminal. Useful when sharing a buffer
30
+ * with other features (e.g. context injection, terminal_read tool).
31
+ */
32
+ terminalBuffer?: TerminalBuffer;
33
+ /**
34
+ * Handler namespace prefix. Default: "panel".
35
+ * All handlers are registered as `{prefix}:render-content`,
36
+ * `{prefix}:submit`, etc. Use different prefixes for multiple panels.
37
+ */
38
+ handlerPrefix?: string;
39
+ }
40
+ /**
41
+ * Context passed to the render-content handler.
42
+ */
43
+ export interface RenderContext {
44
+ /** Available width for content (inside box, excluding borders and padding). */
45
+ width: number;
46
+ /** Available height for content (rows inside box). */
47
+ height: number;
48
+ /** Current panel phase. */
49
+ phase: Phase;
50
+ /** Current input buffer text (during input phase). */
51
+ inputBuffer: string;
52
+ /** Current input cursor position (during input phase). */
53
+ inputCursor: number;
54
+ /** Current scroll offset. */
55
+ scrollOffset: number;
56
+ /** Built-in content lines (from appendText/appendLine). */
57
+ contentLines: readonly string[];
58
+ /** Current partial line being streamed. */
59
+ partialLine: string;
60
+ }
61
+ /**
62
+ * Result from render-content handler.
63
+ */
64
+ export interface RenderResult {
65
+ lines: string[];
66
+ /** Optional cursor position within the content area. */
67
+ cursor?: {
68
+ row: number;
69
+ col: number;
70
+ };
71
+ }
72
+ /**
73
+ * Box geometry computed from config + terminal size.
74
+ */
75
+ export interface BoxGeometry {
76
+ /** Terminal columns. */
77
+ cols: number;
78
+ /** Terminal rows. */
79
+ rows: number;
80
+ /** Box width in columns (including borders). */
81
+ boxW: number;
82
+ /** Box height in rows (including borders). */
83
+ boxH: number;
84
+ /** Box top offset (0-indexed row). */
85
+ boxTop: number;
86
+ /** Box left offset (0-indexed column). */
87
+ boxLeft: number;
88
+ /** Usable content width inside box. */
89
+ contentW: number;
90
+ /** Usable content height inside box. */
91
+ contentH: number;
92
+ }
93
+ /**
94
+ * Context passed to the render-frame handler.
95
+ */
96
+ export interface FrameContext {
97
+ /** Box geometry. */
98
+ geo: BoxGeometry;
99
+ /** Content render result (from render-content handler). */
100
+ content: RenderResult;
101
+ /** Background lines from the terminal buffer (null if no dimming). */
102
+ bgLines: string[] | null;
103
+ /** Current panel phase. */
104
+ phase: Phase;
105
+ /** Current title text. */
106
+ title: string;
107
+ /** Current footer text. */
108
+ footer: string;
109
+ /** Border characters for the configured border style. */
110
+ border: {
111
+ tl: string;
112
+ tr: string;
113
+ bl: string;
114
+ br: string;
115
+ h: string;
116
+ v: string;
117
+ };
118
+ }
119
+ /**
120
+ * Result from render-frame handler.
121
+ */
122
+ export interface FrameResult {
123
+ /** One string per terminal row. */
124
+ rows: string[];
125
+ /** ANSI sequence to position the cursor (empty string if no cursor). */
126
+ cursorSeq: string;
127
+ }
128
+ export type Phase = "idle" | "input" | "active" | "done";
129
+ export declare class FloatingPanel {
130
+ private readonly config;
131
+ private readonly bus;
132
+ private readonly border;
133
+ private readonly externalBuffer;
134
+ private readonly prefix;
135
+ /**
136
+ * Handler registry for this panel. Extensions use `handlers.advise()`
137
+ * to customize rendering and behavior.
138
+ *
139
+ * Registered handlers:
140
+ * - `{prefix}:render-content(ctx: RenderContext) -> RenderResult`
141
+ * - `{prefix}:render-frame(ctx: FrameContext) -> FrameResult`
142
+ * - `{prefix}:render-border-top(ctx: FrameContext) -> string`
143
+ * - `{prefix}:render-border-bottom(ctx: FrameContext) -> string`
144
+ * - `{prefix}:composite-row(content: string, bgLine: string|null, boxLeft: number, boxW: number, cols: number) -> string`
145
+ * - `{prefix}:submit(query: string) -> void`
146
+ * - `{prefix}:dismiss() -> void`
147
+ * - `{prefix}:input(data: string) -> boolean`
148
+ * - `{prefix}:build-row(content: string, width: number) -> string`
149
+ */
150
+ readonly handlers: HandlerRegistry;
151
+ private buffer;
152
+ private bufferInitialized;
153
+ /** All byte sequences that should be recognized as the trigger key. */
154
+ private readonly triggerSeqs;
155
+ private phase;
156
+ private editor;
157
+ private contentLines;
158
+ private currentPartialLine;
159
+ private scrollOffset;
160
+ private title;
161
+ private footer;
162
+ private renderTimer;
163
+ private autoDismissTimer;
164
+ private resizeHandler;
165
+ private prevFrame;
166
+ private suppressNextRedraw;
167
+ private ptyBuffer;
168
+ private usedAltScreen;
169
+ constructor(bus: EventBus, config: FloatingPanelConfig, handlers?: HandlerRegistry);
170
+ private registerDefaultHandlers;
171
+ private wireEvents;
172
+ /** Check whether data matches any encoding of the trigger key. */
173
+ private isTrigger;
174
+ private ensureBuffer;
175
+ get active(): boolean;
176
+ get terminalBuffer(): TerminalBuffer | null;
177
+ open(): void;
178
+ dismiss(): void;
179
+ appendText(text: string): void;
180
+ appendLine(line: string): void;
181
+ updateLastLine(fn: (line: string) => string): void;
182
+ clearContent(): void;
183
+ setTitle(title: string): void;
184
+ setFooter(footer: string): void;
185
+ setActive(): void;
186
+ setDone(): void;
187
+ getInput(): string;
188
+ requestRender(): void;
189
+ private handleIntercept;
190
+ private handleInputKey;
191
+ /** Compute box geometry from config + current terminal size. */
192
+ computeGeometry(): BoxGeometry;
193
+ private buildFrame;
194
+ private scheduleRender;
195
+ private render;
196
+ private restoreScreen;
197
+ private resolveSize;
198
+ }