@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/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-3 = the scroll region (the body
8
- * streams and scrolls here, never touching the bottom), row H-2 = the dim
9
- * dotted separator (╌), row H-1 = the live status bar (or a takeover
10
- * question), row H = the input line (the blue brick ▌you> + the v2c
11
- * editor's row readline is gone from the TTY path). Bottom redraws are
12
- * wrapped in CSI 2026 (synchronized output) to avoid flicker the pi
13
- * trick. The visual identity is the kiso brick motif half-block,
14
- * dotted separator deliberately NOT the CC rounded frame nor the pi
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: DECSTBM the scroll region, draw the chrome. A
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 - 2; row <= H; row += 1) {
76
- process.stdout.write(`\x1b[${row};1H\x1b[0K`); // clear the three rows
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 region, redraw the chrome. */
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
- /** Body output: position the cursor inside the scroll region at the
90
- * body's tracked position, write, and hand the cursor back to the
91
- * input line's EDIT position. The row/col tracking is approximate for
92
- * width-wrapped and wide-char lines (documented) the region clamp
93
- * keeps the bottom rows safe regardless.
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 three rows, wrapped in CSI 2026 (synchronized output —
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. v2c: the separator is the dim dotted (a weaker
158
- * presence than the solid ─), the status line is dim (blue accents
159
- * inside come from the CLI's composition), the input row is the blue
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
- out.push(`\x1b[${H - 2};1H\x1b[0K${sep}`);
177
- out.push(`\x1b[${H - 1};1H\x1b[0K${statusLine}`);
178
- out.push(`\x1b[${H};1H\x1b[0K${this.#inputPrompt}${inp.line}`);
179
- out.push(`\x1b[${H};${this.#inputCol()}H`); // back to the edit position
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.#onRender();
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.#onRender();
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.#onRender();
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
- const line = String.fromCodePoint(...this.#chars);
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) {