@vincemakes/kiso-tui-cells 0.30.0 → 0.31.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kiso contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @vincemakes/kiso-tui-cells
2
+
3
+ The pure cell renderer under kiso's terminal UI: the components
4
+ (containers, folding, the spinner, the settled row), the diff renderer
5
+ for write_file / edit_file results, display-width measurement, the
6
+ ground resolver (the terminal's background colour from its OSC reply and
7
+ the palette that follows it), and the strings the panels and banners are
8
+ built from. Zero runtime dependencies: input is data, output is bytes;
9
+ no terminal is touched here.
10
+
11
+ `@vincemakes/kiso-tui` composes these into the live screen; the CLI
12
+ (`@vincemakes/kiso-code`) is the consumer. See the repository README for
13
+ the framework overview.
@@ -209,7 +209,39 @@ export interface PickOption {
209
209
  /** the dim qualifier ("profile: ds \u00b7 current") \u2014 what tells two
210
210
  * similar rows apart */
211
211
  readonly note?: string;
212
+ /** OR-7 — the SECOND axis. The model's NATIVE levels in vendor order,
213
+ * from the registry. Absent = this option has no second axis and the
214
+ * row renders exactly as it did before. */
215
+ readonly levels?: readonly string[];
216
+ /** where the level cursor starts. UNDEFINED is a real state, not a
217
+ * missing value: a row whose registry default is null (the page
218
+ * states none) marks nothing, and enter applies the profile alone.
219
+ * The panel never invents a default. */
220
+ readonly level?: number;
221
+ /** indexes forbidden for the current thinking mode. The cursor never
222
+ * rests on one \u2014 see `enabledLevel`. */
223
+ readonly disabled?: readonly number[];
224
+ /** shown after the strip when the cursor did NOT land where the
225
+ * previous selection asked ("effort xhigh \u2192 high: the nearest this
226
+ * model supports"). The caller's sentence, reproduced verbatim. */
227
+ readonly levelNote?: string;
212
228
  }
229
+ /** The level cursor, CORRECTED at read time: never off the end, never on
230
+ * a forbidden index, and never invented where the caller supplied none.
231
+ *
232
+ * One rule instead of two. The alternative \u2014 letting the cursor rest on
233
+ * a forbidden index and refusing at enter \u2014 needs a second mechanism to
234
+ * say why nothing happened, and a key that silently does nothing is the
235
+ * defect this panel already fixed once (DC-36). Correcting on read is
236
+ * the discipline the session picker uses for its own selection. */
237
+ export declare function enabledLevel(o: PickOption | undefined, want: number | null): number | null;
238
+ /** Where the cursor OPENS on an option: what the caller asked for,
239
+ * corrected. The two callers (the panel opening, and the highlight
240
+ * moving to another row) mean exactly this and nothing else. */
241
+ export declare function startLevel(o: PickOption | undefined): number | null;
242
+ /** The next enabled index in `dir`, or the current one at the end of the
243
+ * ladder. Forbidden indexes are stepped OVER, never landed on. */
244
+ export declare function stepLevel(o: PickOption, from: number | null, dir: -1 | 1): number | null;
213
245
  /** The whole pick: the header sentence, the options, the free-text
214
246
  * escape hatch, and the honest empty state. */
215
247
  export interface PickSpec {
@@ -231,6 +263,9 @@ export interface PickSpec {
231
263
  export interface PickRuntime {
232
264
  readonly cursor: number;
233
265
  readonly phase: "options" | "custom";
266
+ /** OR-7: the level cursor within the highlighted option. null = the
267
+ * option has no levels, or has no default to mark. */
268
+ readonly level: number | null;
234
269
  }
235
270
  /** What was picked: a listed option by INDEX (never a label the caller
236
271
  * would have to re-match against its own list), or typed text. */
@@ -322,10 +357,12 @@ export type PanelVerdict = {
322
357
  }
323
358
  /** TUI2-R2 \u2463: the pick's verdict \u2014 the chosen index or the typed
324
359
  * text. Only pick views ever produce it, so the approval path's
325
- * switch is untouched. */
360
+ * switch is untouched. OR-7 adds the second axis beside it: the
361
+ * level INDEX, absent when the option had no levels or marked none. */
326
362
  | {
327
363
  readonly action: "picked";
328
364
  readonly result: PickResult;
365
+ readonly level?: number;
329
366
  };
330
367
  /** The bound panel state the compositor reads — the editor owns the
331
368
  * phase/selection state machine and the key routing; the compositor
@@ -432,7 +469,7 @@ export declare function pickLead(view: PanelView, state: PickRuntime): string;
432
469
  /** The status row's left text \u2014 the CALLER's, because only the caller
433
470
  * knows whether a run is paused behind this panel. */
434
471
  export declare function pickStatus(view: PanelView): string;
435
- export declare function pickAffordance(state: PickRuntime): string;
472
+ export declare function pickAffordance(state: PickRuntime, axis?: boolean): string;
436
473
  /** Compose a pick view. The flavor/name/title/args fields exist for the
437
474
  * approval path and are given inert values here \u2014 the pick block
438
475
  * reads none of them. */
@@ -164,6 +164,58 @@ function segmentRisk(segment) {
164
164
  }
165
165
  return null;
166
166
  }
167
+ /** The level cursor, CORRECTED at read time: never off the end, never on
168
+ * a forbidden index, and never invented where the caller supplied none.
169
+ *
170
+ * One rule instead of two. The alternative \u2014 letting the cursor rest on
171
+ * a forbidden index and refusing at enter \u2014 needs a second mechanism to
172
+ * say why nothing happened, and a key that silently does nothing is the
173
+ * defect this panel already fixed once (DC-36). Correcting on read is
174
+ * the discipline the session picker uses for its own selection. */
175
+ export function enabledLevel(o, want) {
176
+ const levels = o?.levels;
177
+ if (levels === undefined || levels.length === 0 || want === null)
178
+ return null;
179
+ const off = new Set(o?.disabled ?? []);
180
+ if (off.size >= levels.length)
181
+ return null; // every level forbidden: nothing to point at
182
+ const clamped = Math.max(0, Math.min(levels.length - 1, want));
183
+ if (!off.has(clamped))
184
+ return clamped;
185
+ // walk outward from the asked-for index, nearest first
186
+ for (let d = 1; d < levels.length; d += 1) {
187
+ const hi = clamped + d;
188
+ if (hi < levels.length && !off.has(hi))
189
+ return hi;
190
+ const lo = clamped - d;
191
+ if (lo >= 0 && !off.has(lo))
192
+ return lo;
193
+ }
194
+ return null;
195
+ }
196
+ /** Where the cursor OPENS on an option: what the caller asked for,
197
+ * corrected. The two callers (the panel opening, and the highlight
198
+ * moving to another row) mean exactly this and nothing else. */
199
+ export function startLevel(o) {
200
+ return enabledLevel(o, o?.level ?? null);
201
+ }
202
+ /** The next enabled index in `dir`, or the current one at the end of the
203
+ * ladder. Forbidden indexes are stepped OVER, never landed on. */
204
+ export function stepLevel(o, from, dir) {
205
+ const levels = o.levels;
206
+ if (levels === undefined || levels.length === 0)
207
+ return null;
208
+ // the first press on a row that marks nothing enters the ladder at
209
+ // its near end rather than guessing a middle.
210
+ if (from === null)
211
+ return enabledLevel(o, dir === 1 ? 0 : levels.length - 1);
212
+ const off = new Set(o.disabled ?? []);
213
+ for (let i = from + dir; i >= 0 && i < levels.length; i += dir) {
214
+ if (!off.has(i))
215
+ return i;
216
+ }
217
+ return from;
218
+ }
167
219
  /** The rule line's text — the why-asked line (the R3 chain): the tool
168
220
  * name, the first non-abstain speaker, the fix hint (the §3.5 table,
169
221
  * code-accented). The simple flavor carries the CLI's own question
@@ -516,7 +568,14 @@ export function pickBlockRows(view, state, W, maxRows) {
516
568
  // content were scrolled irreversibly into the scrollback every time
517
569
  // `/model` opened on a tight screen. The `+N more` row is a sixth
518
570
  // when it appears, so it is paid for too.
519
- const chrome = 5 + (spec.options.length > Math.min(Math.max(1, maxRows - 5), PICK_MAX) ? 1 : 0);
571
+ // OR-7: the level strip is a row of its own under the highlighted
572
+ // option, so it is chrome and it is paid for here. Inline after the
573
+ // note was the coordination note's shape and it does not fit: five
574
+ // levels need 42 columns and the note column has 48 at width 100,
575
+ // which it already spends on `profile: <name>`. A second axis that
576
+ // truncates on a normal terminal is not a second axis.
577
+ const strip = spec.options[state.cursor]?.levels !== undefined && state.phase === "options" ? 1 : 0;
578
+ const chrome = 5 + strip + (spec.options.length > Math.min(Math.max(1, maxRows - 5 - strip), PICK_MAX) ? 1 : 0);
520
579
  const budget = Math.max(1, maxRows - chrome);
521
580
  const shown = spec.options.slice(0, Math.min(budget, PICK_MAX));
522
581
  // R2: the note takes a COLUMN, not three spaces after a label of
@@ -548,13 +607,35 @@ export function pickBlockRows(view, state, W, maxRows) {
548
607
  if (spec.options.length > shown.length) {
549
608
  rows.push(` ${cutLine(`${p.dim} \u2514 +${spec.options.length - shown.length} more \u2014 /model <name> takes any of them${p.reset}`, room)}`);
550
609
  }
610
+ const axis = shown[state.cursor];
611
+ if (strip === 1 && axis?.levels !== undefined) {
612
+ const off = new Set(axis.disabled ?? []);
613
+ // the RUNTIME cursor, not the option's starting index: the option
614
+ // says where the cursor opens, the state says where it is now.
615
+ // Reading the option here rendered a bracket that never moved
616
+ // while the state underneath it did — the silent movement this
617
+ // whole axis exists to replace.
618
+ const cur = enabledLevel(axis, state.level);
619
+ // the bracket means THE CURSOR IS HERE. It used to mean "the
620
+ // registry default" in the note's text form; a row whose
621
+ // default is null now marks nothing at all rather than
622
+ // promoting the first level into one.
623
+ const cells = axis.levels.map((l, i) => {
624
+ const text = escapeTerminal(l);
625
+ if (off.has(i))
626
+ return `${p.dim}${text}${p.reset}`;
627
+ return i === cur ? `${p.bold}[${text}]${p.reset}` : text;
628
+ });
629
+ const note = axis.levelNote === undefined ? "" : ` ${p.dim}\u2014 ${escapeTerminal(axis.levelNote)}${p.reset}`;
630
+ rows.push(` ${cutLine(`${p.dim}effort: ${p.reset}${cells.join(`${p.dim} \u00b7 ${p.reset}`)}${note}`, room)}`);
631
+ }
551
632
  }
552
633
  const typing = state.phase === "custom";
553
634
  if (spec.typeHint !== undefined) {
554
635
  const tText = cutLine(`${typing ? p.bold : ""}${typing ? "\u2192" : " "} t ${p.reset}${p.dim}${escapeTerminal(spec.typeHint)}${p.reset}`, room);
555
636
  rows.push(typing ? selectionBar(tText, visibleWidth(tText), W) : ` ${tText}`);
556
637
  }
557
- rows.push(` ${p.dim}${cutLine(pickAffordance(state), room)}${p.reset}`);
638
+ rows.push(` ${p.dim}${cutLine(pickAffordance(state, spec.options[state.cursor]?.levels !== undefined), room)}${p.reset}`);
558
639
  rows.push(`${p.dim}${"\u2500".repeat(Math.max(0, W))}${p.reset}`);
559
640
  return rows;
560
641
  }
@@ -579,14 +660,19 @@ export function pickLead(view, state) {
579
660
  export function pickStatus(view) {
580
661
  return view.statusText;
581
662
  }
582
- export function pickAffordance(state) {
663
+ export function pickAffordance(state, axis = false) {
583
664
  // DC-36 — the row NAMES the arrows. TUI2-R2 ④ bound ↑↓ to the pick's
584
665
  // cursor and the keys sheet has said `panels: ↑↓ move` ever since,
585
666
  // but this row — the one a human is actually looking at while the
586
667
  // panel is up — advertised only the digits. The owner read it as
587
668
  // "type the answer", which is the same lesson DC-30 filed: a hint
588
669
  // that omits the gesture is why the gesture goes unused.
589
- return state.phase === "custom" ? "enter commits \u00b7 esc backs out" : "\u2191\u2193 move \u00b7 digits pick \u00b7 \u23ce confirms \u00b7 esc";
670
+ // OR-7 names \u2190\u2192 here for the same reason DC-36 named \u2191\u2193: a gesture
671
+ // this row omits is a gesture that goes unused. It appears only when
672
+ // there is a second axis to walk.
673
+ if (state.phase === "custom")
674
+ return "enter commits \u00b7 esc backs out";
675
+ return axis ? "\u2191\u2193 move \u00b7 \u2190\u2192 effort \u00b7 digits pick \u00b7 \u23ce confirms \u00b7 esc" : "\u2191\u2193 move \u00b7 digits pick \u00b7 \u23ce confirms \u00b7 esc";
590
676
  }
591
677
  /** Compose a pick view. The flavor/name/title/args fields exist for the
592
678
  * approval path and are given inert values here \u2014 the pick block
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.30.0",
3
+ "version": "0.31.1",
4
4
  "description": "kiso tui-cells — the components cell renderer (components, diff, width, the render slice). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",