@vincemakes/kiso-tui 0.1.30 → 0.1.32

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,116 @@
1
+ /**
2
+ * TUI v6 (ADR-0046) — the components: EVERY screen line's renderer.
3
+ *
4
+ * Each component turns one piece of state into display lines (SGR
5
+ * included, raw — the compositor writes them verbatim). The folding
6
+ * lives HERE: every line a component returns must fit the terminal
7
+ * width — the compositor's crash-on-violation invariant backs it up
8
+ * (a component that forgets to fold CRASHES with a diagnostic, never
9
+ * silently truncates — pi tui-main-screen.ts:447-473).
10
+ *
11
+ * The fold is SGR-AWARE: a line whose bold/dim span would straddle a
12
+ * fold boundary closes the span at the break and reopens it on the
13
+ * next row — the #16b contract (no literal "[2m" fragments) survives
14
+ * folding. displayWidth/charWidth (editor.ts) are the width primitives
15
+ * (untouched); render.ts supplies the original text (palette, escape,
16
+ * tint, fold wording).
17
+ */
18
+ import { foldThinking, foldResult, renderToolSummary } from "./render.js";
19
+ /** The spinner glyphs, cycled by the compositor's on-demand tick. */
20
+ export declare const SPINNER: string[];
21
+ /** The frame context the compositor passes down — the pieces of time
22
+ * that make a live render non-deterministic (the running tool's glyph
23
+ * and elapsed). Everything else is a pure function of the cell. */
24
+ export interface FrameCtx {
25
+ readonly spinnerI: number;
26
+ readonly now: number;
27
+ }
28
+ /** ONE screen line a component emits (raw, SGR included). */
29
+ export type RenderLine = string;
30
+ /**
31
+ * The fold — split a display-width line into ≤W rows, preserving SGR
32
+ * spans across the break: a span open at the break closes (reset) at
33
+ * the row's end and reopens on the next row. The rows are what the
34
+ * terminal's own soft-wrap would have produced — except the compositor
35
+ * folds FIRST, so the terminal never reflows a component's line (the
36
+ * #17 merge class cannot reach committed content).
37
+ */
38
+ export declare function foldLine(line: string, W: number): string[];
39
+ /** The visible width of a rendered line (SGR stripped — the invariant
40
+ * the compositor enforces on every emitted line). */
41
+ export declare function visibleWidth(line: string): number;
42
+ /** A component: render the display lines for one piece of state. */
43
+ export interface Component {
44
+ render(width: number, ctx: FrameCtx): string[];
45
+ }
46
+ /** The container — vertical concatenation of its children. */
47
+ export declare class Container implements Component {
48
+ private readonly children;
49
+ constructor(children: Component[]);
50
+ render(width: number, ctx: FrameCtx): string[];
51
+ }
52
+ export type BodyCell = {
53
+ kind: "user";
54
+ text: string;
55
+ done: true;
56
+ } | {
57
+ kind: "thinking";
58
+ text: string;
59
+ done: boolean;
60
+ } | {
61
+ kind: "tool";
62
+ name: string;
63
+ input: string;
64
+ state: "pending" | "approval" | "running" | "done";
65
+ isError: boolean;
66
+ resultText: string;
67
+ diff: import("./diff.js").DiffLine[] | null;
68
+ added: number;
69
+ removed: number;
70
+ startedAt: number | null;
71
+ doneAt: number | null;
72
+ done: boolean;
73
+ } | {
74
+ kind: "text";
75
+ text: string;
76
+ done: boolean;
77
+ } | {
78
+ kind: "notice";
79
+ text: string;
80
+ done: true;
81
+ } | {
82
+ kind: "raw";
83
+ lines: string[];
84
+ done: true;
85
+ } | {
86
+ kind: "terminal";
87
+ label: string;
88
+ line: string;
89
+ done: true;
90
+ } | {
91
+ kind: "checklist";
92
+ header: string;
93
+ items: {
94
+ text: string;
95
+ status: "pending" | "active" | "done";
96
+ }[];
97
+ done: true;
98
+ };
99
+ declare const TOOL_SUMMARY_MAX = 60;
100
+ /** The component for one cell — the mapping table lives here so the
101
+ * compositor stays a pure writer. */
102
+ export declare function cellComponent(cell: BodyCell): Component;
103
+ /** The status container's row: the status text (+ the tail) with the
104
+ * right-aligned "/ commands · ↑ history" hint in the idle state —
105
+ * the hint CUT FIRST when the width is short (the #16g rule); when
106
+ * the STATUS ITSELF cannot fit, it cuts with a "…" — the last resort,
107
+ * enforced by invariant ① (the old code let the status soft-wrap). */
108
+ export declare function statusLine(status: string, tail: string, question: boolean, W: number): string;
109
+ /** The footer — the ONE dotted row (the old two-row chrome is gone;
110
+ * the wall cannot return by construction). */
111
+ export declare function footerLine(W: number): string;
112
+ /** The terminal label + rhythm gap (the pipe path's v2c bytes — the
113
+ * exact render the passthrough needs). */
114
+ export declare function terminalPipe(label: string, statusLineText: string): string;
115
+ /** The pipe-path pieces the passthrough reuses (byte-identical). */
116
+ export { foldThinking, foldResult, renderToolSummary, TOOL_SUMMARY_MAX };
@@ -0,0 +1,333 @@
1
+ /**
2
+ * TUI v6 (ADR-0046) — the components: EVERY screen line's renderer.
3
+ *
4
+ * Each component turns one piece of state into display lines (SGR
5
+ * included, raw — the compositor writes them verbatim). The folding
6
+ * lives HERE: every line a component returns must fit the terminal
7
+ * width — the compositor's crash-on-violation invariant backs it up
8
+ * (a component that forgets to fold CRASHES with a diagnostic, never
9
+ * silently truncates — pi tui-main-screen.ts:447-473).
10
+ *
11
+ * The fold is SGR-AWARE: a line whose bold/dim span would straddle a
12
+ * fold boundary closes the span at the break and reopens it on the
13
+ * next row — the #16b contract (no literal "[2m" fragments) survives
14
+ * folding. displayWidth/charWidth (editor.ts) are the width primitives
15
+ * (untouched); render.ts supplies the original text (palette, escape,
16
+ * tint, fold wording).
17
+ */
18
+ import { displayWidth } from "./editor.js";
19
+ import { escapeTerminal, foldThinking, foldResult, colorInlineCode, renderTerminalGap, renderToolSummary, palette, } from "./render.js";
20
+ /** The spinner glyphs, cycled by the compositor's on-demand tick. */
21
+ export const SPINNER = ["▖", "▘", "▝", "▗"];
22
+ /**
23
+ * The fold — split a display-width line into ≤W rows, preserving SGR
24
+ * spans across the break: a span open at the break closes (reset) at
25
+ * the row's end and reopens on the next row. The rows are what the
26
+ * terminal's own soft-wrap would have produced — except the compositor
27
+ * folds FIRST, so the terminal never reflows a component's line (the
28
+ * #17 merge class cannot reach committed content).
29
+ */
30
+ export function foldLine(line, W) {
31
+ if (W < 1)
32
+ return [line];
33
+ // collect the plain text + the SGR segments so the walk can track
34
+ // the open span state
35
+ const out = [];
36
+ let current = "";
37
+ let width = 0;
38
+ let open = []; // the SGR sequences seen since the last reset
39
+ for (let i = 0; i < line.length;) {
40
+ if (line[i] === "\n") {
41
+ // a real line break — the row ends here (the same close/reopen
42
+ // as a fold boundary, so a span never leaks across the break)
43
+ const close = open.length > 0 ? "\x1b[0m" : "";
44
+ out.push(current + close);
45
+ current = open.join("");
46
+ width = 0;
47
+ i += 1;
48
+ continue;
49
+ }
50
+ if (line[i] === "\x1b") {
51
+ const m = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
52
+ if (m !== null) {
53
+ if (m[0] === "\x1b[0m")
54
+ open = [];
55
+ else
56
+ open.push(m[0]);
57
+ current += m[0];
58
+ i += m[0].length;
59
+ continue;
60
+ }
61
+ // a non-SGR CSI (the raw cell's own content, escaped at
62
+ // composition) — copy verbatim, zero width
63
+ const csi = /^\x1b\[[0-9;?]*[A-Za-z]/.exec(line.slice(i));
64
+ if (csi !== null) {
65
+ current += csi[0];
66
+ i += csi[0].length;
67
+ continue;
68
+ }
69
+ current += line[i];
70
+ i += 1;
71
+ continue;
72
+ }
73
+ const cw = displayWidth(line[i]);
74
+ if (width + cw > W && width > 0) {
75
+ // the fold — close the open spans, push, reopen on the next row
76
+ const close = open.length > 0 ? "\x1b[0m" : "";
77
+ out.push(current + close);
78
+ current = open.join("");
79
+ width = 0;
80
+ continue;
81
+ }
82
+ current += line[i];
83
+ width += cw;
84
+ i += 1;
85
+ }
86
+ if (current !== "" || out.length === 0)
87
+ out.push(current);
88
+ return out;
89
+ }
90
+ /** The visible width of a rendered line (SGR stripped — the invariant
91
+ * the compositor enforces on every emitted line). */
92
+ export function visibleWidth(line) {
93
+ let w = 0;
94
+ for (let i = 0; i < line.length;) {
95
+ if (line[i] === "\x1b") {
96
+ const m = /^\x1b\[[0-9;?]*[A-Za-z]/.exec(line.slice(i));
97
+ if (m !== null) {
98
+ i += m[0].length;
99
+ continue;
100
+ }
101
+ i += 1;
102
+ continue;
103
+ }
104
+ w += displayWidth(line[i]);
105
+ i += 1;
106
+ }
107
+ return w;
108
+ }
109
+ /** The container — vertical concatenation of its children. */
110
+ export class Container {
111
+ children;
112
+ constructor(children) {
113
+ this.children = children;
114
+ }
115
+ render(width, ctx) {
116
+ return this.children.flatMap((c) => c.render(width, ctx));
117
+ }
118
+ }
119
+ const TOOL_SUMMARY_MAX = 60; // the tool line's parameter summary, chars
120
+ /** The component for one cell — the mapping table lives here so the
121
+ * compositor stays a pure writer. */
122
+ export function cellComponent(cell) {
123
+ switch (cell.kind) {
124
+ case "user":
125
+ return new UserMessage(cell);
126
+ case "thinking":
127
+ return new ThinkingFold(cell);
128
+ case "tool":
129
+ return new ToolExecution(cell);
130
+ case "text":
131
+ return new AssistantMessage(cell);
132
+ case "notice":
133
+ return new ErrorLine(cell);
134
+ case "raw":
135
+ return new RawBlock(cell);
136
+ case "terminal":
137
+ return new TerminalBlock(cell);
138
+ case "checklist":
139
+ return new Checklist(cell);
140
+ }
141
+ }
142
+ /**
143
+ * The user message — the left rail (bright-white BOLD ▍ per row, the
144
+ * v4.1 design). The text folds at W−2 (the rail + space) so every row
145
+ * carries the rail and NO row exceeds the width (the v5 code split on
146
+ * "\n" only — a long line soft-wrapped and its continuation row had no
147
+ * rail).
148
+ */
149
+ class UserMessage {
150
+ cell;
151
+ constructor(cell) {
152
+ this.cell = cell;
153
+ }
154
+ render(W, _ctx) {
155
+ const p = palette();
156
+ const rail = `${p.bold}▍${p.reset} `;
157
+ const textW = Math.max(1, W - 2);
158
+ const rows = [];
159
+ for (const para of this.cell.text.split("\n")) {
160
+ const folded = foldLine(escapeTerminal(para), textW);
161
+ for (const row of folded)
162
+ rows.push(`${rail}${row}`);
163
+ }
164
+ return rows.length > 0 ? rows : [rail.trimEnd()];
165
+ }
166
+ }
167
+ /** The thinking fold — one dim line, width-capped so the /think suffix
168
+ * rides the fold's own row (the #17 fix's slice, componentized). */
169
+ class ThinkingFold {
170
+ cell;
171
+ constructor(cell) {
172
+ this.cell = cell;
173
+ }
174
+ render(W, _ctx) {
175
+ const block = this.cell.text;
176
+ const trimmed = escapeTerminal(block.trim());
177
+ if (trimmed.length <= 100)
178
+ return [`${palette().dim}…${trimmed}${palette().reset}`];
179
+ const suffix = ` (${block.length} chars · /think)`;
180
+ const slice = Math.max(1, W - 1 - suffix.length);
181
+ return [`${palette().dim}…${trimmed.slice(0, slice)}${suffix}${palette().reset}`];
182
+ }
183
+ }
184
+ /** The tool execution line + the approval mini-diff — every state is
185
+ * its own render; the lines fold (the summary gives way first). */
186
+ class ToolExecution {
187
+ cell;
188
+ constructor(cell) {
189
+ this.cell = cell;
190
+ }
191
+ render(W, ctx) {
192
+ const p = palette();
193
+ const c = this.cell;
194
+ const name = escapeTerminal(c.name);
195
+ const summary = escapeTerminal(c.input);
196
+ if (c.state === "done") {
197
+ const elapsed = c.startedAt !== null && c.doneAt !== null ? ((c.doneAt - c.startedAt) / 1000).toFixed(1) : "?";
198
+ const line = c.isError
199
+ ? `${p.red}✗ ${name} (${escapeTerminal(c.resultText.split("\n")[0].slice(0, 60))}, ${elapsed}s)${p.reset}`
200
+ : `${p.bold}✓ ${name}${p.reset} (${summary}${c.added + c.removed > 0 ? `, +${c.added} -${c.removed}` : ""}, ${elapsed}s)`;
201
+ return foldLine(line, W);
202
+ }
203
+ if (c.state === "approval") {
204
+ const lines = foldLine(`→ ${name} ${summary} ${p.bold}⏸${p.reset}`, W);
205
+ if (c.diff !== null) {
206
+ for (const d of c.diff) {
207
+ const body = d.kind === "-"
208
+ ? `${p.red}- ${escapeTerminal(d.text)}${p.reset}`
209
+ : d.kind === "+"
210
+ ? `${p.green}+ ${escapeTerminal(d.text)}${p.reset}`
211
+ : `${p.dim} ${escapeTerminal(d.text)}${p.reset}`;
212
+ lines.push(...foldLine(`${p.bold}▎${p.reset}${body}`, W));
213
+ }
214
+ }
215
+ return lines;
216
+ }
217
+ if (c.state === "running") {
218
+ const elapsed = c.startedAt !== null ? Math.max(1, Math.round((ctx.now - c.startedAt) / 1000)) : 1;
219
+ return foldLine(`→ ${name} ${summary} ${p.bold}${SPINNER[ctx.spinnerI % SPINNER.length]}${p.reset} ${elapsed}s`, W);
220
+ }
221
+ return foldLine(`→ ${name} ${summary}`, W);
222
+ }
223
+ }
224
+ /** The assistant body text — wrapped at W, the inline-code tint per
225
+ * row (the #16e rule: a span never matches across rows). */
226
+ class AssistantMessage {
227
+ cell;
228
+ constructor(cell) {
229
+ this.cell = cell;
230
+ }
231
+ render(W, _ctx) {
232
+ const text = escapeTerminal(this.cell.text);
233
+ const wrapped = foldLine(text, W);
234
+ return wrapped.length > 0 ? wrapped.map((l) => colorInlineCode(l)) : [""];
235
+ }
236
+ }
237
+ /** The ⚠ / notice lines — the error surface. */
238
+ class ErrorLine {
239
+ cell;
240
+ constructor(cell) {
241
+ this.cell = cell;
242
+ }
243
+ render(W, _ctx) {
244
+ return foldLine(escapeTerminal(this.cell.text), W);
245
+ }
246
+ }
247
+ /** The CLI's pre-rendered blocks (the banner, the recap, slash-command
248
+ * output) — the SGR applied at composition (render.ts), folded here
249
+ * verbatim: the #16b contract (no re-escaping) holds, and the fold is
250
+ * SGR-aware so the accent spans survive a break. */
251
+ class RawBlock {
252
+ cell;
253
+ constructor(cell) {
254
+ this.cell = cell;
255
+ }
256
+ render(W, _ctx) {
257
+ return this.cell.lines.flatMap((l) => foldLine(l, W));
258
+ }
259
+ }
260
+ /** The terminal label + the status line + the rhythm gap blank. */
261
+ class TerminalBlock {
262
+ cell;
263
+ constructor(cell) {
264
+ this.cell = cell;
265
+ }
266
+ render(W, _ctx) {
267
+ const lines = [...foldLine(this.cell.label, W), ...foldLine(this.cell.line, W)];
268
+ if (this.cell.label !== "")
269
+ lines.push("");
270
+ return lines;
271
+ }
272
+ }
273
+ /** The durable checklist — the ▞ header + one brick-glyph row per item. */
274
+ class Checklist {
275
+ cell;
276
+ constructor(cell) {
277
+ this.cell = cell;
278
+ }
279
+ render(W, _ctx) {
280
+ const p = palette();
281
+ const glyphOf = (status) => (status === "pending" ? "□" : status === "active" ? "▖" : "▣");
282
+ const rows = foldLine(`${p.bold}▞${p.reset} ${escapeTerminal(this.cell.header)}`, W);
283
+ for (const item of this.cell.items) {
284
+ rows.push(...foldLine(` ${glyphOf(item.status)} ${escapeTerminal(item.text)}`, W));
285
+ }
286
+ return rows;
287
+ }
288
+ }
289
+ // ---- the chrome components (the status container, the slot, the footer) ----
290
+ /** The status container's row: the status text (+ the tail) with the
291
+ * right-aligned "/ commands · ↑ history" hint in the idle state —
292
+ * the hint CUT FIRST when the width is short (the #16g rule); when
293
+ * the STATUS ITSELF cannot fit, it cuts with a "…" — the last resort,
294
+ * enforced by invariant ① (the old code let the status soft-wrap). */
295
+ export function statusLine(status, tail, question, W) {
296
+ const p = palette();
297
+ const text = `${status}${tail === "" ? "" : ` · ${tail}`}`;
298
+ if (question)
299
+ return `${p.dim}${widthCut(text, W)}${p.reset}`;
300
+ const hint = " / commands · ↑ history";
301
+ const statusW = visibleWidth(text);
302
+ if (statusW > W) {
303
+ return `${p.dim}${widthCut(text, W - 1)}…${p.reset}`;
304
+ }
305
+ const hintW = visibleWidth(hint);
306
+ if (statusW + hintW > W)
307
+ return `${p.dim}${text}${p.reset}`;
308
+ return `${p.dim}${text}${" ".repeat(Math.max(0, W - statusW - hintW))}${hint}${p.reset}`;
309
+ }
310
+ /** The display-width prefix of a plain (SGR-free) text. */
311
+ function widthCut(text, max) {
312
+ let w = 0;
313
+ let i = 0;
314
+ for (; i < text.length; i += 1) {
315
+ const cw = displayWidth(text[i]);
316
+ if (w + cw > max)
317
+ break;
318
+ w += cw;
319
+ }
320
+ return text.slice(0, i);
321
+ }
322
+ /** The footer — the ONE dotted row (the old two-row chrome is gone;
323
+ * the wall cannot return by construction). */
324
+ export function footerLine(W) {
325
+ return `\x1b[2m${"╌".repeat(W)}\x1b[0m`;
326
+ }
327
+ /** The terminal label + rhythm gap (the pipe path's v2c bytes — the
328
+ * exact render the passthrough needs). */
329
+ export function terminalPipe(label, statusLineText) {
330
+ return label + renderTerminalGap(statusLineText);
331
+ }
332
+ /** The pipe-path pieces the passthrough reuses (byte-identical). */
333
+ export { foldThinking, foldResult, renderToolSummary, TOOL_SUMMARY_MAX };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * TUI v6 (ADR-0046) — THE one-compositor: every byte of the screen's
3
+ * stdout comes from this file's doRender. `body.ts` + `dock.ts` are
4
+ * RETIRED — this class implements BOTH façades (the `Body` mutations
5
+ * the CLI's consumeRun calls, and the `Dock` chrome API), so the CLI
6
+ * itself is untouched (zero diff outside the tui package + tests).
7
+ *
8
+ * The model:
9
+ * - cells (the CLI's mutation surface) → components → lines;
10
+ * - `lines[] + commitIndex` (the scrollback fork — the departure from
11
+ * pi): a line COMMITS (leaves the live region) via the real-LF
12
+ * scroll at the last row (`\x1b[1B\n` — CUP-free) when its cell is
13
+ * DONE and the region needs the room. Committed bytes are never
14
+ * re-emitted — the native scrollback gets them, reflow-safe, and
15
+ * the user's shell history is never touched (zero \x1b[3J, zero
16
+ * replay);
17
+ * - the live region (content + status + editor + footer + menu) is
18
+ * hard-capped at H−1 lines; overflow FORCE-commits the oldest live
19
+ * line regardless of done-ness — the one sharp edge this round
20
+ * introduces (asserted by the VT-emulator gate);
21
+ * - two crash invariants: ① every emitted line's visible width ≤ W
22
+ * (components fold; a violation THROWS with diagnostics — pi
23
+ * tui-main-screen.ts:447-473, no silent truncate); ② within the
24
+ * live region only RELATIVE cursor moves (vertical A/B, horizontal
25
+ * G/D) — CUP exists only in the full-redraw path (the first frame,
26
+ * the resize repaint);
27
+ * - the cursor DERIVES from the frame: the focus component embeds the
28
+ * APC marker in its rendered line; the compositor locates, strips,
29
+ * and relatively positions from the frame — no side-channel cursor
30
+ * bookkeeping that could desync from the picture;
31
+ * - zero timers: the spinner animation is a dirty flag through the
32
+ * scheduler — a one-shot setTimeout re-armed only while a running
33
+ * tool exists (the #14/#15 zero-output contract is structural).
34
+ *
35
+ * Layout at H rows: content rows 1..H−3, status H−2, editor (the slot)
36
+ * H−1, footer ╌ at H. Pipes / NO_COLOR: the passthrough branches below
37
+ * keep the v2a/v2b line-mode bytes byte-for-byte (the e2e guards them).
38
+ */
39
+ import { type MenuItem } from "./editor.js";
40
+ /** The cursor marker — an APC private sequence the focus component
41
+ * embeds at the edit position; the compositor strips it and moves
42
+ * relatively (it never reaches the terminal). */
43
+ export declare const CURSOR_MARKER = "\u001B_[kiso-cur]\u001B\\";
44
+ export interface BodyOptions {
45
+ /** Is the cell renderer live? A color TTY with a real size — checked
46
+ * per mutation (the TIOCSWINSZ can land after main constructs us). */
47
+ active: () => boolean;
48
+ /** The terminal height (rows) — live, for the region geometry. */
49
+ height: () => number;
50
+ /** The terminal width (cols) — live, for wrap estimates. */
51
+ width: () => number;
52
+ /** v6: RETIRED — the cursor derives from the frame's marker. Kept in
53
+ * the interface so the CLI's construction is untouched. */
54
+ editCol: () => number;
55
+ /** v6: RETIRED — the compositor draws the chrome itself. Same. */
56
+ onDock?: () => void;
57
+ /** The stdout writer — injectable for unit tests (default: stdout). */
58
+ write?: (s: string) => void;
59
+ }
60
+ /** The one compositor — implements the Body façade AND the Dock chrome
61
+ * API (see the class comments on each method group). */
62
+ export declare class Body {
63
+ #private;
64
+ constructor(opts: BodyOptions);
65
+ userLine(text: string): void;
66
+ thinkingAppend(text: string): void;
67
+ thinkingEnd(): void;
68
+ toolStart(name: string, callId: string, input: Record<string, unknown>): void;
69
+ toolApproval(callId: string, diff: import("./diff.js").DiffResult | null): void;
70
+ toolRunning(callId: string): void;
71
+ toolSucceeded(callId: string): void;
72
+ toolFailed(callId: string, error: string): void;
73
+ toolResult(callId: string, result: {
74
+ content: string;
75
+ isError: boolean;
76
+ }): void;
77
+ textAppend(text: string): void;
78
+ textEnd(): void;
79
+ terminal(label: string, statusLineText: string): void;
80
+ notice(text: string): void;
81
+ checklist(header: string, items: {
82
+ text: string;
83
+ status: "pending" | "active" | "done";
84
+ }[]): void;
85
+ raw(lines: string[]): void;
86
+ /** The last COMPLETE thinking block, for /think. */
87
+ lastThinking(): string | null;
88
+ /** The last completed tool call, for /last. */
89
+ lastTool(): {
90
+ name: string;
91
+ input: Record<string, unknown>;
92
+ result: {
93
+ content: string;
94
+ isError: boolean;
95
+ };
96
+ } | null;
97
+ /** Docked = the chrome is live (a color TTY with a real size). */
98
+ get active(): boolean;
99
+ enter(): void;
100
+ /** Teardown — CSI r (the "no broken terminal" contract byte), the
101
+ * chrome rows cleared, the cursor home at the input line. */
102
+ exit(): void;
103
+ /** SIGWINCH: clear the OLD live area (recorded geometry, ED only —
104
+ * zero LF, zero \x1b[3J — the shell history untouched), then the
105
+ * full-redraw path at the NEW geometry (O(height), zero replay).
106
+ * The clear starts at the ON-SCREEN live top (the bottom-anchored
107
+ * live region's first row) — NEVER at the formula's committed-count
108
+ * top: external writes (the CLI's console.error CRLF) can shift the
109
+ * committed content down, and a formula-top ED0 would clear it. */
110
+ onResize(): void;
111
+ setStatus(text: string): void;
112
+ setTail(tail: string): void;
113
+ showQuestion(question: string): void;
114
+ clearQuestion(): void;
115
+ /** Bind the CURRENT input line's state — the focus component reads it. */
116
+ bindInput(state: () => {
117
+ line: string;
118
+ cursor: number;
119
+ }, prompt: string): void;
120
+ /** Bind the editor's slash-command menu state — the MenuSelect slot
121
+ * occupant (the menu replaces the editor's view while open). */
122
+ bindMenu(state: () => {
123
+ items: readonly MenuItem[];
124
+ selected: number;
125
+ } | null): void;
126
+ /** The input line's edit column — the old dock's API. v6: the CURSOR
127
+ * derives from the frame's marker; this is the same value computed
128
+ * from the bound input state (the CLI's BodyOptions.editCol callback
129
+ * reads it — the marker math never desyncs by construction). */
130
+ editCol(): number;
131
+ /** The old dock's redraw — the editor's onRender target: mark + the
132
+ * scheduler (16ms coalescing — the old sync draw coalesces the same). */
133
+ redraw(): void;
134
+ /** Teardown — flush a pending frame, stop the timers. */
135
+ close(): void;
136
+ /** The live region's scalar — the unit tests assert the cap directly
137
+ * (the e2e gate pins the screen consequence). */
138
+ liveCount(): number;
139
+ render(): void;
140
+ }
141
+ /** The Dock — the CLI's module-scope singleton façade. Every method
142
+ * delegates to the one compositor (registered at construction); the
143
+ * input/menu bindings, which the CLI performs BEFORE the Body exists,
144
+ * are buffered and applied by the Body's constructor. */
145
+ export declare class Dock {
146
+ #private;
147
+ get active(): boolean;
148
+ enter(): void;
149
+ exit(): void;
150
+ onResize(): void;
151
+ setStatus(text: string): void;
152
+ setTail(tail: string): void;
153
+ showQuestion(question: string): void;
154
+ clearQuestion(): void;
155
+ bindInput(state: () => {
156
+ line: string;
157
+ cursor: number;
158
+ }, prompt: string): void;
159
+ bindMenu(state: () => {
160
+ items: readonly MenuItem[];
161
+ selected: number;
162
+ } | null): void;
163
+ editCol(): number;
164
+ redraw(): void;
165
+ }