@vincemakes/kiso-tui-cells 0.39.2 → 0.40.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.
@@ -162,6 +162,19 @@ export type BodyCell = {
162
162
  kind: "notice";
163
163
  text: string;
164
164
  done: true;
165
+ }
166
+ /** 4c — the resumed session's earlier history, replayed into cells and
167
+ * FOLDED: the row is ONE line on screen and never expands there (a
168
+ * resize reprint of a six-thousand-event session redraws one row);
169
+ * the children are the replayed cells, read in the ctrl+r viewer.
170
+ * `summary` is a compaction checkpoint's text — what the model sees in
171
+ * place of the turns it covers — null for a plain fold. */
172
+ | {
173
+ kind: "fold";
174
+ label: string;
175
+ children: BodyCell[];
176
+ summary: string | null;
177
+ done: true;
165
178
  } | {
166
179
  kind: "banner";
167
180
  version: string;
@@ -272,7 +285,9 @@ export declare function foldTerms(reads: number, edits: number, others: readonly
272
285
  * reads a payload it did not write, and a guess about what it means
273
286
  * would be a row the product cannot stand behind.
274
287
  */
275
- export declare function askedBlock(resultText: string, seconds: number, W: number): string[];
288
+ /** `seconds` null: 4c's replayed card, settled from a log with no clock —
289
+ * the head names the outcome and says nothing about time. */
290
+ export declare function askedBlock(resultText: string, seconds: number | null, W: number): string[];
276
291
  /** The caps — screen rows counted AFTER the fold, at the current width
277
292
  * (the W7 table). The renderer-cut row is inside the cap. */
278
293
  /** R13 — ONE preview cap, every tool. It was the shell's alone while
@@ -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, foldThinkingRow, foldResult, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, currentGround, } from "./render.js";
33
+ import { bannerLines, breathFrame, cutLine, escapeTerminal, stripAnsi, 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
@@ -179,6 +179,8 @@ export function cellComponent(cell) {
179
179
  return new MarkdownBlock(cell);
180
180
  case "notice":
181
181
  return new ErrorLine(cell);
182
+ case "fold":
183
+ return new FoldRow(cell);
182
184
  case "banner":
183
185
  return new Banner(cell);
184
186
  case "raw":
@@ -627,7 +629,7 @@ class ToolExecution {
627
629
  // that is not the ask's own JSON, so a payload this renderer
628
630
  // did not write can never be guessed at.
629
631
  if (c.name === "ask_user" && c.reason === null && !c.isError) {
630
- const asked = askedBlock(c.resultText, c.startedAt !== null && c.doneAt !== null ? (c.doneAt - c.startedAt) / 1000 : 0, W);
632
+ const asked = askedBlock(c.resultText, c.startedAt !== null && c.doneAt !== null ? (c.doneAt - c.startedAt) / 1000 : c.startedAt === null && c.doneAt === null ? null : 0, W);
631
633
  if (asked.length > 0)
632
634
  return asked;
633
635
  }
@@ -644,7 +646,10 @@ class ToolExecution {
644
646
  out.push(...toolBlockBody(c, W, ctx));
645
647
  return out;
646
648
  }
647
- const elapsed = c.startedAt !== null && c.doneAt !== null ? settledLabel((c.doneAt - c.startedAt) / 1000) : "?s";
649
+ // 4c: a card settled from the durable log carries no clock at all
650
+ // (the log's events have no timestamps) — it says nothing about
651
+ // time rather than `?s`, which reads as a measurement that failed.
652
+ const elapsed = c.startedAt !== null && c.doneAt !== null ? settledLabel((c.doneAt - c.startedAt) / 1000) : c.startedAt === null && c.doneAt === null ? "" : "?s";
648
653
  // TUI2-R1.5 ⑤ (VD-6): the line count is stated EXACTLY ONCE. Every
649
654
  // read card carried it twice — `(2 lines, 0.0s) · 2 lines · ctrl+o
650
655
  // expands` — because the parens and the suffix were written by
@@ -1099,6 +1104,8 @@ export function foldTerms(reads, edits, others) {
1099
1104
  * reads a payload it did not write, and a guess about what it means
1100
1105
  * would be a row the product cannot stand behind.
1101
1106
  */
1107
+ /** `seconds` null: 4c's replayed card, settled from a log with no clock —
1108
+ * the head names the outcome and says nothing about time. */
1102
1109
  export function askedBlock(resultText, seconds, W) {
1103
1110
  let parsed;
1104
1111
  try {
@@ -1111,7 +1118,7 @@ export function askedBlock(resultText, seconds, W) {
1111
1118
  return [];
1112
1119
  const asked = parsed;
1113
1120
  const p = palette();
1114
- const head = (n, outcome) => cutLine(` ${p.bold}asked${p.reset} ${n} ${n === 1 ? "question" : "questions"} ${p.dim}(${outcome}, ${seconds.toFixed(1)}s)${p.reset}`, W);
1121
+ const head = (n, outcome) => cutLine(` ${p.bold}asked${p.reset} ${n} ${n === 1 ? "question" : "questions"} ${p.dim}(${seconds === null ? outcome : `${outcome}, ${seconds.toFixed(1)}s`})${p.reset}`, W);
1115
1122
  const row = (body) => cutLine(` ${p.dim}│${p.reset} ${body}`, W);
1116
1123
  if (Array.isArray(asked.declined) && asked.declined.length > 0) {
1117
1124
  // the honest decline record: WHAT went unanswered, and what the
@@ -1439,7 +1446,8 @@ function blockRows(text, W, tone = "dim") {
1439
1446
  // rows around them take `washDim`, which was chosen for that ground.
1440
1447
  const open = tone === "dim" ? p.dim : "";
1441
1448
  const close = tone === "dim" ? p.reset : "";
1442
- for (const raw of escapeTerminal(text).split("\n")) {
1449
+ // 0.40.0: the output's own styling is dropped whole before the escape
1450
+ for (const raw of escapeTerminal(stripAnsi(text)).split("\n")) {
1443
1451
  for (const row of foldLine(raw, textW))
1444
1452
  rows.push(`${open}${bodyRow()}${row}${close}`);
1445
1453
  }
@@ -1643,7 +1651,12 @@ export function diffBody(diff, W, expanded = false) {
1643
1651
  ? `${p.red}- ${escapeTerminal(d.text)}${p.reset}`
1644
1652
  : d.kind === "+"
1645
1653
  ? `${p.green}+ ${escapeTerminal(d.text)}${p.reset}`
1646
- : `${p.dim} ${escapeTerminal(d.text)}${p.reset}`;
1654
+ : d.kind === "note"
1655
+ ? // 0.40.0: a note starts in the MARKER column, where no line
1656
+ // of the file ever does — it reads as kiso's sentence about
1657
+ // the diff, not as an unchanged line inside it
1658
+ `${p.dim}${escapeTerminal(d.text)}${p.reset}`
1659
+ : `${p.dim} ${escapeTerminal(d.text)}${p.reset}`;
1647
1660
  // W2: the diff body is a bounded block's body — the │ gutter
1648
1661
  // (dim), never the old bold ▎ rail (the table lists no ▎); the
1649
1662
  // +/- marks and their colors ride the content
@@ -1728,6 +1741,19 @@ class ErrorLine {
1728
1741
  return foldWords(escapeTerminal(this.cell.text), W);
1729
1742
  }
1730
1743
  }
1744
+ /** 4c — the folded history's ONE row. Dim, like every row that is about
1745
+ * the transcript rather than in it; cut, never wrapped, so it is one row
1746
+ * at every width. */
1747
+ class FoldRow {
1748
+ cell;
1749
+ constructor(cell) {
1750
+ this.cell = cell;
1751
+ }
1752
+ render(W, _ctx) {
1753
+ const p = palette();
1754
+ return [cutLine(` ${p.dim}${escapeTerminal(this.cell.label)}${p.reset}`, W)];
1755
+ }
1756
+ }
1731
1757
  /** The CLI's pre-rendered blocks (the banner, the recap, slash-command
1732
1758
  * output) — the SGR applied at composition (render.ts), folded here
1733
1759
  * verbatim: the #16b contract (no re-escaping) holds, and the fold is
package/dist/diff.d.ts CHANGED
@@ -10,9 +10,12 @@
10
10
  * RENDERER truncates (18 head + 18 tail + "… N lines"); the stats come
11
11
  * from the full diff.
12
12
  */
13
- /** The diff block's per-row kind. */
13
+ /** The diff block's per-row kind. `note` (0.40.0) is kiso's sentence
14
+ * ABOUT the diff — the renderer's cut, a search that is not there or is
15
+ * there more than once — never a line of the file, so never drawn where
16
+ * the file's lines are. */
14
17
  export type DiffLine = {
15
- kind: "-" | "+" | " ";
18
+ kind: "-" | "+" | " " | "note";
16
19
  text: string;
17
20
  };
18
21
  export interface DiffResult {
package/dist/diff.js CHANGED
@@ -76,7 +76,7 @@ export function truncateDiff(diff) {
76
76
  const omitted = diff.length - 2 * TRUNCATE_KEEP;
77
77
  return [
78
78
  ...diff.slice(0, TRUNCATE_KEEP),
79
- { kind: " ", text: `… ${omitted} lines (/last for full)` },
79
+ { kind: "note", text: `… ${omitted} lines (/last for full)` },
80
80
  ...diff.slice(diff.length - TRUNCATE_KEEP),
81
81
  ];
82
82
  }
@@ -117,7 +117,7 @@ export function editFileDiff(oldContent, search, replace, path) {
117
117
  const at = oldContent.indexOf(search);
118
118
  if (at < 0) {
119
119
  return {
120
- lines: [{ kind: " ", text: `pattern not found in ${path ?? "the file"}` }],
120
+ lines: [{ kind: "note", text: `pattern not found in ${path ?? "the file"}` }],
121
121
  added: 0,
122
122
  removed: 0,
123
123
  outcome: "not-found",
@@ -131,7 +131,7 @@ export function editFileDiff(oldContent, search, replace, path) {
131
131
  // same rule as the miss above, for the same reason.
132
132
  if (search.length > 0 && oldContent.indexOf(search, at + 1) > at) {
133
133
  return {
134
- lines: [{ kind: " ", text: `pattern matches more than one place in ${path ?? "the file"}` }],
134
+ lines: [{ kind: "note", text: `pattern matches more than one place in ${path ?? "the file"}` }],
135
135
  added: 0,
136
136
  removed: 0,
137
137
  outcome: "ambiguous",
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { pendingQueueRows } from "./components.js";
12
12
  export { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
13
13
  export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, type PanelArgs, type PanelFlavor, type PanelPhase, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, type SaferOption, type SaferRuntime, panelOptions, type PanelOption, type PanelOptionKind, type PanelState, type PanelVerdict, type PanelView, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, } from "./approval-panel.js";
14
14
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact, } from "./strings.js";
15
- export { extensionsBannerText, helpRows, unansweredAskView, type BannerExtension } from "./strings.js";
15
+ export { coldResumeLine, coldResumeView, extensionsBannerText, helpRows, unansweredAskView, type BannerExtension } from "./strings.js";
16
16
  export { displayVerb } from "./strings.js";
17
17
  export { bannerLines, COLOR_OFF, COLOR_DARK, COLOR_LIGHT, COLOR_NEUTRAL, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, MOTION_FRAMES, TWINKLE, breathFrame, twinkleFrame, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type ResumeMeta, } from "./render.js";
18
18
  /** DC-3 — the ground: is the terminal light or dark. Pure; see the
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWi
24
24
  // the cli; what the human reads is presentation.
25
25
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, } from "./strings.js";
26
26
  // KC3.5: the interrupted-ask copy and the extracted /help table.
27
- export { extensionsBannerText, helpRows, unansweredAskView } from "./strings.js";
27
+ export { coldResumeLine, coldResumeView, extensionsBannerText, helpRows, unansweredAskView } from "./strings.js";
28
28
  // TUI2-R2pre ④: the ONE display-verb table — the screen names the act,
29
29
  // the tool table names the call.
30
30
  export { displayVerb } from "./strings.js";
package/dist/render.d.ts CHANGED
@@ -146,12 +146,7 @@ export declare const COLOR_OFF: Palette;
146
146
  export declare function setGround(g: Ground): void;
147
147
  export declare function currentGround(): Ground;
148
148
  export declare function palette(): Palette;
149
- /**
150
- * E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
151
- * reaches the terminal — ESC, C0 (except \t \n), C1, CR, backspace, and
152
- * bidi overrides. The kiso colors are applied by render, not by the data.
153
- * EVERY externally-sourced string must pass through this before any output.
154
- */
149
+ export declare function stripAnsi(text: string): string;
155
150
  export declare function escapeTerminal(text: string): string;
156
151
  /**
157
152
  * v2b — one thinking BLOCK folds to ONE dim line: the first 100 chars, a
package/dist/render.js CHANGED
@@ -97,6 +97,25 @@ export function palette() {
97
97
  * bidi overrides. The kiso colors are applied by render, not by the data.
98
98
  * EVERY externally-sourced string must pass through this before any output.
99
99
  */
100
+ /**
101
+ * 0.40.0 (the owner's dogfood) — a tool's OUTPUT, shown without its
102
+ * terminal styling. escapeTerminal drops the ESC byte and nothing else, so
103
+ * a coloured test run reached the card as `[31m─── [1m[41m Failed Tests`.
104
+ * Here the whole sequence goes: CSI (colours, cursor moves), OSC (titles,
105
+ * links), DCS/SOS/PM/APC strings, charset selections, the two-byte escapes,
106
+ * and the 8-bit CSI. Every branch is linear: a string body stops at the
107
+ * first ESC (only its terminator may hold one), so hostile output — an
108
+ * unterminated `ESC P` repeated — never scans to the end of the text once
109
+ * per sequence (the lead's review: that was n²/2 on the render path).
110
+ * Only output bodies pass through here — in a NAME the
111
+ * `[31m` remnant is the visible sign of an injected sequence, and stripping
112
+ * it would let `sh<ESC>[31mell` read as `shell`.
113
+ */
114
+ // eslint-disable-next-line no-control-regex
115
+ const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[PX^_][^\x1b]*\x1b\\|\x1b[ -/]+[0-~]|\x1b[@-Z\\-_]|\x9b[0-?]*[ -/]*[@-~]/g;
116
+ export function stripAnsi(text) {
117
+ return text.replace(ANSI_SEQUENCE, "");
118
+ }
100
119
  export function escapeTerminal(text) {
101
120
  // eslint-disable-next-line no-control-regex
102
121
  return text
package/dist/strings.d.ts CHANGED
@@ -96,6 +96,16 @@ export declare function uncertainView(name: string, executionId: string): PanelV
96
96
  * back. Re-asking is safe — that is what the first option says out loud.)
97
97
  */
98
98
  export declare function unansweredAskView(executionId: string): PanelView;
99
+ /**
100
+ * 0.40.0 (the owner's session) — the cold resume. A session resumed 27
101
+ * minutes after its last request re-sent a 727k prefix the provider had
102
+ * evicted. The first request after a long pause pays for the whole prefix
103
+ * either way; compacting first turns that one expensive request into a
104
+ * summary call, and every turn after it is small. The line names the size
105
+ * and the age, so the person can judge.
106
+ */
107
+ export declare function coldResumeLine(tokens: number, minutesAgo: number): string;
108
+ export declare function coldResumeView(tokens: number, minutesAgo: number): PanelView;
99
109
  /** An extension as the banner names it — the live `connecting` flag is
100
110
  * the MCP bridge's in-flight state ("mcp (connecting…)"). Structural on
101
111
  * purpose: the runtime's KisoExtension satisfies it without this
@@ -103,6 +113,9 @@ export declare function unansweredAskView(executionId: string): PanelView;
103
113
  export interface BannerExtension {
104
114
  readonly name: string;
105
115
  readonly connecting?: boolean;
116
+ /** A fact about the extension in this session, printed in parentheses
117
+ * after its name — "ask (off in dontAsk)". `connecting` wins. */
118
+ readonly note?: string;
106
119
  }
107
120
  /**
108
121
  * KC3.5 slice ⓪ (the extraction) — the `[N extensions: …]` banner text:
@@ -188,7 +201,7 @@ export declare function keysSheetRows(W: number): string[];
188
201
  * sheet renders, joined — so the two can disagree only by deleting a
189
202
  * test. The sheet is the readable form; this is the greppable one. */
190
203
  export declare function keysHelpRow(): string;
191
- /** The slash commands `/help` lists. The keys and the `!` gestures in the
192
- * table are not commands and are not here. */
204
+ /** The slash commands `/help` lists. The keys, the `!` gestures and the
205
+ * `/<name>` skill rule in the table are not commands and are not here. */
193
206
  export declare function slashCommandNames(): readonly string[];
194
207
  export declare function helpRows(): string[];
package/dist/strings.js CHANGED
@@ -143,6 +143,31 @@ export function unansweredAskView(executionId) {
143
143
  fallbackQuestion: `an unanswered question was interrupted (${executionId}) — ask it again? (y)es / (n)o `,
144
144
  };
145
145
  }
146
+ /**
147
+ * 0.40.0 (the owner's session) — the cold resume. A session resumed 27
148
+ * minutes after its last request re-sent a 727k prefix the provider had
149
+ * evicted. The first request after a long pause pays for the whole prefix
150
+ * either way; compacting first turns that one expensive request into a
151
+ * summary call, and every turn after it is small. The line names the size
152
+ * and the age, so the person can judge.
153
+ */
154
+ export function coldResumeLine(tokens, minutesAgo) {
155
+ return `this session is ${Math.round(tokens / 1000)}k tokens, last used ${minutesAgo} min ago, and its cache is cold`;
156
+ }
157
+ export function coldResumeView(tokens, minutesAgo) {
158
+ const line = coldResumeLine(tokens, minutesAgo);
159
+ return {
160
+ flavor: "simple",
161
+ name: "cold cache",
162
+ title: `compact first? (${Math.round(tokens / 1000)}k tokens, ${minutesAgo} min idle)`,
163
+ speaker: "kiso",
164
+ statusText: "❯ resumed session",
165
+ args: { kind: "text", lines: [line, "compacting first is one summary call, then every turn is cheap"] },
166
+ ruleOverride: `${line} — compact first? (one summary call, then every turn is cheap)`,
167
+ simpleOptions: ["compact first", "keep the full history"],
168
+ fallbackQuestion: `${line} — compact first? (y)es / (n)o `,
169
+ };
170
+ }
146
171
  /**
147
172
  * KC3.5 slice ⓪ (the extraction) — the `[N extensions: …]` banner text:
148
173
  * the built-in column, then the user-level names, then the project-level
@@ -158,7 +183,7 @@ export function extensionsBannerText(builtIn, user, project) {
158
183
  const total = builtIn.length + user.length + project.length;
159
184
  if (total === 0)
160
185
  return "";
161
- const label = (e) => (e.connecting === true ? `${e.name} (connecting…)` : e.name);
186
+ const label = (e) => e.connecting === true ? `${e.name} (connecting…)` : e.note !== undefined ? `${e.name} (${e.note})` : e.name;
162
187
  const parts = [];
163
188
  if (builtIn.length > 0)
164
189
  parts.push(`built-in: ${builtIn.map(label).join(", ")}`);
@@ -447,7 +472,7 @@ const HELP_TABLE = [
447
472
  // been dispatchable since TUI2-R1 slice 6 and was never listed here,
448
473
  // so the only way to learn it existed was to read the source.
449
474
  ["/context", "show where the context went — the per-request rent ledger"],
450
- ["/mode", "show the approval tier; /mode <name> switches (manual/default/accept-edits/plan/bypass)"],
475
+ ["/mode", "show the approval tier; /mode <name> switches (default/accept-edits/plan/dontAsk/bypass)"],
451
476
  ["/model", "list model profiles; /model <name|provider/model> switches"],
452
477
  ["/compact", "summarize the older conversation to free context"],
453
478
  ["/clear", "start a fresh conversation (the old session stays resumable)"],
@@ -455,6 +480,12 @@ const HELP_TABLE = [
455
480
  // §2.5: the conversation is untouched — this rereads what kiso was
456
481
  // built with, not what it has said.
457
482
  ["/reload", "reread extensions, skills and config into this session"],
483
+ // 0.40.0: a skill is a user turn — its SKILL.md, then your args. The
484
+ // `/<name>` row says the rule a person needs before they install a
485
+ // skill named like a command: the command wins.
486
+ ["/skills", "list the installed skills, and any that cannot load"],
487
+ ["/skill", "run a skill as your turn: /skill <name> [args]"],
488
+ ["/<name>", "runs the skill <name> when no command above has that name"],
458
489
  // §2.2: the two shell gestures and their one escape. They sit
459
490
  // beside the slash commands because that is what a reader is
460
491
  // looking for when they look here, even though `!` is not one.
@@ -477,10 +508,10 @@ const HELP_TABLE = [
477
508
  // table is mentioned here. DC-1 changes the PADDING, not the words.
478
509
  ["keys", "enter sends \u00b7 ctrl+J newline (shift+enter where encoded) \u00b7 esc stops the run \u00b7 alt+\u23ce stops it and sends this instead \u00b7 @ files \u00b7 1-4 answers an ask"],
479
510
  ];
480
- /** The slash commands `/help` lists. The keys and the `!` gestures in the
481
- * table are not commands and are not here. */
511
+ /** The slash commands `/help` lists. The keys, the `!` gestures and the
512
+ * `/<name>` skill rule in the table are not commands and are not here. */
482
513
  export function slashCommandNames() {
483
- return HELP_TABLE.map(([name]) => name).filter((n) => n.startsWith("/"));
514
+ return HELP_TABLE.map(([name]) => name).filter((n) => /^\/[a-z]+$/.test(n));
484
515
  }
485
516
  export function helpRows() {
486
517
  const p = palette();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.39.2",
3
+ "version": "0.40.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",