@vincemakes/kiso-code 0.1.15 → 0.1.17
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/body.d.ts +4 -1
- package/dist/body.js +102 -32
- package/dist/diff.d.ts +32 -0
- package/dist/diff.js +122 -0
- package/dist/dock.d.ts +27 -28
- package/dist/dock.js +47 -54
- package/dist/editor.d.ts +11 -0
- package/dist/editor.js +70 -4
- package/dist/index.js +196 -57
- package/dist/mode.d.ts +33 -0
- package/dist/mode.js +93 -0
- package/dist/render.d.ts +30 -0
- package/dist/render.js +64 -4
- package/package.json +7 -7
package/dist/dock.js
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* implementation: zero dependencies, line-level ANSI, no differential
|
|
5
5
|
* renderer.
|
|
6
6
|
*
|
|
7
|
-
* Layout (H = terminal height): rows 1..H-
|
|
8
|
-
* streams and scrolls here, never touching the bottom), row H-
|
|
9
|
-
* dotted separator (╌), row H-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
15
17
|
* editor (ADR-0039 Amendment 2).
|
|
16
18
|
*
|
|
17
19
|
* Pipes / NO_COLOR: the dock never activates; the v2a line mode stays
|
|
@@ -41,7 +43,11 @@ export class Dock {
|
|
|
41
43
|
this.#inputState = state;
|
|
42
44
|
this.#inputPrompt = prompt;
|
|
43
45
|
}
|
|
44
|
-
/** Enter docked mode:
|
|
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
|
|
45
51
|
* TTY without a real window size (rows < 4) stays in the v2a line
|
|
46
52
|
* mode — the bottom three rows need room to exist. */
|
|
47
53
|
enter() {
|
|
@@ -53,7 +59,6 @@ export class Dock {
|
|
|
53
59
|
this.#width = process.stdout.columns ?? 80;
|
|
54
60
|
this.#bodyRow = 1;
|
|
55
61
|
this.#bodyCol = 1;
|
|
56
|
-
process.stdout.write(`\x1b[1;${this.#height - 3}r`); // scroll region: top .. H-3
|
|
57
62
|
this.redraw();
|
|
58
63
|
this.#resizeHandler = () => this.onResize();
|
|
59
64
|
process.stdout.on("resize", this.#resizeHandler);
|
|
@@ -72,51 +77,25 @@ export class Dock {
|
|
|
72
77
|
}
|
|
73
78
|
const H = this.#height;
|
|
74
79
|
process.stdout.write("\x1b[r"); // reset the scroll region
|
|
75
|
-
for (let row = H -
|
|
76
|
-
process.stdout.write(`\x1b[${row};1H\x1b[0K`); // clear the
|
|
80
|
+
for (let row = H - 3; row <= H; row += 1) {
|
|
81
|
+
process.stdout.write(`\x1b[${row};1H\x1b[0K`); // clear the four rows
|
|
77
82
|
}
|
|
78
83
|
process.stdout.write(`\x1b[${H};1H`);
|
|
79
84
|
}
|
|
80
|
-
/** SIGWINCH: recompute the
|
|
85
|
+
/** SIGWINCH: recompute the size, redraw the chrome. */
|
|
81
86
|
onResize() {
|
|
82
87
|
if (!this.#active)
|
|
83
88
|
return;
|
|
84
89
|
this.#height = process.stdout.rows ?? this.#height;
|
|
85
90
|
this.#width = process.stdout.columns ?? this.#width;
|
|
86
|
-
process.stdout.write(`\x1b[1;${this.#height - 3}r`);
|
|
87
91
|
this.redraw();
|
|
88
92
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* The edit-position return is a correctness requirement, not
|
|
96
|
-
* cosmetics: readline tracks its cursor internally and NEVER
|
|
97
|
-
* re-syncs after an external move — a body write that left the
|
|
98
|
-
* cursor at column 1 made the next keystroke overwrite the prompt
|
|
99
|
-
* (probe-confirmed; the dock's redraw self-repaired ~200ms later,
|
|
100
|
-
* which read the user as cursor drift). */
|
|
101
|
-
writeBody(text) {
|
|
102
|
-
if (!this.#active) {
|
|
103
|
-
process.stdout.write(text);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const row = Math.min(this.#bodyRow, this.#height - 3);
|
|
107
|
-
const col = this.#bodyCol > this.#width ? this.#width : this.#bodyCol;
|
|
108
|
-
process.stdout.write(`\x1b[${row};${col}H`);
|
|
109
|
-
process.stdout.write(text);
|
|
110
|
-
for (const ch of text) {
|
|
111
|
-
if (ch === "\n") {
|
|
112
|
-
this.#bodyRow += 1;
|
|
113
|
-
this.#bodyCol = 1;
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
this.#bodyCol += 1;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
process.stdout.write(`\x1b[${this.#height};${this.#inputCol()}H`); // back to the edit position
|
|
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;
|
|
120
99
|
}
|
|
121
100
|
/** The input line's edit column — prompt width + cursor + 1. The
|
|
122
101
|
* dock's redraw and the body's cursor return both end here, so the
|
|
@@ -152,12 +131,11 @@ export class Dock {
|
|
|
152
131
|
this.#question = null;
|
|
153
132
|
this.redraw();
|
|
154
133
|
}
|
|
155
|
-
/** The bottom
|
|
134
|
+
/** The bottom four rows, wrapped in CSI 2026 (synchronized output —
|
|
156
135
|
* the pi trick against flicker). The cursor ends at the input line's
|
|
157
|
-
* edit position.
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* brick ▌you> + the editor's visible slice. */
|
|
136
|
+
* edit position. v3 §03: the upper ╌ row, the input row, the lower
|
|
137
|
+
* ╌ row, the status row — the status is dim (blue accents inside
|
|
138
|
+
* come from the CLI's composition). */
|
|
161
139
|
redraw() {
|
|
162
140
|
if (!this.#active)
|
|
163
141
|
return;
|
|
@@ -173,10 +151,25 @@ export class Dock {
|
|
|
173
151
|
// \x1b[?2026h/l, the pi source's exact form. Without it terminals
|
|
174
152
|
// silently ignore the mode and the anti-flicker never engages.
|
|
175
153
|
out.push("\x1b[?2026h"); // synchronized output ON (DEC 2026)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
154
|
+
// v3 §04: the slash-command menu — above the chrome, one row per
|
|
155
|
+
// filtered command, the selection highlighted. Drawn first so the
|
|
156
|
+
// chrome rows repaint on top of any overlap.
|
|
157
|
+
const menu = this.#menuState?.();
|
|
158
|
+
if (menu !== null && menu !== undefined) {
|
|
159
|
+
for (let i = 0; i < menu.items.length; i += 1) {
|
|
160
|
+
const item = menu.items[i];
|
|
161
|
+
const row = H - 4 - (menu.items.length - 1 - i);
|
|
162
|
+
const text = i === menu.selected
|
|
163
|
+
? `${p.blue}▸ ${item.name}${p.reset} ${item.desc}`
|
|
164
|
+
: `${p.dim} ${item.name} ${item.desc}${p.reset}`;
|
|
165
|
+
out.push(`\x1b[${row};1H\x1b[0K${text}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
out.push(`\x1b[${H - 3};1H\x1b[0K${sep}`);
|
|
169
|
+
out.push(`\x1b[${H - 2};1H\x1b[0K${this.#inputPrompt}${inp.line}`);
|
|
170
|
+
out.push(`\x1b[${H - 1};1H\x1b[0K${sep}`);
|
|
171
|
+
out.push(`\x1b[${H};1H\x1b[0K${statusLine}`);
|
|
172
|
+
out.push(`\x1b[${H - 2};${this.#inputCol()}H`); // back to the edit position
|
|
180
173
|
out.push("\x1b[?2026l"); // synchronized output OFF
|
|
181
174
|
process.stdout.write(out.join(""));
|
|
182
175
|
}
|
package/dist/editor.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export declare function widthOf(chars: readonly number[]): number;
|
|
|
24
24
|
export declare function displayWidth(text: string): number;
|
|
25
25
|
export declare const PROMPT = "\u258Cyou> ";
|
|
26
26
|
export declare const PROMPT_WIDTH: number;
|
|
27
|
+
/** v3 §04 — the slash-command menu's command table (English one-liners). */
|
|
28
|
+
export interface MenuItem {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly desc: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const MENU_ITEMS: readonly MenuItem[];
|
|
27
33
|
/**
|
|
28
34
|
* The editor. Raw mode + bracketed paste (?2004h) on enter, restored on
|
|
29
35
|
* exit. The input row is rendered by `onRender` (the CLI wires it to the
|
|
@@ -48,6 +54,11 @@ export declare class Editor {
|
|
|
48
54
|
line: string;
|
|
49
55
|
cursor: number;
|
|
50
56
|
};
|
|
57
|
+
/** v3 §04: the menu's visible state for the dock — null when closed. */
|
|
58
|
+
menuState(): {
|
|
59
|
+
items: readonly MenuItem[];
|
|
60
|
+
selected: number;
|
|
61
|
+
} | null;
|
|
51
62
|
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
52
63
|
question(_query: string, cb: (answer: string) => void): void;
|
|
53
64
|
/** Cancel a pending question — the buffer stays (its text becomes the
|
package/dist/editor.js
CHANGED
|
@@ -69,6 +69,13 @@ export function displayWidth(text) {
|
|
|
69
69
|
import { palette } from "./render.js";
|
|
70
70
|
export const PROMPT = "▌you> "; // the kiso brick motif: one blue half-block, then you>
|
|
71
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
|
+
];
|
|
72
79
|
/**
|
|
73
80
|
* The editor. Raw mode + bracketed paste (?2004h) on enter, restored on
|
|
74
81
|
* exit. The input row is rendered by `onRender` (the CLI wires it to the
|
|
@@ -88,6 +95,8 @@ export class Editor {
|
|
|
88
95
|
#eotCb = null;
|
|
89
96
|
#escapeCb = null;
|
|
90
97
|
#onRender;
|
|
98
|
+
#menuOpen = false; // v3 §04: the slash-command menu
|
|
99
|
+
#menuSel = 0;
|
|
91
100
|
#pending = ""; // an incomplete ESC/CSI prefix across chunks
|
|
92
101
|
#decoder = new TextDecoder();
|
|
93
102
|
#entered = false;
|
|
@@ -136,6 +145,27 @@ export class Editor {
|
|
|
136
145
|
const col = (this.#scroll > 0 ? 1 : 0) + widthOf(this.#chars.slice(this.#scroll, this.#cursor));
|
|
137
146
|
return { line: `${prefix}${visible}`, cursor: col };
|
|
138
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
|
+
}
|
|
139
169
|
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
140
170
|
question(_query, cb) {
|
|
141
171
|
this.#questionCb = cb;
|
|
@@ -195,6 +225,13 @@ export class Editor {
|
|
|
195
225
|
else if (rest.startsWith("O")) {
|
|
196
226
|
i += 3; // SS3 (function keys) — ignored
|
|
197
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
|
+
}
|
|
198
235
|
else {
|
|
199
236
|
this.#escapeCb?.();
|
|
200
237
|
i += 1;
|
|
@@ -245,6 +282,18 @@ export class Editor {
|
|
|
245
282
|
this.#onRender();
|
|
246
283
|
i += 1;
|
|
247
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
|
+
}
|
|
248
297
|
else if (c !== undefined && c < " ") {
|
|
249
298
|
i += 1; // other control — ignored
|
|
250
299
|
}
|
|
@@ -266,6 +315,15 @@ export class Editor {
|
|
|
266
315
|
this.#onRender();
|
|
267
316
|
}
|
|
268
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
|
+
}
|
|
269
327
|
else if (final === "D") {
|
|
270
328
|
this.#move(-1);
|
|
271
329
|
}
|
|
@@ -287,7 +345,7 @@ export class Editor {
|
|
|
287
345
|
this.#cursor += 1;
|
|
288
346
|
this.#reflow();
|
|
289
347
|
if (!this.#pasting)
|
|
290
|
-
this.#
|
|
348
|
+
this.#refreshMenu();
|
|
291
349
|
}
|
|
292
350
|
#backspace() {
|
|
293
351
|
if (this.#cursor === 0)
|
|
@@ -296,7 +354,7 @@ export class Editor {
|
|
|
296
354
|
this.#cursor -= 1;
|
|
297
355
|
this.#reflow();
|
|
298
356
|
if (!this.#pasting)
|
|
299
|
-
this.#
|
|
357
|
+
this.#refreshMenu();
|
|
300
358
|
}
|
|
301
359
|
#delete() {
|
|
302
360
|
if (this.#cursor >= this.#chars.length)
|
|
@@ -304,7 +362,7 @@ export class Editor {
|
|
|
304
362
|
this.#chars.splice(this.#cursor, 1);
|
|
305
363
|
this.#reflow();
|
|
306
364
|
if (!this.#pasting)
|
|
307
|
-
this.#
|
|
365
|
+
this.#refreshMenu();
|
|
308
366
|
}
|
|
309
367
|
#move(delta) {
|
|
310
368
|
this.#cursor = Math.max(0, Math.min(this.#chars.length, this.#cursor + delta));
|
|
@@ -336,10 +394,18 @@ export class Editor {
|
|
|
336
394
|
this.#reflow();
|
|
337
395
|
}
|
|
338
396
|
#submit() {
|
|
339
|
-
|
|
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
|
+
}
|
|
340
404
|
this.#chars = [];
|
|
341
405
|
this.#cursor = 0;
|
|
342
406
|
this.#scroll = 0;
|
|
407
|
+
this.#menuOpen = false;
|
|
408
|
+
this.#menuSel = 0;
|
|
343
409
|
const cb = this.#questionCb;
|
|
344
410
|
this.#questionCb = null;
|
|
345
411
|
if (cb !== null) {
|