@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/ask-panel.d.ts +5 -7
- package/dist/ask-panel.js +51 -8
- package/dist/editor.js +119 -6
- package/package.json +2 -2
- package/dist/body.d.ts +0 -151
- package/dist/body.js +0 -606
- package/dist/dock.d.ts +0 -76
- package/dist/dock.js +0 -199
package/dist/ask-panel.d.ts
CHANGED
|
@@ -53,13 +53,11 @@ export interface AskStep {
|
|
|
53
53
|
readonly state: AskRuntime;
|
|
54
54
|
readonly result?: AskResult;
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
* committed line to `askCommitCustom`.
|
|
62
|
-
*/
|
|
56
|
+
/** REL-0152-D4: is the cursor on the type-your-own row? The editor asks
|
|
57
|
+
* before it decides what a printable key means — on this row a key is
|
|
58
|
+
* the first character of an answer, everywhere else it is a shortcut.
|
|
59
|
+
* Exported so that rule lives on ONE definition of the row. */
|
|
60
|
+
export declare function askOnCustomRow(spec: AskSpec, state: AskRuntime): boolean;
|
|
63
61
|
export declare function askKey(spec: AskSpec, state: AskRuntime, key: string): AskStep;
|
|
64
62
|
/** The typed answer commits: it becomes THE answer for this question
|
|
65
63
|
* (clearing its picks) and the walk advances. An empty line is a
|
package/dist/ask-panel.js
CHANGED
|
@@ -103,6 +103,18 @@ function toggle(state, option, multi) {
|
|
|
103
103
|
* the buffer exactly as it does for the rule-input phase, and hands the
|
|
104
104
|
* committed line to `askCommitCustom`.
|
|
105
105
|
*/
|
|
106
|
+
/** REL-0152-D3 — the index of the type-your-own row: one past the last
|
|
107
|
+
* option, which is where `askBlockRows` draws it. Options and this row
|
|
108
|
+
* are one list to the eye, so they are one list to the cursor. */
|
|
109
|
+
const customRow = (q) => q.options.length;
|
|
110
|
+
/** REL-0152-D4: is the cursor on the type-your-own row? The editor asks
|
|
111
|
+
* before it decides what a printable key means — on this row a key is
|
|
112
|
+
* the first character of an answer, everywhere else it is a shortcut.
|
|
113
|
+
* Exported so that rule lives on ONE definition of the row. */
|
|
114
|
+
export function askOnCustomRow(spec, state) {
|
|
115
|
+
const q = spec.questions[state.qIndex];
|
|
116
|
+
return q !== undefined && state.phase === "options" && state.cursor === customRow(q);
|
|
117
|
+
}
|
|
106
118
|
export function askKey(spec, state, key) {
|
|
107
119
|
const q = spec.questions[state.qIndex];
|
|
108
120
|
const multi = q.multiSelect === true;
|
|
@@ -115,23 +127,39 @@ export function askKey(spec, state, key) {
|
|
|
115
127
|
}
|
|
116
128
|
if (key === "esc")
|
|
117
129
|
return { state, result: askDeclineAll(spec) };
|
|
118
|
-
|
|
130
|
+
// `t` is the shortcut from anywhere in the list; `type` is the
|
|
131
|
+
// REL-0152-D4 gesture — the editor sends it when the cursor is
|
|
132
|
+
// already on the custom row and a printable key arrives, so the
|
|
133
|
+
// keystroke that opened the phase is also its first character.
|
|
134
|
+
if (key === "t" || key === "type")
|
|
119
135
|
return { state: { ...state, phase: "custom" } };
|
|
120
136
|
if (key === "left")
|
|
121
137
|
return { state: state.qIndex === 0 ? state : { ...state, qIndex: state.qIndex - 1, cursor: 0 } };
|
|
122
138
|
if (key === "up")
|
|
123
139
|
return { state: { ...state, cursor: Math.max(0, state.cursor - 1) } };
|
|
140
|
+
// REL-0152-D3: the cursor range includes the type-your-own row, which
|
|
141
|
+
// askBlockRows renders as the list's last item. It used to stop one
|
|
142
|
+
// short, so a row the eye counts as fourth could not be reached by
|
|
143
|
+
// the key that walks the list — the affordance promised a list and
|
|
144
|
+
// delivered four of its five rows.
|
|
124
145
|
if (key === "down")
|
|
125
|
-
return { state: { ...state, cursor: Math.min(q
|
|
126
|
-
if (key === "enter")
|
|
146
|
+
return { state: { ...state, cursor: Math.min(customRow(q), state.cursor + 1) } };
|
|
147
|
+
if (key === "enter") {
|
|
148
|
+
// on the custom row, enter is the way in — the same gesture the
|
|
149
|
+
// row's neighbours answer with.
|
|
150
|
+
if (state.cursor === customRow(q))
|
|
151
|
+
return { state: { ...state, phase: "custom" } };
|
|
127
152
|
return answered(state, state.qIndex) ? advance(spec, state) : { state };
|
|
153
|
+
}
|
|
128
154
|
// SPACE selects at the cursor and NEVER commits — in either mode. It
|
|
129
155
|
// used to answer-and-advance a single-select question, which made a
|
|
130
156
|
// stray space (the most pressable key there is) an instant answer of
|
|
131
157
|
// whatever the cursor happened to be on. Enter and the digits are the
|
|
132
158
|
// only gestures that commit; space is how you point at something.
|
|
159
|
+
// space points at an option; on the custom row it opens the typing
|
|
160
|
+
// phase rather than toggling an option that is not there.
|
|
133
161
|
if (key === "space")
|
|
134
|
-
return { state: toggle(state, state.cursor, multi) };
|
|
162
|
+
return state.cursor === customRow(q) ? { state: { ...state, phase: "custom" } } : { state: toggle(state, state.cursor, multi) };
|
|
135
163
|
const digit = Number.parseInt(key, 10);
|
|
136
164
|
if (Number.isInteger(digit) && digit >= 1 && digit <= q.options.length) {
|
|
137
165
|
const next = toggle(state, digit - 1, multi);
|
|
@@ -181,10 +209,22 @@ export function askBlockRows(view, state, W, maxRows) {
|
|
|
181
209
|
rows.push(cutLine(`${p.dim}─ ${multi ? "pick any — space toggles" : "pick one"} ─${p.reset}`, Math.max(1, W - 2)));
|
|
182
210
|
const picks = state.picks[state.qIndex] ?? [];
|
|
183
211
|
const body = q.options.map((o, i) => `${gutter}${optionRow(o, i + 1, picks.includes(i), state.cursor === i, multi, W)}`);
|
|
212
|
+
// REL-0152-D3: the row is part of the list, so it carries the same
|
|
213
|
+
// cursor affordance the options do. Dim-always made a reachable row
|
|
214
|
+
// look like a footnote.
|
|
184
215
|
const typed = state.custom[state.qIndex];
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
216
|
+
const onCustom = state.cursor === customRow(q);
|
|
217
|
+
// REL-0152-D4: while the phase is OPEN the row becomes the answer's
|
|
218
|
+
// box — a faint placeholder standing where the text will land, so an
|
|
219
|
+
// empty typing phase looks like somewhere to type instead of looking
|
|
220
|
+
// like nothing happened. The placeholder is dim and the answer is
|
|
221
|
+
// not: the two can never be mistaken for each other.
|
|
222
|
+
const typingHere = state.phase === "custom";
|
|
223
|
+
body.push(`${gutter}${cutLine(typed !== null && typed !== undefined
|
|
224
|
+
? `${onCustom || typingHere ? p.bold : ""} t ◉ ${escapeTerminal(typed)}${p.reset}`
|
|
225
|
+
: typingHere
|
|
226
|
+
? `${p.bold} t ▸${p.reset} ${p.dim}type your answer — enter sends, esc backs out${p.reset}`
|
|
227
|
+
: `${onCustom ? p.bold : p.dim} t type your own answer${p.reset}`, Math.max(1, W - 2))}`);
|
|
188
228
|
// the bounded block: the options fold nothing and cut individually,
|
|
189
229
|
// so the cap drops whole rows with the W21 notice row.
|
|
190
230
|
const budget = Math.max(1, maxRows - 5);
|
|
@@ -222,7 +262,10 @@ export function askStatus(view, state) {
|
|
|
222
262
|
/** The input row's lead: the digit lead while picking, the typing lead
|
|
223
263
|
* in the custom phase (the rule-input phase's shape, reused). */
|
|
224
264
|
export function askLeadPlain(state) {
|
|
225
|
-
|
|
265
|
+
// REL-0152-D3: "1-4> " was hard-coded and wrong twice over — it named
|
|
266
|
+
// a range even when there were two options, and it excluded the
|
|
267
|
+
// type-your-own row the list shows.
|
|
268
|
+
return state.phase === "custom" ? "your answer: " : "pick> ";
|
|
226
269
|
}
|
|
227
270
|
// ── the dispatchers: the panel slot, with the ask branch folded in ────
|
|
228
271
|
export function panelBlockRows(view, phase, cursor, W, maxRows, ask, pick, note, safer) {
|
package/dist/editor.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* (?2004h) unwraps and inserts its newlines LITERALLY; every newline
|
|
22
22
|
* source funnels through the ONE normalizer in feed() (§3).
|
|
23
23
|
*/
|
|
24
|
+
var _a;
|
|
24
25
|
import { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
|
|
25
26
|
// the width primitives moved to width.ts (W1, the single width
|
|
26
27
|
// authority) — re-exported so the editor's public surface is unchanged.
|
|
@@ -29,7 +30,7 @@ import { palette } from "./render.js";
|
|
|
29
30
|
import { PICK_MAX, panelOptions, saferDegradedNote, } from "./approval-panel.js";
|
|
30
31
|
// KC3.5: the panel-slot dispatchers — the ask branch folded into the
|
|
31
32
|
// W21 lead/rows, so this file keeps ONE panel and one key owner.
|
|
32
|
-
import { askCommitCustom, askKey, askStart, panelLead } from "./ask-panel.js";
|
|
33
|
+
import { askCommitCustom, askKey, askOnCustomRow, askStart, panelLead } from "./ask-panel.js";
|
|
33
34
|
import { AT_VISIBLE, atFilter } from "./at-picker.js";
|
|
34
35
|
// TUI2-R2 ②: the session picker — the band's THIRD occupant. Its filter
|
|
35
36
|
// is the @ picker's rank aimed at the session id; the editor owns the
|
|
@@ -110,6 +111,29 @@ export class Editor {
|
|
|
110
111
|
// user's next turn.
|
|
111
112
|
#panel = null;
|
|
112
113
|
#pasting = false;
|
|
114
|
+
/**
|
|
115
|
+
* REL-0152-D8 — the paste capsule.
|
|
116
|
+
*
|
|
117
|
+
* A paste large enough to break the composer's layout is held HERE
|
|
118
|
+
* and shown in the buffer as `[Pasted text #N +M lines]`. The buffer
|
|
119
|
+
* is the display; this map is the content; the line that LEAVES the
|
|
120
|
+
* editor is the content again. That ordering is the whole design —
|
|
121
|
+
* the capsule can never truncate what gets sent, because expansion
|
|
122
|
+
* happens on the way out and reads from a map the display cannot
|
|
123
|
+
* edit.
|
|
124
|
+
*
|
|
125
|
+
* A capsule the human deletes is a paste that never happened: the
|
|
126
|
+
* token is gone, the expansion finds nothing to replace, and the
|
|
127
|
+
* entry is simply never read. That is how you take a paste back.
|
|
128
|
+
*
|
|
129
|
+
* The map is per-editor and grows by one entry per large paste in a
|
|
130
|
+
* session — bounded by how many times a human can press cmd-V, and
|
|
131
|
+
* every entry is text they chose to paste and may still submit.
|
|
132
|
+
*/
|
|
133
|
+
#pastes = new Map();
|
|
134
|
+
#pasteSeq = 0;
|
|
135
|
+
/** The buffer index where the in-flight paste began; null outside one. */
|
|
136
|
+
#pasteAt = null;
|
|
113
137
|
/** TUI2-R3v2 ①: one-shot — a panel that just closed swallows the
|
|
114
138
|
* habitual trailing enter rather than submitting the restored draft. */
|
|
115
139
|
#swallowEnter = false;
|
|
@@ -795,6 +819,22 @@ export class Editor {
|
|
|
795
819
|
i += 1;
|
|
796
820
|
continue;
|
|
797
821
|
}
|
|
822
|
+
// REL-0152-D4 — on the custom row a printable key is TEXT.
|
|
823
|
+
// The row names typing as its purpose and then swallowed
|
|
824
|
+
// the first thing you typed; only enter or `t` opened the
|
|
825
|
+
// phase. Now the keystroke opens it AND lands in the
|
|
826
|
+
// buffer, so the character you meant is the character you
|
|
827
|
+
// get. This is checked BEFORE the shortcut branch below on
|
|
828
|
+
// purpose: on this row "3" and "t" are the start of an
|
|
829
|
+
// answer, not a pick and not a mode key. Everywhere else
|
|
830
|
+
// in the list they keep their fast-path meaning exactly.
|
|
831
|
+
if (askOnCustomRow(panel.view.ask, panel.ask) && c !== undefined && c >= " " && c !== "\x7f") {
|
|
832
|
+
this.#askStep("type");
|
|
833
|
+
this.#insert(c.codePointAt(0));
|
|
834
|
+
this.#onRender();
|
|
835
|
+
i += 1;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
798
838
|
if (!typing && (c === " " || (c !== undefined && c >= "1" && c <= "4") || c === "t" || c === "T")) {
|
|
799
839
|
this.#askStep(c === " " ? "space" : c === "T" ? "t" : c);
|
|
800
840
|
i += 1;
|
|
@@ -1186,10 +1226,13 @@ export class Editor {
|
|
|
1186
1226
|
const n = Number(params);
|
|
1187
1227
|
if (n === 3)
|
|
1188
1228
|
this.#delete();
|
|
1189
|
-
else if (n === 200)
|
|
1229
|
+
else if (n === 200) {
|
|
1190
1230
|
this.#pasting = true;
|
|
1231
|
+
this.#pasteAt = this.#cursor; // REL-0152-D8: where the capsule will go
|
|
1232
|
+
}
|
|
1191
1233
|
else if (n === 201) {
|
|
1192
1234
|
this.#pasting = false;
|
|
1235
|
+
this.#encapsulate();
|
|
1193
1236
|
this.#onRender();
|
|
1194
1237
|
}
|
|
1195
1238
|
}
|
|
@@ -1568,7 +1611,10 @@ export class Editor {
|
|
|
1568
1611
|
return;
|
|
1569
1612
|
const spec = panel.view.ask;
|
|
1570
1613
|
const before = panel.ask.phase;
|
|
1571
|
-
|
|
1614
|
+
// REL-0152-D8: a typed ask answer is a line leaving the editor too —
|
|
1615
|
+
// pasting a stack trace into "type your own answer" must send the
|
|
1616
|
+
// stack trace, not the capsule that stands for it.
|
|
1617
|
+
const step = key === "commit" ? askCommitCustom(spec, panel.ask, this.#expandPastes(this.line())) : askKey(spec, panel.ask, key);
|
|
1572
1618
|
panel.ask = step.state;
|
|
1573
1619
|
if (step.state.phase !== before) {
|
|
1574
1620
|
this.#chars = [];
|
|
@@ -1737,6 +1783,67 @@ export class Editor {
|
|
|
1737
1783
|
* reset together (W22: a departing line ends the pop-walk, so the
|
|
1738
1784
|
* next esc at rest interrupts again). Shared by the submit and the
|
|
1739
1785
|
* redirect — the two doors a line can leave by. */
|
|
1786
|
+
/**
|
|
1787
|
+
* REL-0152-D8 — how big a paste has to be before it is a capsule.
|
|
1788
|
+
*
|
|
1789
|
+
* LINES first, because lines are what actually break the layout: the
|
|
1790
|
+
* composer grows a row per line and walks up the terminal. The
|
|
1791
|
+
* character bound catches the pathological one-liner, which wraps to
|
|
1792
|
+
* the same screenful by another route.
|
|
1793
|
+
*
|
|
1794
|
+
* Below both, the paste is left exactly as it arrived. A four-line
|
|
1795
|
+
* snippet is something you want to SEE in the composer, and a capsule
|
|
1796
|
+
* there would be pure obstruction.
|
|
1797
|
+
*/
|
|
1798
|
+
static #PASTE_LINES = 8;
|
|
1799
|
+
static #PASTE_CHARS = 900;
|
|
1800
|
+
/** The token a capsule shows as. Parsed back by the same regexp on
|
|
1801
|
+
* the way out — one definition, so the two can never drift. */
|
|
1802
|
+
static #capsuleText(id, lines) {
|
|
1803
|
+
return `[Pasted text #${id} +${lines} line${lines === 1 ? "" : "s"}]`;
|
|
1804
|
+
}
|
|
1805
|
+
static #CAPSULE = /\[Pasted text #(\d+) \+\d+ lines?\]/g;
|
|
1806
|
+
/**
|
|
1807
|
+
* Close an in-flight paste: if it was large, swap the pasted run out
|
|
1808
|
+
* of the buffer for its capsule and keep the text.
|
|
1809
|
+
*
|
|
1810
|
+
* The swap is a splice at the recorded start, so a paste in the
|
|
1811
|
+
* MIDDLE of a line leaves the prose on both sides of it untouched —
|
|
1812
|
+
* the capsule is a character run like any other from here on, and
|
|
1813
|
+
* every editing operation in this file works on it without knowing
|
|
1814
|
+
* it exists.
|
|
1815
|
+
*/
|
|
1816
|
+
#encapsulate() {
|
|
1817
|
+
const start = this.#pasteAt;
|
|
1818
|
+
this.#pasteAt = null;
|
|
1819
|
+
if (start === null || this.#cursor <= start)
|
|
1820
|
+
return;
|
|
1821
|
+
const pasted = String.fromCodePoint(...this.#chars.slice(start, this.#cursor));
|
|
1822
|
+
const lines = pasted.split("\n").length;
|
|
1823
|
+
if (lines < _a.#PASTE_LINES && pasted.length < _a.#PASTE_CHARS)
|
|
1824
|
+
return;
|
|
1825
|
+
this.#pasteSeq += 1;
|
|
1826
|
+
this.#pastes.set(this.#pasteSeq, pasted);
|
|
1827
|
+
const capsule = [..._a.#capsuleText(this.#pasteSeq, lines)].map((ch) => ch.codePointAt(0));
|
|
1828
|
+
this.#chars.splice(start, this.#cursor - start, ...capsule);
|
|
1829
|
+
this.#cursor = start + capsule.length;
|
|
1830
|
+
this.#reflow();
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* The way out: every capsule token becomes its text again.
|
|
1834
|
+
*
|
|
1835
|
+
* Applied to the line the editor HANDS OVER, never to the buffer —
|
|
1836
|
+
* so what the human sees stays short and what the model receives is
|
|
1837
|
+
* what the human pasted. A token whose entry is missing (a stale id
|
|
1838
|
+
* recalled from history after the map moved on) is left standing as
|
|
1839
|
+
* literal text rather than silently becoming an empty string: a
|
|
1840
|
+
* visible oddity beats a silent deletion of someone's paste.
|
|
1841
|
+
*/
|
|
1842
|
+
#expandPastes(line) {
|
|
1843
|
+
if (this.#pastes.size === 0)
|
|
1844
|
+
return line;
|
|
1845
|
+
return line.replace(_a.#CAPSULE, (whole, id) => this.#pastes.get(Number(id)) ?? whole);
|
|
1846
|
+
}
|
|
1740
1847
|
#takeLine() {
|
|
1741
1848
|
const line = String.fromCodePoint(...this.#chars);
|
|
1742
1849
|
this.#chars = [];
|
|
@@ -1850,16 +1957,21 @@ export class Editor {
|
|
|
1850
1957
|
}
|
|
1851
1958
|
}
|
|
1852
1959
|
const line = this.#takeLine();
|
|
1960
|
+
// REL-0152-D8: the capsule expands ON THE WAY OUT. The consumer
|
|
1961
|
+
// gets what was pasted; the HISTORY keeps the short form, so ↑
|
|
1962
|
+
// recalls a readable line that still expands when it is sent
|
|
1963
|
+
// again (the map outlives the buffer, by design).
|
|
1964
|
+
const sent = this.#expandPastes(line);
|
|
1853
1965
|
const cb = this.#questionCb;
|
|
1854
1966
|
this.#questionCb = null;
|
|
1855
1967
|
if (cb !== null) {
|
|
1856
|
-
cb(
|
|
1968
|
+
cb(sent);
|
|
1857
1969
|
}
|
|
1858
1970
|
else if (this.#lineCb !== null) {
|
|
1859
|
-
this.#lineCb(
|
|
1971
|
+
this.#lineCb(sent);
|
|
1860
1972
|
}
|
|
1861
1973
|
else {
|
|
1862
|
-
this.#pendingLines.push(
|
|
1974
|
+
this.#pendingLines.push(sent); // nobody wired yet — hold it
|
|
1863
1975
|
}
|
|
1864
1976
|
if (cb === null && line !== "")
|
|
1865
1977
|
this.#remember(line);
|
|
@@ -1949,3 +2061,4 @@ export class Editor {
|
|
|
1949
2061
|
return end;
|
|
1950
2062
|
}
|
|
1951
2063
|
}
|
|
2064
|
+
_a = Editor;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.4",
|
|
4
4
|
"description": "kiso tui \u2014 the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@vincemakes/kiso-tui-cells": "0.15.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.15.4"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/dist/body.d.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2d — the body renderer: ONE writer for the stdout scroll region.
|
|
3
|
-
*
|
|
4
|
-
* The v2b/v2c body writes streamed directly (each event wrote its bytes),
|
|
5
|
-
* so the tool lines, thinking folds, and text deltas could interleave in
|
|
6
|
-
* the same frame — and a tool's life was scattered across several writes
|
|
7
|
-
* (the "leak"). v2d: every event handler ONLY mutates cell state; the
|
|
8
|
-
* Body's render loop is the only thing that writes the region.
|
|
9
|
-
*
|
|
10
|
-
* Frozen semantics: a completed cell prints its final form into the
|
|
11
|
-
* scroll region ONCE and is never touched again. The ACTIVE TAIL — the
|
|
12
|
-
* unfinished cells — renders at the region's bottom (between the frozen
|
|
13
|
-
* area and the dock) and redraws in place, CSI 2026 wrapped. State
|
|
14
|
-
* changes coalesce to ≥16ms frames; a 200ms heartbeat drives the running
|
|
15
|
-
* spinners and elapsed timers. An over-height tail (rare) overflows to
|
|
16
|
-
* freeze by completion order.
|
|
17
|
-
*
|
|
18
|
-
* Pipes / NO_COLOR: the Body runs in PASSTHROUGH — every mutation writes
|
|
19
|
-
* the v2b/v2c line-mode bytes immediately, byte-for-byte (the existing
|
|
20
|
-
* e2e guards it). The cell renderer never activates.
|
|
21
|
-
*
|
|
22
|
-
* The cell model is ours (UserCell / ThinkingCell / ToolCell / TextCell /
|
|
23
|
-
* NoticeCell / raw block) — deliberately NOT pi's Component interface
|
|
24
|
-
* shape (ADR-0040).
|
|
25
|
-
*/
|
|
26
|
-
/** One completed-or-active body line. The renderer's only state. */
|
|
27
|
-
export type BodyCell = {
|
|
28
|
-
kind: "user";
|
|
29
|
-
text: string;
|
|
30
|
-
done: true;
|
|
31
|
-
} | {
|
|
32
|
-
kind: "thinking";
|
|
33
|
-
text: string;
|
|
34
|
-
done: boolean;
|
|
35
|
-
} | {
|
|
36
|
-
kind: "tool";
|
|
37
|
-
name: string;
|
|
38
|
-
input: string;
|
|
39
|
-
state: "pending" | "approval" | "running" | "done";
|
|
40
|
-
isError: boolean;
|
|
41
|
-
resultText: string;
|
|
42
|
-
diff: import("./diff.js").DiffLine[] | null;
|
|
43
|
-
added: number;
|
|
44
|
-
removed: number;
|
|
45
|
-
startedAt: number | null;
|
|
46
|
-
doneAt: number | null;
|
|
47
|
-
done: boolean;
|
|
48
|
-
} | {
|
|
49
|
-
kind: "text";
|
|
50
|
-
text: string;
|
|
51
|
-
done: boolean;
|
|
52
|
-
} | {
|
|
53
|
-
kind: "notice";
|
|
54
|
-
text: string;
|
|
55
|
-
done: true;
|
|
56
|
-
} | {
|
|
57
|
-
kind: "raw";
|
|
58
|
-
lines: string[];
|
|
59
|
-
done: true;
|
|
60
|
-
} | {
|
|
61
|
-
kind: "terminal";
|
|
62
|
-
label: string;
|
|
63
|
-
line: string;
|
|
64
|
-
done: true;
|
|
65
|
-
} | {
|
|
66
|
-
kind: "checklist";
|
|
67
|
-
header: string;
|
|
68
|
-
items: {
|
|
69
|
-
text: string;
|
|
70
|
-
status: "pending" | "active" | "done";
|
|
71
|
-
}[];
|
|
72
|
-
done: true;
|
|
73
|
-
};
|
|
74
|
-
export interface BodyOptions {
|
|
75
|
-
/** Is the cell renderer live? A color TTY with a real size — checked
|
|
76
|
-
* per mutation (the TIOCSWINSZ can land after main constructs us). */
|
|
77
|
-
active: () => boolean;
|
|
78
|
-
/** The terminal height (rows) — live, for the region geometry. */
|
|
79
|
-
height: () => number;
|
|
80
|
-
/** The terminal width (cols) — live, for wrap estimates. */
|
|
81
|
-
width: () => number;
|
|
82
|
-
/** The input line's edit column — the render's cursor home. */
|
|
83
|
-
editCol: () => number;
|
|
84
|
-
/** The dock's redraw (the bottom three rows) — the body never writes
|
|
85
|
-
* below the region, but the frame may call it to re-pin the chrome. */
|
|
86
|
-
onDock?: () => void;
|
|
87
|
-
/** The stdout writer — injectable for unit tests (default: stdout). */
|
|
88
|
-
write?: (s: string) => void;
|
|
89
|
-
}
|
|
90
|
-
export declare class Body {
|
|
91
|
-
#private;
|
|
92
|
-
constructor(opts: BodyOptions);
|
|
93
|
-
/** Teardown — flush a pending frame, stop the timers. */
|
|
94
|
-
close(): void;
|
|
95
|
-
/**
|
|
96
|
-
* #17 (P1): a resize reflows the terminal's buffer — the old chrome
|
|
97
|
-
* rows SURVIVE the reflow at their shifted positions (the recorded
|
|
98
|
-
* separator wall + the tail ghost — the #16a assumption that the
|
|
99
|
-
* reflow erases them was wrong). The handler: (1) clear the old tail +
|
|
100
|
-
* dock area with the OLD geometry (one ED from the last-drawn tail
|
|
101
|
-
* top; EL/ED only, zero LF — the #16 storm gate's invariants hold);
|
|
102
|
-
* (2) redraw immediately at the NEW geometry (the tail, the cursor
|
|
103
|
-
* home, the dock — via the normal render). The frozen content is
|
|
104
|
-
* strictly ABOVE the old tail top — the clear never touches it (the
|
|
105
|
-
* frozen bytes stay emitted exactly once). Consecutive resizes are
|
|
106
|
-
* idempotent: the clear covers an already-clear area.
|
|
107
|
-
*/
|
|
108
|
-
onResize(): void;
|
|
109
|
-
/** The last COMPLETE thinking block, for /think. */
|
|
110
|
-
lastThinking(): string | null;
|
|
111
|
-
/** The last completed tool call, for /last. */
|
|
112
|
-
lastTool(): {
|
|
113
|
-
name: string;
|
|
114
|
-
input: Record<string, unknown>;
|
|
115
|
-
result: {
|
|
116
|
-
content: string;
|
|
117
|
-
isError: boolean;
|
|
118
|
-
};
|
|
119
|
-
} | null;
|
|
120
|
-
userLine(text: string): void;
|
|
121
|
-
thinkingAppend(text: string): void;
|
|
122
|
-
thinkingEnd(): void;
|
|
123
|
-
toolStart(name: string, callId: string, input: Record<string, unknown>): void;
|
|
124
|
-
toolApproval(callId: string, diff: import("./diff.js").DiffResult | null): void;
|
|
125
|
-
toolRunning(callId: string): void;
|
|
126
|
-
toolSucceeded(callId: string): void;
|
|
127
|
-
toolFailed(callId: string, error: string): void;
|
|
128
|
-
toolResult(callId: string, result: {
|
|
129
|
-
content: string;
|
|
130
|
-
isError: boolean;
|
|
131
|
-
}): void;
|
|
132
|
-
textAppend(text: string): void;
|
|
133
|
-
textEnd(): void;
|
|
134
|
-
/** The terminal's status line + the rhythm gap (one blank). */
|
|
135
|
-
terminal(label: string, statusLine: string): void;
|
|
136
|
-
notice(text: string): void;
|
|
137
|
-
/** ⑥ todo round: the durable checklist — header + one brick-glyph line
|
|
138
|
-
* per item, frozen immediately (it is static content). The CLI
|
|
139
|
-
* translates a tagged tool result into the structured items; the
|
|
140
|
-
* passthrough writes the same lines (byte-identical in pipes). */
|
|
141
|
-
checklist(header: string, items: {
|
|
142
|
-
text: string;
|
|
143
|
-
status: "pending" | "active" | "done";
|
|
144
|
-
}[]): void;
|
|
145
|
-
/** A pre-rendered block (the banner, the session line, slash-command
|
|
146
|
-
* outputs) — frozen immediately. */
|
|
147
|
-
raw(lines: string[]): void;
|
|
148
|
-
/** The one writer. Frozen cells print once; the tail redraws in place,
|
|
149
|
-
* CSI 2026 wrapped; the cursor lands at the input edit column. */
|
|
150
|
-
render(): void;
|
|
151
|
-
}
|