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.
@@ -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;
@@ -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 resizeHandler;
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 terminal size. */
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
- resizeHandler = null;
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
- process.stdout.write("\x1b[?1049h");
442
+ this.surface.write("\x1b[?1049h");
440
443
  }
441
- this.resizeHandler = () => { this.prevFrame = []; this.render(); };
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 terminal size. */
679
+ /** Compute box geometry from config + current viewport. */
678
680
  computeGeometry() {
679
- const cols = process.stdout.columns || 80;
680
- const rows = process.stdout.rows || 24;
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
- process.stdout.write(out.join(""));
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
- if (this.resizeHandler) {
755
- process.stdout.off("resize", this.resizeHandler);
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
- process.stdout.write("\x1b[?1049l");
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
- process.stdout.write(this.ptyBuffer);
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 = process.stdout.columns || 80;
782
- const rows = process.stdout.rows || 24;
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
- process.stdout.write(`${SYNC_START}\x1b[H${serialized}${SYNC_END}`);
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 declare class RefCounter {
3
+ private count;
4
+ increment(): void;
5
+ decrement(): void;
6
+ reset(): void;
7
+ get active(): boolean;
8
+ get value(): number;
9
+ }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",
@@ -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
- }