@vincemakes/kiso-tui-cells 0.31.1 → 0.32.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.
@@ -99,6 +99,7 @@ export type BodyCell = {
99
99
  text: string;
100
100
  done: boolean;
101
101
  turn: number;
102
+ folded?: boolean;
102
103
  } | {
103
104
  kind: "tool";
104
105
  name: string;
@@ -30,7 +30,7 @@ import { displayWidth, visibleWidth, widthCut } from "./width.js";
30
30
  // KEY_BINDINGS). strings.js imports only render/width here, so this edge
31
31
  // adds no cycle.
32
32
  import { displayVerb } from "./strings.js";
33
- import { bannerLines, breathFrame, cutLine, escapeTerminal, foldThinking, foldResult, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, currentGround, } from "./render.js";
33
+ import { bannerLines, breathFrame, cutLine, escapeTerminal, foldThinking, foldThinkingRow, foldResult, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, currentGround, } from "./render.js";
34
34
  // TUI2-MD: the markdown renderer's surface reaches the tui through this
35
35
  // module (the tui's components shim re-exports it) — one import edge,
36
36
  // and it points one way: md.ts measures with the width authority, never
@@ -370,6 +370,17 @@ class ThinkingBlock {
370
370
  const text = escapeTerminal(this.cell.text).trim();
371
371
  if (text === "")
372
372
  return [];
373
+ // §2.3 — ctrl+t folded this block. The row is the pipe's own fold,
374
+ // fitted: `foldThinkingRow` is the single source of the shape and
375
+ // with unlimited room it IS `foldThinking`, so thinking has two
376
+ // renderings in the product and not three. It is width-aware here
377
+ // because a row must measure ≤ W and the pipe's line does not —
378
+ // cutting it from the right would take the `/think` suffix, which
379
+ // is the one part of a folded block that says how to read the rest.
380
+ // It keeps the block's own indent, so a folded block sits in the
381
+ // column an unfolded one does (DC-47).
382
+ if (this.cell.folded)
383
+ return [`${THINK_COL}${foldThinkingRow(this.cell.text, Math.max(1, W - THINK_COL.length))}`];
373
384
  // DC-47 — THINKING GOES ONE LEVEL DEEPER THAN PROSE, and the
374
385
  // reason is a law rather than a taste.
375
386
  //
package/dist/render.d.ts CHANGED
@@ -161,6 +161,21 @@ export declare function escapeTerminal(text: string): string;
161
161
  * strategy is presentation-independent.
162
162
  */
163
163
  export declare function foldThinking(block: string): string;
164
+ /** §2.3 — the same fold, as a ROW that fits `room` columns.
165
+ *
166
+ * A row must measure ≤ W (invariant ①), and the pipe's line does not:
167
+ * `…` + 100 characters + " (N chars · /think)" is about 122 columns, so
168
+ * on an 80-column terminal the frame's cut takes the SUFFIX — which is
169
+ * the affordance, the one part of a folded block that says how to read
170
+ * the rest of it. Cutting from the right removes exactly the thing the
171
+ * fold exists to leave behind.
172
+ *
173
+ * So the suffix is reserved FIRST and the head takes what is left. The
174
+ * vocabulary is unchanged — the leading `…`, the character count, the
175
+ * `/think` route — and with unlimited room the result is byte-for-byte
176
+ * the line the pipe has always written, which is what keeps the two
177
+ * renderings one shape rather than two. */
178
+ export declare function foldThinkingRow(block: string, room: number): string;
164
179
  /** v2b — the [result] echo truncates at 160 chars + a /last hint. */
165
180
  export declare function foldResult(content: string): string;
166
181
  /**
@@ -179,6 +194,22 @@ export declare function renderToolSummary(name: string, input: Record<string, un
179
194
  * the summary detail uses, WITHOUT the counts (the header names what
180
195
  * was expanded, not its size). */
181
196
  export declare function toolTarget(name: string, input: Record<string, unknown>): string;
197
+ /**
198
+ * HF-1 (0.32.1) — a head row is ONE row, whatever the model wrote.
199
+ *
200
+ * The owner's 0.32.0 dogfood: the model issued a heredoc shell command
201
+ * (`python3 - <<'EOF' …`), the running card's head carried its newlines
202
+ * into the compositor, invariant ①b threw in the field and the process
203
+ * died. `escapeTerminal` keeps `\n` on purpose (text blocks need it); a
204
+ * HEAD ROW does not — it is one physical row by construction. So every
205
+ * builder that puts a tool's target on a row projects the breaks to a
206
+ * visible mark first: a line break becomes `⏎` (CRLF is one), a tab
207
+ * becomes one cell of space (its width is a property of the column, and
208
+ * a cut row has no column to give it). The full command is still on the
209
+ * approval panel and in the durable log; the row says what ran, in one
210
+ * row.
211
+ */
212
+ export declare function oneRow(text: string): string;
182
213
  /** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
183
214
  export declare function kUnit(value: number | null): string;
184
215
  /**
package/dist/render.js CHANGED
@@ -113,10 +113,34 @@ export function escapeTerminal(text) {
113
113
  * strategy is presentation-independent.
114
114
  */
115
115
  export function foldThinking(block) {
116
+ // the PIPE's line: no room limit, so the row path below reproduces
117
+ // today's bytes exactly and this stays the one source of the shape.
118
+ return `${foldThinkingRow(block, Number.POSITIVE_INFINITY)}\n`;
119
+ }
120
+ /** §2.3 — the same fold, as a ROW that fits `room` columns.
121
+ *
122
+ * A row must measure ≤ W (invariant ①), and the pipe's line does not:
123
+ * `…` + 100 characters + " (N chars · /think)" is about 122 columns, so
124
+ * on an 80-column terminal the frame's cut takes the SUFFIX — which is
125
+ * the affordance, the one part of a folded block that says how to read
126
+ * the rest of it. Cutting from the right removes exactly the thing the
127
+ * fold exists to leave behind.
128
+ *
129
+ * So the suffix is reserved FIRST and the head takes what is left. The
130
+ * vocabulary is unchanged — the leading `…`, the character count, the
131
+ * `/think` route — and with unlimited room the result is byte-for-byte
132
+ * the line the pipe has always written, which is what keeps the two
133
+ * renderings one shape rather than two. */
134
+ export function foldThinkingRow(block, room) {
116
135
  const p = palette();
117
136
  const trimmed = escapeTerminal(block.trim());
118
137
  const truncated = trimmed.length > 100;
119
- return `${p.dim}…${trimmed.slice(0, 100)}${truncated ? ` (${block.length} chars · /think)` : ""}${p.reset}\n`;
138
+ const suffix = truncated ? ` (${block.length} chars · /think)` : "";
139
+ const head = Number.isFinite(room)
140
+ ? // the leading … costs one column, the suffix costs its own width
141
+ widthCut(trimmed.slice(0, 100), Math.max(1, room - 1 - displayWidth(suffix)))
142
+ : trimmed.slice(0, 100);
143
+ return `${p.dim}…${head}${suffix}${p.reset}`;
120
144
  }
121
145
  /** v2b — the [result] echo truncates at 160 chars + a /last hint. */
122
146
  export function foldResult(content) {
@@ -143,7 +167,7 @@ export function renderToolSummary(name, input, result, reason = null) {
143
167
  }
144
168
  const mark = result.isError ? `${p.red}✗${p.reset}` : `${p.bold}✓${p.reset}`;
145
169
  const shortName = name.replace("_file", "");
146
- const detail = toolSummaryDetail(name, input, result);
170
+ const detail = oneRow(toolSummaryDetail(name, input, result)); // HF-1: one row, whatever the command
147
171
  return `${mark} ${escapeTerminal(`${shortName} ${detail}`)}`;
148
172
  }
149
173
  function toolSummaryDetail(name, input, result) {
@@ -189,6 +213,27 @@ function toolSummaryDetail(name, input, result) {
189
213
  * the summary detail uses, WITHOUT the counts (the header names what
190
214
  * was expanded, not its size). */
191
215
  export function toolTarget(name, input) {
216
+ return oneRow(toolTargetRaw(name, input));
217
+ }
218
+ /**
219
+ * HF-1 (0.32.1) — a head row is ONE row, whatever the model wrote.
220
+ *
221
+ * The owner's 0.32.0 dogfood: the model issued a heredoc shell command
222
+ * (`python3 - <<'EOF' …`), the running card's head carried its newlines
223
+ * into the compositor, invariant ①b threw in the field and the process
224
+ * died. `escapeTerminal` keeps `\n` on purpose (text blocks need it); a
225
+ * HEAD ROW does not — it is one physical row by construction. So every
226
+ * builder that puts a tool's target on a row projects the breaks to a
227
+ * visible mark first: a line break becomes `⏎` (CRLF is one), a tab
228
+ * becomes one cell of space (its width is a property of the column, and
229
+ * a cut row has no column to give it). The full command is still on the
230
+ * approval panel and in the durable log; the row says what ran, in one
231
+ * row.
232
+ */
233
+ export function oneRow(text) {
234
+ return text.replace(/\r\n|\n|\r/g, "\u23ce").replace(/\t/g, " ");
235
+ }
236
+ function toolTargetRaw(name, input) {
192
237
  switch (name) {
193
238
  case "read_file":
194
239
  case "write_file":
package/dist/strings.js CHANGED
@@ -440,6 +440,21 @@ export function helpRows() {
440
440
  ["/compact", "summarize the older conversation to free context"],
441
441
  ["/clear", "start a fresh conversation (the old session stays resumable)"],
442
442
  ["/resume", "switch to another session; /resume <id> goes directly"],
443
+ // §2.5: the conversation is untouched — this rereads what kiso was
444
+ // built with, not what it has said.
445
+ ["/reload", "reread extensions, skills and config into this session"],
446
+ // §2.2: the two shell gestures and their one escape. They sit
447
+ // beside the slash commands because that is what a reader is
448
+ // looking for when they look here, even though `!` is not one.
449
+ // §2.3: the switch belongs beside ctrl+o's job, and a gesture the
450
+ // sheet does not name is a gesture nobody uses (DC-30, DC-36).
451
+ ["ctrl+t", "fold the thinking blocks, and fold them back"],
452
+ // §2.4: the composer, in your own editor. It names the variables
453
+ // because that is what a reader has to set for it to work.
454
+ ["ctrl+g", "edit the composer in $VISUAL or $EDITOR — the text comes back unsent"],
455
+ ["!<cmd>", "run a shell command and send it with its output as your turn"],
456
+ ["!!<cmd>", "run one and show it here only — the model never sees it"],
457
+ ["\\!", "send a line that really starts with ! (the only escape)"],
443
458
  ["exit", "leave the session"],
444
459
  // TUI2-R1 (D): the SENTENCE is deliberately unchanged. Deriving it
445
460
  // from KEY_BINDINGS would be an improvement and it would also move
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui-cells",
3
- "version": "0.31.1",
3
+ "version": "0.32.1",
4
4
  "description": "kiso tui-cells — the components cell renderer (components, diff, width, the render slice). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",