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.
- package/README.md +28 -33
- package/dist/agent/agent-loop.d.ts +31 -8
- package/dist/agent/agent-loop.js +277 -66
- package/dist/agent/conversation-state.d.ts +41 -9
- package/dist/agent/conversation-state.js +340 -17
- package/dist/agent/history-file.d.ts +36 -0
- package/dist/agent/history-file.js +167 -0
- package/dist/agent/nuclear-form.d.ts +41 -0
- package/dist/agent/nuclear-form.js +176 -0
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +16 -11
- package/dist/agent/token-budget.d.ts +13 -0
- package/dist/agent/token-budget.js +50 -0
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/agent/types.d.ts +21 -1
- package/dist/context-manager.d.ts +0 -1
- package/dist/context-manager.js +5 -110
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -180
- package/dist/event-bus.d.ts +40 -0
- package/dist/event-bus.js +20 -1
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +104 -17
- package/dist/extensions/agent-backend.d.ts +13 -0
- package/dist/extensions/agent-backend.js +167 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +25 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +44 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +22 -8
- package/dist/extensions/tui-renderer.js +177 -122
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +25 -2
- package/dist/settings.js +25 -4
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
- package/dist/{input-handler.js → shell/input-handler.js} +60 -43
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +24 -6
- package/dist/types.d.ts +49 -32
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/compositor.d.ts +62 -0
- package/dist/utils/compositor.js +88 -0
- package/dist/utils/diff-renderer.js +92 -4
- package/dist/utils/floating-panel.d.ts +34 -3
- package/dist/utils/floating-panel.js +315 -82
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +32 -3
- package/dist/utils/line-editor.js +218 -36
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +9 -1
- package/dist/utils/terminal-buffer.js +31 -2
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +77 -1
- package/examples/extensions/interactive-prompts.ts +82 -110
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +450 -0
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/examples/extensions/questionnaire.ts +249 -0
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/web-access.ts +327 -0
- package/package.json +9 -1
- package/dist/extensions/overlay-agent.d.ts +0 -11
- package/dist/extensions/overlay-agent.js +0 -43
- package/examples/extensions/terminal-buffer.ts +0 -184
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* import { FloatingPanel } from "agent-sh/utils/floating-panel.js";
|
|
32
32
|
*/
|
|
33
33
|
import { stripAnsi } from "./ansi.js";
|
|
34
|
+
import { wrapLine } from "./markdown.js";
|
|
34
35
|
import { LineEditor } from "./line-editor.js";
|
|
35
36
|
import { TerminalBuffer } from "./terminal-buffer.js";
|
|
36
37
|
import { HandlerRegistry } from "./handler-registry.js";
|
|
@@ -88,6 +89,7 @@ export class FloatingPanel {
|
|
|
88
89
|
* - `{prefix}:composite-row(content: string, bgLine: string|null, boxLeft: number, boxW: number, cols: number) -> string`
|
|
89
90
|
* - `{prefix}:submit(query: string) -> void`
|
|
90
91
|
* - `{prefix}:dismiss() -> void`
|
|
92
|
+
* - `{prefix}:show() -> void`
|
|
91
93
|
* - `{prefix}:input(data: string) -> boolean`
|
|
92
94
|
* - `{prefix}:build-row(content: string, width: number) -> string`
|
|
93
95
|
*/
|
|
@@ -100,19 +102,26 @@ export class FloatingPanel {
|
|
|
100
102
|
triggerSeqs;
|
|
101
103
|
// ── State ───────────────────────────────────────────────────
|
|
102
104
|
phase = "idle";
|
|
105
|
+
_visible = false; // whether the panel box is shown on screen
|
|
106
|
+
_passthrough = false; // hidden but still rendering TerminalBuffer
|
|
103
107
|
editor = new LineEditor();
|
|
104
108
|
contentLines = [];
|
|
105
109
|
currentPartialLine = "";
|
|
106
110
|
scrollOffset = 0;
|
|
111
|
+
userScrolled = false; // true when user manually scrolled away from bottom
|
|
107
112
|
title = "";
|
|
108
113
|
footer = "";
|
|
109
114
|
renderTimer = null;
|
|
110
|
-
autoDismissTimer = null;
|
|
111
115
|
resizeHandler = null;
|
|
112
116
|
prevFrame = [];
|
|
113
117
|
suppressNextRedraw = false;
|
|
118
|
+
autoDismissTimer = null;
|
|
114
119
|
ptyBuffer = ""; // PTY output accumulated while overlay is open
|
|
115
120
|
usedAltScreen = false; // whether we entered our own alt screen
|
|
121
|
+
wrapCache = new Map(); // line → wrapped lines (invalidated on width change)
|
|
122
|
+
wrapCacheWidth = 0;
|
|
123
|
+
passthroughTimer = null;
|
|
124
|
+
prevSerialized = "";
|
|
116
125
|
constructor(bus, config, handlers) {
|
|
117
126
|
this.bus = bus;
|
|
118
127
|
this.externalBuffer = config.terminalBuffer;
|
|
@@ -140,28 +149,59 @@ export class FloatingPanel {
|
|
|
140
149
|
const p = this.prefix;
|
|
141
150
|
// Default content renderer: uses built-in appendText/appendLine buffer
|
|
142
151
|
this.handlers.define(`${p}:render-content`, (ctx) => {
|
|
152
|
+
const raw = [...ctx.contentLines, ...(ctx.partialLine ? [ctx.partialLine] : [])];
|
|
153
|
+
// Invalidate wrap cache if width changed
|
|
154
|
+
if (ctx.width !== this.wrapCacheWidth) {
|
|
155
|
+
this.wrapCache.clear();
|
|
156
|
+
this.wrapCacheWidth = ctx.width;
|
|
157
|
+
}
|
|
158
|
+
const all = [];
|
|
159
|
+
for (const line of raw) {
|
|
160
|
+
let wrapped = this.wrapCache.get(line);
|
|
161
|
+
if (!wrapped) {
|
|
162
|
+
wrapped = wrapLine(line, ctx.width);
|
|
163
|
+
this.wrapCache.set(line, wrapped);
|
|
164
|
+
}
|
|
165
|
+
all.push(...wrapped);
|
|
166
|
+
}
|
|
167
|
+
// In input phase, append the prompt line at the bottom of content
|
|
143
168
|
if (ctx.phase === "input") {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
cursor: { row: 0, col: this.config.promptIcon.length + 1 + ctx.inputCursor },
|
|
147
|
-
};
|
|
169
|
+
const promptLine = `\x1b[36m${this.config.promptIcon}${RESET} ${ctx.inputBuffer}`;
|
|
170
|
+
all.push(promptLine);
|
|
148
171
|
}
|
|
149
|
-
|
|
150
|
-
// Auto-scroll
|
|
172
|
+
// Scroll: auto-scroll to bottom unless user manually scrolled
|
|
151
173
|
let offset = ctx.scrollOffset;
|
|
152
|
-
|
|
153
|
-
|
|
174
|
+
const maxOffset = Math.max(0, all.length - ctx.height);
|
|
175
|
+
if (this.userScrolled) {
|
|
176
|
+
offset = Math.min(offset, maxOffset);
|
|
177
|
+
// Resume auto-scroll if user scrolled back to bottom
|
|
178
|
+
if (offset >= maxOffset)
|
|
179
|
+
this.userScrolled = false;
|
|
154
180
|
}
|
|
155
181
|
else {
|
|
156
|
-
offset =
|
|
182
|
+
offset = maxOffset;
|
|
157
183
|
}
|
|
158
184
|
this.scrollOffset = offset;
|
|
159
|
-
|
|
185
|
+
const visible = all.slice(offset, offset + ctx.height);
|
|
186
|
+
// Cursor position for input mode
|
|
187
|
+
if (ctx.phase === "input") {
|
|
188
|
+
const promptRow = visible.length - 1;
|
|
189
|
+
// If prompt is visible, set cursor
|
|
190
|
+
if (promptRow >= 0) {
|
|
191
|
+
return {
|
|
192
|
+
lines: visible,
|
|
193
|
+
cursor: { row: promptRow, col: this.config.promptIcon.length + 1 + ctx.inputCursor },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { lines: visible };
|
|
160
198
|
});
|
|
161
199
|
// Default submit: no-op (extension overrides)
|
|
162
200
|
this.handlers.define(`${p}:submit`, (_query) => { });
|
|
163
201
|
// Default dismiss: no-op
|
|
164
202
|
this.handlers.define(`${p}:dismiss`, () => { });
|
|
203
|
+
// Default show: no-op (extension overrides to rebuild content on re-show)
|
|
204
|
+
this.handlers.define(`${p}:show`, () => { });
|
|
165
205
|
// Default custom input handler: don't consume
|
|
166
206
|
this.handlers.define(`${p}:input`, (_data) => false);
|
|
167
207
|
// Default row builder: truncate and pad
|
|
@@ -186,7 +226,8 @@ export class FloatingPanel {
|
|
|
186
226
|
this.handlers.define(`${p}:render-border-bottom`, (ctx) => {
|
|
187
227
|
const { geo, border: b } = ctx;
|
|
188
228
|
if (ctx.footer) {
|
|
189
|
-
const
|
|
229
|
+
const visLen = stripAnsi(ctx.footer).length;
|
|
230
|
+
const footerPad = Math.max(0, geo.boxW - visLen - 3);
|
|
190
231
|
return `${b.bl}${b.h.repeat(footerPad)}${DIM}${ctx.footer}${RESET}${b.h}${b.br}`;
|
|
191
232
|
}
|
|
192
233
|
return `${b.bl}${b.h.repeat(geo.boxW - 2)}${b.br}`;
|
|
@@ -243,15 +284,15 @@ export class FloatingPanel {
|
|
|
243
284
|
}
|
|
244
285
|
// ── Bus event wiring ───────────────────────────────────────
|
|
245
286
|
wireEvents() {
|
|
246
|
-
// Buffer PTY output while overlay is
|
|
247
|
-
//
|
|
287
|
+
// Buffer PTY output while overlay is visible (alt screen discards it).
|
|
288
|
+
// Don't buffer when hidden — PTY flows to terminal directly via stdout-show.
|
|
248
289
|
this.bus.on("shell:pty-data", ({ raw }) => {
|
|
249
|
-
if (this.
|
|
290
|
+
if (this._visible)
|
|
250
291
|
this.ptyBuffer += raw;
|
|
251
292
|
});
|
|
252
293
|
this.bus.onPipe("input:intercept", (payload) => this.handleIntercept(payload));
|
|
253
294
|
this.bus.onPipe("shell:redraw-prompt", (payload) => {
|
|
254
|
-
if (this.
|
|
295
|
+
if (this._visible || this._passthrough) {
|
|
255
296
|
return { ...payload, handled: true };
|
|
256
297
|
}
|
|
257
298
|
// After dismiss, suppress one redraw — restoreScreen already
|
|
@@ -283,12 +324,22 @@ export class FloatingPanel {
|
|
|
283
324
|
return this.buffer;
|
|
284
325
|
}
|
|
285
326
|
// ── Public lifecycle ────────────────────────────────────────
|
|
327
|
+
/** Whether the panel has an active conversation (may be hidden). */
|
|
286
328
|
get active() {
|
|
287
329
|
return this.phase !== "idle";
|
|
288
330
|
}
|
|
331
|
+
/** Whether the agent is currently processing a query. */
|
|
332
|
+
get processing() {
|
|
333
|
+
return this.phase === "active";
|
|
334
|
+
}
|
|
335
|
+
/** Whether the panel is currently visible on screen. */
|
|
336
|
+
get visible() {
|
|
337
|
+
return this._visible;
|
|
338
|
+
}
|
|
289
339
|
get terminalBuffer() {
|
|
290
340
|
return this.buffer;
|
|
291
341
|
}
|
|
342
|
+
/** Open a fresh panel with a new conversation. */
|
|
292
343
|
open() {
|
|
293
344
|
if (this.phase !== "idle")
|
|
294
345
|
return;
|
|
@@ -298,54 +349,98 @@ export class FloatingPanel {
|
|
|
298
349
|
this.contentLines = [];
|
|
299
350
|
this.currentPartialLine = "";
|
|
300
351
|
this.scrollOffset = 0;
|
|
352
|
+
this.userScrolled = false;
|
|
301
353
|
this.title = "";
|
|
302
354
|
this.footer = "";
|
|
303
355
|
this.prevFrame = [];
|
|
304
|
-
this.
|
|
305
|
-
this.bus.emit("shell:stdout-hold", {});
|
|
306
|
-
// If a foreground program (vim, htop) is already on alt screen,
|
|
307
|
-
// don't enter a second alt screen — it doesn't nest. Instead,
|
|
308
|
-
// render directly on the current screen and restore from the
|
|
309
|
-
// xterm buffer on dismiss.
|
|
310
|
-
this.usedAltScreen = !(this.buffer?.altScreen);
|
|
311
|
-
if (this.usedAltScreen) {
|
|
312
|
-
process.stdout.write("\x1b[?1049h");
|
|
313
|
-
}
|
|
314
|
-
this.resizeHandler = () => { this.prevFrame = []; this.render(); };
|
|
315
|
-
process.stdout.on("resize", this.resizeHandler);
|
|
316
|
-
this.render();
|
|
356
|
+
this.enterScreen();
|
|
317
357
|
}
|
|
318
|
-
|
|
319
|
-
|
|
358
|
+
/** Hide the panel without destroying conversation state. */
|
|
359
|
+
hide() {
|
|
360
|
+
if (!this._visible)
|
|
320
361
|
return;
|
|
321
362
|
if (this.renderTimer) {
|
|
322
363
|
clearTimeout(this.renderTimer);
|
|
323
364
|
this.renderTimer = null;
|
|
324
365
|
}
|
|
366
|
+
this._visible = false;
|
|
367
|
+
this.prevFrame = [];
|
|
368
|
+
if (this.phase === "active" && this.buffer) {
|
|
369
|
+
// Agent still working — enter passthrough mode.
|
|
370
|
+
// Keep alt screen + stdout held. Render TerminalBuffer directly
|
|
371
|
+
// so the background program's screen stays correct without
|
|
372
|
+
// handing rendering control back to ncurses.
|
|
373
|
+
this._passthrough = true;
|
|
374
|
+
this.ptyBuffer = "";
|
|
375
|
+
this.startPassthrough();
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// Agent idle or done — full teardown, hand back control.
|
|
379
|
+
this.teardownScreen();
|
|
380
|
+
}
|
|
381
|
+
this.handlers.call(`${this.prefix}:dismiss`);
|
|
382
|
+
}
|
|
383
|
+
/** Show the panel again after hide(), preserving conversation. */
|
|
384
|
+
show() {
|
|
385
|
+
if (this._visible || this.phase === "idle")
|
|
386
|
+
return;
|
|
387
|
+
if (this._passthrough) {
|
|
388
|
+
// Resume from passthrough — alt screen + stdout hold already active.
|
|
389
|
+
this.stopPassthrough();
|
|
390
|
+
this._passthrough = false;
|
|
391
|
+
this._visible = true;
|
|
392
|
+
this.prevFrame = [];
|
|
393
|
+
this.render();
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// Cold show — need full screen setup.
|
|
397
|
+
this.prevFrame = [];
|
|
398
|
+
this.enterScreen();
|
|
399
|
+
}
|
|
400
|
+
this.handlers.call(`${this.prefix}:show`);
|
|
401
|
+
}
|
|
402
|
+
/** Fully destroy the panel, resetting all state. */
|
|
403
|
+
dismiss() {
|
|
404
|
+
if (this.phase === "idle")
|
|
405
|
+
return;
|
|
325
406
|
if (this.autoDismissTimer) {
|
|
326
407
|
clearTimeout(this.autoDismissTimer);
|
|
327
408
|
this.autoDismissTimer = null;
|
|
328
409
|
}
|
|
329
|
-
if (this.
|
|
330
|
-
|
|
331
|
-
this.
|
|
410
|
+
if (this._passthrough) {
|
|
411
|
+
this.stopPassthrough();
|
|
412
|
+
this._passthrough = false;
|
|
413
|
+
this.teardownScreen();
|
|
414
|
+
}
|
|
415
|
+
else if (this._visible) {
|
|
416
|
+
this._visible = false;
|
|
417
|
+
if (this.renderTimer) {
|
|
418
|
+
clearTimeout(this.renderTimer);
|
|
419
|
+
this.renderTimer = null;
|
|
420
|
+
}
|
|
421
|
+
this.prevFrame = [];
|
|
422
|
+
this.teardownScreen();
|
|
332
423
|
}
|
|
333
|
-
this.suppressNextRedraw = true;
|
|
334
424
|
this.phase = "idle";
|
|
335
425
|
this.editor.clear();
|
|
336
|
-
this.
|
|
337
|
-
this.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
426
|
+
this.contentLines = [];
|
|
427
|
+
this.currentPartialLine = "";
|
|
428
|
+
this.scrollOffset = 0;
|
|
429
|
+
this.title = "";
|
|
430
|
+
this.footer = "";
|
|
431
|
+
}
|
|
432
|
+
/** Common screen enter logic shared by open() and show(). */
|
|
433
|
+
enterScreen() {
|
|
434
|
+
this._visible = true;
|
|
435
|
+
this.ptyBuffer = "";
|
|
436
|
+
this.bus.emit("shell:stdout-hold", {});
|
|
437
|
+
this.usedAltScreen = !(this.buffer?.altScreen);
|
|
438
|
+
if (this.usedAltScreen) {
|
|
439
|
+
process.stdout.write("\x1b[?1049h");
|
|
344
440
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
this.
|
|
348
|
-
this.handlers.call(`${this.prefix}:dismiss`);
|
|
441
|
+
this.resizeHandler = () => { this.prevFrame = []; this.render(); };
|
|
442
|
+
process.stdout.on("resize", this.resizeHandler);
|
|
443
|
+
this.render();
|
|
349
444
|
}
|
|
350
445
|
// ── Public content API ──────────────────────────────────────
|
|
351
446
|
appendText(text) {
|
|
@@ -392,17 +487,39 @@ export class FloatingPanel {
|
|
|
392
487
|
this.phase = "active";
|
|
393
488
|
}
|
|
394
489
|
setDone() {
|
|
395
|
-
this.
|
|
396
|
-
|
|
490
|
+
if (this._passthrough) {
|
|
491
|
+
// Agent finished while hidden — session over, hand back control.
|
|
492
|
+
this.dismiss();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
397
495
|
if (this.config.autoDismissMs > 0) {
|
|
496
|
+
// Legacy behavior: enter done state, auto-dismiss after delay
|
|
497
|
+
this.phase = "done";
|
|
498
|
+
this.render();
|
|
398
499
|
this.autoDismissTimer = setTimeout(() => {
|
|
399
500
|
if (this.phase === "done")
|
|
400
501
|
this.dismiss();
|
|
401
502
|
}, this.config.autoDismissMs);
|
|
402
503
|
}
|
|
504
|
+
else {
|
|
505
|
+
// Auto-prompt: transition to input for follow-up conversation
|
|
506
|
+
this.phase = "input";
|
|
507
|
+
this.editor.clear();
|
|
508
|
+
this.render();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
scrollUp(lines = 3) {
|
|
512
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - lines);
|
|
513
|
+
this.userScrolled = true;
|
|
514
|
+
this.render();
|
|
515
|
+
}
|
|
516
|
+
scrollDown(lines = 3) {
|
|
517
|
+
this.scrollOffset += lines;
|
|
518
|
+
this.userScrolled = true;
|
|
519
|
+
this.render();
|
|
403
520
|
}
|
|
404
521
|
getInput() {
|
|
405
|
-
return this.editor.
|
|
522
|
+
return this.editor.text;
|
|
406
523
|
}
|
|
407
524
|
requestRender() {
|
|
408
525
|
this.scheduleRender();
|
|
@@ -411,6 +528,15 @@ export class FloatingPanel {
|
|
|
411
528
|
handleIntercept(payload) {
|
|
412
529
|
const consumed = { ...payload, consumed: true };
|
|
413
530
|
const { data } = payload;
|
|
531
|
+
// Toggle visibility when trigger is pressed and panel is hidden but active
|
|
532
|
+
if (this.isTrigger(data) && this.phase !== "idle" && !this._visible) {
|
|
533
|
+
this.show();
|
|
534
|
+
return consumed;
|
|
535
|
+
}
|
|
536
|
+
// When not visible, only intercept the trigger key
|
|
537
|
+
if (!this._visible && this.phase !== "idle") {
|
|
538
|
+
return payload;
|
|
539
|
+
}
|
|
414
540
|
switch (this.phase) {
|
|
415
541
|
case "done":
|
|
416
542
|
this.dismiss();
|
|
@@ -419,12 +545,18 @@ export class FloatingPanel {
|
|
|
419
545
|
this.handleInputKey(data);
|
|
420
546
|
return consumed;
|
|
421
547
|
case "active":
|
|
422
|
-
if (data === "\x03")
|
|
548
|
+
if (data === "\x03") {
|
|
423
549
|
this.bus.emit("agent:cancel-request", {});
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
550
|
+
}
|
|
551
|
+
else if (data === "\x1b" || this.isTrigger(data)) {
|
|
552
|
+
this.hide();
|
|
553
|
+
}
|
|
554
|
+
else if (this.handleScroll(data)) {
|
|
555
|
+
// scroll handled
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
427
558
|
this.handlers.call(`${this.prefix}:input`, data);
|
|
559
|
+
}
|
|
428
560
|
return consumed;
|
|
429
561
|
default: // idle
|
|
430
562
|
if (this.isTrigger(data)) {
|
|
@@ -434,45 +566,107 @@ export class FloatingPanel {
|
|
|
434
566
|
return payload;
|
|
435
567
|
}
|
|
436
568
|
}
|
|
569
|
+
/** Handle scroll input. Returns true if consumed. */
|
|
570
|
+
handleScroll(data) {
|
|
571
|
+
// Arrow up / mouse wheel up
|
|
572
|
+
if (data === "\x1b[A" || data === "\x1bOA") {
|
|
573
|
+
this.scrollUp(1);
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
// Arrow down / mouse wheel down
|
|
577
|
+
if (data === "\x1b[B" || data === "\x1bOB") {
|
|
578
|
+
this.scrollDown(1);
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
// Page up (CSI 5~)
|
|
582
|
+
if (data === "\x1b[5~") {
|
|
583
|
+
this.scrollUp(this.computeGeometry().contentH - 1);
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
// Page down (CSI 6~)
|
|
587
|
+
if (data === "\x1b[6~") {
|
|
588
|
+
this.scrollDown(this.computeGeometry().contentH - 1);
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
// Mouse wheel: CSI M followed by button byte (64 = wheel up, 65 = wheel down)
|
|
592
|
+
if (data.length >= 6 && data.startsWith("\x1b[M")) {
|
|
593
|
+
const button = data.charCodeAt(3);
|
|
594
|
+
if (button === 96) {
|
|
595
|
+
this.scrollUp(3);
|
|
596
|
+
return true;
|
|
597
|
+
} // wheel up
|
|
598
|
+
if (button === 97) {
|
|
599
|
+
this.scrollDown(3);
|
|
600
|
+
return true;
|
|
601
|
+
} // wheel down
|
|
602
|
+
}
|
|
603
|
+
// SGR mouse: CSI < 64;x;yM (wheel up) / CSI < 65;x;yM (wheel down)
|
|
604
|
+
const sgr = data.match(/^\x1b\[<(64|65);\d+;\d+M$/);
|
|
605
|
+
if (sgr) {
|
|
606
|
+
if (sgr[1] === "64") {
|
|
607
|
+
this.scrollUp(3);
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
if (sgr[1] === "65") {
|
|
611
|
+
this.scrollDown(3);
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
437
617
|
handleInputKey(data) {
|
|
438
618
|
// Check full data string against trigger sequences (may be multi-byte)
|
|
439
619
|
if (this.isTrigger(data)) {
|
|
440
|
-
this.
|
|
620
|
+
this.hide();
|
|
441
621
|
return;
|
|
442
622
|
}
|
|
443
623
|
for (let i = 0; i < data.length; i++) {
|
|
444
624
|
const ch = data[i];
|
|
445
625
|
if (ch === "\x1b" && data[i + 1] == null) {
|
|
446
|
-
this.
|
|
626
|
+
this.hide();
|
|
447
627
|
return;
|
|
448
628
|
}
|
|
449
629
|
if (ch.charCodeAt(0) === 0x03) {
|
|
450
|
-
this.
|
|
630
|
+
this.hide();
|
|
451
631
|
return;
|
|
452
632
|
}
|
|
453
633
|
}
|
|
634
|
+
// Page Up/Down and mouse wheel scroll even in input phase
|
|
635
|
+
if (this.handleScroll(data))
|
|
636
|
+
return;
|
|
454
637
|
const actions = this.editor.feed(data);
|
|
455
638
|
for (const action of actions) {
|
|
456
639
|
switch (action.action) {
|
|
457
640
|
case "submit": {
|
|
458
|
-
const query = this.editor.
|
|
641
|
+
const query = this.editor.text.trim();
|
|
459
642
|
if (!query) {
|
|
460
|
-
this.
|
|
643
|
+
this.hide();
|
|
461
644
|
return;
|
|
462
645
|
}
|
|
646
|
+
this.editor.pushHistory(query);
|
|
463
647
|
this.phase = "active";
|
|
464
648
|
this.editor.clear();
|
|
465
649
|
this.handlers.call(`${this.prefix}:submit`, query);
|
|
466
650
|
return;
|
|
467
651
|
}
|
|
468
652
|
case "cancel":
|
|
469
|
-
this.
|
|
653
|
+
this.hide();
|
|
470
654
|
return;
|
|
655
|
+
case "arrow-up": {
|
|
656
|
+
const hist = this.editor.historyBack();
|
|
657
|
+
if (hist)
|
|
658
|
+
this.render();
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case "arrow-down": {
|
|
662
|
+
const hist = this.editor.historyForward();
|
|
663
|
+
if (hist)
|
|
664
|
+
this.render();
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
471
667
|
case "changed":
|
|
472
668
|
case "tab":
|
|
473
669
|
case "shift+tab":
|
|
474
|
-
case "arrow-up":
|
|
475
|
-
case "arrow-down":
|
|
476
670
|
case "delete-empty":
|
|
477
671
|
this.render();
|
|
478
672
|
break;
|
|
@@ -498,8 +692,8 @@ export class FloatingPanel {
|
|
|
498
692
|
width: geo.contentW,
|
|
499
693
|
height: geo.contentH,
|
|
500
694
|
phase: this.phase,
|
|
501
|
-
inputBuffer: this.editor.
|
|
502
|
-
inputCursor: this.editor.
|
|
695
|
+
inputBuffer: this.editor.displayText,
|
|
696
|
+
inputCursor: this.editor.displayCursor,
|
|
503
697
|
scrollOffset: this.scrollOffset,
|
|
504
698
|
contentLines: this.contentLines,
|
|
505
699
|
partialLine: this.currentPartialLine,
|
|
@@ -529,7 +723,7 @@ export class FloatingPanel {
|
|
|
529
723
|
}, 32);
|
|
530
724
|
}
|
|
531
725
|
render() {
|
|
532
|
-
if (this.phase === "idle")
|
|
726
|
+
if (this.phase === "idle" || !this._visible)
|
|
533
727
|
return;
|
|
534
728
|
const { rows: frame, cursorSeq } = this.buildFrame();
|
|
535
729
|
// Differential write — only send rows that changed
|
|
@@ -555,27 +749,66 @@ export class FloatingPanel {
|
|
|
555
749
|
this.prevFrame = frame;
|
|
556
750
|
}
|
|
557
751
|
// ── Screen helpers ────────────────────────────────────────
|
|
558
|
-
|
|
752
|
+
/** Full screen teardown: exit alt screen, release stdout, force redraw. */
|
|
753
|
+
teardownScreen() {
|
|
754
|
+
if (this.resizeHandler) {
|
|
755
|
+
process.stdout.off("resize", this.resizeHandler);
|
|
756
|
+
this.resizeHandler = null;
|
|
757
|
+
}
|
|
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;
|
|
559
763
|
if (this.usedAltScreen) {
|
|
560
|
-
// Leave alt screen — the terminal restores the saved main buffer.
|
|
561
764
|
process.stdout.write("\x1b[?1049l");
|
|
562
765
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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) {
|
|
772
|
+
process.stdout.write(this.ptyBuffer);
|
|
773
|
+
}
|
|
774
|
+
this.ptyBuffer = "";
|
|
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;
|
|
569
782
|
const rows = process.stdout.rows || 24;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// ── Passthrough rendering ─────────────────────────────────
|
|
790
|
+
/** Start rendering TerminalBuffer directly (no overlay box). */
|
|
791
|
+
startPassthrough() {
|
|
792
|
+
this.prevSerialized = "";
|
|
793
|
+
this.renderPassthrough();
|
|
794
|
+
this.passthroughTimer = setInterval(() => this.renderPassthrough(), 50);
|
|
795
|
+
}
|
|
796
|
+
stopPassthrough() {
|
|
797
|
+
if (this.passthroughTimer) {
|
|
798
|
+
clearInterval(this.passthroughTimer);
|
|
799
|
+
this.passthroughTimer = null;
|
|
800
|
+
}
|
|
801
|
+
this.prevSerialized = "";
|
|
802
|
+
}
|
|
803
|
+
/** Render the TerminalBuffer's screen content directly (no overlay). */
|
|
804
|
+
renderPassthrough() {
|
|
805
|
+
if (!this.buffer)
|
|
806
|
+
return;
|
|
807
|
+
this.buffer.flush();
|
|
808
|
+
const serialized = this.buffer.serialize();
|
|
809
|
+
if (serialized && serialized !== this.prevSerialized) {
|
|
810
|
+
this.prevSerialized = serialized;
|
|
811
|
+
process.stdout.write(`${SYNC_START}\x1b[H${serialized}${SYNC_END}`);
|
|
579
812
|
}
|
|
580
813
|
}
|
|
581
814
|
resolveSize(spec, available) {
|
|
@@ -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
|
|
28
|
+
private entries;
|
|
17
29
|
/**
|
|
18
|
-
* Register a named handler. If one already exists,
|
|
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:
|
|
33
|
+
define(name: string, fn: HandlerFn): void;
|
|
21
34
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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
|
-
*
|
|
30
|
-
*
|
|
42
|
+
* Advisors run outermost-first (last added = outermost).
|
|
43
|
+
* Returns an unadvise function that cleanly removes this advisor.
|
|
31
44
|
*/
|
|
32
|
-
advise(name: string,
|
|
45
|
+
advise(name: string, advisor: Advisor): () => void;
|
|
33
46
|
/**
|
|
34
|
-
* Call a named handler.
|
|
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 {};
|