@vincemakes/kiso-tui 0.15.2 → 0.15.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/dock.js DELETED
@@ -1,199 +0,0 @@
1
- /**
2
- * v2b/v2c — the bottom-anchored UI (TTY + color only). Borrows pi-tui's
3
- * IDEA — a DECSTBM scroll region with a reserved bottom — without its
4
- * implementation: zero dependencies, line-level ANSI, no differential
5
- * renderer.
6
- *
7
- * Layout (H = terminal height): rows 1..H-4 = the scroll region (the body
8
- * streams and scrolls here, never touching the bottom), row H-3 = the
9
- * upper dim dotted separator (╌), row H-2 = the input line (the blue
10
- * brick ▌you> + the v2c editor's row — readline is gone from the TTY
11
- * path), row H-1 = the lower dotted separator, row H = the live status
12
- * bar (v3 §03: idle "▸ <mode> · /mode to switch · …", running
13
- * "▖ working Ns · …"; a takeover question replaces it). Bottom redraws
14
- * are wrapped in CSI 2026 (synchronized output) to avoid flicker — the
15
- * pi trick. The visual identity is the kiso brick motif — ▌ half-block,
16
- * dotted separators — deliberately NOT the CC rounded frame nor the pi
17
- * editor (ADR-0039 Amendment 2).
18
- *
19
- * Pipes / NO_COLOR: the dock never activates; the v2a line mode stays
20
- * byte-for-byte (the existing e2e assertions guard it).
21
- */
22
- import { displayWidth } from "./editor.js";
23
- import { palette } from "./render.js";
24
- export class Dock {
25
- #active = false;
26
- #height = 0;
27
- #width = 0;
28
- #status = "";
29
- #tail = ""; // the live tail: the spinner glyph or "running <tool> Ns"
30
- #question = null; // a takeover question shown at H-1
31
- #inputState = () => ({ line: "", cursor: 0 });
32
- #inputPrompt = "";
33
- #bodyRow = 1; // the body's logical row inside the scroll region
34
- #bodyCol = 1; // …and column (mid-line continuations survive cursor jumps)
35
- #resizeHandler = null;
36
- /** v2b: docked only on a color TTY — pipes and NO_COLOR stay v2a. */
37
- get active() {
38
- return this.#active;
39
- }
40
- /** Bind the CURRENT input line's state — called when a readline takes
41
- * over (the chat REPL, the trust question's short-lived rl, resume). */
42
- bindInput(state, prompt) {
43
- this.#inputState = state;
44
- this.#inputPrompt = prompt;
45
- }
46
- /** Enter docked mode: draw the chrome. #13 (P1): the DECSTBM scroll
47
- * region is GONE — v2d-B (ADR-0040): the body uses plain LF scrolling
48
- * so frozen lines enter the native scrollback deterministically
49
- * (region-scrolled lines are terminal-dependent — some terminals drop
50
- * them). The dock rows are redrawn by the body after every scroll. A
51
- * TTY without a real window size (rows < 4) stays in the v2a line
52
- * mode — the bottom three rows need room to exist. */
53
- enter() {
54
- const rows = process.stdout.rows ?? 0;
55
- if (process.stdout.isTTY !== true || palette().bold === "" || rows < 4)
56
- return;
57
- this.#active = true;
58
- this.#height = rows;
59
- this.#width = process.stdout.columns ?? 80;
60
- this.#bodyRow = 1;
61
- this.#bodyCol = 1;
62
- this.redraw();
63
- this.#resizeHandler = () => this.onResize();
64
- process.stdout.on("resize", this.#resizeHandler);
65
- }
66
- /** Teardown — CSI r resets the scroll region, the cursor lands at the
67
- * input line, the bottom rows are cleared: no broken terminal. Called
68
- * from main's finally on EVERY exit path (kill -9 excepted — README:
69
- * `reset` saves it). */
70
- exit() {
71
- if (!this.#active)
72
- return;
73
- this.#active = false;
74
- if (this.#resizeHandler !== null) {
75
- process.stdout.off("resize", this.#resizeHandler);
76
- this.#resizeHandler = null;
77
- }
78
- const H = this.#height;
79
- process.stdout.write("\x1b[r"); // reset the scroll region
80
- for (let row = H - 3; row <= H; row += 1) {
81
- process.stdout.write(`\x1b[${row};1H\x1b[0K`); // clear the four rows
82
- }
83
- process.stdout.write(`\x1b[${H};1H`);
84
- }
85
- /** SIGWINCH: recompute the size, redraw the chrome. */
86
- onResize() {
87
- if (!this.#active)
88
- return;
89
- this.#height = process.stdout.rows ?? this.#height;
90
- this.#width = process.stdout.columns ?? this.#width;
91
- this.redraw();
92
- }
93
- #menuState = null;
94
- /** v3 §04: bind the editor's slash-command menu state — the menu rows
95
- * render ABOVE the chrome (over the body's bottom rows; the menu
96
- * opens while the buffer is a "/" prefix, when no tail is live). */
97
- bindMenu(state) {
98
- this.#menuState = state;
99
- }
100
- /** The input line's edit column — prompt width + cursor + 1. The
101
- * dock's redraw and the body's cursor return both end here, so the
102
- * ACTUAL cursor always equals what the editor tracks. The width is
103
- * DISPLAY width (the editor's cursor column is already width-based —
104
- * the CJK drift root cause, editor.ts). v2d: public — the Body's
105
- * render loop ends at this column. */
106
- editCol() {
107
- return this.#inputCol();
108
- }
109
- #inputCol() {
110
- const inp = this.#inputState();
111
- const promptWidth = displayWidth(this.#inputPrompt.replace(/\x1b\[[0-9;]*m/g, ""));
112
- return promptWidth + inp.cursor + 1;
113
- }
114
- /** The status bar's base text (usage, ctx, session, …). */
115
- setStatus(text) {
116
- this.#status = text;
117
- this.redraw();
118
- }
119
- /** The live tail — the spinner glyph or "running <tool> Ns". */
120
- setTail(tail) {
121
- this.#tail = tail;
122
- this.redraw();
123
- }
124
- /** Show a takeover question at the status position (answered at the
125
- * input line by the caller's readline); clearQuestion() restores. */
126
- showQuestion(question) {
127
- this.#question = question;
128
- this.redraw();
129
- }
130
- clearQuestion() {
131
- this.#question = null;
132
- this.redraw();
133
- }
134
- /** The bottom four rows, wrapped in CSI 2026 (synchronized output —
135
- * the pi trick against flicker). The cursor ends at the input line's
136
- * edit position. v3 §03: the upper ╌ row, the input row, the lower
137
- * ╌ row, the status row — the status is dim (bold accents inside
138
- * come from the CLI's composition). TUI v5 #16g: the idle status
139
- * row carries the right-aligned "/ commands · ↑ history" hint. */
140
- redraw() {
141
- if (!this.#active)
142
- return;
143
- const p = palette();
144
- // #17 (P1): read the LIVE size, not the cache — the body's resize
145
- // render calls onDock (this redraw) BEFORE this dock's own resize
146
- // handler runs, so the cached geometry would draw the chrome at
147
- // stale rows (clamped into the body — the separator residue wall).
148
- // The live read makes the handler order irrelevant; the cache keeps
149
- // serving exit().
150
- const H = process.stdout.rows ?? this.#height;
151
- const W = process.stdout.columns ?? this.#width;
152
- const sep = `${p.dim}${"╌".repeat(W)}${p.reset}`;
153
- const status = `${this.#status}${this.#tail === "" ? "" : ` · ${this.#tail}`}`;
154
- const statusLine = this.#question ?? this.#statusRow(status, p, W);
155
- const inp = this.#inputState();
156
- const out = [];
157
- // P3 (review): the DEC private-mode SET/RESET needs the "?" prefix —
158
- // \x1b[?2026h/l, the pi source's exact form. Without it terminals
159
- // silently ignore the mode and the anti-flicker never engages.
160
- out.push("\x1b[?2026h"); // synchronized output ON (DEC 2026)
161
- // v3 §04: the slash-command menu — above the chrome, one row per
162
- // filtered command, the selection highlighted. Drawn first so the
163
- // chrome rows repaint on top of any overlap.
164
- const menu = this.#menuState?.();
165
- if (menu !== null && menu !== undefined) {
166
- for (let i = 0; i < menu.items.length; i += 1) {
167
- const item = menu.items[i];
168
- const row = H - 4 - (menu.items.length - 1 - i);
169
- const text = i === menu.selected
170
- ? `${p.bold}▸ ${item.name}${p.reset} ${item.desc}`
171
- : `${p.dim} ${item.name} ${item.desc}${p.reset}`;
172
- out.push(`\x1b[${row};1H\x1b[0K${text}`);
173
- }
174
- }
175
- out.push(`\x1b[${H - 3};1H\x1b[0K${sep}`);
176
- out.push(`\x1b[${H - 2};1H\x1b[0K${this.#inputPrompt}${inp.line}`);
177
- out.push(`\x1b[${H - 1};1H\x1b[0K${sep}`);
178
- out.push(`\x1b[${H};1H\x1b[0K${statusLine}`);
179
- out.push(`\x1b[${H - 2};${this.#inputCol()}H`); // back to the edit position
180
- out.push("\x1b[?2026l"); // synchronized output OFF
181
- process.stdout.write(out.join(""));
182
- }
183
- /** TUI v5 #16g: the status row — the base status left-aligned, the
184
- * "/ commands · ↑ history" hint right-aligned in the idle state
185
- * (tail empty, no takeover question). The hint is CUT FIRST when
186
- * the width is short — the status itself is never truncated for it;
187
- * the running state carries its own esc hint in the status text, so
188
- * the non-empty tail suppresses this one. */
189
- #statusRow(status, p, W) {
190
- const hint = this.#tail === "" && this.#question === null ? " / commands · ↑ history" : "";
191
- if (hint === "")
192
- return `${p.dim}${status}${p.reset}`;
193
- const statusW = displayWidth(status.replace(/\x1b\[[0-9;]*m/g, ""));
194
- const hintW = displayWidth(hint);
195
- if (statusW + hintW > W)
196
- return `${p.dim}${status}${p.reset}`;
197
- return `${p.dim}${status}${" ".repeat(W - statusW - hintW)}${hint}${p.reset}`;
198
- }
199
- }