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.
- package/README.md +12 -43
- package/dist/agent/agent-loop.d.ts +1 -0
- package/dist/agent/agent-loop.js +119 -26
- package/dist/agent/subagent.js +3 -1
- package/dist/agent/system-prompt.d.ts +1 -1
- package/dist/agent/system-prompt.js +21 -16
- package/dist/agent/tools/bash.js +10 -1
- package/dist/agent/tools/display.d.ts +13 -0
- package/dist/agent/tools/display.js +70 -0
- package/dist/agent/tools/edit-file.js +60 -7
- package/dist/agent/tools/glob.js +39 -7
- package/dist/agent/tools/grep.js +111 -20
- package/dist/agent/tools/ls.js +31 -2
- package/dist/agent/tools/read-file.d.ts +9 -1
- package/dist/agent/tools/read-file.js +50 -4
- package/dist/agent/tools/user-shell.js +40 -13
- package/dist/agent/tools/write-file.js +9 -1
- package/dist/agent/types.d.ts +35 -1
- package/dist/context-manager.d.ts +3 -1
- package/dist/context-manager.js +11 -1
- package/dist/core.d.ts +1 -3
- package/dist/core.js +23 -12
- package/dist/event-bus.d.ts +41 -3
- package/dist/extension-loader.d.ts +1 -1
- package/dist/extension-loader.js +1 -3
- package/dist/extensions/overlay-agent.d.ts +11 -0
- package/dist/extensions/overlay-agent.js +43 -0
- package/dist/extensions/terminal-buffer.d.ts +14 -0
- package/dist/extensions/terminal-buffer.js +120 -0
- package/dist/extensions/tui-renderer.js +344 -83
- package/dist/index.js +45 -36
- package/dist/input-handler.js +10 -3
- package/dist/output-parser.js +8 -0
- package/dist/settings.js +1 -1
- package/dist/shell.d.ts +5 -0
- package/dist/shell.js +29 -4
- package/dist/types.d.ts +13 -0
- package/dist/utils/diff.js +10 -0
- package/dist/utils/floating-panel.d.ts +198 -0
- package/dist/utils/floating-panel.js +590 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +23 -1
- package/dist/utils/output-writer.d.ts +14 -0
- package/dist/utils/output-writer.js +16 -0
- package/dist/utils/terminal-buffer.d.ts +65 -0
- package/dist/utils/terminal-buffer.js +166 -0
- package/dist/utils/tool-display.d.ts +4 -0
- package/dist/utils/tool-display.js +22 -5
- package/examples/extensions/claude-code-bridge/index.ts +8 -12
- package/examples/extensions/overlay-agent.ts +70 -0
- package/examples/extensions/pi-bridge/index.ts +10 -12
- package/examples/extensions/secret-guard.ts +100 -0
- package/examples/extensions/terminal-buffer.ts +184 -0
- 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
|
|
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
|
|
215
|
+
// ── Input mode ───────────────────────────────────────────────
|
|
214
216
|
bus.emit("input-mode:register", {
|
|
215
|
-
id: "
|
|
217
|
+
id: "agent",
|
|
216
218
|
trigger: ">",
|
|
217
|
-
label: "
|
|
219
|
+
label: "agent",
|
|
218
220
|
promptIcon: "❯",
|
|
219
221
|
indicator: "●",
|
|
220
222
|
onSubmit(query, b) {
|
|
221
|
-
b.emit("agent:submit", { query
|
|
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
|
|
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
|
-
" " +
|
|
286
|
-
|
|
293
|
+
" " + productName +
|
|
294
|
+
sections + "\n" +
|
|
295
|
+
"\n " + hint + "\n" +
|
|
287
296
|
borderLine + "\n\n");
|
|
288
297
|
}
|
|
289
298
|
// ── Terminal lifecycle ────────────────────────────────────────
|
package/dist/input-handler.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
541
|
+
this.clearAutocompleteLines();
|
|
542
|
+
this.writeModePromptLine();
|
|
536
543
|
}
|
|
537
544
|
break;
|
|
538
545
|
}
|
package/dist/output-parser.js
CHANGED
|
@@ -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
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.
|
|
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.
|
|
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.
|
package/dist/utils/diff.js
CHANGED
|
@@ -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
|
+
}
|