@vincemakes/kiso-tui-cells 0.31.0 → 0.32.0
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/approval-panel.d.ts +39 -2
- package/dist/approval-panel.js +90 -4
- package/dist/components.d.ts +1 -0
- package/dist/components.js +12 -1
- package/dist/render.d.ts +15 -0
- package/dist/render.js +25 -1
- package/dist/strings.js +15 -0
- package/package.json +1 -1
package/dist/approval-panel.d.ts
CHANGED
|
@@ -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. */
|
package/dist/approval-panel.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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/dist/components.d.ts
CHANGED
package/dist/components.js
CHANGED
|
@@ -30,7 +30,7 @@ import { displayWidth, visibleWidth, widthCut } from "./width.js";
|
|
|
30
30
|
// KEY_BINDINGS). strings.js imports only render/width here, so this edge
|
|
31
31
|
// adds no cycle.
|
|
32
32
|
import { displayVerb } from "./strings.js";
|
|
33
|
-
import { bannerLines, breathFrame, cutLine, escapeTerminal, foldThinking, foldResult, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, currentGround, } from "./render.js";
|
|
33
|
+
import { bannerLines, breathFrame, cutLine, escapeTerminal, foldThinking, foldThinkingRow, foldResult, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, currentGround, } from "./render.js";
|
|
34
34
|
// TUI2-MD: the markdown renderer's surface reaches the tui through this
|
|
35
35
|
// module (the tui's components shim re-exports it) — one import edge,
|
|
36
36
|
// and it points one way: md.ts measures with the width authority, never
|
|
@@ -370,6 +370,17 @@ class ThinkingBlock {
|
|
|
370
370
|
const text = escapeTerminal(this.cell.text).trim();
|
|
371
371
|
if (text === "")
|
|
372
372
|
return [];
|
|
373
|
+
// §2.3 — ctrl+t folded this block. The row is the pipe's own fold,
|
|
374
|
+
// fitted: `foldThinkingRow` is the single source of the shape and
|
|
375
|
+
// with unlimited room it IS `foldThinking`, so thinking has two
|
|
376
|
+
// renderings in the product and not three. It is width-aware here
|
|
377
|
+
// because a row must measure ≤ W and the pipe's line does not —
|
|
378
|
+
// cutting it from the right would take the `/think` suffix, which
|
|
379
|
+
// is the one part of a folded block that says how to read the rest.
|
|
380
|
+
// It keeps the block's own indent, so a folded block sits in the
|
|
381
|
+
// column an unfolded one does (DC-47).
|
|
382
|
+
if (this.cell.folded)
|
|
383
|
+
return [`${THINK_COL}${foldThinkingRow(this.cell.text, Math.max(1, W - THINK_COL.length))}`];
|
|
373
384
|
// DC-47 — THINKING GOES ONE LEVEL DEEPER THAN PROSE, and the
|
|
374
385
|
// reason is a law rather than a taste.
|
|
375
386
|
//
|
package/dist/render.d.ts
CHANGED
|
@@ -161,6 +161,21 @@ export declare function escapeTerminal(text: string): string;
|
|
|
161
161
|
* strategy is presentation-independent.
|
|
162
162
|
*/
|
|
163
163
|
export declare function foldThinking(block: string): string;
|
|
164
|
+
/** §2.3 — the same fold, as a ROW that fits `room` columns.
|
|
165
|
+
*
|
|
166
|
+
* A row must measure ≤ W (invariant ①), and the pipe's line does not:
|
|
167
|
+
* `…` + 100 characters + " (N chars · /think)" is about 122 columns, so
|
|
168
|
+
* on an 80-column terminal the frame's cut takes the SUFFIX — which is
|
|
169
|
+
* the affordance, the one part of a folded block that says how to read
|
|
170
|
+
* the rest of it. Cutting from the right removes exactly the thing the
|
|
171
|
+
* fold exists to leave behind.
|
|
172
|
+
*
|
|
173
|
+
* So the suffix is reserved FIRST and the head takes what is left. The
|
|
174
|
+
* vocabulary is unchanged — the leading `…`, the character count, the
|
|
175
|
+
* `/think` route — and with unlimited room the result is byte-for-byte
|
|
176
|
+
* the line the pipe has always written, which is what keeps the two
|
|
177
|
+
* renderings one shape rather than two. */
|
|
178
|
+
export declare function foldThinkingRow(block: string, room: number): string;
|
|
164
179
|
/** v2b — the [result] echo truncates at 160 chars + a /last hint. */
|
|
165
180
|
export declare function foldResult(content: string): string;
|
|
166
181
|
/**
|
package/dist/render.js
CHANGED
|
@@ -113,10 +113,34 @@ export function escapeTerminal(text) {
|
|
|
113
113
|
* strategy is presentation-independent.
|
|
114
114
|
*/
|
|
115
115
|
export function foldThinking(block) {
|
|
116
|
+
// the PIPE's line: no room limit, so the row path below reproduces
|
|
117
|
+
// today's bytes exactly and this stays the one source of the shape.
|
|
118
|
+
return `${foldThinkingRow(block, Number.POSITIVE_INFINITY)}\n`;
|
|
119
|
+
}
|
|
120
|
+
/** §2.3 — the same fold, as a ROW that fits `room` columns.
|
|
121
|
+
*
|
|
122
|
+
* A row must measure ≤ W (invariant ①), and the pipe's line does not:
|
|
123
|
+
* `…` + 100 characters + " (N chars · /think)" is about 122 columns, so
|
|
124
|
+
* on an 80-column terminal the frame's cut takes the SUFFIX — which is
|
|
125
|
+
* the affordance, the one part of a folded block that says how to read
|
|
126
|
+
* the rest of it. Cutting from the right removes exactly the thing the
|
|
127
|
+
* fold exists to leave behind.
|
|
128
|
+
*
|
|
129
|
+
* So the suffix is reserved FIRST and the head takes what is left. The
|
|
130
|
+
* vocabulary is unchanged — the leading `…`, the character count, the
|
|
131
|
+
* `/think` route — and with unlimited room the result is byte-for-byte
|
|
132
|
+
* the line the pipe has always written, which is what keeps the two
|
|
133
|
+
* renderings one shape rather than two. */
|
|
134
|
+
export function foldThinkingRow(block, room) {
|
|
116
135
|
const p = palette();
|
|
117
136
|
const trimmed = escapeTerminal(block.trim());
|
|
118
137
|
const truncated = trimmed.length > 100;
|
|
119
|
-
|
|
138
|
+
const suffix = truncated ? ` (${block.length} chars · /think)` : "";
|
|
139
|
+
const head = Number.isFinite(room)
|
|
140
|
+
? // the leading … costs one column, the suffix costs its own width
|
|
141
|
+
widthCut(trimmed.slice(0, 100), Math.max(1, room - 1 - displayWidth(suffix)))
|
|
142
|
+
: trimmed.slice(0, 100);
|
|
143
|
+
return `${p.dim}…${head}${suffix}${p.reset}`;
|
|
120
144
|
}
|
|
121
145
|
/** v2b — the [result] echo truncates at 160 chars + a /last hint. */
|
|
122
146
|
export function foldResult(content) {
|
package/dist/strings.js
CHANGED
|
@@ -440,6 +440,21 @@ export function helpRows() {
|
|
|
440
440
|
["/compact", "summarize the older conversation to free context"],
|
|
441
441
|
["/clear", "start a fresh conversation (the old session stays resumable)"],
|
|
442
442
|
["/resume", "switch to another session; /resume <id> goes directly"],
|
|
443
|
+
// §2.5: the conversation is untouched — this rereads what kiso was
|
|
444
|
+
// built with, not what it has said.
|
|
445
|
+
["/reload", "reread extensions, skills and config into this session"],
|
|
446
|
+
// §2.2: the two shell gestures and their one escape. They sit
|
|
447
|
+
// beside the slash commands because that is what a reader is
|
|
448
|
+
// looking for when they look here, even though `!` is not one.
|
|
449
|
+
// §2.3: the switch belongs beside ctrl+o's job, and a gesture the
|
|
450
|
+
// sheet does not name is a gesture nobody uses (DC-30, DC-36).
|
|
451
|
+
["ctrl+t", "fold the thinking blocks, and fold them back"],
|
|
452
|
+
// §2.4: the composer, in your own editor. It names the variables
|
|
453
|
+
// because that is what a reader has to set for it to work.
|
|
454
|
+
["ctrl+g", "edit the composer in $VISUAL or $EDITOR — the text comes back unsent"],
|
|
455
|
+
["!<cmd>", "run a shell command and send it with its output as your turn"],
|
|
456
|
+
["!!<cmd>", "run one and show it here only — the model never sees it"],
|
|
457
|
+
["\\!", "send a line that really starts with ! (the only escape)"],
|
|
443
458
|
["exit", "leave the session"],
|
|
444
459
|
// TUI2-R1 (D): the SENTENCE is deliberately unchanged. Deriving it
|
|
445
460
|
// from KEY_BINDINGS would be an improvement and it would also move
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui-cells",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
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",
|