@vincemakes/kiso-tui 0.46.2 → 0.47.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.
@@ -67,7 +67,7 @@ export declare function askCommitCustom(spec: AskSpec, state: AskRuntime, text:
67
67
  * the widest label, never past the half-width, and 0 (meaning "no
68
68
  * column, use the em dash") when what is left would not hold a
69
69
  * readable description. */
70
- export declare function askDescriptionStop(q: AskQuestion, W: number): number;
70
+ export declare function askDescriptionStop(q: AskQuestion, W: number, markCol?: boolean): number;
71
71
  /** The ask block's rows — the question as the rule line, the header (or
72
72
  * the counter) as the title, the options as the body, and the
73
73
  * type-your-own line last. R2: the shape is the RULE's — rule,
@@ -110,8 +110,8 @@ export declare function askAffordanceFit(state: AskRuntime, q: AskQuestion | und
110
110
  export declare function askStatus(view: PanelView, state: AskRuntime): string;
111
111
  /** The input row's lead: the digit lead while picking, the typing lead
112
112
  * in the custom phase (the rule-input phase's shape, reused). */
113
- export declare function askLeadPlain(state: AskRuntime): string;
114
- export declare function panelBlockRows(view: PanelView, phase: PanelPhase, cursor: number, W: number, maxRows: number, ask?: AskRuntime, pick?: PickRuntime, note?: string, safer?: SaferRuntime): string[];
113
+ export declare function askLeadPlain(_state: AskRuntime): string;
114
+ export declare function panelBlockRows(view: PanelView, phase: PanelPhase, cursor: number, W: number, maxRows: number, ask?: AskRuntime, pick?: PickRuntime, note?: string, safer?: SaferRuntime, height?: number): string[];
115
115
  export declare function panelLead(view: PanelView, phase: PanelPhase, cursor: number, ask?: AskRuntime, pick?: PickRuntime): string;
116
116
  export declare function panelLeadPlain(view: PanelView, phase: PanelPhase, cursor: number, ask?: AskRuntime, pick?: PickRuntime): string;
117
117
  export declare function panelStatus(view: PanelView, phase: PanelPhase, cursor: number, ask?: AskRuntime, pick?: PickRuntime): string;
@@ -119,7 +119,7 @@ export declare function panelAffordance(view: PanelView, phase: PanelPhase, curs
119
119
  /** The whole panel state in one call — the compositor's four reads share
120
120
  * one source, so an ask can never render half as an approval (TUI2-R2
121
121
  * ④: nor a pick as either). */
122
- export declare const panelRowsOf: (s: PanelState, W: number, maxRows: number) => string[];
122
+ export declare const panelRowsOf: (s: PanelState, W: number, maxRows: number, height?: number) => string[];
123
123
  /**
124
124
  * TUI2-R3v2 ② — the rows AND where the clickable ones are, from ONE
125
125
  * call.
@@ -136,7 +136,7 @@ export declare const panelRowsOf: (s: PanelState, W: number, maxRows: number) =>
136
136
  * not). A null here means every click is inert, which is the safe way
137
137
  * to not-implement something.
138
138
  */
139
- export declare const panelFrameOf: (s: PanelState, W: number, maxRows: number) => {
139
+ export declare const panelFrameOf: (s: PanelState, W: number, maxRows: number, height?: number) => {
140
140
  rows: string[];
141
141
  options: {
142
142
  offset: number;
@@ -145,6 +145,11 @@ export declare const panelFrameOf: (s: PanelState, W: number, maxRows: number) =
145
145
  } | null;
146
146
  };
147
147
  export declare const panelLeadOf: (s: PanelState) => string;
148
+ /** Graphite P3/P4: what an EMPTY input row says while a panel is up and the
149
+ * composer is where the person types — a pick's filter, the note an
150
+ * approval's amend sends, a typed answer to a question. Null: the row stays
151
+ * empty, as §7.8 has it everywhere else. */
152
+ export declare const panelHintOf: (s: PanelState) => string | null;
148
153
  export declare const panelStatusOf: (s: PanelState) => string;
149
154
  export declare const panelAffordanceOf: (s: PanelState) => string;
150
155
  /** The ask's PanelView. The dock-less fallback question is HONEST: a
package/dist/ask-panel.js CHANGED
@@ -27,8 +27,9 @@
27
27
  * durability would need a new durable mechanism), no "chat about this"
28
28
  * hand-off, no timeout and no countdown.
29
29
  */
30
- import { panelBlockLayout, panelAffordance as basePanelAffordance, panelBlockRows as basePanelBlockRows, panelLead as basePanelLead, panelLeadPlain as basePanelLeadPlain, panelStatus as basePanelStatus, pickAffordance, pickBlockRows, pickLead, pickLeadPlain, pickStatus, } from "./approval-panel.js";
31
- import { cutLine, selectionBar, visibleWidth, widthCut } from "@vincemakes/kiso-tui-cells/components";
30
+ import { panelBlockLayout, panelAffordance as basePanelAffordance, panelBlockRows as basePanelBlockRows, panelLead as basePanelLead, panelLeadPlain as basePanelLeadPlain, panelStatus as basePanelStatus, pickAffordance, pickBlockRows, pickLead, pickList, pickLeadPlain, pickStatus, foldAtSpaces, } from "./approval-panel.js";
31
+ import { boxBottom, cutLine, selectionBar, visibleWidth, widthCut } from "@vincemakes/kiso-tui-cells/components";
32
+ import { bandHeader } from "@vincemakes/kiso-tui-cells/strings";
32
33
  import { escapeTerminal, palette } from "./lines.js";
33
34
  /** The schema's own bounds — the registry refuses anything outside them
34
35
  * (extensions/ask validates; these are the numbers it validates to). */
@@ -223,17 +224,26 @@ export function askCommitCustom(spec, state, text) {
223
224
  * A narrow block has no room for two columns; `stop` arrives as 0 and
224
225
  * the em-dash form is what it degrades to.
225
226
  */
226
- function optionRow(o, n, picked, cursor, multi, W, stop = 0) {
227
+ function optionRow(o, n, picked, cursor, multi, W, stop = 0, markCol = multi, opened = false) {
227
228
  const p = palette();
228
- const mark = multi ? (picked ? "◉" : "◯") : picked ? "◉" : " ";
229
+ // Graphite P4 follow-up: a single-select spends no mark column until one
230
+ // of its options is picked (walking back to an answered question shows
231
+ // its ◉) — the approved `1 vite`, where `1 vite` was drawn
232
+ const mark = multi ? (picked ? "◉" : "◯") : !markCol ? "" : picked ? "◉" : " ";
229
233
  const lead = askOptionLead(o, n, mark, cursor);
230
234
  const head = `${cursor ? p.bold : ""}${lead}${p.reset}`;
231
235
  const room = Math.max(1, W - 2);
232
236
  let body;
233
- if (o.description === undefined)
237
+ // an OPENED option says its description on the rows under it, whole —
238
+ // its own row carries the label alone, so nothing is said twice
239
+ if (o.description === undefined || opened)
234
240
  body = "";
235
241
  else if (stop > 0) {
236
- const desc = widthCut(escapeTerminal(o.description), Math.max(0, room - stop));
242
+ // cut by cells WITH an ellipsis — a bare cut reads as the whole
243
+ // sentence (the P2/P3 class, here in the model's descriptions)
244
+ const left = Math.max(0, room - stop);
245
+ const said = escapeTerminal(o.description);
246
+ const desc = visibleWidth(said) <= left ? said : left <= 1 ? "" : `${widthCut(said, left - 1)}…`;
237
247
  body = `${" ".repeat(Math.max(1, stop - visibleWidth(lead)))}${p.dim}${desc}${p.reset}`;
238
248
  }
239
249
  else
@@ -246,12 +256,35 @@ function optionRow(o, n, picked, cursor, multi, W, stop = 0) {
246
256
  // ruling, applied in the second place it was always about.
247
257
  return cursor ? selectionBar(text, visibleWidth(text), W) : ` ${text}`; // R2: the frame is a rule, so a row is just indented
248
258
  }
259
+ /** Whether an option's description fits its row as drawn — in the column
260
+ * when there is one, after an em dash when there is not. */
261
+ function descriptionFits(o, n, mark, stop, W) {
262
+ const room = Math.max(1, W - 2);
263
+ const said = escapeTerminal(o.description ?? "");
264
+ if (stop > 0)
265
+ return visibleWidth(said) <= Math.max(0, room - stop);
266
+ return visibleWidth(askOptionLead(o, n, mark, false)) + 3 + visibleWidth(said) <= room;
267
+ }
268
+ /** The OPENED rows of the selected option: its description, whole, folded
269
+ * at its spaces under the label's first cell, on the selection's wash. At
270
+ * most three rows, the third cut with an ellipsis when it must be. */
271
+ const OPENED_MAX = 3;
272
+ function openedDescription(description, labelCol, W) {
273
+ const p = palette();
274
+ const width = Math.max(8, W - labelCol - 1);
275
+ let rows = foldAtSpaces(escapeTerminal(description), width);
276
+ if (rows.length > OPENED_MAX) {
277
+ const last = rows.slice(OPENED_MAX - 1).join(" ");
278
+ rows = [...rows.slice(0, OPENED_MAX - 1), visibleWidth(last) <= width ? last : `${widthCut(last, width - 1)}\u2026`];
279
+ }
280
+ return rows.map((r) => selectionBar(`${" ".repeat(Math.max(0, labelCol - 1))}${p.dim}${r}${p.reset}`, labelCol - 1 + visibleWidth(r), W));
281
+ }
249
282
  /** The row's left span, PLAIN — the one place its shape is written, so
250
283
  * the column arithmetic below and the row above cannot disagree about
251
284
  * how wide it is. The arrow's cell is spent on every row so the digit
252
285
  * column does not move as the cursor walks. */
253
286
  function askOptionLead(o, n, mark, cursor) {
254
- return `${cursor ? "→" : " "} ${n} ${mark} ${escapeTerminal(o.label)}`;
287
+ return `${cursor ? "→" : " "} ${n} ${mark === "" ? "" : `${mark} `}${escapeTerminal(o.label)}`;
255
288
  }
256
289
  /** Below this many cells a right column is not a column, it is a
257
290
  * three-word stub — the em-dash form carries more of the sentence. */
@@ -260,11 +293,11 @@ const ASK_DESC_MIN = 18;
260
293
  * the widest label, never past the half-width, and 0 (meaning "no
261
294
  * column, use the em dash") when what is left would not hold a
262
295
  * readable description. */
263
- export function askDescriptionStop(q, W) {
296
+ export function askDescriptionStop(q, W, markCol = q.multiSelect === true) {
264
297
  const multi = q.multiSelect === true;
265
298
  if (!q.options.some((o) => o.description !== undefined))
266
299
  return 0;
267
- const widest = Math.max(...q.options.map((o, i) => visibleWidth(askOptionLead(o, i + 1, multi ? "◯" : " ", false))));
300
+ const widest = Math.max(...q.options.map((o, i) => visibleWidth(askOptionLead(o, i + 1, multi ? "◯" : markCol ? " " : "", false))));
268
301
  const room = Math.max(1, W - 2);
269
302
  const stop = widest + 2;
270
303
  if (stop > Math.floor(room / 2) || room - stop < ASK_DESC_MIN)
@@ -280,25 +313,34 @@ export function askBlockRows(view, state, W, maxRows) {
280
313
  const spec = view.ask;
281
314
  const q = spec.questions[state.qIndex];
282
315
  const multi = q.multiSelect === true;
283
- const counter = spec.questions.length > 1 ? `${p.dim} ‹ ${state.qIndex + 1}/${spec.questions.length} ›${p.reset}` : "";
284
316
  const rows = [];
285
- // R2: the same dashed rule the composer and the approval panel use.
286
- rows.push(`${p.dim}${"\u2500".repeat(Math.max(0, W))}${p.reset}`);
287
- rows.push(` ${cutLine(`${p.bold}${escapeTerminal(q.question)}${p.reset}${counter}`, Math.max(1, W - 2))}`);
288
- const header = q.header === undefined ? "the question" : escapeTerminal(q.header.slice(0, ASK_HEADER_CAP));
289
- // R2: the divider row is gone (the opening rule says a block starts
290
- // here), but the multi-select gesture it carried is INFORMATION and
291
- // rides the header instead — dropping it would have been a regression
292
- // wearing a restyle's clothes.
293
- // R6/D2: the header names the MODE; the keys live on the keys row,
294
- // once. It used to name `space toggles` there and nowhere say what
295
- // finishes the set.
296
- const gesture = multi ? `${p.dim} · pick any${p.reset}` : "";
297
- rows.push(` ${cutLine(`${p.dim}${header}${p.reset}${gesture}`, Math.max(1, W - 2))}`);
298
- rows.push("");
317
+ // Graphite P4 (owner, 2026-10-04): the band's name carries the facts — the
318
+ // question's header, its place in the set, `pick any` — and the question
319
+ // is the first row, bold. DECLARED REVERSAL of the `‹ 1/2 ›` after the
320
+ // question, of the header's row of its own and of the blank row (R2,
321
+ // R6/D2): three rows before the options became one.
322
+ const facts = [
323
+ ...(q.header === undefined ? [] : [escapeTerminal(q.header.slice(0, ASK_HEADER_CAP))]),
324
+ ...(spec.questions.length > 1 ? [`${state.qIndex + 1} of ${spec.questions.length}`] : []),
325
+ ...(multi ? ["pick any"] : []),
326
+ ];
327
+ rows.push(bandHeader(["question", ...facts].join(" \u00b7 "), W));
328
+ rows.push(` ${cutLine(`${p.bold}${escapeTerminal(q.question)}${p.reset}`, Math.max(1, W - 2))}`);
299
329
  const picks = state.picks[state.qIndex] ?? [];
300
- const stop = askDescriptionStop(q, W);
301
- const body = q.options.map((o, i) => optionRow(o, i + 1, picks.includes(i), state.cursor === i, multi, W, stop));
330
+ const markCol = multi || picks.length > 0;
331
+ const stop = askDescriptionStop(q, W, markCol);
332
+ // Graphite P4 (owner, 2026-10-05): the selected option OPENS when its
333
+ // description does not fit its row — its row keeps the label and the
334
+ // rows under it, on the same wash, say the description whole (the
335
+ // /resume and /model shape). The others keep their one row, cut.
336
+ const body = [];
337
+ q.options.forEach((o, i) => {
338
+ const on = state.cursor === i;
339
+ const opens = on && state.phase !== "custom" && o.description !== undefined && !descriptionFits(o, i + 1, multi ? "◯" : markCol ? " " : "", stop, W);
340
+ body.push(optionRow(o, i + 1, picks.includes(i), on, multi, W, stop, markCol, opens));
341
+ if (opens)
342
+ body.push(...openedDescription(o.description, visibleWidth(askOptionLead(o, i + 1, multi ? "◯" : markCol ? " " : "", false)) - visibleWidth(escapeTerminal(o.label)) + 1, W));
343
+ });
302
344
  // REL-0152-D3: the row is part of the list, so it carries the same
303
345
  // cursor affordance the options do. Dim-always made a reachable row
304
346
  // look like a footnote.
@@ -319,13 +361,13 @@ export function askBlockRows(view, state, W, maxRows) {
319
361
  ? `${onCustom || typingHere ? p.bold : ""}${customLead}◉ ${escapeTerminal(typed)}${p.reset}`
320
362
  : typingHere
321
363
  ? `${p.bold}${customLead}▸${p.reset} ${p.dim}type your answer — enter sends, esc backs out${p.reset}`
322
- : `${onCustom ? p.bold : p.dim}${customLead} type your own answer${p.reset}`, Math.max(1, W - 2));
364
+ : `${onCustom ? p.bold : p.dim}${customLead}${markCol ? " " : ""}type your own answer${p.reset}`, Math.max(1, W - 2));
323
365
  body.push(onCustom ? selectionBar(customText, visibleWidth(customText), W) : ` ${customText}`);
324
366
  // the bounded block: the options fold nothing and cut individually,
325
367
  // so the cap drops whole rows with the W21 notice row.
326
- // R2: SIX rows of frame — the block opens with a rule as well as
327
- // closing with one, and the divider became a blank.
328
- const budget = Math.max(1, maxRows - 6);
368
+ // P4: THREE rows of frame — the band's name, the question and the key
369
+ // row (no header row, no blank, no closing rule)
370
+ const budget = Math.max(1, maxRows - 3);
329
371
  if (body.length > budget) {
330
372
  const kept = Math.max(0, budget - 1);
331
373
  rows.push(...body.slice(0, kept));
@@ -335,14 +377,12 @@ export function askBlockRows(view, state, W, maxRows) {
335
377
  rows.push(...body);
336
378
  }
337
379
  // R6/D2: FITTED, and the two-space indent is inside the budget —
338
- // the uncut push here is what made a narrow ask throw.
339
- rows.push(` ${p.dim}${askAffordanceFit(state, q, Math.max(1, W - 2))}${p.reset}`);
340
- // R2, shared with the approval panel: the block closes with the SAME
341
- // dashed rule it opened with, and the same one the composer uses.
342
- // TUI2-R1.5 ⑪ had already replaced a two-cell `\u2514 ` stub with a
343
- // real rule for the reason that stub read as the cut-notice prefix it
344
- // collides with; this keeps that finding and only changes which rule.
345
- rows.push(`${p.dim}${"\u2500".repeat(Math.max(0, W))}${p.reset}`);
380
+ // the uncut push here is what made a narrow ask throw. Graphite P4: the
381
+ // counter rides the right margin (§8.2), and the composer's rail closes
382
+ // the band — DECLARED REVERSAL of the block's own bottom rule (R2).
383
+ const count = `${state.cursor + 1}/${customRow(q) + 1}`;
384
+ const keys = askAffordanceFit(state, q, Math.max(1, W - 1 - 2 - count.length - 2));
385
+ rows.push(` ${p.dim}${keys}${" ".repeat(Math.max(2, W - 1 - 2 - visibleWidth(keys) - count.length))}${count}${p.reset}`);
346
386
  return rows;
347
387
  }
348
388
  /**
@@ -368,8 +408,8 @@ export function askAffordance(state, q) {
368
408
  const multi = q?.multiSelect === true;
369
409
  const back = state.qIndex > 0 ? ["← back"] : [];
370
410
  return (multi
371
- ? ["↑↓ move", `space or 1-${n} marks`, "⏎ sends the set", "t types", ...back, "esc declines"]
372
- : ["↑↓ move", "⏎ confirms", `1-${n} instant`, "t types", ...back, "esc declines"]).join(" · ");
411
+ ? ["↑↓ move", `space or 1–${n} marks`, "⏎ sends the set", "t types", ...back, "esc declines"]
412
+ : ["↑↓ move", "⏎ confirms", `1–${n} instant`, "t types", ...back, "esc declines"]).join(" · ");
373
413
  }
374
414
  /**
375
415
  * The affordance at a width, by the DC-2 clause ladder.
@@ -392,13 +432,13 @@ export function askAffordanceFit(state, q, W) {
392
432
  const n = q?.options.length ?? 4;
393
433
  const multi = q?.multiSelect === true;
394
434
  const back = state.qIndex > 0 ? ["← back"] : [];
395
- const act = multi ? `space or 1-${n} marks` : `1-${n} instant`;
435
+ const act = multi ? `space or 1–${n} marks` : `1–${n} instant`;
396
436
  const go = multi ? "⏎ sends the set" : "⏎ confirms";
397
437
  const tiers = [
398
438
  ["↑↓ move", act, go, "t types", ...back, "esc declines"],
399
439
  ["↑↓ move", act, go, ...back, "esc declines"],
400
440
  [act, go, ...back, "esc declines"],
401
- [multi ? "space marks" : `1-${n} picks`, multi ? "⏎ sends" : "⏎ confirms", "esc declines"],
441
+ [multi ? "space marks" : `1–${n} picks`, multi ? "⏎ sends" : "⏎ confirms", "esc declines"],
402
442
  [multi ? "⏎ sends" : "⏎ confirms", "esc declines"],
403
443
  ];
404
444
  for (const tier of tiers) {
@@ -422,16 +462,18 @@ export function askStatus(view, state) {
422
462
  }
423
463
  /** The input row's lead: the digit lead while picking, the typing lead
424
464
  * in the custom phase (the rule-input phase's shape, reused). */
425
- export function askLeadPlain(state) {
426
- // REL-0152-D3: "1-4> " was hard-coded and wrong twice over — it named
427
- // a range even when there were two options, and it excluded the
428
- // type-your-own row the list shows.
429
- return state.phase === "custom" ? "your answer: " : "pick> ";
465
+ export function askLeadPlain(_state) {
466
+ // REL-0152-D3: "1-4> " was hard-coded and wrong twice over. Graphite P4
467
+ // (owner, 2026-10-04): a panel leaves the input row to the composer —
468
+ // `pick>` and `your answer:` retired; an empty answer row carries a dim
469
+ // hint instead (`panelHintOf`).
470
+ return "";
430
471
  }
431
472
  // ── the dispatchers: the panel slot, with the ask branch folded in ────
432
- export function panelBlockRows(view, phase, cursor, W, maxRows, ask, pick, note, safer) {
473
+ export function panelBlockRows(view, phase, cursor, W, maxRows, ask, pick, note, safer, height = 24) {
474
+ // Graphite P3: a pick's window is §8.2's, so it reads the terminal's height
433
475
  if (view.pick !== undefined && pick !== undefined)
434
- return pickBlockRows(view, pick, W, maxRows);
476
+ return pickBlockRows(view, pick, W, maxRows, height);
435
477
  if (view.ask !== undefined && ask !== undefined)
436
478
  return askBlockRows(view, ask, W, maxRows);
437
479
  return basePanelBlockRows(view, phase, cursor, W, maxRows, note, safer);
@@ -459,8 +501,10 @@ export function panelStatus(view, phase, cursor, ask, pick) {
459
501
  return basePanelStatus(view, phase, cursor);
460
502
  }
461
503
  export function panelAffordance(view, phase, cursor, ask, pick, safer) {
462
- if (view.pick !== undefined && pick !== undefined)
463
- return pickAffordance(pick, view.pick.options[pick.cursor]?.levels !== undefined);
504
+ if (view.pick !== undefined && pick !== undefined) {
505
+ const here = view.pick.options[pickList(view.pick, pick).cursor];
506
+ return pickAffordance(pick, here?.levels !== undefined ? (here.axisLabel ?? true) : false);
507
+ }
464
508
  if (view.ask !== undefined && ask !== undefined)
465
509
  return askAffordance(ask, view.ask.questions[ask.qIndex]);
466
510
  return basePanelAffordance(view, phase, cursor, safer);
@@ -468,7 +512,7 @@ export function panelAffordance(view, phase, cursor, ask, pick, safer) {
468
512
  /** The whole panel state in one call — the compositor's four reads share
469
513
  * one source, so an ask can never render half as an approval (TUI2-R2
470
514
  * ④: nor a pick as either). */
471
- export const panelRowsOf = (s, W, maxRows) => panelBlockRows(s.view, s.phase, s.cursor, W, maxRows, s.ask, s.pick, s.note, s.safer);
515
+ export const panelRowsOf = (s, W, maxRows, height = 24) => panelBlockRows(s.view, s.phase, s.cursor, W, maxRows, s.ask, s.pick, s.note, s.safer, height);
472
516
  /**
473
517
  * TUI2-R3v2 ② — the rows AND where the clickable ones are, from ONE
474
518
  * call.
@@ -485,9 +529,9 @@ export const panelRowsOf = (s, W, maxRows) => panelBlockRows(s.view, s.phase, s.
485
529
  * not). A null here means every click is inert, which is the safe way
486
530
  * to not-implement something.
487
531
  */
488
- export const panelFrameOf = (s, W, maxRows) => {
532
+ export const panelFrameOf = (s, W, maxRows, height = 24) => {
489
533
  if (s.view.pick !== undefined || s.view.ask !== undefined)
490
- return { rows: panelRowsOf(s, W, maxRows), options: null };
534
+ return { rows: panelRowsOf(s, W, maxRows, height), options: null };
491
535
  const layout = panelBlockLayout(s.view, s.phase, s.cursor, W, maxRows, s.note, s.safer);
492
536
  return {
493
537
  rows: layout.rows,
@@ -495,6 +539,17 @@ export const panelFrameOf = (s, W, maxRows) => {
495
539
  };
496
540
  };
497
541
  export const panelLeadOf = (s) => panelLead(s.view, s.phase, s.cursor, s.ask, s.pick);
542
+ /** Graphite P3/P4: what an EMPTY input row says while a panel is up and the
543
+ * composer is where the person types — a pick's filter, the note an
544
+ * approval's amend sends, a typed answer to a question. Null: the row stays
545
+ * empty, as §7.8 has it everywhere else. */
546
+ export const panelHintOf = (s) => {
547
+ if (s.view.pick !== undefined)
548
+ return s.view.pick.input === "filter" ? (s.view.pick.filterHint ?? null) : null;
549
+ if (s.view.ask !== undefined)
550
+ return s.ask?.phase === "custom" ? "your answer" : null;
551
+ return s.phase === "amend" ? "tell kiso what to do instead" : null;
552
+ };
498
553
  export const panelStatusOf = (s) => panelStatus(s.view, s.phase, s.cursor, s.ask, s.pick);
499
554
  export const panelAffordanceOf = (s) => panelAffordance(s.view, s.phase, s.cursor, s.ask, s.pick, s.safer);
500
555
  // ── the view: what the human reads when the model asks ────────────────
@@ -12,6 +12,8 @@
12
12
  * lexical order of the path (never localeCompare, whose result depends
13
13
  * on the machine's locale).
14
14
  */
15
+ import { atEmbed, bandKeyRow, bandVisible, bandWindow, moreMark } from "./components.js";
16
+ export { atEmbed, bandKeyRow, bandVisible, bandWindow, moreMark };
15
17
  import { bandHeader } from "@vincemakes/kiso-tui-cells/strings";
16
18
  /** The bound source's item — a repo-relative path and nothing else.
17
19
  * Structural: the CLI passes whatever it likes as long as it has a
@@ -54,26 +56,6 @@ export declare const AT_SKIP: ReadonlySet<string>;
54
56
  /** KC3 §4 — the panel's visible height. A ceiling, not a promise: the
55
57
  * compositor clamps further when the terminal is short. */
56
58
  export declare const AT_VISIBLE = 5;
57
- /**
58
- * The subsequence embedding, TIGHTENED — the two-pass walk every good
59
- * fuzzy finder uses, and the reason `@ra` emboldens the "ra" of
60
- * "src/range.js" rather than the r of "src" and the a of "range".
61
- *
62
- * Pass 1 walks forward and stops at the EARLIEST index that completes
63
- * the query — this both answers "does it match at all" and fixes the
64
- * right-hand edge. Pass 2 walks backward from that edge, taking the
65
- * LATEST position for each query character in turn, which slides every
66
- * matched character as far right as it can go without crossing the
67
- * next one. The result is the most clustered embedding that ends where
68
- * the earliest match ends.
69
- *
70
- * Case-insensitive: both sides are lowercased by the caller once per
71
- * query rather than once per character.
72
- *
73
- * Returns the ascending match indices, or null when the query is not a
74
- * subsequence of the path at all.
75
- */
76
- export declare function atEmbed(lowerPath: string, lowerQuery: string): number[] | null;
77
59
  /** The longest run of CONSECUTIVE indices in an ascending list. An
78
60
  * empty query has no run — every path ties on it, and the rank falls
79
61
  * through to path length. */
@@ -103,61 +85,49 @@ export declare function atFilter(items: readonly AtItem[], query: string): {
103
85
  matches: AtMatch[];
104
86
  capped: boolean;
105
87
  };
88
+ /** The path split into the two columns the panel draws: the file's own
89
+ * name, and the directory that qualifies it. A path with no slash is
90
+ * all name and no directory. */
91
+ /** The cells a file's NAME takes in its row — the measure the band's name
92
+ * column is laid over (escaped as the row draws it, so the column a
93
+ * wide or control-character name asks for is the one it gets). */
94
+ export declare function atNameWidth(path: string): number;
106
95
  /**
107
- * KC3 §4 — the picker's WINDOW: which slice of the ranked list is on
108
- * screen. The window TRAILS the selection exactly as the composer's
109
- * own viewport trails the cursor (KC1 §5) — derived per read, never
110
- * stored, so it can never disagree with the selection it is meant to
111
- * follow.
112
- */
113
- export declare function atWindow(total: number, selected: number, visible?: number): {
114
- first: number;
115
- count: number;
116
- };
117
- /**
118
- * KC3 §4 — ONE row of the panel.
96
+ * KC3 §4, Graphite P2 (owner, 2026-10-03) — ONE row of the panel: the
97
+ * file's NAME, padded to the name column, then its FOLDER as a dim column,
98
+ * so the folders line up. The letters the query matched are gold in both.
119
99
  *
120
- * Two columns: the file's NAME on the left with its matched characters
121
- * bold, and the DIRECTORY dim on the right, pushed to the far edge.
122
- * The name is what the user is aiming at; the directory is what tells
123
- * two same-named files apart, which is why it is present but quiet.
100
+ * DECLARED REVERSAL of TUI2-R1.5 ⑧ (VD-9) in two parts: the folder no
101
+ * longer trails each name after an em dash — it is a column one gap after
102
+ * the longest name in the list (never the far edge, which was VD-9's
103
+ * complaint), so twenty rows read as a table; and a match inside the
104
+ * folder is drawn, gold, because the person typed it.
124
105
  *
125
- * The selected row carries `→` on the inverse band (SGR 7, closed with
126
- * 27 — never SGR 0, so it composes inside the row's own spans).
127
- *
128
- * The `hit` indices are over the FULL path, so they are shifted by the
129
- * directory's length to land on the name. A hit that falls INSIDE the
130
- * directory is simply not drawn bold — the directory column is
131
- * uniformly dim by design (a bold fragment in a right-aligned dim
132
- * column reads as damage, not as information).
133
- *
134
- * The row never exceeds W: the name cuts first (it is the flexible
135
- * column), and the directory is dropped entirely before the name is
136
- * cut to nothing.
106
+ * The name is the flexible column when the row is short: the folder gives
107
+ * way first, then the name is cut with an ellipsis.
137
108
  */
138
- export declare function atRow(match: AtMatch, selected: boolean, W: number): string;
139
- /**
140
- * KC3 §4 — the counter row: `(n/total)`, where n is the 1-based
141
- * position of the SELECTION in the whole ranked list, not in the
142
- * visible window. The user needs to know where they are in the list,
143
- * which the five visible rows cannot tell them.
144
- *
145
- * When the source list was truncated the row SAYS SO. A file picker
146
- * that quietly lists 2,000 of 40,000 files and shows a confident
147
- * "(3/1998)" is lying by omission; this one admits the horizon.
148
- */
149
- export declare function atCounterRow(selected: number, total: number, capped: boolean, W: number): string;
109
+ export declare function atRow(match: AtMatch, selected: boolean, W: number, nameCol?: number, mark?: string | null): string;
110
+ /** The band's state as the editor hands it over. `query`, `total` and
111
+ * `nameCol` (the widest file name in the whole list, so the folder
112
+ * column never moves while the person types) are Graphite P2's; absent,
113
+ * the band still draws. */
114
+ export interface AtPanelState {
115
+ readonly matches: readonly AtMatch[];
116
+ readonly selected: number;
117
+ readonly capped: boolean;
118
+ readonly query?: string;
119
+ readonly total?: number;
120
+ readonly nameCol?: number;
121
+ }
150
122
  /**
151
- * KC3 §4 — the whole band: at most AT_VISIBLE windowed rows, then the
123
+ * KC3 §4, Graphite P2 — the whole band: the named hairline with the count
124
+ * (`files · 8`, `2 of 8 match`, and `first 2000 only` when the walk was
125
+ * cut), the windowed rows with their more-marks, and the key row with the
152
126
  * counter. Returned as plain strings for the menu-rows channel, which
153
- * already accounts them in chromeRows — the picker needs no geometry
154
- * of its own, which is the entire reason it rides that channel.
127
+ * already accounts them in chromeRows — the picker needs no geometry of
128
+ * its own, which is the entire reason it rides that channel.
155
129
  */
156
- export declare function atPanelRows(state: {
157
- matches: readonly AtMatch[];
158
- selected: number;
159
- capped: boolean;
160
- }, W: number): string[];
130
+ export declare function atPanelRows(state: AtPanelState, W: number, height?: number): string[];
161
131
  /**
162
132
  * TUI2-R1.5 ⑦(b) — the one row that turns a band into a surface. Shared
163
133
  * by the @ picker, the / menu and the session picker so the three read