@vincemakes/kiso-tui-cells 0.46.2 → 0.47.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/strings.d.ts CHANGED
@@ -47,7 +47,7 @@ export declare function projectTrustRows(files: readonly TrustArtifact[], indent
47
47
  * the artifact listing as the always-verbose args, and the question as
48
48
  * the rule override. The same rows the scrollback records — the panel
49
49
  * is a bounded block, the record is not. */
50
- export declare function projectTrustView(root: string, files: readonly TrustArtifact[]): PanelView;
50
+ export declare function projectTrustView(root: string, files: readonly TrustArtifact[], shownRoot?: string): PanelView;
51
51
  /** E3 — the non-TTY note: artifacts were found and deliberately NOT
52
52
  * loaded, with the one path back (run once interactively). Printed to
53
53
  * stderr by the caller; never silent. */
@@ -72,7 +72,7 @@ export declare function projectUntrustedNote(count: number, root: string): strin
72
72
  * applied"); what follows it is the action the answer performs. The
73
73
  * invariant a test now holds for every simple view: the dock-less
74
74
  * question names `simpleOptions[0]`, the action `y` performs. */
75
- export declare function uncertainView(name: string, executionId: string): PanelView;
75
+ export declare function uncertainView(name: string, executionId: string, target?: readonly string[]): PanelView;
76
76
  /**
77
77
  * KC3.5 — the SAME uncertainty gate, said honestly for an ask_user call.
78
78
  *
@@ -87,7 +87,7 @@ export declare function uncertainView(name: string, executionId: string): PanelV
87
87
  * of idempotency, so an interrupted ask meets this gate on the way
88
88
  * back. Re-asking is safe — that is what the first option says out loud.)
89
89
  */
90
- export declare function unansweredAskView(executionId: string): PanelView;
90
+ export declare function unansweredAskView(executionId: string, questions?: readonly string[]): PanelView;
91
91
  /**
92
92
  * 0.40.0 (the owner's session) — the cold resume. A session resumed 27
93
93
  * minutes after its last request re-sent a 727k prefix the provider had
@@ -122,6 +122,15 @@ export interface BannerExtension {
122
122
  * reads `built-in: mcp, skills, subagent`, from this one composition.
123
123
  */
124
124
  export declare function extensionsBannerText(builtIn: readonly BannerExtension[], user: readonly BannerExtension[], project: readonly BannerExtension[]): string;
125
+ /** The last sweep (owner, 2026-10-06) — the same three lists as the
126
+ * opening's EXTENSIONS row: how many, then the names — the built-in ones
127
+ * bare, the user's after `user:`, the project's after `project:`. The
128
+ * pipe's `[N extensions: …]` line stays `extensionsBannerText`'s. Null
129
+ * when nothing loaded. */
130
+ export declare function extensionsFact(builtIn: readonly BannerExtension[], user: readonly BannerExtension[], project: readonly BannerExtension[]): {
131
+ readonly value: string;
132
+ readonly note: string;
133
+ } | null;
125
134
  /** One gesture: what you press, and what it does. */
126
135
  export interface KeyBinding {
127
136
  readonly keys: string;
@@ -129,59 +138,39 @@ export interface KeyBinding {
129
138
  }
130
139
  /**
131
140
  * TUI2-R1 (D) — THE key table. Every reader derives from it: the `?`
132
- * sheet and /help's keys row. A sheet that has drifted from the keys is
141
+ * sheet and `keysHelpRow`. A sheet that has drifted from the keys is
133
142
  * worse than no sheet, and the only way to make drift impossible is to
134
143
  * have one table and no second copy of it.
135
144
  *
136
- * The order is the sheet's reading order, which is why it is grouped by
137
- * WHAT A HUMAN IS DOING rather than alphabetically: the three ways to
138
- * put something in (send, newline, files), the three ways to change
139
- * course (stop, redirect, commands), then the walks (history, expand),
140
- * then the completions.
145
+ * The order is the sheet's reading order, two bindings to a row — the
146
+ * sheets round (owner, 2026-10-06): the sheet is two columns, left then
147
+ * right, so the table is read in pairs. Grouped by WHAT A HUMAN IS DOING:
148
+ * putting something in and changing course, then finding things, then
149
+ * seeing more, then editing, then the clipboard, then the shell. A key
150
+ * with two spellings is named by one (alt+⏎ also answers to ctrl+⏎,
151
+ * alt+←→ to ctrl+←→); ctrl+w stays named because it is the word deletion
152
+ * that works on every terminal (§8.6). `ctrl+t`, `ctrl+g` and the `!`
153
+ * gestures joined from /help's table, which named them and the sheet did
154
+ * not; `? this sheet` left, because `?` is how you got here.
141
155
  */
142
156
  export declare const KEY_BINDINGS: readonly KeyBinding[];
143
157
  /** A tool's name as the SCREEN says it. Display-only: the raw name stays
144
158
  * on the cell, and dispatch, the mode gate, the policy keys, the /last
145
159
  * RAW block and every model-facing byte keep reading that. */
146
160
  export declare function displayVerb(name: string): string;
147
- /** The panel keys, which belong to a panel rather than the composer —
148
- * one dim line rather than four table rows, because they apply only
149
- * while a panel is up. */
150
- /**
151
- * TUI2-R1.5 pin 6 — this row has to be true of BOTH panel flavors.
152
- *
153
- * The R1.5 wording ("digits pick · ⏎ confirms") was the sentence that
154
- * covered an approval where a digit only SELECTED and an ask where a
155
- * digit ANSWERED. TUI2-R3v2 ① removed the disagreement it was papering
156
- * over: every panel is a list with a bar on it, ↑↓ move the bar, ⏎ (or a
157
- * click) takes the row under it, and a digit takes its row outright.
158
- * One sentence, now true of every flavor for the same reason rather than
159
- * by careful omission.
160
- *
161
- * The two ask-only gestures keep their clauses because the ask is the
162
- * only flavor with a set to build and an answer to write.
163
- */
164
- /**
165
- * R6/D2 — the row claims only what is TRUE OF EVERY panel.
166
- *
167
- * Its old sentence ("⏎ or click confirms · 1-4 instant · space toggles")
168
- * was documented as true of every flavor, and had silently stopped
169
- * being: `1-4 instant` is false on the ask's multi-select and on the
170
- * pick panel (where a digit only moves the cursor and never commits),
171
- * `or click` is false on both (their frames deliberately report no
172
- * clickable span), and "1-4" is wrong whenever a panel has a different
173
- * option count. Layered honesty: this row states the invariant, and each
174
- * panel's own affordance row states that panel's whole truth.
175
- */
176
- export declare const PANEL_KEYS_ROW = "panels: \u2191\u2193 move \u00B7 \u23CE confirms \u00B7 digits act on their row \u00B7 t types";
177
- /**
178
- * TUI2-R1 (D) — the sheet, one screen, static.
161
+ /** Graphite R3e — how every read-only sheet closes, on its last row: esc
162
+ * closes it, and what is typed goes to the input. The sheets round
163
+ * (owner, 2026-10-06) gave the keys sheet the same row and the same
164
+ * behaviour as `/status`.
179
165
  *
180
- * Rows CUT at the width rather than folding: the sheet's contract with
181
- * the reader is "one screen", and a folded grid at 40 columns is two
182
- * screens pretending to be one. A narrow terminal shows fewer columns
183
- * of the same truth, which is the honest degradation.
184
- */
166
+ * DECLARED REVERSAL (the sheets round): the keys sheet's last row was
167
+ * `PANEL_KEYS_ROW` (`panels: ↑↓ move · ⏎ confirms · digits act on their
168
+ * row · t types`), with its own grid (`SHEET_GRID`, `SHEET_STOPS`) and
169
+ * a clause-dropping ladder (DC-2). Every panel's own key row now says
170
+ * that panel's keys (§8.2, P3, P4), the row had gone stale (`t types`
171
+ * left `/model` in P3), and the grid was a second and third table that
172
+ * had to agree with KEY_BINDINGS by hand. */
173
+ export declare const SHEET_CLOSE = "esc closes \u00B7 typing goes to the input";
185
174
  /** R8b — the band's own opening row: a labelled rule at full width.
186
175
  *
187
176
  * Moved here from the @ picker, unchanged in every byte, because the
@@ -189,6 +178,18 @@ export declare const PANEL_KEYS_ROW = "panels: \u2191\u2193 move \u00B7 \u23CE c
189
178
  * module — the dependency only runs one way. `at-picker.ts` re-exports
190
179
  * it, so every existing import site is untouched. */
191
180
  export declare function bandHeader(label: string, W: number): string;
181
+ /**
182
+ * TUI2-R1 (D), the sheets round (owner, 2026-10-06) — the keys sheet: the
183
+ * band's named hairline, the bindings two to a row at the content edge,
184
+ * the key in ink and what it does dim, each column measured over the
185
+ * whole table so they line up, and the closing row. Narrower than two
186
+ * columns need, it is one column in the table's order. Rows CUT at the
187
+ * width rather than folding: the sheet's contract is "one screen".
188
+ *
189
+ * DECLARED REVERSAL: the sheet began at column 0 with the keys bold and
190
+ * hand-set column stops (DC-1/DC-3); §1.8's one content edge and the
191
+ * command list's name/description tones replace them.
192
+ */
192
193
  export declare function keysSheetRows(W: number): string[];
193
194
  /** TUI2-R1 (D) — the keys as ONE line, for /help. The same table the
194
195
  * sheet renders, joined — so the two can disagree only by deleting a
package/dist/strings.js CHANGED
@@ -45,7 +45,7 @@ export function projectTrustRows(files, indent = "") {
45
45
  * the artifact listing as the always-verbose args, and the question as
46
46
  * the rule override. The same rows the scrollback records — the panel
47
47
  * is a bounded block, the record is not. */
48
- export function projectTrustView(root, files) {
48
+ export function projectTrustView(root, files, shownRoot = root) {
49
49
  return {
50
50
  flavor: "simple",
51
51
  name: "project trust",
@@ -54,6 +54,10 @@ export function projectTrustView(root, files) {
54
54
  statusText: "❯ project trust",
55
55
  args: { kind: "text", lines: projectTrustRows(files) },
56
56
  ruleOverride: "trust this project's .kiso?",
57
+ // Graphite P1b (owner, 2026-09-30): kiso's own question, and the
58
+ // answers say what they do, like the other gates'
59
+ asked: { question: "trust this project?", facts: [shownRoot], prose: "Its .kiso folder holds these files. kiso loads none of them until you trust the project." },
60
+ simpleOptions: ["trust it", "not now"],
57
61
  fallbackQuestion: `trust this project's .kiso? (y/n) `,
58
62
  };
59
63
  }
@@ -83,15 +87,20 @@ export function projectUntrustedNote(count, root) {
83
87
  * applied"); what follows it is the action the answer performs. The
84
88
  * invariant a test now holds for every simple view: the dock-less
85
89
  * question names `simpleOptions[0]`, the action `y` performs. */
86
- export function uncertainView(name, executionId) {
90
+ export function uncertainView(name, executionId, target = []) {
91
+ // Graphite P1b: the panel quotes WHAT may have run — the command, or the
92
+ // call's target as its tool card names it (the CLI reads it from the
93
+ // execution record) — where it used to quote the execution id twice
94
+ const what = name === "shell" ? "command" : "call";
87
95
  return {
88
96
  flavor: "simple",
89
97
  name: "uncertain execution",
90
98
  title: `${name} (${executionId})`,
91
99
  speaker: "kiso",
92
100
  statusText: "❯ uncertain execution",
93
- args: { kind: "text", lines: [executionId] },
101
+ args: { kind: "text", lines: target.length > 0 ? [...target] : [executionId] },
94
102
  ruleOverride: "an interrupted execution may have applied — rerun it?",
103
+ asked: { question: "rerun it?", facts: [name], prose: `kiso stopped before this ${what}'s result was saved, so it may already have run. Check the workspace, then choose.` },
95
104
  simpleOptions: ["rerun it", "abandon it"],
96
105
  fallbackQuestion: `interrupted execution: ${escapeTerminal(name)} (${executionId}) — rerun it? (y)es / (n)o `,
97
106
  };
@@ -110,15 +119,18 @@ export function uncertainView(name, executionId) {
110
119
  * of idempotency, so an interrupted ask meets this gate on the way
111
120
  * back. Re-asking is safe — that is what the first option says out loud.)
112
121
  */
113
- export function unansweredAskView(executionId) {
122
+ export function unansweredAskView(executionId, questions = []) {
123
+ const many = questions.length > 1;
114
124
  return {
115
125
  flavor: "simple",
116
126
  name: "unanswered question",
117
127
  title: `ask_user (${executionId})`,
118
128
  speaker: "kiso",
119
129
  statusText: "❯ unanswered question",
120
- args: { kind: "text", lines: [executionId] },
130
+ // Graphite P1b: the question itself, quoted — not its execution id
131
+ args: { kind: "text", lines: questions.length > 0 ? [...questions] : [executionId] },
121
132
  ruleOverride: "an unanswered question was interrupted — ask it again?",
133
+ asked: { question: many ? "ask them again?" : "ask it again?", facts: ["never answered"], prose: `The session stopped while ${many ? "these questions" : "this question"} waited for you.` },
122
134
  simpleOptions: ["ask it again", "drop it"],
123
135
  fallbackQuestion: `an unanswered question was interrupted (${executionId}) — ask it again? (y)es / (n)o `,
124
136
  };
@@ -136,13 +148,18 @@ export function coldResumeLine(tokens, minutesAgo) {
136
148
  }
137
149
  export function coldResumeView(tokens, minutesAgo) {
138
150
  const line = coldResumeLine(tokens, minutesAgo);
151
+ const k = `${Math.round(tokens / 1000)}k`;
139
152
  return {
140
153
  flavor: "simple",
141
154
  name: "cold cache",
142
- title: `compact first? (${Math.round(tokens / 1000)}k tokens, ${minutesAgo} min idle)`,
155
+ title: `compact first? (${k} tokens, ${minutesAgo} min idle)`,
143
156
  speaker: "kiso",
144
157
  statusText: "❯ resumed session",
145
- args: { kind: "text", lines: [line, "compacting first is one summary call, then every turn is cheap"] },
158
+ // Graphite P1b: the size and the idle time ride the band; one
159
+ // sentence says what happens and what compacting costs — nothing is
160
+ // quoted, so the gutter is empty
161
+ args: { kind: "text", lines: [] },
162
+ asked: { question: "compact first?", facts: [`${k} tokens`, `idle ${minutesAgo} min`], prose: `The cache expired while the session was idle, so the next request sends all ${k} tokens again. Compacting first is one summary call; the turns after it are small.` },
146
163
  ruleOverride: `${line} — compact first? (one summary call, then every turn is cheap)`,
147
164
  simpleOptions: ["compact first", "keep the full history"],
148
165
  fallbackQuestion: `${line} — compact first? (y)es / (n)o `,
@@ -173,49 +190,76 @@ export function extensionsBannerText(builtIn, user, project) {
173
190
  parts.push(`project: ${project.map(label).join(", ")}`);
174
191
  return ` · [${total} extension${total === 1 ? "" : "s"}: ${parts.join(" · ")}]`;
175
192
  }
193
+ /** The last sweep (owner, 2026-10-06) — the same three lists as the
194
+ * opening's EXTENSIONS row: how many, then the names — the built-in ones
195
+ * bare, the user's after `user:`, the project's after `project:`. The
196
+ * pipe's `[N extensions: …]` line stays `extensionsBannerText`'s. Null
197
+ * when nothing loaded. */
198
+ export function extensionsFact(builtIn, user, project) {
199
+ const total = builtIn.length + user.length + project.length;
200
+ if (total === 0)
201
+ return null;
202
+ const label = (e) => e.connecting === true ? `${e.name} (connecting…)` : e.note !== undefined ? `${e.name} (${e.note})` : e.name;
203
+ const parts = [];
204
+ if (builtIn.length > 0)
205
+ parts.push(builtIn.map(label).join(", "));
206
+ if (user.length > 0)
207
+ parts.push(`user: ${user.map(label).join(", ")}`);
208
+ if (project.length > 0)
209
+ parts.push(`project: ${project.map(label).join(", ")}`);
210
+ return { value: String(total), note: parts.join(" \u00b7 ") };
211
+ }
176
212
  /**
177
213
  * TUI2-R1 (D) — THE key table. Every reader derives from it: the `?`
178
- * sheet and /help's keys row. A sheet that has drifted from the keys is
214
+ * sheet and `keysHelpRow`. A sheet that has drifted from the keys is
179
215
  * worse than no sheet, and the only way to make drift impossible is to
180
216
  * have one table and no second copy of it.
181
217
  *
182
- * The order is the sheet's reading order, which is why it is grouped by
183
- * WHAT A HUMAN IS DOING rather than alphabetically: the three ways to
184
- * put something in (send, newline, files), the three ways to change
185
- * course (stop, redirect, commands), then the walks (history, expand),
186
- * then the completions.
218
+ * The order is the sheet's reading order, two bindings to a row — the
219
+ * sheets round (owner, 2026-10-06): the sheet is two columns, left then
220
+ * right, so the table is read in pairs. Grouped by WHAT A HUMAN IS DOING:
221
+ * putting something in and changing course, then finding things, then
222
+ * seeing more, then editing, then the clipboard, then the shell. A key
223
+ * with two spellings is named by one (alt+⏎ also answers to ctrl+⏎,
224
+ * alt+←→ to ctrl+←→); ctrl+w stays named because it is the word deletion
225
+ * that works on every terminal (§8.6). `ctrl+t`, `ctrl+g` and the `!`
226
+ * gestures joined from /help's table, which named them and the sheet did
227
+ * not; `? this sheet` left, because `?` is how you got here.
187
228
  */
188
229
  export const KEY_BINDINGS = [
189
230
  { keys: "enter", what: "send" },
190
- { keys: "ctrl+j / shift+⏎", what: "newline" },
231
+ { keys: "esc", what: "stop the run" },
232
+ { keys: "ctrl+j", what: "newline (shift+\u23ce too)" },
233
+ { keys: "alt+\u23ce", what: "stop it, send this" },
191
234
  { keys: "@", what: "files" },
192
- { keys: "esc", what: "stop" },
193
- { keys: "alt+⏎ / ctrl+⏎", what: "redirect" },
194
235
  { keys: "/", what: "commands" },
195
- { keys: "↑↓", what: "history / take back a steer" },
196
- { keys: "ctrl+o", what: "expand cells" },
236
+ { keys: "\u2191\u2193", what: "history \u00b7 take back a steer" },
237
+ { keys: "tab", what: "complete" },
238
+ { keys: "ctrl+o", what: "expand all" },
197
239
  // R5 — the transcript viewer. It has to be HERE or it does not exist:
198
240
  // R4a retired the printed key from the fold row on the ground that a
199
241
  // row cannot say which fold a key opens, and the sheet is where the
200
242
  // discoverability moved. A surface nobody can find is not a feature.
201
243
  { keys: "ctrl+r", what: "transcript" },
202
- { keys: "tab", what: "complete (menu / @)" },
203
- { keys: "?", what: "this sheet" },
204
- // E1 §1/§3 — the editor's daily three. Three spellings of the word
205
- // gestures reach the same code (alt, ctrl+arrow, and alt+b/f where
206
- // the terminal sends meta); the sheet names the two a reader is most
207
- // likely to have. ctrl+w is listed beside them because it works on
208
- // every terminal, including the ones that send none of the three.
209
- { keys: "alt+←→ / ctrl+←→", what: "word motion" },
210
- { keys: "alt+⌫ / alt+d", what: "delete word (ctrl+w too)" },
211
- { keys: "ctrl+x", what: "copy the last answer" },
212
- { keys: "ctrl+z / ctrl+y", what: "undo / redo" },
244
+ { keys: "ctrl+t", what: "hide thinking" },
245
+ { keys: "ctrl+g", what: "edit in $EDITOR" },
246
+ // E1 §1/§3 — the editor's daily three (word motion, word deletion,
247
+ // copy); three encodings of the word gestures reach the same code.
248
+ { keys: "alt+\u2190\u2192", what: "word motion" },
249
+ { keys: "alt+\u232b", what: "delete a word (ctrl+w too)" },
250
+ { keys: "ctrl+z", what: "undo" },
251
+ { keys: "ctrl+y", what: "redo" },
252
+ { keys: "ctrl+x", what: "copy the answer" },
213
253
  // REL-0152-D15/D16 — the image paste. It is on the sheet because a
214
254
  // terminal's own Cmd+V only ever pastes TEXT: a human with an image on
215
255
  // the clipboard has no way to discover this key by trying the obvious
216
- // one, which is exactly the case the sheet exists for. It pairs with
217
- // the undo row above and keeps the table's count even.
218
- { keys: "ctrl+v", what: "attach a clipboard image" },
256
+ // one, which is exactly the case the sheet exists for.
257
+ { keys: "ctrl+v", what: "attach an image" },
258
+ { keys: "!cmd", what: "run it and send it" },
259
+ { keys: "!!cmd", what: "run it, show it here only" },
260
+ // the main-sync round (ADR-0058 3e): the background key, beside the
261
+ // shell gestures it serves; the live row teaches it while it applies
262
+ { keys: "ctrl+b", what: "background a command" },
219
263
  ];
220
264
  /**
221
265
  * TUI2-R2pre ④ — THE display-verb table (the integrator's ruling).
@@ -251,93 +295,19 @@ const DISPLAY_VERB = {
251
295
  export function displayVerb(name) {
252
296
  return DISPLAY_VERB[name] ?? name;
253
297
  }
254
- /** The panel keys, which belong to a panel rather than the composer —
255
- * one dim line rather than four table rows, because they apply only
256
- * while a panel is up. */
257
- /**
258
- * TUI2-R1.5 pin 6 — this row has to be true of BOTH panel flavors.
259
- *
260
- * The R1.5 wording ("digits pick · ⏎ confirms") was the sentence that
261
- * covered an approval where a digit only SELECTED and an ask where a
262
- * digit ANSWERED. TUI2-R3v2 ① removed the disagreement it was papering
263
- * over: every panel is a list with a bar on it, ↑↓ move the bar, ⏎ (or a
264
- * click) takes the row under it, and a digit takes its row outright.
265
- * One sentence, now true of every flavor for the same reason rather than
266
- * by careful omission.
267
- *
268
- * The two ask-only gestures keep their clauses because the ask is the
269
- * only flavor with a set to build and an answer to write.
270
- */
271
- /**
272
- * R6/D2 — the row claims only what is TRUE OF EVERY panel.
298
+ /** Graphite R3e — how every read-only sheet closes, on its last row: esc
299
+ * closes it, and what is typed goes to the input. The sheets round
300
+ * (owner, 2026-10-06) gave the keys sheet the same row and the same
301
+ * behaviour as `/status`.
273
302
  *
274
- * Its old sentence ("⏎ or click confirms · 1-4 instant · space toggles")
275
- * was documented as true of every flavor, and had silently stopped
276
- * being: `1-4 instant` is false on the ask's multi-select and on the
277
- * pick panel (where a digit only moves the cursor and never commits),
278
- * `or click` is false on both (their frames deliberately report no
279
- * clickable span), and "1-4" is wrong whenever a panel has a different
280
- * option count. Layered honesty: this row states the invariant, and each
281
- * panel's own affordance row states that panel's whole truth.
282
- */
283
- export const PANEL_KEYS_ROW = "panels: ↑↓ move \u00b7 ⏎ confirms \u00b7 digits act on their row \u00b7 t types";
284
- /** The sheet's grid: the first six bindings in two 3-column rows, the
285
- * last four in two 2-column rows (the wide entries get the room). The
286
- * COLUMN STOPS are the prototype's absolute positions, floored by the
287
- * content (a future binding widens its column rather than overrunning
288
- * it — the grid degrades to one space, never to a collision). */
289
- const SHEET_GRID = [
290
- [0, 1, 2],
291
- [3, 4, 5],
292
- [6, 7],
293
- // R5: ctrl+r joins at index 8, so the tail shifts by one. The grid is
294
- // a SECOND table that must agree with KEY_BINDINGS and nothing makes
295
- // it — adding a binding without touching this silently drops the LAST
296
- // one off the sheet, which is exactly what happened on the first
297
- // attempt and exactly what the "ONE SOURCE" gate exists to catch.
298
- [8, 9],
299
- // UD-1's undo row shares this one now — the table has an even count
300
- // again, so no binding needs a row to itself.
301
- //
302
- // E1 §1/§3 — three more, and the warning above earned itself a second
303
- // time: adding them to KEY_BINDINGS without touching this grid pushed
304
- // the last three off the sheet, and the "ONE SOURCE" gate caught it
305
- // exactly as its comment predicted it would.
306
- [10, 11],
307
- [12, 13],
308
- // ctrl+v joins the undo row's neighbour: the count is even again, so
309
- // the last row pairs instead of standing alone. The warning three
310
- // comments up applies unchanged — this grid does not update itself.
311
- [14, 15],
312
- ];
313
- const SHEET_STOPS = [
314
- [16, 43],
315
- [16, 43],
316
- [36],
317
- [36],
318
- [36],
319
- // E1 §1/§3 — the two-column rows the three new bindings land on.
320
- // A THIRD table that must agree with the other two and nothing makes
321
- // it: `SHEET_GRID` says which bindings share a row, this says where
322
- // their second column starts, and `KEY_BINDINGS` says what they are.
323
- // The grid's own comment warned about the pair; the trio is worse.
324
- // The stop is 39 here because `alt+⌫ / alt+d delete word (ctrl+w too)`
325
- // is the widest first cell on the sheet — a narrower stop packs the
326
- // two cells together with a single space and the column disappears.
327
- // The last row PAIRS now that ctrl+v joined (it had one binding and an
328
- // empty stop list); it takes a stop like its neighbours so the second
329
- // column lines up rather than following a single space.
330
- [39],
331
- [39],
332
- ];
333
- /**
334
- * TUI2-R1 (D) — the sheet, one screen, static.
335
- *
336
- * Rows CUT at the width rather than folding: the sheet's contract with
337
- * the reader is "one screen", and a folded grid at 40 columns is two
338
- * screens pretending to be one. A narrow terminal shows fewer columns
339
- * of the same truth, which is the honest degradation.
340
- */
303
+ * DECLARED REVERSAL (the sheets round): the keys sheet's last row was
304
+ * `PANEL_KEYS_ROW` (`panels: ↑↓ move · ⏎ confirms · digits act on their
305
+ * row · t types`), with its own grid (`SHEET_GRID`, `SHEET_STOPS`) and
306
+ * a clause-dropping ladder (DC-2). Every panel's own key row now says
307
+ * that panel's keys (§8.2, P3, P4), the row had gone stale (`t types`
308
+ * left `/model` in P3), and the grid was a second and third table that
309
+ * had to agree with KEY_BINDINGS by hand. */
310
+ export const SHEET_CLOSE = "esc closes \u00b7 typing goes to the input";
341
311
  /** R8b — the band's own opening row: a labelled rule at full width.
342
312
  *
343
313
  * Moved here from the @ picker, unchanged in every byte, because the
@@ -357,66 +327,54 @@ export function bandHeader(label, W) {
357
327
  out += ch;
358
328
  w += cw;
359
329
  }
360
- return `${p.dim}${out}${p.reset}`;
330
+ // Graphite §8.1 (R3a): the hairline in `line`, the band's name bold
331
+ // gold — the band names itself; off a known ground, one dim span
332
+ if (p.line === "" || p.gold === "")
333
+ return `${p.dim}${out}${p.reset}`;
334
+ const at = out.indexOf("\u2500 ") + 2;
335
+ // the NAME is the label up to its first ` · `; what follows (a scope,
336
+ // a count, a key) is the band's facts, dim
337
+ const sep = label.indexOf(" \u00b7 ");
338
+ const nameEnd = Math.min(out.length, at + (sep < 0 ? label.length : sep));
339
+ const labelEnd = Math.min(out.length, at + label.length);
340
+ return `${p.line}${out.slice(0, at)}${p.fgEnd}${p.bold}${p.gold}${out.slice(at, nameEnd)}${p.reset}${p.dim}${out.slice(nameEnd, labelEnd)}${p.reset}${p.line}${out.slice(labelEnd)}${p.fgEnd}`;
361
341
  }
342
+ /**
343
+ * TUI2-R1 (D), the sheets round (owner, 2026-10-06) — the keys sheet: the
344
+ * band's named hairline, the bindings two to a row at the content edge,
345
+ * the key in ink and what it does dim, each column measured over the
346
+ * whole table so they line up, and the closing row. Narrower than two
347
+ * columns need, it is one column in the table's order. Rows CUT at the
348
+ * width rather than folding: the sheet's contract is "one screen".
349
+ *
350
+ * DECLARED REVERSAL: the sheet began at column 0 with the keys bold and
351
+ * hand-set column stops (DC-1/DC-3); §1.8's one content edge and the
352
+ * command list's name/description tones replace them.
353
+ */
362
354
  export function keysSheetRows(W) {
363
355
  const p = palette();
364
- const cell = (i) => {
365
- const b = KEY_BINDINGS[i];
366
- // DC-3: the key NAMES are the sheet's content, not a code span —
367
- // they borrowed the inline-code tint and became the least readable
368
- // thing on the one screen whose whole job is being read.
369
- return b === undefined ? "" : `${p.bold}${b.keys}${p.reset} ${b.what}`;
370
- };
371
- const plainCell = (i) => {
372
- const b = KEY_BINDINGS[i];
373
- return b === undefined ? "" : `${b.keys} ${b.what}`;
374
- };
375
- // R8b — THE SHEET NAMES ITSELF, in the band vocabulary.
376
- //
377
- // Every other overlay does: `─── commands ───`, `─── files ───`,
378
- // `─── sessions ───`, `── transcript · N folds ──`. This one opened
379
- // with a bare bold word at column 0, which is the exact condition
380
- // TUI2-R1.5 ⑦(b) named when it made the rule — with scrollback
381
- // behind an overlay, nothing said where the surface began.
356
+ const width = (col, f) => Math.max(0, ...col.map((b) => displayWidth(f(b))));
357
+ const cell = (b, kw, dw) => `${b.keys}${" ".repeat(kw - displayWidth(b.keys) + 2)}${p.dim}${b.what}${p.reset}${dw === 0 ? "" : " ".repeat(dw - displayWidth(b.what))}`;
358
+ const left = KEY_BINDINGS.filter((_, i) => i % 2 === 0);
359
+ const right = KEY_BINDINGS.filter((_, i) => i % 2 === 1);
360
+ const kl = width(left, (b) => b.keys);
361
+ const dl = width(left, (b) => b.what);
362
+ const kr = width(right, (b) => b.keys);
363
+ const dr = width(right, (b) => b.what);
382
364
  const rows = [bandHeader("keys", W)];
383
- for (let r = 0; r < SHEET_GRID.length; r += 1) {
384
- const indexes = SHEET_GRID[r];
385
- let row = "";
386
- let width = 0;
387
- for (let c = 0; c < indexes.length; c += 1) {
388
- row += cell(indexes[c]);
389
- width += displayWidth(plainCell(indexes[c]));
390
- const stop = SHEET_STOPS[r][c];
391
- if (stop === undefined)
392
- continue; // the last column pads nothing
393
- const pad = Math.max(1, stop - width);
394
- row += " ".repeat(pad);
395
- width += pad;
365
+ if (2 + kl + 2 + dl + 3 + kr + 2 + dr <= W) {
366
+ for (let i = 0; i < left.length; i += 1) {
367
+ const r = right[i];
368
+ rows.push(` ${cell(left[i], kl, r === undefined ? 0 : dl)}${r === undefined ? "" : ` ${cell(r, kr, 0)}`}`);
396
369
  }
397
- rows.push(row);
398
370
  }
399
- rows.push(`${p.dim}${panelKeysRow(W)}${p.reset}`);
400
- return rows.map((row) => cutLine(row, W));
401
- }
402
- /**
403
- * DC-2 — the panel row degrades by CLAUSE.
404
- *
405
- * The row is 76 columns of independent clauses joined by ` · `. At 72 it
406
- * used to lose the tail of the last one, so `t types` became `t`: the
407
- * reader was told a key existed and not told what it did, on a row that
408
- * still looked complete. Dropping a whole clause says less; it never
409
- * says something false. `cutLine`'s ellipsis is the floor below this, for
410
- * a width that cannot hold even the first clause.
411
- */
412
- function panelKeysRow(W) {
413
- const clauses = PANEL_KEYS_ROW.split(" \u00b7 ");
414
- for (let n = clauses.length; n > 1; n -= 1) {
415
- const row = clauses.slice(0, n).join(" \u00b7 ");
416
- if (displayWidth(row) <= W)
417
- return row;
371
+ else {
372
+ const k = width(KEY_BINDINGS, (b) => b.keys);
373
+ for (const b of KEY_BINDINGS)
374
+ rows.push(` ${cell(b, k, 0)}`);
418
375
  }
419
- return clauses[0];
376
+ rows.push(` ${p.dim}${SHEET_CLOSE}${p.reset}`);
377
+ return rows.map((row) => cutLine(row, W));
420
378
  }
421
379
  /** TUI2-R1 (D) — the keys as ONE line, for /help. The same table the
422
380
  * sheet renders, joined — so the two can disagree only by deleting a
@@ -450,6 +408,7 @@ const HELP_TABLE = [
450
408
  // ADR-0058 (3e): the session's tasks, and the key that makes one
451
409
  ["/tasks", "list this session's background tasks; stop one or show its output"],
452
410
  ["/status", "show session id, event count, and context estimate"],
411
+ ["/name", "name this session · /name shows it · /name - clears it"],
453
412
  // 0.40.6: what kiso runs with, and where each value came from
454
413
  ["/settings", "show the settings in force, each value's source, and how to change it"],
455
414
  // A command with no row is a command nobody can find. `/context` has
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.46.2",
3
+ "version": "0.47.0",
4
4
  "description": "kiso tui-cells \u2014 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",