agent-sh 0.7.0 → 0.8.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 +5 -1
- package/dist/agent/agent-loop.d.ts +2 -2
- package/dist/agent/agent-loop.js +106 -13
- package/dist/agent/conversation-state.d.ts +39 -9
- package/dist/agent/conversation-state.js +336 -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 +175 -0
- package/dist/agent/system-prompt.d.ts +2 -2
- package/dist/agent/system-prompt.js +25 -4
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/context-manager.d.ts +0 -1
- package/dist/context-manager.js +5 -110
- package/dist/core.js +14 -0
- package/dist/event-bus.d.ts +14 -0
- package/dist/extensions/overlay-agent.d.ts +4 -1
- package/dist/extensions/overlay-agent.js +115 -11
- package/dist/extensions/slash-commands.js +28 -0
- package/dist/extensions/terminal-buffer.js +9 -4
- package/dist/extensions/tui-renderer.js +119 -84
- package/dist/settings.d.ts +19 -2
- package/dist/settings.js +21 -3
- package/dist/shell.js +4 -0
- package/dist/token-budget.d.ts +13 -0
- package/dist/token-budget.js +50 -0
- package/dist/types.d.ts +0 -22
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/floating-panel.d.ts +32 -3
- package/dist/utils/floating-panel.js +296 -79
- package/dist/utils/line-editor.d.ts +9 -0
- package/dist/utils/line-editor.js +44 -0
- package/dist/utils/markdown.js +3 -3
- package/dist/utils/terminal-buffer.d.ts +4 -0
- package/dist/utils/terminal-buffer.js +13 -0
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- package/examples/extensions/claude-code-bridge/index.ts +77 -1
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/package.json +1 -1
|
@@ -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,18 @@ 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 panel is currently visible on screen. */
|
|
332
|
+
get visible() {
|
|
333
|
+
return this._visible;
|
|
334
|
+
}
|
|
289
335
|
get terminalBuffer() {
|
|
290
336
|
return this.buffer;
|
|
291
337
|
}
|
|
338
|
+
/** Open a fresh panel with a new conversation. */
|
|
292
339
|
open() {
|
|
293
340
|
if (this.phase !== "idle")
|
|
294
341
|
return;
|
|
@@ -298,54 +345,98 @@ export class FloatingPanel {
|
|
|
298
345
|
this.contentLines = [];
|
|
299
346
|
this.currentPartialLine = "";
|
|
300
347
|
this.scrollOffset = 0;
|
|
348
|
+
this.userScrolled = false;
|
|
301
349
|
this.title = "";
|
|
302
350
|
this.footer = "";
|
|
303
351
|
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();
|
|
352
|
+
this.enterScreen();
|
|
317
353
|
}
|
|
318
|
-
|
|
319
|
-
|
|
354
|
+
/** Hide the panel without destroying conversation state. */
|
|
355
|
+
hide() {
|
|
356
|
+
if (!this._visible)
|
|
320
357
|
return;
|
|
321
358
|
if (this.renderTimer) {
|
|
322
359
|
clearTimeout(this.renderTimer);
|
|
323
360
|
this.renderTimer = null;
|
|
324
361
|
}
|
|
362
|
+
this._visible = false;
|
|
363
|
+
this.prevFrame = [];
|
|
364
|
+
if (this.phase === "active" && this.buffer) {
|
|
365
|
+
// Agent still working — enter passthrough mode.
|
|
366
|
+
// Keep alt screen + stdout held. Render TerminalBuffer directly
|
|
367
|
+
// so the background program's screen stays correct without
|
|
368
|
+
// handing rendering control back to ncurses.
|
|
369
|
+
this._passthrough = true;
|
|
370
|
+
this.ptyBuffer = "";
|
|
371
|
+
this.startPassthrough();
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
// Agent idle or done — full teardown, hand back control.
|
|
375
|
+
this.teardownScreen();
|
|
376
|
+
}
|
|
377
|
+
this.handlers.call(`${this.prefix}:dismiss`);
|
|
378
|
+
}
|
|
379
|
+
/** Show the panel again after hide(), preserving conversation. */
|
|
380
|
+
show() {
|
|
381
|
+
if (this._visible || this.phase === "idle")
|
|
382
|
+
return;
|
|
383
|
+
if (this._passthrough) {
|
|
384
|
+
// Resume from passthrough — alt screen + stdout hold already active.
|
|
385
|
+
this.stopPassthrough();
|
|
386
|
+
this._passthrough = false;
|
|
387
|
+
this._visible = true;
|
|
388
|
+
this.prevFrame = [];
|
|
389
|
+
this.render();
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// Cold show — need full screen setup.
|
|
393
|
+
this.prevFrame = [];
|
|
394
|
+
this.enterScreen();
|
|
395
|
+
}
|
|
396
|
+
this.handlers.call(`${this.prefix}:show`);
|
|
397
|
+
}
|
|
398
|
+
/** Fully destroy the panel, resetting all state. */
|
|
399
|
+
dismiss() {
|
|
400
|
+
if (this.phase === "idle")
|
|
401
|
+
return;
|
|
325
402
|
if (this.autoDismissTimer) {
|
|
326
403
|
clearTimeout(this.autoDismissTimer);
|
|
327
404
|
this.autoDismissTimer = null;
|
|
328
405
|
}
|
|
329
|
-
if (this.
|
|
330
|
-
|
|
331
|
-
this.
|
|
406
|
+
if (this._passthrough) {
|
|
407
|
+
this.stopPassthrough();
|
|
408
|
+
this._passthrough = false;
|
|
409
|
+
this.teardownScreen();
|
|
410
|
+
}
|
|
411
|
+
else if (this._visible) {
|
|
412
|
+
this._visible = false;
|
|
413
|
+
if (this.renderTimer) {
|
|
414
|
+
clearTimeout(this.renderTimer);
|
|
415
|
+
this.renderTimer = null;
|
|
416
|
+
}
|
|
417
|
+
this.prevFrame = [];
|
|
418
|
+
this.teardownScreen();
|
|
332
419
|
}
|
|
333
|
-
this.suppressNextRedraw = true;
|
|
334
420
|
this.phase = "idle";
|
|
335
421
|
this.editor.clear();
|
|
336
|
-
this.
|
|
337
|
-
this.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
422
|
+
this.contentLines = [];
|
|
423
|
+
this.currentPartialLine = "";
|
|
424
|
+
this.scrollOffset = 0;
|
|
425
|
+
this.title = "";
|
|
426
|
+
this.footer = "";
|
|
427
|
+
}
|
|
428
|
+
/** Common screen enter logic shared by open() and show(). */
|
|
429
|
+
enterScreen() {
|
|
430
|
+
this._visible = true;
|
|
431
|
+
this.ptyBuffer = "";
|
|
432
|
+
this.bus.emit("shell:stdout-hold", {});
|
|
433
|
+
this.usedAltScreen = !(this.buffer?.altScreen);
|
|
434
|
+
if (this.usedAltScreen) {
|
|
435
|
+
process.stdout.write("\x1b[?1049h");
|
|
344
436
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
this.
|
|
348
|
-
this.handlers.call(`${this.prefix}:dismiss`);
|
|
437
|
+
this.resizeHandler = () => { this.prevFrame = []; this.render(); };
|
|
438
|
+
process.stdout.on("resize", this.resizeHandler);
|
|
439
|
+
this.render();
|
|
349
440
|
}
|
|
350
441
|
// ── Public content API ──────────────────────────────────────
|
|
351
442
|
appendText(text) {
|
|
@@ -392,14 +483,36 @@ export class FloatingPanel {
|
|
|
392
483
|
this.phase = "active";
|
|
393
484
|
}
|
|
394
485
|
setDone() {
|
|
395
|
-
this.
|
|
396
|
-
|
|
486
|
+
if (this._passthrough) {
|
|
487
|
+
// Agent finished while hidden — session over, hand back control.
|
|
488
|
+
this.dismiss();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
397
491
|
if (this.config.autoDismissMs > 0) {
|
|
492
|
+
// Legacy behavior: enter done state, auto-dismiss after delay
|
|
493
|
+
this.phase = "done";
|
|
494
|
+
this.render();
|
|
398
495
|
this.autoDismissTimer = setTimeout(() => {
|
|
399
496
|
if (this.phase === "done")
|
|
400
497
|
this.dismiss();
|
|
401
498
|
}, this.config.autoDismissMs);
|
|
402
499
|
}
|
|
500
|
+
else {
|
|
501
|
+
// Auto-prompt: transition to input for follow-up conversation
|
|
502
|
+
this.phase = "input";
|
|
503
|
+
this.editor.clear();
|
|
504
|
+
this.render();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
scrollUp(lines = 3) {
|
|
508
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - lines);
|
|
509
|
+
this.userScrolled = true;
|
|
510
|
+
this.render();
|
|
511
|
+
}
|
|
512
|
+
scrollDown(lines = 3) {
|
|
513
|
+
this.scrollOffset += lines;
|
|
514
|
+
this.userScrolled = true;
|
|
515
|
+
this.render();
|
|
403
516
|
}
|
|
404
517
|
getInput() {
|
|
405
518
|
return this.editor.buffer;
|
|
@@ -411,6 +524,15 @@ export class FloatingPanel {
|
|
|
411
524
|
handleIntercept(payload) {
|
|
412
525
|
const consumed = { ...payload, consumed: true };
|
|
413
526
|
const { data } = payload;
|
|
527
|
+
// Toggle visibility when trigger is pressed and panel is hidden but active
|
|
528
|
+
if (this.isTrigger(data) && this.phase !== "idle" && !this._visible) {
|
|
529
|
+
this.show();
|
|
530
|
+
return consumed;
|
|
531
|
+
}
|
|
532
|
+
// When not visible, only intercept the trigger key
|
|
533
|
+
if (!this._visible && this.phase !== "idle") {
|
|
534
|
+
return payload;
|
|
535
|
+
}
|
|
414
536
|
switch (this.phase) {
|
|
415
537
|
case "done":
|
|
416
538
|
this.dismiss();
|
|
@@ -419,12 +541,18 @@ export class FloatingPanel {
|
|
|
419
541
|
this.handleInputKey(data);
|
|
420
542
|
return consumed;
|
|
421
543
|
case "active":
|
|
422
|
-
if (data === "\x03")
|
|
544
|
+
if (data === "\x03") {
|
|
423
545
|
this.bus.emit("agent:cancel-request", {});
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
546
|
+
}
|
|
547
|
+
else if (data === "\x1b" || this.isTrigger(data)) {
|
|
548
|
+
this.hide();
|
|
549
|
+
}
|
|
550
|
+
else if (this.handleScroll(data)) {
|
|
551
|
+
// scroll handled
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
427
554
|
this.handlers.call(`${this.prefix}:input`, data);
|
|
555
|
+
}
|
|
428
556
|
return consumed;
|
|
429
557
|
default: // idle
|
|
430
558
|
if (this.isTrigger(data)) {
|
|
@@ -434,45 +562,107 @@ export class FloatingPanel {
|
|
|
434
562
|
return payload;
|
|
435
563
|
}
|
|
436
564
|
}
|
|
565
|
+
/** Handle scroll input. Returns true if consumed. */
|
|
566
|
+
handleScroll(data) {
|
|
567
|
+
// Arrow up / mouse wheel up
|
|
568
|
+
if (data === "\x1b[A" || data === "\x1bOA") {
|
|
569
|
+
this.scrollUp(1);
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
// Arrow down / mouse wheel down
|
|
573
|
+
if (data === "\x1b[B" || data === "\x1bOB") {
|
|
574
|
+
this.scrollDown(1);
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
// Page up (CSI 5~)
|
|
578
|
+
if (data === "\x1b[5~") {
|
|
579
|
+
this.scrollUp(this.computeGeometry().contentH - 1);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
// Page down (CSI 6~)
|
|
583
|
+
if (data === "\x1b[6~") {
|
|
584
|
+
this.scrollDown(this.computeGeometry().contentH - 1);
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
// Mouse wheel: CSI M followed by button byte (64 = wheel up, 65 = wheel down)
|
|
588
|
+
if (data.length >= 6 && data.startsWith("\x1b[M")) {
|
|
589
|
+
const button = data.charCodeAt(3);
|
|
590
|
+
if (button === 96) {
|
|
591
|
+
this.scrollUp(3);
|
|
592
|
+
return true;
|
|
593
|
+
} // wheel up
|
|
594
|
+
if (button === 97) {
|
|
595
|
+
this.scrollDown(3);
|
|
596
|
+
return true;
|
|
597
|
+
} // wheel down
|
|
598
|
+
}
|
|
599
|
+
// SGR mouse: CSI < 64;x;yM (wheel up) / CSI < 65;x;yM (wheel down)
|
|
600
|
+
const sgr = data.match(/^\x1b\[<(64|65);\d+;\d+M$/);
|
|
601
|
+
if (sgr) {
|
|
602
|
+
if (sgr[1] === "64") {
|
|
603
|
+
this.scrollUp(3);
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
if (sgr[1] === "65") {
|
|
607
|
+
this.scrollDown(3);
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
437
613
|
handleInputKey(data) {
|
|
438
614
|
// Check full data string against trigger sequences (may be multi-byte)
|
|
439
615
|
if (this.isTrigger(data)) {
|
|
440
|
-
this.
|
|
616
|
+
this.hide();
|
|
441
617
|
return;
|
|
442
618
|
}
|
|
443
619
|
for (let i = 0; i < data.length; i++) {
|
|
444
620
|
const ch = data[i];
|
|
445
621
|
if (ch === "\x1b" && data[i + 1] == null) {
|
|
446
|
-
this.
|
|
622
|
+
this.hide();
|
|
447
623
|
return;
|
|
448
624
|
}
|
|
449
625
|
if (ch.charCodeAt(0) === 0x03) {
|
|
450
|
-
this.
|
|
626
|
+
this.hide();
|
|
451
627
|
return;
|
|
452
628
|
}
|
|
453
629
|
}
|
|
630
|
+
// Page Up/Down and mouse wheel scroll even in input phase
|
|
631
|
+
if (this.handleScroll(data))
|
|
632
|
+
return;
|
|
454
633
|
const actions = this.editor.feed(data);
|
|
455
634
|
for (const action of actions) {
|
|
456
635
|
switch (action.action) {
|
|
457
636
|
case "submit": {
|
|
458
637
|
const query = this.editor.buffer.trim();
|
|
459
638
|
if (!query) {
|
|
460
|
-
this.
|
|
639
|
+
this.hide();
|
|
461
640
|
return;
|
|
462
641
|
}
|
|
642
|
+
this.editor.pushHistory(query);
|
|
463
643
|
this.phase = "active";
|
|
464
644
|
this.editor.clear();
|
|
465
645
|
this.handlers.call(`${this.prefix}:submit`, query);
|
|
466
646
|
return;
|
|
467
647
|
}
|
|
468
648
|
case "cancel":
|
|
469
|
-
this.
|
|
649
|
+
this.hide();
|
|
470
650
|
return;
|
|
651
|
+
case "arrow-up": {
|
|
652
|
+
const hist = this.editor.historyBack();
|
|
653
|
+
if (hist)
|
|
654
|
+
this.render();
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case "arrow-down": {
|
|
658
|
+
const hist = this.editor.historyForward();
|
|
659
|
+
if (hist)
|
|
660
|
+
this.render();
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
471
663
|
case "changed":
|
|
472
664
|
case "tab":
|
|
473
665
|
case "shift+tab":
|
|
474
|
-
case "arrow-up":
|
|
475
|
-
case "arrow-down":
|
|
476
666
|
case "delete-empty":
|
|
477
667
|
this.render();
|
|
478
668
|
break;
|
|
@@ -529,7 +719,7 @@ export class FloatingPanel {
|
|
|
529
719
|
}, 32);
|
|
530
720
|
}
|
|
531
721
|
render() {
|
|
532
|
-
if (this.phase === "idle")
|
|
722
|
+
if (this.phase === "idle" || !this._visible)
|
|
533
723
|
return;
|
|
534
724
|
const { rows: frame, cursorSeq } = this.buildFrame();
|
|
535
725
|
// Differential write — only send rows that changed
|
|
@@ -555,27 +745,54 @@ export class FloatingPanel {
|
|
|
555
745
|
this.prevFrame = frame;
|
|
556
746
|
}
|
|
557
747
|
// ── Screen helpers ────────────────────────────────────────
|
|
558
|
-
|
|
748
|
+
/** Full screen teardown: exit alt screen, release stdout, force redraw. */
|
|
749
|
+
teardownScreen() {
|
|
750
|
+
if (this.resizeHandler) {
|
|
751
|
+
process.stdout.off("resize", this.resizeHandler);
|
|
752
|
+
this.resizeHandler = null;
|
|
753
|
+
}
|
|
754
|
+
this.suppressNextRedraw = true;
|
|
559
755
|
if (this.usedAltScreen) {
|
|
560
|
-
// Leave alt screen — the terminal restores the saved main buffer.
|
|
561
756
|
process.stdout.write("\x1b[?1049l");
|
|
562
757
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
758
|
+
// ncurses's curscr is stale — only a real dimension change triggers
|
|
759
|
+
// clearok + full repaint (same-size SIGWINCH is a no-op).
|
|
760
|
+
const cols = process.stdout.columns || 80;
|
|
761
|
+
const rows = process.stdout.rows || 24;
|
|
762
|
+
this.bus.emit("shell:pty-resize", { cols, rows: rows - 1 });
|
|
763
|
+
setTimeout(() => {
|
|
764
|
+
this.bus.emit("shell:pty-resize", { cols, rows });
|
|
765
|
+
}, 50);
|
|
766
|
+
if (!this.buffer && this.ptyBuffer) {
|
|
767
|
+
process.stdout.write(this.ptyBuffer);
|
|
768
|
+
}
|
|
769
|
+
this.ptyBuffer = "";
|
|
770
|
+
this.bus.emit("shell:stdout-hide", {});
|
|
771
|
+
this.bus.emit("shell:stdout-release", {});
|
|
772
|
+
}
|
|
773
|
+
// ── Passthrough rendering ─────────────────────────────────
|
|
774
|
+
/** Start rendering TerminalBuffer directly (no overlay box). */
|
|
775
|
+
startPassthrough() {
|
|
776
|
+
this.prevSerialized = "";
|
|
777
|
+
this.renderPassthrough();
|
|
778
|
+
this.passthroughTimer = setInterval(() => this.renderPassthrough(), 50);
|
|
779
|
+
}
|
|
780
|
+
stopPassthrough() {
|
|
781
|
+
if (this.passthroughTimer) {
|
|
782
|
+
clearInterval(this.passthroughTimer);
|
|
783
|
+
this.passthroughTimer = null;
|
|
784
|
+
}
|
|
785
|
+
this.prevSerialized = "";
|
|
786
|
+
}
|
|
787
|
+
/** Render the TerminalBuffer's screen content directly (no overlay). */
|
|
788
|
+
renderPassthrough() {
|
|
789
|
+
if (!this.buffer)
|
|
790
|
+
return;
|
|
791
|
+
this.buffer.flush();
|
|
792
|
+
const serialized = this.buffer.serialize();
|
|
793
|
+
if (serialized && serialized !== this.prevSerialized) {
|
|
794
|
+
this.prevSerialized = serialized;
|
|
795
|
+
process.stdout.write(`${SYNC_START}\x1b[H${serialized}${SYNC_END}`);
|
|
579
796
|
}
|
|
580
797
|
}
|
|
581
798
|
resolveSize(spec, available) {
|
|
@@ -27,6 +27,9 @@ export declare class LineEditor {
|
|
|
27
27
|
buffer: string;
|
|
28
28
|
cursor: number;
|
|
29
29
|
private pendingSeq;
|
|
30
|
+
private history;
|
|
31
|
+
private historyIndex;
|
|
32
|
+
private savedBuffer;
|
|
30
33
|
/** Process raw terminal input, return actions for the consumer. */
|
|
31
34
|
feed(data: string): LineEditAction[];
|
|
32
35
|
/** Check if there's a pending incomplete escape sequence. */
|
|
@@ -34,6 +37,12 @@ export declare class LineEditor {
|
|
|
34
37
|
/** Flush a pending sequence — treat bare \x1b as cancel, discard incomplete CSI. */
|
|
35
38
|
flushPendingEscape(): LineEditAction[];
|
|
36
39
|
clear(): void;
|
|
40
|
+
/** Add a line to history (most recent first). */
|
|
41
|
+
pushHistory(line: string): void;
|
|
42
|
+
/** Navigate to a previous history entry. Returns changed action or null. */
|
|
43
|
+
historyBack(): LineEditAction | null;
|
|
44
|
+
/** Navigate to a more recent history entry. Returns changed action or null. */
|
|
45
|
+
historyForward(): LineEditAction | null;
|
|
37
46
|
private readonly bindings;
|
|
38
47
|
/** Resolve a key name from the bindings table and execute it. */
|
|
39
48
|
private dispatch;
|
|
@@ -14,6 +14,10 @@ export class LineEditor {
|
|
|
14
14
|
buffer = "";
|
|
15
15
|
cursor = 0;
|
|
16
16
|
pendingSeq = ""; // buffered incomplete escape sequence
|
|
17
|
+
// ── History ──────────────────────────────────────────────────
|
|
18
|
+
history = [];
|
|
19
|
+
historyIndex = -1; // -1 = current input, 0..N = history entries (newest first)
|
|
20
|
+
savedBuffer = ""; // saves current input when browsing history
|
|
17
21
|
/** Process raw terminal input, return actions for the consumer. */
|
|
18
22
|
feed(data) {
|
|
19
23
|
// If we had a pending incomplete escape sequence, prepend it
|
|
@@ -147,6 +151,46 @@ export class LineEditor {
|
|
|
147
151
|
this.buffer = "";
|
|
148
152
|
this.cursor = 0;
|
|
149
153
|
this.pendingSeq = "";
|
|
154
|
+
this.historyIndex = -1;
|
|
155
|
+
this.savedBuffer = "";
|
|
156
|
+
}
|
|
157
|
+
/** Add a line to history (most recent first). */
|
|
158
|
+
pushHistory(line) {
|
|
159
|
+
if (!line.trim())
|
|
160
|
+
return;
|
|
161
|
+
// Deduplicate: remove if already at top
|
|
162
|
+
if (this.history.length > 0 && this.history[0] === line)
|
|
163
|
+
return;
|
|
164
|
+
this.history.unshift(line);
|
|
165
|
+
// Cap history size
|
|
166
|
+
if (this.history.length > 100)
|
|
167
|
+
this.history.pop();
|
|
168
|
+
}
|
|
169
|
+
/** Navigate to a previous history entry. Returns changed action or null. */
|
|
170
|
+
historyBack() {
|
|
171
|
+
if (this.historyIndex + 1 >= this.history.length)
|
|
172
|
+
return null;
|
|
173
|
+
if (this.historyIndex === -1) {
|
|
174
|
+
this.savedBuffer = this.buffer; // save current input
|
|
175
|
+
}
|
|
176
|
+
this.historyIndex++;
|
|
177
|
+
this.buffer = this.history[this.historyIndex];
|
|
178
|
+
this.cursor = this.buffer.length;
|
|
179
|
+
return { action: "changed" };
|
|
180
|
+
}
|
|
181
|
+
/** Navigate to a more recent history entry. Returns changed action or null. */
|
|
182
|
+
historyForward() {
|
|
183
|
+
if (this.historyIndex <= -1)
|
|
184
|
+
return null;
|
|
185
|
+
this.historyIndex--;
|
|
186
|
+
if (this.historyIndex === -1) {
|
|
187
|
+
this.buffer = this.savedBuffer;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.buffer = this.history[this.historyIndex];
|
|
191
|
+
}
|
|
192
|
+
this.cursor = this.buffer.length;
|
|
193
|
+
return { action: "changed" };
|
|
150
194
|
}
|
|
151
195
|
// ── Key bindings ────────────────────────────────────────────
|
|
152
196
|
//
|
package/dist/utils/markdown.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { visibleLen } from "./ansi.js";
|
|
1
|
+
import { visibleLen, truncateToWidth, padEndToWidth } from "./ansi.js";
|
|
2
2
|
import { palette as p } from "./palette.js";
|
|
3
3
|
const MAX_CONTENT_WIDTH = 90;
|
|
4
4
|
/**
|
|
@@ -177,7 +177,7 @@ export class MarkdownRenderer {
|
|
|
177
177
|
const colWidths = new Array(numCols).fill(0);
|
|
178
178
|
for (const row of dataRows) {
|
|
179
179
|
for (let c = 0; c < numCols; c++) {
|
|
180
|
-
colWidths[c] = Math.max(colWidths[c], row[c]
|
|
180
|
+
colWidths[c] = Math.max(colWidths[c], visibleLen(row[c]));
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
// Shrink columns proportionally if total exceeds content width
|
|
@@ -201,7 +201,7 @@ export class MarkdownRenderer {
|
|
|
201
201
|
const isHeader = hasHeader && i === 0;
|
|
202
202
|
const cells = row.map((cell, c) => {
|
|
203
203
|
const w = colWidths[c];
|
|
204
|
-
const text = cell
|
|
204
|
+
const text = visibleLen(cell) > w ? truncateToWidth(cell, w) : padEndToWidth(cell, w);
|
|
205
205
|
return isHeader ? `${p.bold}${text}${p.reset}` : text;
|
|
206
206
|
});
|
|
207
207
|
this.writeLine(`${p.dim}│${p.reset} ${cells.join(` ${p.dim}│${p.reset} `)} ${p.dim}│${p.reset}`);
|