@vincemakes/kiso-tui 0.20.0 → 0.20.2

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.
@@ -12,6 +12,7 @@
12
12
  * lexical order of the path (never localeCompare, whose result depends
13
13
  * on the machine's locale).
14
14
  */
15
+ import { bandHeader } from "@vincemakes/kiso-tui-cells/strings";
15
16
  /** The bound source's item — a repo-relative path and nothing else.
16
17
  * Structural: the CLI passes whatever it likes as long as it has a
17
18
  * path (slice 5 passes exactly this). */
@@ -168,4 +169,4 @@ export declare function atPanelRows(state: {
168
169
  * composer and every panel now share, so a band opens the way everything
169
170
  * else does and the label tells you WHICH band in the same row.
170
171
  */
171
- export declare function bandHeader(label: string, W: number): string;
172
+ export { bandHeader };
package/dist/at-picker.js CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { escapeTerminal, palette } from "./render.js";
16
16
  import { selectionBar, visibleWidth, widthCut } from "./components.js";
17
+ import { bandHeader } from "@vincemakes/kiso-tui-cells/strings";
17
18
  /**
18
19
  * KC3 §5 — the ONE cap. The file list is computed per open with no
19
20
  * index and no watcher, so its cost is bounded here rather than
@@ -263,8 +264,8 @@ export function atPanelRows(state, W) {
263
264
  * composer and every panel now share, so a band opens the way everything
264
265
  * else does and the label tells you WHICH band in the same row.
265
266
  */
266
- export function bandHeader(label, W) {
267
- const p = palette();
268
- const head = `\u2500\u2500\u2500 ${label} `;
269
- return `${p.dim}${widthCut(`${head}${"\u2500".repeat(Math.max(1, W - head.length))}`, Math.max(1, W))}${p.reset}`;
270
- }
267
+ // R8b: bandHeader MOVED to tui-cells/strings.ts — the keys sheet needs
268
+ // it and lives there, and `components.ts` already imports that module,
269
+ // so the dependency only runs one way. Re-exported here so every
270
+ // existing import site is untouched.
271
+ export { bandHeader };
@@ -227,6 +227,22 @@ export declare class Body {
227
227
  * there is no pointer, so there is nothing a pointer could reach
228
228
  * that a keyboard cannot. */
229
229
  viewerKey(cmd: "up" | "down" | "toggle" | "all" | "pageUp" | "pageDown" | "home" | "end"): void;
230
+ /**
231
+ * DC-35 — ctrl+r does not print the same expansion twice in a row.
232
+ *
233
+ * The ring walks newest-back and restarts its cycle once every entry
234
+ * has been opened (R4/C1, which is what makes the walk immune to the
235
+ * ring growing underneath it). With a ring of ONE the restart is
236
+ * immediate, so holding the key appended the identical block over
237
+ * and over — the owner got three copies of the same four rows, each
238
+ * closing with `ctrl+r opens the one before it`, a footer naming
239
+ * something that does not exist.
240
+ *
241
+ * The bar is the BOTTOM of the transcript, not "ever shown": once
242
+ * other content has arrived the expansion has scrolled up and
243
+ * re-opening it is the point of the key, so the guard clears itself
244
+ * the moment a cell is added.
245
+ */
230
246
  expandNext(): {
231
247
  kind: "toggled";
232
248
  } | {
@@ -234,6 +250,7 @@ export declare class Body {
234
250
  lines: string[];
235
251
  } | {
236
252
  kind: "none";
253
+ why?: "already-last";
237
254
  };
238
255
  /** Docked = the chrome is live (a color TTY with a real size). */
239
256
  get active(): boolean;
@@ -43,7 +43,7 @@
43
43
  * line-mode bytes byte-for-byte (the e2e guards them).
44
44
  */
45
45
  import { truncateDiff } from "./diff.js";
46
- import { displayWidth } from "./editor.js";
46
+ import { MENU_ITEMS, displayWidth } from "./editor.js";
47
47
  import { leadWidth } from "./width.js"; // W23: the ONE width authority (the editor, #inputRow, and editCol share it)
48
48
  // KC3.5: the panel-slot reads come from the DISPATCHERS — one source
49
49
  // for four reads, so an ask can never render half as an approval.
@@ -52,7 +52,7 @@ import { MOUSE_OFF } from "./editor.js";
52
52
  import { atPanelRows, bandHeader } from "./at-picker.js";
53
53
  // TUI2-R2 ②: the session picker's rows — the band's third occupant.
54
54
  import { sessionPickerRows } from "./session-picker.js";
55
- import { ACT_SLOT_ROWS, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, cutLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, statusLine, stretchLine, turnFold, visibleWidth, breathFrame, } from "./components.js";
55
+ import { ACT_SLOT_ROWS, Container, ROLLUP_NOUN, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, exploreCounts, foldCountsObjects, foldTerms, focusToken, exploreRows, foldLine, gutterCut, cutLine, isExploreTool, moreRunningRow, pendingQueueRows, slotPad, slotTail, statusLine, stretchLine, turnFold, visibleWidth, breathFrame, } from "./components.js";
56
56
  import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, toolTarget } from "./render.js";
57
57
  import { displayVerb, keysSheetRows } from "./strings.js";
58
58
  // R5 — the transcript viewer's PURE projection. The compositor supplies
@@ -77,6 +77,9 @@ const CHROME_ROWS = 4; // box top + input + box bottom + status — the design
77
77
  * same unbounded height the projection exists to remove; the rest are
78
78
  * COUNTED, never dropped silently. */
79
79
  const LIVE_ACT_HEADS = 3;
80
+ /** R8 — the command band's window: five rows plus a counter, the same
81
+ * budget the composer's own ceiling can afford above it. */
82
+ const MENU_WINDOW = 5;
80
83
  /** W13 / TUI2-R1 (B) — a rolled run's TITLE: the exploration sentence on
81
84
  * a mixed run, W13's verb+count on a single-name one. */
82
85
  function rolledTitle(cell) {
@@ -1333,7 +1336,42 @@ export class Body {
1333
1336
  cutLine(`${p.dim} ${viewerHint(this.#viewer, entries)}${p.reset}`, W),
1334
1337
  ];
1335
1338
  }
1339
+ /** DC-35 — the last block this key appended, and the cell count when
1340
+ * it did. An expansion earns its rows by showing something the
1341
+ * transcript does not already END with. */
1342
+ #lastAppend = null;
1343
+ /**
1344
+ * DC-35 — ctrl+r does not print the same expansion twice in a row.
1345
+ *
1346
+ * The ring walks newest-back and restarts its cycle once every entry
1347
+ * has been opened (R4/C1, which is what makes the walk immune to the
1348
+ * ring growing underneath it). With a ring of ONE the restart is
1349
+ * immediate, so holding the key appended the identical block over
1350
+ * and over — the owner got three copies of the same four rows, each
1351
+ * closing with `ctrl+r opens the one before it`, a footer naming
1352
+ * something that does not exist.
1353
+ *
1354
+ * The bar is the BOTTOM of the transcript, not "ever shown": once
1355
+ * other content has arrived the expansion has scrolled up and
1356
+ * re-opening it is the point of the key, so the guard clears itself
1357
+ * the moment a cell is added.
1358
+ */
1336
1359
  expandNext() {
1360
+ const out = this.#expandNextRaw();
1361
+ if (out.kind !== "appended")
1362
+ return out;
1363
+ const lines = out.lines.join("\n");
1364
+ if (this.#lastAppend !== null && this.#lastAppend.lines === lines && this.#lastAppend.atCells === this.#cells.length) {
1365
+ // NOT the same answer as "nothing is folded". The caller says
1366
+ // which, because a reader who pressed the key deserves to know
1367
+ // whether there is nothing to open or whether they are already
1368
+ // looking at it.
1369
+ return { kind: "none", why: "already-last" };
1370
+ }
1371
+ this.#lastAppend = { lines, atCells: this.#cells.length };
1372
+ return out;
1373
+ }
1374
+ #expandNextRaw() {
1337
1375
  for (let i = this.#cells.length - 1; i >= this.#committed; i -= 1) {
1338
1376
  const cell = this.#cells[i];
1339
1377
  if (cell.kind === "tool" && cell.state !== "pending") {
@@ -1426,7 +1464,9 @@ export class Body {
1426
1464
  // always the part a reader could use.
1427
1465
  `${p.bold}✦${p.reset} expanded · ${escapeTerminal(head.length === 0 ? "thinking" : head.join(" · "))} · ${back}`,
1428
1466
  ...body,
1429
- ` ${p.dim}└ end of expansion · ctrl+r opens the one before it${p.reset}`,
1467
+ // R8a: an in-block note takes the block's indent, not a
1468
+ // second corner — the corner opens the body above it.
1469
+ `${p.dim} end of expansion · ctrl+r opens the one before it${p.reset}`,
1430
1470
  ],
1431
1471
  };
1432
1472
  }
@@ -3261,14 +3301,40 @@ export class Body {
3261
3301
  // picker's does. Both render frameless directly above the composer,
3262
3302
  // so with scrollback behind them there was nothing to say where the
3263
3303
  // surface began — the rows read as more history.
3304
+ // R8 — THE BAND IS A WINDOW, and the rows are a table.
3305
+ //
3306
+ // It used to draw every match and fold each long description over
3307
+ // as many rows as it took, which is why a bare `/` could not open
3308
+ // it: eleven commands plus wraps is most of a short terminal. A
3309
+ // fixed window is what lets the trigger be the `/` the banner
3310
+ // advertises (see the editor's #menuFiltered).
3311
+ //
3312
+ // Three shape rules, all of them §1.3 or §1.2:
3313
+ // - the leading `/` comes off the rows. It is already on the
3314
+ // input line directly below, so printing it eleven more times
3315
+ // is a mark carrying no fact the screen does not have.
3316
+ // - the name column is padded to the longest command in the
3317
+ // WHOLE list, not the visible slice, so the descriptions do
3318
+ // not shift sideways as the window scrolls.
3319
+ // - a description is CUT, never folded — a folded row would
3320
+ // break the window's height, which is the thing being bought.
3321
+ const items = menu.items;
3322
+ const col = MENU_ITEMS.reduce((n, m) => Math.max(n, m.name.length - 1), 0);
3323
+ const windowed = items.length > MENU_WINDOW;
3324
+ // the window's top is derived from the selection alone (this
3325
+ // method is re-entered per frame and keeps no state): centre it,
3326
+ // clamped to the ends.
3327
+ const top = windowed ? Math.max(0, Math.min(menu.selected - ((MENU_WINDOW - 1) >> 1), items.length - MENU_WINDOW)) : 0;
3264
3328
  const rows = [bandHeader("commands", W)];
3265
- for (let i = 0; i < menu.items.length; i += 1) {
3266
- const item = menu.items[i];
3267
- const text = i === menu.selected
3268
- ? `${p.bold} ${item.name}${p.reset} ${item.desc}`
3269
- : `${p.dim} ${item.name} ${item.desc}${p.reset}`;
3270
- rows.push(...foldLine(text, W));
3271
- }
3329
+ for (let i = top; i < Math.min(items.length, top + MENU_WINDOW); i += 1) {
3330
+ const item = items[i];
3331
+ const label = `${item.name.slice(1).padEnd(col)} ${item.desc}`;
3332
+ rows.push(...(i === menu.selected ? gutterCut(`${p.bold}▸${p.reset} `, `${p.bold}${label}${p.reset}`, W) : gutterCut(" ", `${p.dim}${label}${p.reset}`, W)));
3333
+ }
3334
+ // the counter earns its row only when the list is CUT — over a
3335
+ // list you can see all of, it says nothing the rows do not.
3336
+ if (windowed)
3337
+ rows.push(` ${p.dim}(${menu.selected + 1}/${items.length})${p.reset}`);
3272
3338
  return rows;
3273
3339
  }
3274
3340
  /** ONE input row's bytes — the marker embedded at `embedAt` (the
package/dist/editor.js CHANGED
@@ -542,11 +542,20 @@ export class Editor {
542
542
  return null;
543
543
  return { items: this.#menuFiltered(), selected: this.#menuSel };
544
544
  }
545
- /** v3 §04: the filtered command list for the current buffer — open
546
- * only while the line is "/" + something (a bare "/" waits). */
545
+ /** v3 §04: the filtered command list for the current buffer.
546
+ *
547
+ * A BARE `/` OPENS IT (owner-ruled 2026-09-01). It used to wait for
548
+ * a second character, which made the key the banner advertises —
549
+ * `/ commands` — a thing you had to already know the answer to: the
550
+ * list that tells you the commands appeared only once you had typed
551
+ * one. The reason for the wait was real and is fixed on the other
552
+ * side: the band drew EVERY match with no window, so a bare `/`
553
+ * would have piled eleven rows plus wraps above the composer. The
554
+ * band windows now (see the compositor's #menuRows), so the trigger
555
+ * no longer has to do the rationing. */
547
556
  #menuFiltered() {
548
557
  const line = this.line();
549
- if (!line.startsWith("/") || line === "/")
558
+ if (!line.startsWith("/"))
550
559
  return [];
551
560
  return MENU_ITEMS.filter((m) => m.name.startsWith(line));
552
561
  }
@@ -995,7 +1004,11 @@ export class Editor {
995
1004
  i += 1;
996
1005
  continue;
997
1006
  }
998
- if (!typing && (c === "t" || c === "T")) {
1007
+ // DC-36: no `t` row means no custom phase to enter — the
1008
+ // option list IS the world (a closed set), and a key
1009
+ // that leads to a surface the panel does not draw is
1010
+ // worse than an absent key.
1011
+ if (!typing && (c === "t" || c === "T") && this.#panel?.view.pick?.typeHint !== undefined) {
999
1012
  panel.pick = { cursor: panel.pick.cursor, phase: "custom" };
1000
1013
  this.#chars = [];
1001
1014
  this.#cursor = 0;
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * editor, the diff renderer, and the palette.
8
8
  */
9
9
  export { Body, Dock, CURSOR_MARKER, type BodyOptions } from "./compositor.js";
10
- export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, PICK_MAX, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, type PickOption, type PickResult, type PickRuntime, type PickSpec, type PanelArgs, type PanelFlavor, type PanelPhase, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, type SaferAnswer, type SaferFailure, type SaferOption, type SaferRuntime, panelOptions, type PanelOption, type PanelOptionKind, type PanelState, type PanelVerdict, type PanelView, } from "./approval-panel.js";
10
+ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, PICK_MAX, modePickView, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, type PickOption, type PickResult, type PickRuntime, type PickSpec, type PanelArgs, type PanelFlavor, type PanelPhase, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, type SaferAnswer, type SaferFailure, type SaferOption, type SaferRuntime, panelOptions, type PanelOption, type PanelOptionKind, type PanelState, type PanelVerdict, type PanelView, } from "./approval-panel.js";
11
11
  export { Container, foldLine, foldWords, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
12
12
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
13
13
  export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type PathResolver, type RecapStats, type ResumeMeta, type RenderInput, type RenderResult, type RunUsage, } from "./render.js";
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { Body, Dock, CURSOR_MARKER } from "./compositor.js";
12
12
  // approval is pending (the shape authority is the committed preview).
13
13
  export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus,
14
14
  // TUI2-R2 ④: the pick payload — the panel slot's third occupant.
15
- PICK_MAX, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, panelOptions, } from "./approval-panel.js";
15
+ PICK_MAX, modePickView, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, panelOptions, } from "./approval-panel.js";
16
16
  export { Container, foldLine, foldWords, visibleWidth, SPINNER } from "./components.js";
17
17
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
18
18
  export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "kiso tui — 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.20.0"
38
+ "@vincemakes/kiso-tui-cells": "0.20.2"
39
39
  }
40
40
  }