@vincemakes/kiso-tui 0.40.0 → 0.40.1

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.
@@ -235,6 +235,10 @@ export declare class Body {
235
235
  count: number;
236
236
  first: number;
237
237
  } | null;
238
+ visiblePickWindow(): {
239
+ first: number;
240
+ size: number;
241
+ } | null;
238
242
  /** SIGWINCH: clear the OLD live area (recorded geometry, ED only —
239
243
  * zero LF, zero \x1b[3J — the shell history untouched), then the
240
244
  * full-redraw path at the NEW geometry (O(height), zero replay).
@@ -395,6 +399,12 @@ export declare class Dock {
395
399
  count: number;
396
400
  first: number;
397
401
  } | null;
402
+ /** B (review of this round): the pick panel's window for the frame last
403
+ * drawn — what the digit keys must name. */
404
+ visiblePickWindow(): {
405
+ first: number;
406
+ size: number;
407
+ } | null;
398
408
  setStatus(text: string, hint?: string | null): void;
399
409
  setTail(tail: string): void;
400
410
  /** W21: bind the editor's panel state — the PanelSelect slot
@@ -52,6 +52,7 @@ import { leadWidth } from "./width.js"; // W23: the ONE width authority (the edi
52
52
  // for four reads, so an ask can never render half as an approval.
53
53
  import { panelFrameOf, panelLeadOf, panelStatusOf } from "./ask-panel.js";
54
54
  import { MOUSE_OFF } from "./editor.js";
55
+ import { pickWindowOf } from "./approval-panel.js";
55
56
  import { atPanelRows, bandHeader } from "./at-picker.js";
56
57
  // TUI2-R2 ②: the session picker's rows — the band's third occupant.
57
58
  import { sessionPickerRows } from "./session-picker.js";
@@ -1241,6 +1242,13 @@ export class Body {
1241
1242
  panelOptionRows() {
1242
1243
  return this.#panelRowSpan;
1243
1244
  }
1245
+ /** B (review of this round): the pick panel's window for the frame last
1246
+ * drawn — the renderer's own value, not a second derivation. `null` when
1247
+ * no pick panel was on screen. */
1248
+ #pickWin = null;
1249
+ visiblePickWindow() {
1250
+ return this.#pickWin;
1251
+ }
1244
1252
  /** SIGWINCH: clear the OLD live area (recorded geometry, ED only —
1245
1253
  * zero LF, zero \x1b[3J — the shell history untouched), then the
1246
1254
  * full-redraw path at the NEW geometry (O(height), zero replay).
@@ -1853,6 +1861,7 @@ export class Body {
1853
1861
  // at when they clicked, which is this one.
1854
1862
  this.#panelRowSpan =
1855
1863
  live.panelSpan === null ? null : { top: liveTop + live.panelSpan.offset, count: live.panelSpan.count, first: live.panelSpan.first };
1864
+ this.#pickWin = live.pickWin ?? null;
1856
1865
  // 5. the frame bytes.
1857
1866
  this.#paint(W, H, liveTop, liveLines, liveRowsTotal, chrome);
1858
1867
  }
@@ -1953,7 +1962,11 @@ export class Body {
1953
1962
  // fires on it. W22: the queue band sits below the panel — the
1954
1963
  // cap shrinks by it.
1955
1964
  const frame = panelFrameOf(panel, W, capped);
1956
- return { lines: frame.rows, panelSpan: frame.options };
1965
+ // B (review of this round): the pick window THIS frame draws. The size
1966
+ // depends on the budget, so the renderer is the only producer — the
1967
+ // digit keys are handed this value instead of deriving a second one.
1968
+ const pick = panel.pick ?? null;
1969
+ return { lines: frame.rows, panelSpan: frame.options, pickWin: pick === null ? null : pickWindowOf(panel.view, pick.cursor, pick.phase, capped) };
1957
1970
  }
1958
1971
  return { lines: this.#liveProjection(W, ctx, cap), panelSpan: null };
1959
1972
  }
@@ -2869,6 +2882,11 @@ export class Dock {
2869
2882
  panelOptionRows() {
2870
2883
  return compositorRef?.panelOptionRows() ?? null;
2871
2884
  }
2885
+ /** B (review of this round): the pick panel's window for the frame last
2886
+ * drawn — what the digit keys must name. */
2887
+ visiblePickWindow() {
2888
+ return compositorRef?.visiblePickWindow() ?? null;
2889
+ }
2872
2890
  setStatus(text, hint) {
2873
2891
  compositorRef?.setStatus(text, hint ?? null);
2874
2892
  }
package/dist/editor.d.ts CHANGED
@@ -27,13 +27,20 @@ import { type PanelState, type PanelVerdict, type PanelView, type SaferAnswer }
27
27
  import { type AtItem, type AtMatch } from "./at-picker.js";
28
28
  import { type SessionCardView, type SessionPickState } from "./session-picker.js";
29
29
  /**
30
- * TUI2-R3v2 ② — the mouse-mode bytes, stated once.
30
+ * TUI2-R3v2 ②, corrected by the owner's ruling of 2026-09-21 (finding
31
+ * DC-56) — the mouse-mode bytes, stated once.
31
32
  *
32
33
  * ?1000 is the button-event report and ?1006 is the SGR encoding that
33
34
  * makes it parseable past column 95 (the legacy X10 encoding packs the
34
- * coordinate into one byte and simply breaks on a wide terminal). Both
35
- * go on together and come off together; a terminal left with either one
36
- * set is a terminal that prints escape bytes at the shell prompt.
35
+ * coordinate into one byte and simply breaks on a wide terminal). kiso
36
+ * still RESETS them — on entry (a previous process may have died with
37
+ * reporting left on) and on exit — but it no longer ENABLES them, because
38
+ * a terminal that is reporting stops scrolling its own scrollback: a
39
+ * surface that takes the mouse takes the history with it. kiso's
40
+ * transcript lives in that scrollback by design, and the first version of
41
+ * this feature locked it exactly where a person most wants to read back —
42
+ * under a blocking question. The history wins: no surface claims the
43
+ * mouse, and every surface keeps its keys.
37
44
  */
38
45
  /**
39
46
  * R5 — the viewer's key table, as a pure function of the input chunk.
@@ -45,7 +52,6 @@ import { type SessionCardView, type SessionPickState } from "./session-picker.js
45
52
  * different route.
46
53
  */
47
54
  export declare function viewerCommand(text: string): "up" | "down" | "toggle" | "all" | "pageUp" | "pageDown" | "home" | "end" | "close" | null;
48
- export declare const MOUSE_ON = "\u001B[?1000h\u001B[?1006h";
49
55
  export declare const MOUSE_OFF = "\u001B[?1000l\u001B[?1006l";
50
56
  export declare const PROMPT = "\u258C ";
51
57
  export declare const PROMPT_WIDTH: number;
@@ -221,6 +227,12 @@ export declare class Editor {
221
227
  count: number;
222
228
  first?: number;
223
229
  } | null) | null): void;
230
+ /** B: the pick window the last frame drew — the Dock's value, not the
231
+ * editor's guess. Same pull-on-demand shape as `bindPanelRows`. */
232
+ bindPickWindow(fn: (() => {
233
+ first: number;
234
+ size: number;
235
+ } | null) | null): void;
224
236
  /** R3a — cross-session input history: seed the recall buffer and
225
237
  * register the append sink. The cap and the adjacent-duplicate
226
238
  * collapse are unchanged; the seed takes the TAIL of what the CLI
package/dist/editor.js CHANGED
@@ -46,13 +46,20 @@ import { PickInput } from "./pick-input.js";
46
46
  // "you>" text is gone (the brick IS the prompt; the pipe path's readline
47
47
  // prompt keeps its own "you> " — v2a line mode, byte-for-byte).
48
48
  /**
49
- * TUI2-R3v2 ② — the mouse-mode bytes, stated once.
49
+ * TUI2-R3v2 ②, corrected by the owner's ruling of 2026-09-21 (finding
50
+ * DC-56) — the mouse-mode bytes, stated once.
50
51
  *
51
52
  * ?1000 is the button-event report and ?1006 is the SGR encoding that
52
53
  * makes it parseable past column 95 (the legacy X10 encoding packs the
53
- * coordinate into one byte and simply breaks on a wide terminal). Both
54
- * go on together and come off together; a terminal left with either one
55
- * set is a terminal that prints escape bytes at the shell prompt.
54
+ * coordinate into one byte and simply breaks on a wide terminal). kiso
55
+ * still RESETS them — on entry (a previous process may have died with
56
+ * reporting left on) and on exit — but it no longer ENABLES them, because
57
+ * a terminal that is reporting stops scrolling its own scrollback: a
58
+ * surface that takes the mouse takes the history with it. kiso's
59
+ * transcript lives in that scrollback by design, and the first version of
60
+ * this feature locked it exactly where a person most wants to read back —
61
+ * under a blocking question. The history wins: no surface claims the
62
+ * mouse, and every surface keeps its keys.
56
63
  */
57
64
  /**
58
65
  * R5 — the viewer's key table, as a pure function of the input chunk.
@@ -105,7 +112,6 @@ const ALT_WORD = new Map([
105
112
  ["\x7f", "killBack"],
106
113
  ["\x08", "killBack"],
107
114
  ]);
108
- export const MOUSE_ON = "\x1b[?1000h\x1b[?1006h";
109
115
  export const MOUSE_OFF = "\x1b[?1000l\x1b[?1006l";
110
116
  export const PROMPT = "▌ ";
111
117
  export const PROMPT_WIDTH = displayWidth(PROMPT);
@@ -314,12 +320,16 @@ export class Editor {
314
320
  /** TUI2-R3v2 ①: one-shot — a panel that just closed swallows the
315
321
  * habitual trailing enter rather than submitting the restored draft. */
316
322
  #swallowEnter = false;
317
- /** TUI2-R3v2 ②: whether SGR 1006 reporting is currently enabled. */
318
- #mouseOn = false;
319
323
  /** TUI2-R3v2 ②: where the compositor put the panel's option rows this
320
324
  * frame (absolute 1-based screen rows). The editor owns no geometry —
321
325
  * it asks the surface that placed them. */
322
326
  #panelRows = null;
327
+ /** B (review of this round): the pick panel's window, published by the
328
+ * surface that DREW it — the frame's budget decides the size, so the
329
+ * editor PULLS it here instead of letting the input layer re-derive one
330
+ * (two derivations are two sizes, and on a short terminal the digits named
331
+ * rows nobody could see). */
332
+ #pickWin = null;
323
333
  #lineCb = null;
324
334
  #pendingLines = []; // submits before onLine is wired (startup) — never dropped
325
335
  #sigintCb = null;
@@ -525,7 +535,6 @@ export class Editor {
525
535
  // the human has no way to ask for it back.
526
536
  process.stdin.setRawMode(true);
527
537
  process.stdout.write(MOUSE_OFF);
528
- this.#mouseOn = false;
529
538
  process.stdout.write("\x1b[?2004h");
530
539
  process.stdin.on("data", this.#onData);
531
540
  }
@@ -1038,7 +1047,6 @@ export class Editor {
1038
1047
  // chance this process gets, and emitting six harmless bytes twice
1039
1048
  // is not a cost worth reasoning about.
1040
1049
  process.stdout.write(MOUSE_OFF);
1041
- this.#mouseOn = false;
1042
1050
  process.stdout.write("\x1b[?2004l"); // bracketed paste OFF
1043
1051
  // REL-0161: the hardware cursor was hidden for the session's whole
1044
1052
  // life (the compositor's entry reset); this is the one place kiso
@@ -1050,30 +1058,20 @@ export class Editor {
1050
1058
  this.#closedResolve();
1051
1059
  }
1052
1060
  /**
1053
- * TUI2-R3v2 ② — mouse reporting follows the SELECTION SURFACES and
1054
- * nothing else.
1061
+ * TUI2-R3v2 ②, corrected by the owner's ruling of 2026-09-21 (finding
1062
+ * DC-56) — the bands still tell their host when a selection surface
1063
+ * opens or closes, and the host's answer is now "leave the terminal
1064
+ * alone".
1055
1065
  *
1056
- * While it is on, the terminal's own text selection changes behaviour
1057
- * (shift+drag still selects on every terminal that matters, but plain
1058
- * drag-to-copy does not), so leaving it on for the whole session would
1059
- * tax every copy-paste in the product to pay for a gesture that only
1060
- * means something while a list is up. It goes on when one opens and
1061
- * off when it closes — and both calls are idempotent, because the
1062
- * surfaces nest (a panel can open over a picker) and the bytes must
1063
- * not depend on the order they unwind in.
1066
+ * The bands' contract is unchanged — a band reports that it has taken
1067
+ * the composer — but the mouse is not a thing kiso hands out: reporting
1068
+ * on means the terminal stops scrolling its own scrollback, and every
1069
+ * band kiso has (the approval/ask/pick panel, the session picker, the @
1070
+ * picker) is a keyboard surface — digits, arrows, enter, esc, a filter.
1071
+ * The call sites stay so a future surface that DOES want the mouse has
1072
+ * one place to ask, and the ruling is written at that place.
1064
1073
  */
1065
- #setMouse(on) {
1066
- if (this.#mouseOn === on)
1067
- return;
1068
- this.#mouseOn = on;
1069
- if (this.#entered)
1070
- process.stdout.write(on ? MOUSE_ON : MOUSE_OFF);
1071
- }
1072
- /** The surfaces that own a selection — the approval/ask/pick panel, the
1073
- * session picker and the @ picker. Any one of them up = reporting on. */
1074
- #syncMouse() {
1075
- this.#setMouse(this.#panelInput.up() || this.#pickInput.up() || this.#atUp());
1076
- }
1074
+ #syncMouse() { }
1077
1075
  /** OR-11 (a) — the lead the composer's row is DRAWN with, which is not
1078
1076
  * always the brick. The CLI binds the compositor's input lead as `""`
1079
1077
  * (a prompt character is a third thing saying "input lives here", and
@@ -1751,6 +1749,13 @@ export class Editor {
1751
1749
  * and context-menu everywhere else and would mean "approve" here.
1752
1750
  * The stakes are a side effect the human did not ask for, and an
1753
1751
  * ambiguous mouse event is not consent.
1752
+ *
1753
+ * 2026-09-21 (finding DC-56): kiso no longer ENABLES reporting, so on a
1754
+ * terminal this path is reached only when something else left the mouse
1755
+ * on — and then a click still answers the panel. The wheel stays
1756
+ * dropped either way (a scroll past a panel is not a choice), which is
1757
+ * the other half of why the settled rule is "no surface takes the
1758
+ * mouse": a terminal that is reporting cannot scroll its own history.
1754
1759
  */
1755
1760
  #mouseEvent(params, press) {
1756
1761
  if (!press)
@@ -1771,6 +1776,11 @@ export class Editor {
1771
1776
  bindPanelRows(fn) {
1772
1777
  this.#panelRows = fn;
1773
1778
  }
1779
+ /** B: the pick window the last frame drew — the Dock's value, not the
1780
+ * editor's guess. Same pull-on-demand shape as `bindPanelRows`. */
1781
+ bindPickWindow(fn) {
1782
+ this.#pickWin = fn;
1783
+ }
1774
1784
  /** TMUX-F1 ②: `run` identical arrow sequences arrived in ONE read. Every
1775
1785
  * surface but one gets every press — a wheel over the transcript viewer,
1776
1786
  * a panel, a multi-row draft scrolls, as a wheel should. The HISTORY
@@ -2000,6 +2010,7 @@ export class Editor {
2000
2010
  reflow: () => this.#reflow(),
2001
2011
  render: () => this.#onRender(),
2002
2012
  syncMouse: () => this.#syncMouse(),
2013
+ pickWindow: () => this.#pickWin?.() ?? null,
2003
2014
  closeBands: () => {
2004
2015
  this.#menuOpen = false;
2005
2016
  this.#menuSel = 0;
@@ -32,6 +32,14 @@ export interface BandHost {
32
32
  line(): string;
33
33
  /** The buffer with its paste capsules expanded — the text that would leave the editor. */
34
34
  expandPastes(line: string): string;
35
+ /** The pick panel's window as the RENDERER drew it (review of this round):
36
+ * the size depends on the frame's budget, so the digit keys must be handed
37
+ * the value instead of deriving it again from PICK_MAX. Absent → the input
38
+ * layer falls back to its own derivation. */
39
+ pickWindow?(): {
40
+ first: number;
41
+ size: number;
42
+ } | null;
35
43
  /** Empty the buffer: chars, cursor and the ↑↓ goal column. */
36
44
  clear(): void;
37
45
  /** Type one code point at the cursor. */
@@ -16,7 +16,7 @@
16
16
  * and the mouse hit. The bodies are the same; only the buffer access
17
17
  * goes through the host.
18
18
  */
19
- import { PICK_MAX, panelOptions, saferDegradedNote, startLevel, stepLevel, } from "./approval-panel.js";
19
+ import { PICK_MAX, panelOptions, pickWindow, saferDegradedNote, startLevel, stepLevel, } from "./approval-panel.js";
20
20
  import { askCommitCustom, askKey, askOnCustomRow, askStart } from "./ask-panel.js";
21
21
  export class PanelInput {
22
22
  host;
@@ -206,7 +206,11 @@ export class PanelInput {
206
206
  if (panel === null)
207
207
  return false;
208
208
  if (panel.pick !== null && panel.pick.phase === "options") {
209
- const n = Math.min(panel.view.pick.options.length, PICK_MAX);
209
+ // DC-58: EVERY option is reachable by ↑↓ — the window follows the
210
+ // cursor (`pickWindow`) and PICK_MAX is only how many rows one
211
+ // screen holds. This bound used to be `min(count, PICK_MAX)`, which
212
+ // is what left a 60-profile config with nine reachable rows.
213
+ const n = panel.view.pick.options.length;
210
214
  const cur = panel.pick.cursor;
211
215
  const next = dir === "up" ? Math.max(0, cur - 1) : Math.min(Math.max(0, n - 1), cur + 1);
212
216
  // OR-7: the second axis belongs to the highlighted option, so it
@@ -448,9 +452,18 @@ export class PanelInput {
448
452
  const panel = this.#panel;
449
453
  if (panel === null || panel.pick === null)
450
454
  return;
451
- if (index < 0 || index >= Math.min(panel.view.pick.options.length, PICK_MAX))
455
+ const count = panel.view.pick.options.length;
456
+ // DC-58: a digit names the row ON SCREEN. The window's origin is what
457
+ // turns a visible position into an option index — the same
458
+ // `pickWindow` the renderer drew the rows with, so what the row says
459
+ // and what the key does cannot drift apart.
460
+ const { first, size } = this.host.pickWindow?.() ?? pickWindow(panel.pick.cursor, count, PICK_MAX);
461
+ if (index < 0 || index >= size)
452
462
  return;
453
- panel.pick = { cursor: index, phase: "options", level: startLevel(panel.view.pick.options[index]) };
463
+ const target = first + index;
464
+ if (target >= count)
465
+ return;
466
+ panel.pick = { cursor: target, phase: "options", level: startLevel(panel.view.pick.options[target]) };
454
467
  this.host.render();
455
468
  }
456
469
  #pickPanelEnter() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.40.0",
3
+ "version": "0.40.1",
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.40.0"
38
+ "@vincemakes/kiso-tui-cells": "0.40.1"
39
39
  }
40
40
  }