agent-sh 0.12.2 → 0.12.4
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/dist/agent/agent-loop.js +40 -7
- package/dist/agent/skills.js +2 -2
- package/dist/agent/system-prompt.js +2 -3
- package/dist/core.js +4 -3
- package/dist/event-bus.d.ts +46 -0
- package/dist/event-bus.js +51 -3
- package/dist/extension-loader.js +1 -0
- package/dist/extensions/agent-backend.js +4 -1
- package/dist/extensions/openrouter.js +32 -0
- package/dist/init.js +1 -2
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +7 -3
- package/dist/shell/input-handler.d.ts +8 -18
- package/dist/shell/input-handler.js +57 -227
- package/dist/shell/shell.js +1 -1
- package/dist/shell/tui-input-view.d.ts +37 -0
- package/dist/shell/tui-input-view.js +140 -0
- package/dist/types.d.ts +6 -0
- package/dist/utils/compositor.d.ts +7 -1
- package/dist/utils/compositor.js +13 -1
- package/dist/utils/floating-panel.d.ts +6 -2
- package/dist/utils/floating-panel.js +17 -17
- package/dist/utils/ref-counter.d.ts +9 -0
- package/dist/utils/ref-counter.js +9 -0
- package/package.json +1 -1
- package/dist/utils/frame-renderer.d.ts +0 -26
- package/dist/utils/frame-renderer.js +0 -76
- package/dist/utils/output-writer.d.ts +0 -36
- package/dist/utils/output-writer.js +0 -45
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal renderer for the input-mode prompt and autocomplete dropdown.
|
|
3
|
+
* Owns screen state (cursor row/col, autocomplete line count) and the
|
|
4
|
+
* ANSI redraw. The controller drives it via a small VM shape.
|
|
5
|
+
*/
|
|
6
|
+
import { visibleLen } from "../utils/ansi.js";
|
|
7
|
+
import { palette as p } from "../utils/palette.js";
|
|
8
|
+
import { StdoutSurface } from "../utils/compositor.js";
|
|
9
|
+
export class TuiInputView {
|
|
10
|
+
cursorRowsBelow = 0;
|
|
11
|
+
cursorTermCol = 1;
|
|
12
|
+
autocompleteLines = 0;
|
|
13
|
+
surface;
|
|
14
|
+
constructor(surface) {
|
|
15
|
+
this.surface = surface ?? new StdoutSurface();
|
|
16
|
+
}
|
|
17
|
+
resetCursor() {
|
|
18
|
+
this.cursorRowsBelow = 0;
|
|
19
|
+
this.cursorTermCol = 1;
|
|
20
|
+
}
|
|
21
|
+
enableModeKeys() {
|
|
22
|
+
// Kitty progressive enhancement + bracket paste (Shift+Enter → \x1b[13;2u).
|
|
23
|
+
this.surface.write("\x1b[>1u\x1b[?2004h");
|
|
24
|
+
}
|
|
25
|
+
disableModeKeys() {
|
|
26
|
+
this.surface.write("\x1b[<u\x1b[?2004l");
|
|
27
|
+
}
|
|
28
|
+
clearPromptArea() {
|
|
29
|
+
if (this.cursorRowsBelow > 0) {
|
|
30
|
+
this.surface.write(`\x1b[${this.cursorRowsBelow}A`);
|
|
31
|
+
}
|
|
32
|
+
this.surface.write("\r\x1b[J");
|
|
33
|
+
this.cursorRowsBelow = 0;
|
|
34
|
+
}
|
|
35
|
+
drawPrompt(vm) {
|
|
36
|
+
const termW = this.surface.columns;
|
|
37
|
+
if (this.cursorRowsBelow > 0) {
|
|
38
|
+
this.surface.write(`\x1b[${this.cursorRowsBelow}A`);
|
|
39
|
+
}
|
|
40
|
+
this.surface.write("\r\x1b[J");
|
|
41
|
+
const infoPrefix = vm.agentInfo.info
|
|
42
|
+
? `${vm.agentInfo.info} ${p.success}${vm.indicator}${p.reset} `
|
|
43
|
+
: `${p.success}${vm.indicator}${p.reset} `;
|
|
44
|
+
const promptPrefix = infoPrefix + p.warning + p.bold + vm.promptIcon + " " + p.reset;
|
|
45
|
+
const promptVisLen = visibleLen(infoPrefix) + visibleLen(vm.promptIcon) + 1;
|
|
46
|
+
const display = vm.showBuffer ? vm.displayText : "";
|
|
47
|
+
const dCursor = vm.showBuffer ? vm.displayCursor : 0;
|
|
48
|
+
if (!vm.showBuffer) {
|
|
49
|
+
this.surface.write(promptPrefix);
|
|
50
|
+
const N = promptVisLen;
|
|
51
|
+
this.cursorRowsBelow = N > 0 ? Math.ceil(N / termW) - 1 : 0;
|
|
52
|
+
this.cursorTermCol = N === 0 ? 1 : (N % termW === 0 ? termW : (N % termW) + 1);
|
|
53
|
+
}
|
|
54
|
+
else if (!display.includes("\n")) {
|
|
55
|
+
// DECSC/DECRC bracket the after-cursor text so the cursor lands mid-line.
|
|
56
|
+
const before = display.slice(0, dCursor);
|
|
57
|
+
const after = display.slice(dCursor);
|
|
58
|
+
this.surface.write(promptPrefix + p.accent + before + p.reset +
|
|
59
|
+
"\x1b7" +
|
|
60
|
+
p.accent + after + p.reset +
|
|
61
|
+
"\x1b8");
|
|
62
|
+
const cursorVisCol = promptVisLen + visibleLen(before);
|
|
63
|
+
this.cursorRowsBelow = cursorVisCol > 0 ? Math.ceil(cursorVisCol / termW) - 1 : 0;
|
|
64
|
+
this.cursorTermCol = cursorVisCol === 0 ? 1 : (cursorVisCol % termW === 0 ? termW : (cursorVisCol % termW) + 1);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const lines = display.split("\n");
|
|
68
|
+
const indent = " ".repeat(promptVisLen);
|
|
69
|
+
let charsRemaining = dCursor;
|
|
70
|
+
let cursorLine = 0;
|
|
71
|
+
for (let li = 0; li < lines.length; li++) {
|
|
72
|
+
if (charsRemaining <= lines[li].length) {
|
|
73
|
+
cursorLine = li;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
charsRemaining -= lines[li].length + 1;
|
|
77
|
+
cursorLine = li + 1;
|
|
78
|
+
}
|
|
79
|
+
let output = "";
|
|
80
|
+
let cursorRowFromTop = 0;
|
|
81
|
+
let rowsSoFar = 0;
|
|
82
|
+
for (let li = 0; li < lines.length; li++) {
|
|
83
|
+
const prefix = li === 0 ? promptPrefix : indent;
|
|
84
|
+
const lineText = lines[li];
|
|
85
|
+
const lineVisLen = promptVisLen + visibleLen(lineText);
|
|
86
|
+
const lineTermRows = lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
87
|
+
if (li === cursorLine) {
|
|
88
|
+
const before = lineText.slice(0, charsRemaining);
|
|
89
|
+
const after = lineText.slice(charsRemaining);
|
|
90
|
+
output += prefix + p.accent + before + p.reset;
|
|
91
|
+
output += "\x1b7";
|
|
92
|
+
output += p.accent + after + p.reset;
|
|
93
|
+
const beforeVisCol = promptVisLen + visibleLen(before);
|
|
94
|
+
cursorRowFromTop = rowsSoFar + (beforeVisCol > 0 ? Math.ceil(beforeVisCol / termW) - 1 : 0);
|
|
95
|
+
this.cursorTermCol = beforeVisCol === 0 ? 1 : (beforeVisCol % termW === 0 ? termW : (beforeVisCol % termW) + 1);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
output += prefix + p.accent + lineText + p.reset;
|
|
99
|
+
}
|
|
100
|
+
if (li < lines.length - 1)
|
|
101
|
+
output += "\n";
|
|
102
|
+
rowsSoFar += lineTermRows;
|
|
103
|
+
}
|
|
104
|
+
this.surface.write(output + "\x1b8");
|
|
105
|
+
this.cursorRowsBelow = cursorRowFromTop;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
drawAutocomplete(vm) {
|
|
109
|
+
if (vm.items.length === 0)
|
|
110
|
+
return;
|
|
111
|
+
const lines = [];
|
|
112
|
+
for (let i = 0; i < vm.items.length; i++) {
|
|
113
|
+
const item = vm.items[i];
|
|
114
|
+
const selected = i === vm.selected;
|
|
115
|
+
if (selected) {
|
|
116
|
+
lines.push(` \x1b[7m ${p.accent}${item.name.padEnd(12)}${p.reset}\x1b[7m ${item.description} ${p.reset}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
lines.push(` ${p.muted}${item.name.padEnd(12)} ${item.description}${p.reset}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this.surface.write("\n" + lines.join("\n"));
|
|
123
|
+
this.autocompleteLines = lines.length;
|
|
124
|
+
if (this.autocompleteLines > 0) {
|
|
125
|
+
this.surface.write(`\x1b[${this.autocompleteLines}A`);
|
|
126
|
+
}
|
|
127
|
+
// Absolute column set — preceding \n may have scrolled, invalidating DECSC.
|
|
128
|
+
this.surface.write(`\x1b[${this.cursorTermCol}G`);
|
|
129
|
+
}
|
|
130
|
+
clearAutocomplete() {
|
|
131
|
+
if (this.autocompleteLines <= 0)
|
|
132
|
+
return;
|
|
133
|
+
// CSI B (cursor down, bounded) so we don't scroll on the last row.
|
|
134
|
+
for (let i = 0; i < this.autocompleteLines; i++) {
|
|
135
|
+
this.surface.write("\x1b[B\x1b[2K");
|
|
136
|
+
}
|
|
137
|
+
this.surface.write(`\x1b[${this.autocompleteLines}A\x1b[${this.cursorTermCol}G`);
|
|
138
|
+
this.autocompleteLines = 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -48,6 +48,9 @@ export interface AgentMode {
|
|
|
48
48
|
reasoning?: boolean;
|
|
49
49
|
/** Provider supports the reasoning_effort parameter. */
|
|
50
50
|
supportsReasoningEffort?: boolean;
|
|
51
|
+
/** Echo reasoning_content back on assistant turns. Required by DeepSeek;
|
|
52
|
+
* default off (leaky shims may forward it to the model as OOD input). */
|
|
53
|
+
echoReasoning?: boolean;
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
53
56
|
* Backend-agnostic LLM interface exposed via `ctx.llm`. Backends fulfill it
|
|
@@ -146,6 +149,9 @@ export interface ExtensionContext {
|
|
|
146
149
|
* Extensions use `compositor.redirect()` to capture output (e.g. overlay panels).
|
|
147
150
|
*/
|
|
148
151
|
compositor: Compositor;
|
|
152
|
+
/** Teardown callback fired on /reload. For resources the scoped context
|
|
153
|
+
* can't track: process listeners, timers, watchers, sockets. */
|
|
154
|
+
onDispose: (fn: () => void) => void;
|
|
149
155
|
/**
|
|
150
156
|
* Create a remote session that routes agent output to a surface and
|
|
151
157
|
* optionally accepts queries. Handles all compositor routing, shell
|
|
@@ -36,6 +36,10 @@ export interface RenderSurface {
|
|
|
36
36
|
writeLine(line: string): void;
|
|
37
37
|
/** Available width in columns. */
|
|
38
38
|
readonly columns: number;
|
|
39
|
+
/** Available height in rows. */
|
|
40
|
+
readonly rows: number;
|
|
41
|
+
/** Subscribe to size changes. Returns unsubscribe. */
|
|
42
|
+
onResize(cb: (cols: number, rows: number) => void): () => void;
|
|
39
43
|
}
|
|
40
44
|
export interface Compositor {
|
|
41
45
|
/** Get the currently active surface for a stream. */
|
|
@@ -48,11 +52,13 @@ export interface Compositor {
|
|
|
48
52
|
}
|
|
49
53
|
/** Silent sink — drops all output. Used when no surface is registered. */
|
|
50
54
|
export declare const nullSurface: RenderSurface;
|
|
51
|
-
/** Surface backed by process.stdout. */
|
|
55
|
+
/** Surface backed by process.stdout — the only sanctioned bridge to it. */
|
|
52
56
|
export declare class StdoutSurface implements RenderSurface {
|
|
53
57
|
write(text: string): void;
|
|
54
58
|
writeLine(line: string): void;
|
|
55
59
|
get columns(): number;
|
|
60
|
+
get rows(): number;
|
|
61
|
+
onResize(cb: (cols: number, rows: number) => void): () => void;
|
|
56
62
|
}
|
|
57
63
|
export declare class DefaultCompositor implements Compositor {
|
|
58
64
|
private defaults;
|
package/dist/utils/compositor.js
CHANGED
|
@@ -29,8 +29,10 @@ export const nullSurface = {
|
|
|
29
29
|
write() { },
|
|
30
30
|
writeLine() { },
|
|
31
31
|
get columns() { return 80; },
|
|
32
|
+
get rows() { return 24; },
|
|
33
|
+
onResize() { return () => { }; },
|
|
32
34
|
};
|
|
33
|
-
/** Surface backed by process.stdout. */
|
|
35
|
+
/** Surface backed by process.stdout — the only sanctioned bridge to it. */
|
|
34
36
|
export class StdoutSurface {
|
|
35
37
|
write(text) {
|
|
36
38
|
if (process.stdout.writable) {
|
|
@@ -46,6 +48,14 @@ export class StdoutSurface {
|
|
|
46
48
|
get columns() {
|
|
47
49
|
return process.stdout.columns || 80;
|
|
48
50
|
}
|
|
51
|
+
get rows() {
|
|
52
|
+
return process.stdout.rows || 24;
|
|
53
|
+
}
|
|
54
|
+
onResize(cb) {
|
|
55
|
+
const handler = () => cb(this.columns, this.rows);
|
|
56
|
+
process.stdout.on("resize", handler);
|
|
57
|
+
return () => { process.stdout.off("resize", handler); };
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
export class DefaultCompositor {
|
|
51
61
|
defaults = new Map();
|
|
@@ -111,6 +121,8 @@ export class DefaultCompositor {
|
|
|
111
121
|
target.writeLine(line);
|
|
112
122
|
},
|
|
113
123
|
get columns() { return target.columns; },
|
|
124
|
+
get rows() { return target.rows; },
|
|
125
|
+
onResize: (cb) => target.onResize(cb),
|
|
114
126
|
};
|
|
115
127
|
}
|
|
116
128
|
}
|
|
@@ -2,6 +2,7 @@ import { TerminalBuffer } from "./terminal-buffer.js";
|
|
|
2
2
|
import { HandlerRegistry } from "./handler-registry.js";
|
|
3
3
|
import type { EventBus } from "../event-bus.js";
|
|
4
4
|
import type { BorderStyle } from "./box-frame.js";
|
|
5
|
+
import { type RenderSurface } from "./compositor.js";
|
|
5
6
|
export interface FloatingPanelConfig {
|
|
6
7
|
/** Key sequence that toggles the panel (e.g. "\x1c" for Ctrl+\). */
|
|
7
8
|
trigger: string;
|
|
@@ -36,6 +37,8 @@ export interface FloatingPanelConfig {
|
|
|
36
37
|
* `{prefix}:submit`, etc. Use different prefixes for multiple panels.
|
|
37
38
|
*/
|
|
38
39
|
handlerPrefix?: string;
|
|
40
|
+
/** Render sink + viewport. Defaults to a fresh StdoutSurface. */
|
|
41
|
+
surface?: RenderSurface;
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
41
44
|
* Context passed to the render-content handler.
|
|
@@ -129,6 +132,7 @@ export type Phase = "idle" | "input" | "active" | "done";
|
|
|
129
132
|
export declare class FloatingPanel {
|
|
130
133
|
private readonly config;
|
|
131
134
|
private readonly bus;
|
|
135
|
+
private readonly surface;
|
|
132
136
|
private readonly border;
|
|
133
137
|
private readonly externalBuffer;
|
|
134
138
|
private readonly prefix;
|
|
@@ -164,7 +168,7 @@ export declare class FloatingPanel {
|
|
|
164
168
|
private title;
|
|
165
169
|
private footer;
|
|
166
170
|
private renderTimer;
|
|
167
|
-
private
|
|
171
|
+
private resizeUnsub;
|
|
168
172
|
private prevFrame;
|
|
169
173
|
private suppressNextRedraw;
|
|
170
174
|
private autoDismissTimer;
|
|
@@ -213,7 +217,7 @@ export declare class FloatingPanel {
|
|
|
213
217
|
/** Handle scroll input. Returns true if consumed. */
|
|
214
218
|
private handleScroll;
|
|
215
219
|
private handleInputKey;
|
|
216
|
-
/** Compute box geometry from config + current
|
|
220
|
+
/** Compute box geometry from config + current viewport. */
|
|
217
221
|
computeGeometry(): BoxGeometry;
|
|
218
222
|
private buildFrame;
|
|
219
223
|
private scheduleRender;
|
|
@@ -35,6 +35,7 @@ import { wrapLine } from "./markdown.js";
|
|
|
35
35
|
import { LineEditor } from "./line-editor.js";
|
|
36
36
|
import { TerminalBuffer } from "./terminal-buffer.js";
|
|
37
37
|
import { HandlerRegistry } from "./handler-registry.js";
|
|
38
|
+
import { StdoutSurface } from "./compositor.js";
|
|
38
39
|
// ── ANSI constants ──────────────────────────────────────────────
|
|
39
40
|
const DIM = "\x1b[2m";
|
|
40
41
|
const RESET = "\x1b[0m";
|
|
@@ -74,6 +75,7 @@ export class FloatingPanel {
|
|
|
74
75
|
// ── Configuration ───────────────────────────────────────────
|
|
75
76
|
config;
|
|
76
77
|
bus;
|
|
78
|
+
surface;
|
|
77
79
|
border;
|
|
78
80
|
externalBuffer;
|
|
79
81
|
prefix;
|
|
@@ -112,7 +114,7 @@ export class FloatingPanel {
|
|
|
112
114
|
title = "";
|
|
113
115
|
footer = "";
|
|
114
116
|
renderTimer = null;
|
|
115
|
-
|
|
117
|
+
resizeUnsub = null;
|
|
116
118
|
prevFrame = [];
|
|
117
119
|
suppressNextRedraw = false;
|
|
118
120
|
autoDismissTimer = null;
|
|
@@ -124,6 +126,7 @@ export class FloatingPanel {
|
|
|
124
126
|
prevSerialized = "";
|
|
125
127
|
constructor(bus, config, handlers) {
|
|
126
128
|
this.bus = bus;
|
|
129
|
+
this.surface = config.surface ?? new StdoutSurface();
|
|
127
130
|
this.externalBuffer = config.terminalBuffer;
|
|
128
131
|
this.prefix = config.handlerPrefix ?? "panel";
|
|
129
132
|
this.handlers = handlers ?? new HandlerRegistry();
|
|
@@ -436,10 +439,9 @@ export class FloatingPanel {
|
|
|
436
439
|
this.bus.emit("shell:stdout-hold", {});
|
|
437
440
|
this.usedAltScreen = !(this.buffer?.altScreen);
|
|
438
441
|
if (this.usedAltScreen) {
|
|
439
|
-
|
|
442
|
+
this.surface.write("\x1b[?1049h");
|
|
440
443
|
}
|
|
441
|
-
this.
|
|
442
|
-
process.stdout.on("resize", this.resizeHandler);
|
|
444
|
+
this.resizeUnsub = this.surface.onResize(() => { this.prevFrame = []; this.render(); });
|
|
443
445
|
this.render();
|
|
444
446
|
}
|
|
445
447
|
// ── Public content API ──────────────────────────────────────
|
|
@@ -674,10 +676,10 @@ export class FloatingPanel {
|
|
|
674
676
|
}
|
|
675
677
|
}
|
|
676
678
|
// ── Geometry ───────────────────────────────────────────────
|
|
677
|
-
/** Compute box geometry from config + current
|
|
679
|
+
/** Compute box geometry from config + current viewport. */
|
|
678
680
|
computeGeometry() {
|
|
679
|
-
const cols =
|
|
680
|
-
const rows =
|
|
681
|
+
const cols = this.surface.columns;
|
|
682
|
+
const rows = this.surface.rows;
|
|
681
683
|
const boxW = Math.min(this.resolveSize(this.config.width, cols - 4), this.config.maxWidth);
|
|
682
684
|
const boxH = Math.min(this.resolveSize(this.config.height, rows - 4), Math.max(this.config.minHeight + 2, rows - 4));
|
|
683
685
|
const boxTop = Math.floor((rows - boxH) / 2);
|
|
@@ -744,24 +746,22 @@ export class FloatingPanel {
|
|
|
744
746
|
out.push(cursorSeq);
|
|
745
747
|
out.push(SYNC_END);
|
|
746
748
|
if (this.prevFrame.length === 0 || dirty) {
|
|
747
|
-
|
|
749
|
+
this.surface.write(out.join(""));
|
|
748
750
|
}
|
|
749
751
|
this.prevFrame = frame;
|
|
750
752
|
}
|
|
751
753
|
// ── Screen helpers ────────────────────────────────────────
|
|
752
754
|
/** Full screen teardown: exit alt screen, release stdout, force redraw. */
|
|
753
755
|
teardownScreen() {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
this.resizeHandler = null;
|
|
757
|
-
}
|
|
756
|
+
this.resizeUnsub?.();
|
|
757
|
+
this.resizeUnsub = null;
|
|
758
758
|
this.suppressNextRedraw = true;
|
|
759
759
|
// Re-check alt screen state: the program we overlaid may have exited
|
|
760
760
|
// (e.g. agent quit vim via terminal_keys) while the panel was active.
|
|
761
761
|
const stillInAltScreen = !this.usedAltScreen && !!this.buffer?.altScreen;
|
|
762
762
|
const programExited = !this.usedAltScreen && !stillInAltScreen;
|
|
763
763
|
if (this.usedAltScreen) {
|
|
764
|
-
|
|
764
|
+
this.surface.write("\x1b[?1049l");
|
|
765
765
|
}
|
|
766
766
|
// Replay PTY output that arrived while the overlay was active.
|
|
767
767
|
// Without this, commands run by the agent (e.g. user_shell ls)
|
|
@@ -769,7 +769,7 @@ export class FloatingPanel {
|
|
|
769
769
|
// from before the overlay opened, losing any shell output produced
|
|
770
770
|
// during the session.
|
|
771
771
|
if (this.ptyBuffer) {
|
|
772
|
-
|
|
772
|
+
this.surface.write(this.ptyBuffer);
|
|
773
773
|
}
|
|
774
774
|
this.ptyBuffer = "";
|
|
775
775
|
this.bus.emit("shell:stdout-release", {});
|
|
@@ -778,8 +778,8 @@ export class FloatingPanel {
|
|
|
778
778
|
// or the overlaid program exited (e.g. agent quit vim) and we
|
|
779
779
|
// discarded its stale buffer — SIGWINCH makes the shell redraw
|
|
780
780
|
// its prompt cleanly.
|
|
781
|
-
const cols =
|
|
782
|
-
const rows =
|
|
781
|
+
const cols = this.surface.columns;
|
|
782
|
+
const rows = this.surface.rows;
|
|
783
783
|
this.bus.emit("shell:pty-resize", { cols, rows: rows - 1 });
|
|
784
784
|
setTimeout(() => {
|
|
785
785
|
this.bus.emit("shell:pty-resize", { cols, rows });
|
|
@@ -808,7 +808,7 @@ export class FloatingPanel {
|
|
|
808
808
|
const serialized = this.buffer.serialize();
|
|
809
809
|
if (serialized && serialized !== this.prevSerialized) {
|
|
810
810
|
this.prevSerialized = serialized;
|
|
811
|
-
|
|
811
|
+
this.surface.write(`${SYNC_START}\x1b[H${serialized}${SYNC_END}`);
|
|
812
812
|
}
|
|
813
813
|
}
|
|
814
814
|
resolveSize(spec, available) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Simple ref-counted counter. Increment/decrement never goes below zero. */
|
|
2
|
+
export class RefCounter {
|
|
3
|
+
count = 0;
|
|
4
|
+
increment() { this.count++; }
|
|
5
|
+
decrement() { this.count = Math.max(0, this.count - 1); }
|
|
6
|
+
reset() { this.count = 0; }
|
|
7
|
+
get active() { return this.count > 0; }
|
|
8
|
+
get value() { return this.count; }
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Differential frame renderer.
|
|
3
|
-
*
|
|
4
|
-
* Accepts a frame (string[]) and writes only the lines that changed
|
|
5
|
-
* compared to the previous frame. Designed for scrolling content
|
|
6
|
-
* (not full-screen ownership like pi-tui).
|
|
7
|
-
*
|
|
8
|
-
* Fast paths:
|
|
9
|
-
* 1. First render → write everything
|
|
10
|
-
* 2. Append-only → write only new lines
|
|
11
|
-
* 3. Last line changed → \r overwrite (for spinner / partial streaming)
|
|
12
|
-
* 4. General diff → cursor-up, rewrite changed region, cursor-down
|
|
13
|
-
*/
|
|
14
|
-
import type { OutputWriter } from "./output-writer.js";
|
|
15
|
-
export declare class FrameRenderer {
|
|
16
|
-
private writer;
|
|
17
|
-
private prevLines;
|
|
18
|
-
constructor(writer: OutputWriter);
|
|
19
|
-
/**
|
|
20
|
-
* Render a new frame, writing only the diff to the output.
|
|
21
|
-
* Each line in `lines` should NOT include a trailing newline.
|
|
22
|
-
*/
|
|
23
|
-
update(lines: string[]): void;
|
|
24
|
-
/** Reset state — next update will be treated as a first render. */
|
|
25
|
-
reset(): void;
|
|
26
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
export class FrameRenderer {
|
|
2
|
-
writer;
|
|
3
|
-
prevLines = [];
|
|
4
|
-
constructor(writer) {
|
|
5
|
-
this.writer = writer;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Render a new frame, writing only the diff to the output.
|
|
9
|
-
* Each line in `lines` should NOT include a trailing newline.
|
|
10
|
-
*/
|
|
11
|
-
update(lines) {
|
|
12
|
-
const prev = this.prevLines;
|
|
13
|
-
if (prev.length === 0) {
|
|
14
|
-
// Fast path 1: first render
|
|
15
|
-
for (const line of lines) {
|
|
16
|
-
this.writer.write(line + "\n");
|
|
17
|
-
}
|
|
18
|
-
this.prevLines = lines.slice();
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
// Find first and last changed indices
|
|
22
|
-
const minLen = Math.min(prev.length, lines.length);
|
|
23
|
-
let firstChanged = -1;
|
|
24
|
-
let lastChanged = -1;
|
|
25
|
-
for (let i = 0; i < minLen; i++) {
|
|
26
|
-
if (prev[i] !== lines[i]) {
|
|
27
|
-
if (firstChanged === -1)
|
|
28
|
-
firstChanged = i;
|
|
29
|
-
lastChanged = i;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// Check for appended or removed lines
|
|
33
|
-
const appended = lines.length > prev.length;
|
|
34
|
-
const truncated = lines.length < prev.length;
|
|
35
|
-
if (firstChanged === -1 && !appended && !truncated) {
|
|
36
|
-
// No changes at all
|
|
37
|
-
this.prevLines = lines.slice();
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (firstChanged === -1 && appended) {
|
|
41
|
-
// Fast path 2: only new lines appended, existing unchanged
|
|
42
|
-
for (let i = prev.length; i < lines.length; i++) {
|
|
43
|
-
this.writer.write(lines[i] + "\n");
|
|
44
|
-
}
|
|
45
|
-
this.prevLines = lines.slice();
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
// General diff: move cursor up to first changed line, rewrite
|
|
49
|
-
const linesFromBottom = prev.length - (firstChanged === -1 ? prev.length : firstChanged);
|
|
50
|
-
if (linesFromBottom > 0) {
|
|
51
|
-
this.writer.write(`\x1b[${linesFromBottom}A`); // cursor up
|
|
52
|
-
}
|
|
53
|
-
this.writer.write("\r"); // start of line
|
|
54
|
-
// Rewrite from firstChanged to end of new frame
|
|
55
|
-
const start = firstChanged === -1 ? prev.length : firstChanged;
|
|
56
|
-
for (let i = start; i < lines.length; i++) {
|
|
57
|
-
this.writer.write(`\x1b[2K${lines[i]}\n`); // clear line + write + newline
|
|
58
|
-
}
|
|
59
|
-
// If new frame is shorter, clear remaining old lines
|
|
60
|
-
if (truncated) {
|
|
61
|
-
for (let i = lines.length; i < prev.length; i++) {
|
|
62
|
-
this.writer.write("\x1b[2K\n");
|
|
63
|
-
}
|
|
64
|
-
// Move cursor back up to end of new content
|
|
65
|
-
const extra = prev.length - lines.length;
|
|
66
|
-
if (extra > 0) {
|
|
67
|
-
this.writer.write(`\x1b[${extra}A`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
this.prevLines = lines.slice();
|
|
71
|
-
}
|
|
72
|
-
/** Reset state — next update will be treated as a first render. */
|
|
73
|
-
reset() {
|
|
74
|
-
this.prevLines = [];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Abstraction over terminal output.
|
|
3
|
-
*
|
|
4
|
-
* All TUI rendering goes through an OutputWriter instead of calling
|
|
5
|
-
* process.stdout.write directly. This enables testing (BufferWriter),
|
|
6
|
-
* alternative frontends, and a single point of control for output.
|
|
7
|
-
*/
|
|
8
|
-
/** Simple ref-counted counter. Increment/decrement never goes below zero. */
|
|
9
|
-
export declare class RefCounter {
|
|
10
|
-
private count;
|
|
11
|
-
increment(): void;
|
|
12
|
-
decrement(): void;
|
|
13
|
-
reset(): void;
|
|
14
|
-
get active(): boolean;
|
|
15
|
-
get value(): number;
|
|
16
|
-
}
|
|
17
|
-
export interface OutputWriter {
|
|
18
|
-
write(text: string): void;
|
|
19
|
-
get columns(): number;
|
|
20
|
-
}
|
|
21
|
-
/** Default writer that forwards to process.stdout. */
|
|
22
|
-
export declare class StdoutWriter implements OutputWriter {
|
|
23
|
-
/** When > 0, all writes are silently dropped. Ref-counted. */
|
|
24
|
-
private readonly _hold;
|
|
25
|
-
hold(): void;
|
|
26
|
-
release(): void;
|
|
27
|
-
get held(): boolean;
|
|
28
|
-
write(text: string): void;
|
|
29
|
-
get columns(): number;
|
|
30
|
-
}
|
|
31
|
-
/** Captures all output in memory. Useful for testing. */
|
|
32
|
-
export declare class BufferWriter implements OutputWriter {
|
|
33
|
-
output: string[];
|
|
34
|
-
columns: number;
|
|
35
|
-
write(text: string): void;
|
|
36
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Abstraction over terminal output.
|
|
3
|
-
*
|
|
4
|
-
* All TUI rendering goes through an OutputWriter instead of calling
|
|
5
|
-
* process.stdout.write directly. This enables testing (BufferWriter),
|
|
6
|
-
* alternative frontends, and a single point of control for output.
|
|
7
|
-
*/
|
|
8
|
-
/** Simple ref-counted counter. Increment/decrement never goes below zero. */
|
|
9
|
-
export class RefCounter {
|
|
10
|
-
count = 0;
|
|
11
|
-
increment() { this.count++; }
|
|
12
|
-
decrement() { this.count = Math.max(0, this.count - 1); }
|
|
13
|
-
reset() { this.count = 0; }
|
|
14
|
-
get active() { return this.count > 0; }
|
|
15
|
-
get value() { return this.count; }
|
|
16
|
-
}
|
|
17
|
-
/** Default writer that forwards to process.stdout. */
|
|
18
|
-
export class StdoutWriter {
|
|
19
|
-
/** When > 0, all writes are silently dropped. Ref-counted. */
|
|
20
|
-
_hold = new RefCounter();
|
|
21
|
-
hold() { this._hold.increment(); }
|
|
22
|
-
release() { this._hold.decrement(); }
|
|
23
|
-
get held() { return this._hold.active; }
|
|
24
|
-
write(text) {
|
|
25
|
-
if (this._hold.active)
|
|
26
|
-
return;
|
|
27
|
-
if (process.stdout.writable) {
|
|
28
|
-
try {
|
|
29
|
-
process.stdout.write(text);
|
|
30
|
-
}
|
|
31
|
-
catch { }
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
get columns() {
|
|
35
|
-
return process.stdout.columns || 80;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/** Captures all output in memory. Useful for testing. */
|
|
39
|
-
export class BufferWriter {
|
|
40
|
-
output = [];
|
|
41
|
-
columns = 80;
|
|
42
|
-
write(text) {
|
|
43
|
-
this.output.push(text);
|
|
44
|
-
}
|
|
45
|
-
}
|