@vincemakes/kiso-code 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.
- package/package.json +8 -8
- package/dist/body.d.ts +0 -121
- package/dist/body.js +0 -491
- package/dist/dock.d.ts +0 -75
- package/dist/dock.js +0 -176
- package/dist/editor.d.ts +0 -75
- package/dist/editor.js +0 -444
- package/dist/render.d.ts +0 -135
- package/dist/render.js +0 -325
package/dist/editor.js
DELETED
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2c — the raw-mode single-line editor that REPLACES readline on the TTY
|
|
3
|
-
* path. Root cause of the v2b drift (plan 2026-08-05-tui-v2b §11): readline
|
|
4
|
-
* re-renders its line by CHARACTER count and assumes it owns the row
|
|
5
|
-
* exclusively — a CJK wide character (2 cells) shifts every following
|
|
6
|
-
* column, and the dock's redraws make the mismatch permanent. Patching
|
|
7
|
-
* readline is a dead end; the TTY path draws its own input row instead.
|
|
8
|
-
* Non-TTY paths keep readline untouched (pipe bytes unchanged).
|
|
9
|
-
*
|
|
10
|
-
* Zero dependencies. The eastAsianWidth table is a ~40-line subset (CJK
|
|
11
|
-
* ideographs/kana/hangul/fullwidth/common wide symbols = 2, everything
|
|
12
|
-
* else = 1). Known limitation, documented in the README: emoji ZWJ
|
|
13
|
-
* clusters (family emoji etc.) are not guaranteed perfect — each code
|
|
14
|
-
* point counts as its width.
|
|
15
|
-
*
|
|
16
|
-
* The editor is a SINGLE line: bracketed paste (?2004h) unwraps and
|
|
17
|
-
* inserts, internal newlines become spaces.
|
|
18
|
-
*/
|
|
19
|
-
/** A code point's display width: 2 for the wide ranges, 1 otherwise. */
|
|
20
|
-
export function charWidth(cp) {
|
|
21
|
-
if (cp >= 0x1100 && cp <= 0x115f)
|
|
22
|
-
return 2; // hangul jamo
|
|
23
|
-
if (cp >= 0x2e80 && cp <= 0x303e)
|
|
24
|
-
return 2; // radicals .. CJK punctuation
|
|
25
|
-
if (cp >= 0x3041 && cp <= 0x33ff)
|
|
26
|
-
return 2; // kana, CJK compat
|
|
27
|
-
if (cp >= 0x3400 && cp <= 0x4dbf)
|
|
28
|
-
return 2; // CJK ext A
|
|
29
|
-
if (cp >= 0x4e00 && cp <= 0x9fff)
|
|
30
|
-
return 2; // CJK unified
|
|
31
|
-
if (cp >= 0xa000 && cp <= 0xa4cf)
|
|
32
|
-
return 2; // yi
|
|
33
|
-
if (cp >= 0xa960 && cp <= 0xa97f)
|
|
34
|
-
return 2; // hangul jamo ext
|
|
35
|
-
if (cp >= 0xac00 && cp <= 0xd7a3)
|
|
36
|
-
return 2; // hangul syllables
|
|
37
|
-
if (cp >= 0xf900 && cp <= 0xfaff)
|
|
38
|
-
return 2; // CJK compat ideographs
|
|
39
|
-
if (cp >= 0xfe10 && cp <= 0xfe19)
|
|
40
|
-
return 2; // vertical forms
|
|
41
|
-
if (cp >= 0xfe30 && cp <= 0xfe6f)
|
|
42
|
-
return 2; // CJK compat forms
|
|
43
|
-
if (cp >= 0xff00 && cp <= 0xff60)
|
|
44
|
-
return 2; // fullwidth forms
|
|
45
|
-
if (cp >= 0xffe0 && cp <= 0xffe6)
|
|
46
|
-
return 2; // fullwidth signs
|
|
47
|
-
if (cp >= 0x1f300 && cp <= 0x1f64f)
|
|
48
|
-
return 2; // emoji (misc + emoticons)
|
|
49
|
-
if (cp >= 0x1f900 && cp <= 0x1f9ff)
|
|
50
|
-
return 2; // supplemental emoji
|
|
51
|
-
if (cp >= 0x20000 && cp <= 0x3fffd)
|
|
52
|
-
return 2; // CJK ext B..G
|
|
53
|
-
return 1;
|
|
54
|
-
}
|
|
55
|
-
/** Display width of a code-point array (cursor math, scrolling). */
|
|
56
|
-
export function widthOf(chars) {
|
|
57
|
-
let w = 0;
|
|
58
|
-
for (const cp of chars)
|
|
59
|
-
w += charWidth(cp);
|
|
60
|
-
return w;
|
|
61
|
-
}
|
|
62
|
-
/** Display width of a string. */
|
|
63
|
-
export function displayWidth(text) {
|
|
64
|
-
let w = 0;
|
|
65
|
-
for (const ch of text)
|
|
66
|
-
w += charWidth(ch.codePointAt(0));
|
|
67
|
-
return w;
|
|
68
|
-
}
|
|
69
|
-
import { palette } from "./render.js";
|
|
70
|
-
export const PROMPT = "▌you> "; // the kiso brick motif: one blue half-block, then you>
|
|
71
|
-
export const PROMPT_WIDTH = displayWidth(PROMPT);
|
|
72
|
-
export const MENU_ITEMS = [
|
|
73
|
-
{ name: "/mode", desc: "switch the approval tier (manual/default/accept-edits/plan/bypass)" },
|
|
74
|
-
{ name: "/think", desc: "show the last full thinking block" },
|
|
75
|
-
{ name: "/last", desc: "show the most recent tool call's input and output" },
|
|
76
|
-
{ name: "/status", desc: "show session id, event count, and context estimate" },
|
|
77
|
-
{ name: "/help", desc: "print this list of commands" },
|
|
78
|
-
];
|
|
79
|
-
/**
|
|
80
|
-
* The editor. Raw mode + bracketed paste (?2004h) on enter, restored on
|
|
81
|
-
* exit. The input row is rendered by `onRender` (the CLI wires it to the
|
|
82
|
-
* dock's redraw when docked, the editor's own self-render otherwise) —
|
|
83
|
-
* the editor itself never writes while docked. The event handlers are
|
|
84
|
-
* settable — the chat/resume contexts wire them after construction.
|
|
85
|
-
*/
|
|
86
|
-
export class Editor {
|
|
87
|
-
#chars = [];
|
|
88
|
-
#cursor = 0;
|
|
89
|
-
#scroll = 0; // chars scrolled off the left (width-based reflow)
|
|
90
|
-
#questionCb = null;
|
|
91
|
-
#pasting = false;
|
|
92
|
-
#lineCb = null;
|
|
93
|
-
#pendingLines = []; // submits before onLine is wired (startup) — never dropped
|
|
94
|
-
#sigintCb = null;
|
|
95
|
-
#eotCb = null;
|
|
96
|
-
#escapeCb = null;
|
|
97
|
-
#onRender;
|
|
98
|
-
#menuOpen = false; // v3 §04: the slash-command menu
|
|
99
|
-
#menuSel = 0;
|
|
100
|
-
#pending = ""; // an incomplete ESC/CSI prefix across chunks
|
|
101
|
-
#decoder = new TextDecoder();
|
|
102
|
-
#entered = false;
|
|
103
|
-
#onData;
|
|
104
|
-
#closedResolve;
|
|
105
|
-
closed;
|
|
106
|
-
constructor(onRender) {
|
|
107
|
-
this.#onRender = onRender;
|
|
108
|
-
this.#onData = (raw) => this.feed(raw);
|
|
109
|
-
this.closed = new Promise((resolve) => {
|
|
110
|
-
this.#closedResolve = resolve;
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
onLine(cb) {
|
|
114
|
-
this.#lineCb = cb;
|
|
115
|
-
// Flush submits that arrived before the handler was wired (typed
|
|
116
|
-
// during startup) — readline buffered these; the editor must too.
|
|
117
|
-
for (const line of this.#pendingLines)
|
|
118
|
-
cb(line);
|
|
119
|
-
this.#pendingLines.length = 0;
|
|
120
|
-
}
|
|
121
|
-
onSigint(cb) {
|
|
122
|
-
this.#sigintCb = cb;
|
|
123
|
-
}
|
|
124
|
-
onEot(cb) {
|
|
125
|
-
this.#eotCb = cb;
|
|
126
|
-
}
|
|
127
|
-
onEscape(cb) {
|
|
128
|
-
this.#escapeCb = cb;
|
|
129
|
-
}
|
|
130
|
-
/** The whole buffer as text (the CLI's line()/clearLine()). */
|
|
131
|
-
line() {
|
|
132
|
-
return String.fromCodePoint(...this.#chars);
|
|
133
|
-
}
|
|
134
|
-
clearLine() {
|
|
135
|
-
this.#chars = [];
|
|
136
|
-
this.#cursor = 0;
|
|
137
|
-
this.#scroll = 0;
|
|
138
|
-
this.#onRender();
|
|
139
|
-
}
|
|
140
|
-
/** The visible slice (dim "…" prefix when scrolled) + the cursor's
|
|
141
|
-
* display column within it — the dock's input-row state. */
|
|
142
|
-
dockState() {
|
|
143
|
-
const visible = String.fromCodePoint(...this.#chars.slice(this.#scroll));
|
|
144
|
-
const prefix = this.#scroll > 0 ? "\x1b[2m…\x1b[0m" : "";
|
|
145
|
-
const col = (this.#scroll > 0 ? 1 : 0) + widthOf(this.#chars.slice(this.#scroll, this.#cursor));
|
|
146
|
-
return { line: `${prefix}${visible}`, cursor: col };
|
|
147
|
-
}
|
|
148
|
-
/** v3 §04: the menu's visible state for the dock — null when closed. */
|
|
149
|
-
menuState() {
|
|
150
|
-
if (!this.#menuOpen)
|
|
151
|
-
return null;
|
|
152
|
-
return { items: this.#menuFiltered(), selected: this.#menuSel };
|
|
153
|
-
}
|
|
154
|
-
/** v3 §04: the filtered command list for the current buffer — open
|
|
155
|
-
* only while the line is "/" + something (a bare "/" waits). */
|
|
156
|
-
#menuFiltered() {
|
|
157
|
-
const line = this.line();
|
|
158
|
-
if (!line.startsWith("/") || line === "/")
|
|
159
|
-
return [];
|
|
160
|
-
return MENU_ITEMS.filter((m) => m.name.startsWith(line));
|
|
161
|
-
}
|
|
162
|
-
#refreshMenu() {
|
|
163
|
-
const f = this.#menuFiltered();
|
|
164
|
-
this.#menuOpen = f.length > 0;
|
|
165
|
-
if (this.#menuSel >= f.length)
|
|
166
|
-
this.#menuSel = 0;
|
|
167
|
-
this.#onRender();
|
|
168
|
-
}
|
|
169
|
-
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
170
|
-
question(_query, cb) {
|
|
171
|
-
this.#questionCb = cb;
|
|
172
|
-
}
|
|
173
|
-
/** Cancel a pending question — the buffer stays (its text becomes the
|
|
174
|
-
* next turn on Enter, the readline re-emit equivalent). */
|
|
175
|
-
cancelQuestion() {
|
|
176
|
-
this.#questionCb = null;
|
|
177
|
-
}
|
|
178
|
-
enter() {
|
|
179
|
-
if (this.#entered)
|
|
180
|
-
return;
|
|
181
|
-
this.#entered = true;
|
|
182
|
-
process.stdin.setRawMode(true);
|
|
183
|
-
process.stdout.write("\x1b[?2004h"); // bracketed paste ON
|
|
184
|
-
process.stdin.on("data", this.#onData);
|
|
185
|
-
this.#onRender();
|
|
186
|
-
}
|
|
187
|
-
exit() {
|
|
188
|
-
if (!this.#entered)
|
|
189
|
-
return;
|
|
190
|
-
this.#entered = false;
|
|
191
|
-
process.stdin.off("data", this.#onData);
|
|
192
|
-
process.stdout.write("\x1b[?2004l"); // bracketed paste OFF
|
|
193
|
-
process.stdin.setRawMode(false);
|
|
194
|
-
this.#closedResolve();
|
|
195
|
-
}
|
|
196
|
-
/** The row's own render when the dock is inactive (a TTY without a
|
|
197
|
-
* real size): \r + clear + blue brick prompt + visible + cursor
|
|
198
|
-
* column. */
|
|
199
|
-
selfRender() {
|
|
200
|
-
const p = palette();
|
|
201
|
-
const st = this.dockState();
|
|
202
|
-
const W = (process.stdout.columns ?? 0) || 80; // a degenerate 0 size (no TIOCSWINSZ) falls back
|
|
203
|
-
const cursorCol = Math.min(1 + PROMPT_WIDTH + st.cursor, W);
|
|
204
|
-
process.stdout.write(`\r\x1b[0K${p.blue}${PROMPT}${p.reset}${st.line}\x1b[${cursorCol}G`);
|
|
205
|
-
}
|
|
206
|
-
// ---- input ----
|
|
207
|
-
/** Feed raw stdin bytes — the parser. Public for unit tests. */
|
|
208
|
-
feed(raw) {
|
|
209
|
-
const text = this.#pending + this.#decoder.decode(raw, { stream: true });
|
|
210
|
-
this.#pending = "";
|
|
211
|
-
let i = 0;
|
|
212
|
-
while (i < text.length) {
|
|
213
|
-
const c = text[i];
|
|
214
|
-
if (c === "\x1b") {
|
|
215
|
-
const rest = text.slice(i + 1);
|
|
216
|
-
if (rest.startsWith("[")) {
|
|
217
|
-
const m = rest.match(/^\[([0-9;?]*)([A-Za-z~])/);
|
|
218
|
-
if (m === null) {
|
|
219
|
-
this.#pending = text.slice(i); // incomplete CSI — wait for more
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
this.#csi(m[1], m[2]);
|
|
223
|
-
i += m[0].length + 1;
|
|
224
|
-
}
|
|
225
|
-
else if (rest.startsWith("O")) {
|
|
226
|
-
i += 3; // SS3 (function keys) — ignored
|
|
227
|
-
}
|
|
228
|
-
else if (this.#menuOpen) {
|
|
229
|
-
// v3 §04: Esc closes the menu and clears the buffer.
|
|
230
|
-
this.#chars = [];
|
|
231
|
-
this.#cursor = 0;
|
|
232
|
-
this.#scroll = 0;
|
|
233
|
-
this.#refreshMenu();
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
this.#escapeCb?.();
|
|
237
|
-
i += 1;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
else if (c === "\x0d" || c === "\x0a") {
|
|
241
|
-
if (this.#pasting) {
|
|
242
|
-
this.#insert(0x20); // single-line editor: newlines become spaces
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
this.#submit();
|
|
246
|
-
}
|
|
247
|
-
i += 1;
|
|
248
|
-
}
|
|
249
|
-
else if (c === "\x7f" || c === "\x08") {
|
|
250
|
-
this.#backspace();
|
|
251
|
-
i += 1;
|
|
252
|
-
}
|
|
253
|
-
else if (c === "\x03") {
|
|
254
|
-
this.#sigintCb?.();
|
|
255
|
-
i += 1;
|
|
256
|
-
}
|
|
257
|
-
else if (c === "\x04") {
|
|
258
|
-
this.#eotCb?.();
|
|
259
|
-
i += 1;
|
|
260
|
-
}
|
|
261
|
-
else if (c === "\x15") {
|
|
262
|
-
this.#killToStart();
|
|
263
|
-
i += 1;
|
|
264
|
-
}
|
|
265
|
-
else if (c === "\x0b") {
|
|
266
|
-
this.#killToEnd();
|
|
267
|
-
i += 1;
|
|
268
|
-
}
|
|
269
|
-
else if (c === "\x17") {
|
|
270
|
-
this.#killWord();
|
|
271
|
-
i += 1;
|
|
272
|
-
}
|
|
273
|
-
else if (c === "\x01") {
|
|
274
|
-
this.#cursor = 0;
|
|
275
|
-
this.#reflow();
|
|
276
|
-
this.#onRender();
|
|
277
|
-
i += 1;
|
|
278
|
-
}
|
|
279
|
-
else if (c === "\x05") {
|
|
280
|
-
this.#cursor = this.#chars.length;
|
|
281
|
-
this.#reflow();
|
|
282
|
-
this.#onRender();
|
|
283
|
-
i += 1;
|
|
284
|
-
}
|
|
285
|
-
else if (c === "\t" && this.#menuOpen) {
|
|
286
|
-
// v3 §04: Tab completes the buffer to the selected command.
|
|
287
|
-
const f = this.#menuFiltered();
|
|
288
|
-
const m = f[this.#menuSel];
|
|
289
|
-
if (m !== undefined) {
|
|
290
|
-
this.#chars = [...m.name].map((ch) => ch.codePointAt(0));
|
|
291
|
-
this.#cursor = this.#chars.length;
|
|
292
|
-
this.#reflow();
|
|
293
|
-
this.#refreshMenu();
|
|
294
|
-
}
|
|
295
|
-
i += 1;
|
|
296
|
-
}
|
|
297
|
-
else if (c !== undefined && c < " ") {
|
|
298
|
-
i += 1; // other control — ignored
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
this.#insert(text.codePointAt(i));
|
|
302
|
-
i += c.length;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
#csi(params, final) {
|
|
307
|
-
if (final === "~") {
|
|
308
|
-
const n = Number(params);
|
|
309
|
-
if (n === 3)
|
|
310
|
-
this.#delete();
|
|
311
|
-
else if (n === 200)
|
|
312
|
-
this.#pasting = true;
|
|
313
|
-
else if (n === 201) {
|
|
314
|
-
this.#pasting = false;
|
|
315
|
-
this.#onRender();
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
else if (final === "A" && this.#menuOpen) {
|
|
319
|
-
// v3 §04: ↑↓ move the menu selection, never the cursor.
|
|
320
|
-
this.#menuSel = Math.max(0, this.#menuSel - 1);
|
|
321
|
-
this.#onRender();
|
|
322
|
-
}
|
|
323
|
-
else if (final === "B" && this.#menuOpen) {
|
|
324
|
-
this.#menuSel = Math.min(this.#menuFiltered().length - 1, this.#menuSel + 1);
|
|
325
|
-
this.#onRender();
|
|
326
|
-
}
|
|
327
|
-
else if (final === "D") {
|
|
328
|
-
this.#move(-1);
|
|
329
|
-
}
|
|
330
|
-
else if (final === "C") {
|
|
331
|
-
this.#move(1);
|
|
332
|
-
}
|
|
333
|
-
else if (final === "H") {
|
|
334
|
-
this.#cursor = 0;
|
|
335
|
-
this.#reflow();
|
|
336
|
-
}
|
|
337
|
-
else if (final === "F") {
|
|
338
|
-
this.#cursor = this.#chars.length;
|
|
339
|
-
this.#reflow();
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
// ---- editing ----
|
|
343
|
-
#insert(cp) {
|
|
344
|
-
this.#chars.splice(this.#cursor, 0, cp);
|
|
345
|
-
this.#cursor += 1;
|
|
346
|
-
this.#reflow();
|
|
347
|
-
if (!this.#pasting)
|
|
348
|
-
this.#refreshMenu();
|
|
349
|
-
}
|
|
350
|
-
#backspace() {
|
|
351
|
-
if (this.#cursor === 0)
|
|
352
|
-
return;
|
|
353
|
-
this.#chars.splice(this.#cursor - 1, 1);
|
|
354
|
-
this.#cursor -= 1;
|
|
355
|
-
this.#reflow();
|
|
356
|
-
if (!this.#pasting)
|
|
357
|
-
this.#refreshMenu();
|
|
358
|
-
}
|
|
359
|
-
#delete() {
|
|
360
|
-
if (this.#cursor >= this.#chars.length)
|
|
361
|
-
return;
|
|
362
|
-
this.#chars.splice(this.#cursor, 1);
|
|
363
|
-
this.#reflow();
|
|
364
|
-
if (!this.#pasting)
|
|
365
|
-
this.#refreshMenu();
|
|
366
|
-
}
|
|
367
|
-
#move(delta) {
|
|
368
|
-
this.#cursor = Math.max(0, Math.min(this.#chars.length, this.#cursor + delta));
|
|
369
|
-
this.#reflow();
|
|
370
|
-
if (!this.#pasting)
|
|
371
|
-
this.#onRender();
|
|
372
|
-
}
|
|
373
|
-
#killToStart() {
|
|
374
|
-
this.#chars.splice(0, this.#cursor);
|
|
375
|
-
this.#cursor = 0;
|
|
376
|
-
this.#reflow();
|
|
377
|
-
if (!this.#pasting)
|
|
378
|
-
this.#onRender();
|
|
379
|
-
}
|
|
380
|
-
#killToEnd() {
|
|
381
|
-
this.#chars.length = this.#cursor;
|
|
382
|
-
this.#reflow();
|
|
383
|
-
if (!this.#pasting)
|
|
384
|
-
this.#onRender();
|
|
385
|
-
}
|
|
386
|
-
#killWord() {
|
|
387
|
-
let i = this.#cursor;
|
|
388
|
-
while (i > 0 && this.#chars[i - 1] === 0x20)
|
|
389
|
-
i -= 1; // trailing spaces
|
|
390
|
-
while (i > 0 && this.#chars[i - 1] !== 0x20)
|
|
391
|
-
i -= 1; // the word
|
|
392
|
-
this.#chars.splice(i, this.#cursor - i);
|
|
393
|
-
this.#cursor = i;
|
|
394
|
-
this.#reflow();
|
|
395
|
-
}
|
|
396
|
-
#submit() {
|
|
397
|
-
let line = String.fromCodePoint(...this.#chars);
|
|
398
|
-
if (this.#menuOpen) {
|
|
399
|
-
// v3 §04: Enter submits the SELECTED command.
|
|
400
|
-
const m = this.#menuFiltered()[this.#menuSel];
|
|
401
|
-
if (m !== undefined)
|
|
402
|
-
line = m.name;
|
|
403
|
-
}
|
|
404
|
-
this.#chars = [];
|
|
405
|
-
this.#cursor = 0;
|
|
406
|
-
this.#scroll = 0;
|
|
407
|
-
this.#menuOpen = false;
|
|
408
|
-
this.#menuSel = 0;
|
|
409
|
-
const cb = this.#questionCb;
|
|
410
|
-
this.#questionCb = null;
|
|
411
|
-
if (cb !== null) {
|
|
412
|
-
cb(line);
|
|
413
|
-
}
|
|
414
|
-
else if (this.#lineCb !== null) {
|
|
415
|
-
this.#lineCb(line);
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
this.#pendingLines.push(line); // nobody wired yet — hold it
|
|
419
|
-
}
|
|
420
|
-
this.#onRender();
|
|
421
|
-
}
|
|
422
|
-
// ---- width-based horizontal scroll ----
|
|
423
|
-
#reflow() {
|
|
424
|
-
const W = (process.stdout.columns ?? 0) || 80; // degenerate 0 falls back to 80
|
|
425
|
-
const maxW = Math.max(1, W - PROMPT_WIDTH - 1); // 1 col for the "…"
|
|
426
|
-
const curCol = widthOf(this.#chars.slice(0, this.#cursor));
|
|
427
|
-
const scrolledW = widthOf(this.#chars.slice(0, this.#scroll));
|
|
428
|
-
if (curCol < scrolledW) {
|
|
429
|
-
this.#scroll = this.#indexAtWidth(curCol);
|
|
430
|
-
}
|
|
431
|
-
else if (curCol >= scrolledW + maxW) {
|
|
432
|
-
this.#scroll = this.#indexAtWidth(Math.max(0, curCol - maxW + 1));
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
#indexAtWidth(target) {
|
|
436
|
-
let w = 0;
|
|
437
|
-
for (let i = 0; i < this.#chars.length; i += 1) {
|
|
438
|
-
if (w >= target)
|
|
439
|
-
return i;
|
|
440
|
-
w += charWidth(this.#chars[i]);
|
|
441
|
-
}
|
|
442
|
-
return this.#chars.length;
|
|
443
|
-
}
|
|
444
|
-
}
|
package/dist/render.d.ts
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Event rendering for the terminal. Pure (testable): given events, produce
|
|
3
|
-
* the lines a human sees. Colors are raw ANSI — no dependencies.
|
|
4
|
-
*/
|
|
5
|
-
import type { Event } from "@vincemakes/kiso-core";
|
|
6
|
-
import { canonicalTargetPath } from "@vincemakes/kiso-tools-node";
|
|
7
|
-
/**
|
|
8
|
-
* v2a — the palette, centralized (no hard-coded codes elsewhere): ONE
|
|
9
|
-
* accent — blue, ANSI 256 color 75 — for the identity accents (the you>
|
|
10
|
-
* prompt, the banner tagline, ✓ marks, slash-command names); red for
|
|
11
|
-
* errors; dim for metadata. NO_COLOR set, or a non-TTY output → every
|
|
12
|
-
* code is empty, so pipes and CI carry ZERO ANSI (the existing byte-level
|
|
13
|
-
* e2e assertions guard it). Everything not listed here is plain.
|
|
14
|
-
* v3: `bg` — the user-message block background (SGR 48, dark gray 237).
|
|
15
|
-
*/
|
|
16
|
-
export interface Palette {
|
|
17
|
-
readonly blue: string;
|
|
18
|
-
readonly dim: string;
|
|
19
|
-
readonly red: string;
|
|
20
|
-
readonly green: string;
|
|
21
|
-
readonly bg: string;
|
|
22
|
-
readonly reset: string;
|
|
23
|
-
}
|
|
24
|
-
export declare const COLOR_ON: Palette;
|
|
25
|
-
export declare const COLOR_OFF: Palette;
|
|
26
|
-
export declare function palette(): Palette;
|
|
27
|
-
/**
|
|
28
|
-
* E 组/八: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
29
|
-
* reaches the terminal — ESC, C0 (except \t \n), C1, CR, backspace, and
|
|
30
|
-
* bidi overrides. The kiso colors are applied by render, not by the data.
|
|
31
|
-
* EVERY externally-sourced string must pass through this before any output.
|
|
32
|
-
*/
|
|
33
|
-
export declare function escapeTerminal(text: string): string;
|
|
34
|
-
/**
|
|
35
|
-
* 八/十: the path the human is asked to approve is the CANONICAL one the
|
|
36
|
-
* tool will actually touch — the tools' OWN resolution (deepest existing
|
|
37
|
-
* ancestor realpath'd, the not-yet-existing tail re-appended), so a file
|
|
38
|
-
* to be created under a symlinked directory shows the REAL target, and
|
|
39
|
-
* the UI and the tool share ONE resolution (canonicalTargetPath).
|
|
40
|
-
*/
|
|
41
|
-
export declare const canonicalPath: typeof canonicalTargetPath;
|
|
42
|
-
export interface RenderResult {
|
|
43
|
-
readonly text: string;
|
|
44
|
-
readonly newline: boolean;
|
|
45
|
-
readonly prompt: boolean;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Render one event. `text` may be a continuation (text_delta appends to the
|
|
49
|
-
* current line); `newline` says whether the line is complete.
|
|
50
|
-
*
|
|
51
|
-
* 自举 P1: `prevThinking` marks a thinking delta that continues the SAME
|
|
52
|
-
* block — it renders appended to the segment, without the … prefix. The
|
|
53
|
-
* consumer closes the segment with a newline at the next non-thinking
|
|
54
|
-
* event.
|
|
55
|
-
*/
|
|
56
|
-
/**
|
|
57
|
-
* v2b — one thinking BLOCK folds to ONE dim line: the first 100 chars, a
|
|
58
|
-
* " (… /think shows full)" marker when the block is longer. The consumer
|
|
59
|
-
* buffers the block's deltas, renders this at the block's end, and keeps
|
|
60
|
-
* the full text for /think. Pipes get the same fold — the content
|
|
61
|
-
* strategy is presentation-independent.
|
|
62
|
-
*/
|
|
63
|
-
export declare function foldThinking(block: string): string;
|
|
64
|
-
/** v2b — the [result] echo truncates at 160 chars + a /last hint. */
|
|
65
|
-
export declare function foldResult(content: string): string;
|
|
66
|
-
export declare function renderEvent(ev: Event, prevThinking?: boolean): RenderResult;
|
|
67
|
-
/**
|
|
68
|
-
* B 区: one-line summary of a completed tool call, e.g.
|
|
69
|
-
* ✓ edit src/foo.ts (+12 -3) ✓ read src/bar.ts (140 lines)
|
|
70
|
-
* ✗ shell npm test (exit 1)
|
|
71
|
-
* edit/write show +/- line counts, read shows lines, shell shows the exit
|
|
72
|
-
* code; failures (isError) are ✗. Pure and deterministic.
|
|
73
|
-
*/
|
|
74
|
-
export declare function renderToolSummary(name: string, input: Record<string, unknown>, result: {
|
|
75
|
-
content: string;
|
|
76
|
-
isError: boolean;
|
|
77
|
-
}): string;
|
|
78
|
-
/** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
|
|
79
|
-
export declare function kUnit(value: number | null): string;
|
|
80
|
-
/** B 区: usage data gathered from the run's usage events. */
|
|
81
|
-
export interface RunUsage {
|
|
82
|
-
readonly in: number | null;
|
|
83
|
-
readonly out: number | null;
|
|
84
|
-
readonly cache: number | null;
|
|
85
|
-
readonly known: boolean;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* B 区/v2a: the one-line status bar after a terminal, e.g.
|
|
89
|
-
* [turn 3 · in 12.4k out 1.8k · cache 9.2k · ctx ~14%]
|
|
90
|
-
* 降噪: unknown fields are OMITTED ENTIRELY (有什么显什么); a fully unknown
|
|
91
|
-
* usage → null (the caller prints nothing); faux mode → [turn N · faux].
|
|
92
|
-
* All data comes from usage events; ctx is the approximate estimate
|
|
93
|
-
* passed in (chars/4 vs the window), marked with ~.
|
|
94
|
-
*/
|
|
95
|
-
export declare function renderStatusLine(turn: number, usage: RunUsage, ctxRatio: number, faux?: boolean): string | null;
|
|
96
|
-
/**
|
|
97
|
-
* v2a rhythm — the exact bytes after a terminal event: the status line
|
|
98
|
-
* hugs the terminal (有什么显什么 — omitted when there is nothing to show),
|
|
99
|
-
* then EXACTLY one blank line before the next prompt. The consumer prints
|
|
100
|
-
* this verbatim; the render tests pin the sequence.
|
|
101
|
-
*/
|
|
102
|
-
export declare function renderTerminalGap(statusLine: string | null): string;
|
|
103
|
-
/**
|
|
104
|
-
* v3 §01 — the banner, block-split. The logo is three INDEPENDENT rows
|
|
105
|
-
* (TOP / the tagline / BOTTOM), then TWO info rows (version,
|
|
106
|
-
* extensions). Every row truncates at the terminal width with a " (+N)"
|
|
107
|
-
* marker (N = the hidden display width); a window narrower than 40
|
|
108
|
-
* columns skips the logo entirely — only the info rows. Pure.
|
|
109
|
-
*/
|
|
110
|
-
export declare const TAGLINE = "the coding agent that survives kill -9";
|
|
111
|
-
/** v3 §01: truncate a row at `width`, marking the hidden span " (+N)". */
|
|
112
|
-
export declare function truncateRow(row: string, width: number): string;
|
|
113
|
-
/** v3 §01: the banner lines for a width W — logo (skipped under 40
|
|
114
|
-
* columns) + version + extensions. */
|
|
115
|
-
export declare function bannerLines(W: number, version: string, extensionsText: string): string[];
|
|
116
|
-
/** v3 §02 — the recap line that ends a run, replacing the "done" label +
|
|
117
|
-
* the old status line. All fields derive LOCALLY from the event stream
|
|
118
|
-
* (zero tokens): wall seconds, tool counts, usage, cache hit %, ctx left.
|
|
119
|
-
*/
|
|
120
|
-
export interface RecapStats {
|
|
121
|
-
readonly seconds: number;
|
|
122
|
-
readonly tools: number;
|
|
123
|
-
readonly edits: number;
|
|
124
|
-
readonly usage: RunUsage;
|
|
125
|
-
readonly ctxLeftPct: number | null;
|
|
126
|
-
}
|
|
127
|
-
export declare function renderRecap(s: RecapStats): string;
|
|
128
|
-
/** One-line summary of a session, for `kiso sessions`. */
|
|
129
|
-
export declare function renderSessionLine(meta: {
|
|
130
|
-
id: string;
|
|
131
|
-
title: string;
|
|
132
|
-
events: number;
|
|
133
|
-
runs: number;
|
|
134
|
-
updatedAt: number;
|
|
135
|
-
}): string;
|