@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/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;