@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/approval-panel.d.ts +149 -62
- package/dist/approval-panel.js +346 -227
- package/dist/components.d.ts +151 -21
- package/dist/components.js +1190 -821
- package/dist/diff.d.ts +40 -3
- package/dist/diff.js +142 -5
- package/dist/graphite.d.ts +123 -0
- package/dist/graphite.js +257 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -3
- package/dist/md.d.ts +18 -7
- package/dist/md.js +290 -37
- package/dist/render.d.ts +136 -27
- package/dist/render.js +381 -137
- package/dist/strings.d.ts +47 -46
- package/dist/strings.js +130 -171
- package/package.json +1 -1
package/dist/components.js
CHANGED
|
@@ -29,13 +29,14 @@ import { displayWidth, visibleWidth, widthCut } from "./width.js";
|
|
|
29
29
|
// TUI2-R2pre ④: the ONE display-verb table (strings.ts, beside
|
|
30
30
|
// KEY_BINDINGS). strings.js imports only render/width here, so this edge
|
|
31
31
|
// adds no cycle.
|
|
32
|
-
import { displayVerb } from "./strings.js";
|
|
33
|
-
import { bannerLines, breathFrame, cutLine, escapeTerminal, stripAnsi, foldThinking, foldResult, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, currentGround, } from "./render.js";
|
|
32
|
+
import { SHEET_CLOSE, bandHeader, displayVerb } from "./strings.js";
|
|
33
|
+
import { bannerLines, breathFrame, cutLine, escapeTerminal, oneRow, stripAnsi, foldThinking, foldResult, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, currentGround, TWINKLE, fadeRule, } 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
|
|
37
37
|
// back through here.
|
|
38
|
-
import { renderBlock } from "./md.js";
|
|
38
|
+
import { headingLevel, renderBlock, renderMarkdown } from "./md.js";
|
|
39
|
+
import { hunksDiff, hunksOf } from "./diff.js";
|
|
39
40
|
export { MdStream, renderBlock, renderMarkdown } from "./md.js";
|
|
40
41
|
/** The spinner glyphs, cycled by the compositor's on-demand tick. */
|
|
41
42
|
export const SPINNER = ["▖", "▘", "▝", "▗"];
|
|
@@ -161,6 +162,16 @@ export class Container {
|
|
|
161
162
|
return out;
|
|
162
163
|
}
|
|
163
164
|
}
|
|
165
|
+
/** Graphite §1.8 — THE CONTENT EDGE. Every block begins at column 2
|
|
166
|
+
* (owner, 2026-09-29: the 0.44 geometry); columns 0–1 are the mark
|
|
167
|
+
* column — the seal's `✦`, a streaming thought's twinkle, the live row's
|
|
168
|
+
* mark, the person's `▌`, a card's edge and its mark cell. */
|
|
169
|
+
export const EDGE = " ";
|
|
170
|
+
/** The room a block of words has: from the content edge to two columns
|
|
171
|
+
* short of the right edge, the same margin on both sides. No width cap —
|
|
172
|
+
* a cap left the answer and thinking far short of the cards beside them
|
|
173
|
+
* on a wide terminal (owner, 2026-09-29). */
|
|
174
|
+
const proseRoom = (W) => Math.max(1, W - EDGE.length * 2);
|
|
164
175
|
const TOOL_SUMMARY_MAX = 60; // the tool line's parameter summary, chars
|
|
165
176
|
/** The component for one cell — the mapping table lives here so the
|
|
166
177
|
* compositor stays a pure writer. */
|
|
@@ -179,50 +190,85 @@ export function cellComponent(cell) {
|
|
|
179
190
|
return new MarkdownBlock(cell);
|
|
180
191
|
case "notice":
|
|
181
192
|
return new ErrorLine(cell);
|
|
193
|
+
case "seal":
|
|
194
|
+
return new SealLine(cell);
|
|
182
195
|
case "fold":
|
|
183
196
|
return new FoldRow(cell);
|
|
184
197
|
case "banner":
|
|
185
198
|
return new Banner(cell);
|
|
186
199
|
case "raw":
|
|
187
200
|
return new RawBlock(cell);
|
|
201
|
+
case "bang":
|
|
202
|
+
return new BangCard(cell);
|
|
203
|
+
case "recall":
|
|
204
|
+
return new RecallBlock(cell);
|
|
188
205
|
case "terminal":
|
|
189
206
|
return new TerminalBlock(cell);
|
|
190
207
|
}
|
|
191
208
|
}
|
|
192
|
-
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* a
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
209
|
+
/** G3 (R2d) — the block `!cmd` sends: a `console` fence holding `$ <command>`
|
|
210
|
+
* and what it printed (apps/cli dispatch.ts `runBang`). Null for any
|
|
211
|
+
* other text. */
|
|
212
|
+
export function bangOf(text) {
|
|
213
|
+
const m = /^```console\n\$ ([^\n]*)\n([\s\S]*)\n```$/.exec(text);
|
|
214
|
+
return m === null ? null : { command: m[1], output: m[2] };
|
|
215
|
+
}
|
|
216
|
+
/** G3 (R2d) — the person's command as a card on their own warm ground: a
|
|
217
|
+
* pad row, `$ <command>` with what became of it at the right (`exit N`
|
|
218
|
+
* when the output says so, then `sent to the model` or `not sent`), what
|
|
219
|
+
* it printed under the command in `ink2`, a pad row. `cap` keeps the
|
|
220
|
+
* output's TAIL (a command's conclusion is at its end) with the count of
|
|
221
|
+
* what was cut above it; null shows it all. Off a known ground: the same
|
|
222
|
+
* words, no surface. */
|
|
223
|
+
function bangRows(command, output, isError, fate, W, cap) {
|
|
224
|
+
const p = palette();
|
|
225
|
+
const painted = p.human !== "";
|
|
226
|
+
const exit = /^exit (\d+)/.exec(output);
|
|
227
|
+
const outcome = [exit !== null ? `exit ${exit[1]}` : isError ? "failed" : "", fate].filter((x) => x !== "").join(" \u00b7 ");
|
|
228
|
+
const textW = Math.max(1, W - PERSON_COL - 2 - CHIP_RIGHT);
|
|
229
|
+
let lines = escapeTerminal(stripAnsi(output)).replace(/\n+$/, "").split("\n").flatMap((l) => foldLine(l, textW));
|
|
230
|
+
if (lines.length === 1 && lines[0] === "")
|
|
231
|
+
lines = [];
|
|
232
|
+
let cut = 0;
|
|
233
|
+
if (cap !== null && lines.length > cap) {
|
|
234
|
+
cut = lines.length - cap;
|
|
235
|
+
lines = lines.slice(lines.length - cap);
|
|
236
|
+
}
|
|
237
|
+
const cmd = escapeTerminal(command);
|
|
238
|
+
const headRoom = Math.max(1, W - PERSON_COL - CHIP_RIGHT);
|
|
239
|
+
const tail = ` ${outcome}`;
|
|
240
|
+
const headText = visibleWidth(`$ ${cmd}${tail}`) + 1 <= headRoom ? `$ ${cmd}` : widthCut(`$ ${cmd}`, Math.max(1, headRoom - visibleWidth(tail) - 2));
|
|
241
|
+
const gap = " ".repeat(Math.max(1, headRoom - visibleWidth(headText) - visibleWidth(outcome)));
|
|
242
|
+
if (!painted) {
|
|
243
|
+
const rows = [cutLine(`${" ".repeat(PERSON_COL)}${p.bold}${headText}${p.reset}${gap}${p.dim}${outcome}${p.reset}`, W)];
|
|
244
|
+
if (cut > 0)
|
|
245
|
+
rows.push(cutLine(`${" ".repeat(PERSON_COL + 2)}${p.dim}\u2026 ${cut} earlier line${cut === 1 ? "" : "s"}${p.reset}`, W));
|
|
246
|
+
for (const l of lines)
|
|
247
|
+
rows.push(cutLine(`${" ".repeat(PERSON_COL + 2)}${p.dim}${l}${p.reset}`, W));
|
|
248
|
+
return rows;
|
|
249
|
+
}
|
|
250
|
+
const row = (inner) => {
|
|
251
|
+
const pad = " ".repeat(Math.max(0, W - 2 - visibleWidth(inner)));
|
|
252
|
+
return `${p.humanEdge} ${p.human}${p.humanInk} ${inner.replaceAll("\x1b[0m", `\x1b[0m${p.human}${p.humanInk}`)}${pad}${p.fgEnd}${p.washEnd}`;
|
|
253
|
+
};
|
|
254
|
+
const padRow = `${p.humanEdge} ${p.human}${" ".repeat(Math.max(0, W - 1))}${p.washEnd}`;
|
|
255
|
+
// `$ <command>` is ONE span: a reader (and a needle) finds the command
|
|
256
|
+
// line as it was typed
|
|
257
|
+
const head = `${p.bold}${headText}${p.reset}${gap}${p.dim}${outcome}${p.reset}`;
|
|
258
|
+
const body = [...(cut > 0 ? [` ${p.dim}\u2026 ${cut} earlier line${cut === 1 ? "" : "s"}${p.reset}`] : []), ...lines.map((l) => ` ${p.ink2}${l}${p.fgEnd}`)];
|
|
259
|
+
return [padRow, row(head), ...body.map(row), padRow].map((r) => cutLine(r, W));
|
|
260
|
+
}
|
|
261
|
+
/** G3 (R2d) — `!!cmd`: the card, all of the output (the person asked to
|
|
262
|
+
* see it here), `not sent`. */
|
|
263
|
+
class BangCard {
|
|
264
|
+
cell;
|
|
265
|
+
constructor(cell) {
|
|
266
|
+
this.cell = cell;
|
|
267
|
+
}
|
|
268
|
+
render(W, _ctx) {
|
|
269
|
+
return bangRows(this.cell.command, this.cell.output, this.cell.isError, "not sent", W, null);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
226
272
|
/**
|
|
227
273
|
* REL-0152-D13 — how much of a turn the chip shows.
|
|
228
274
|
*
|
|
@@ -232,9 +278,31 @@ export function cellComponent(cell) {
|
|
|
232
278
|
* the same rule as three thousand short ones.
|
|
233
279
|
*/
|
|
234
280
|
const USER_CHIP_ROWS = 12;
|
|
235
|
-
/**
|
|
236
|
-
*
|
|
237
|
-
const
|
|
281
|
+
/** Graphite §7.9 — the text starts at the content edge (column 4) and
|
|
282
|
+
* stops two columns short of the right edge. */
|
|
283
|
+
const CHIP_RIGHT = 2;
|
|
284
|
+
/** Graphite §7.9 — the person's words start at the content edge. */
|
|
285
|
+
const PERSON_COL = EDGE.length;
|
|
286
|
+
/**
|
|
287
|
+
* Graphite §7.9 — THE PERSON'S BLOCK.
|
|
288
|
+
*
|
|
289
|
+
* On a known ground: the warm `human` ground across the full width, a
|
|
290
|
+
* whole row of it above and below (§1.5: a pad is a row of BACKGROUND —
|
|
291
|
+
* the half-row glyphs R1b tried left a seam in Apple Terminal), an EDGE
|
|
292
|
+
* cell of quieted gold in column 0 down every row — the same width as a
|
|
293
|
+
* card's edge, and like it a background, since a `▌` glyph shows a break
|
|
294
|
+
* at every row there — and the text at the content edge in `humanInk`.
|
|
295
|
+
* No label and no time: the block says whose words these are.
|
|
296
|
+
*
|
|
297
|
+
* On an unknown ground nothing is painted that assumes a background
|
|
298
|
+
* (§3.1): the `▌` in column 0, which is a character and so still marks
|
|
299
|
+
* the person's words once the escapes are stripped (§1.2), and reverse
|
|
300
|
+
* video from column 1.
|
|
301
|
+
*
|
|
302
|
+
* The fold is by WORD (R9 Q3: a word wider than the row still breaks,
|
|
303
|
+
* because invariant ① outranks it), ONE width over every row (DC-6), and
|
|
304
|
+
* padding is by display width, so a CJK row pads by cells.
|
|
305
|
+
*/
|
|
238
306
|
class UserMessage {
|
|
239
307
|
cell;
|
|
240
308
|
constructor(cell) {
|
|
@@ -242,163 +310,125 @@ class UserMessage {
|
|
|
242
310
|
}
|
|
243
311
|
render(W, _ctx) {
|
|
244
312
|
const p = palette();
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
// REL-0152-D13: fold only as far as the bound needs. A pasted file
|
|
253
|
-
// has thousands of lines and folding all of them to show twelve is
|
|
254
|
-
// work with no reader — the measurement that opened this finding
|
|
255
|
-
// was a 3000-line turn writing 260,298 bytes in one frame.
|
|
313
|
+
// G3 (R2d): a `!cmd` turn — the block the dispatcher sends — draws as
|
|
314
|
+
// the person's command card; the model's bytes are the text as sent
|
|
315
|
+
const bang = bangOf(this.cell.text);
|
|
316
|
+
if (bang !== null)
|
|
317
|
+
return bangRows(bang.command, bang.output, false, "sent to the model", W, USER_CHIP_ROWS);
|
|
318
|
+
const painted = p.human !== "";
|
|
319
|
+
const chipW = Math.max(1, W - PERSON_COL - CHIP_RIGHT);
|
|
256
320
|
const paras = this.cell.text.split("\n");
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// falls through to foldLine's hard break for a word wider than
|
|
275
|
-
// the row, so invariant ① still outranks the word.
|
|
276
|
-
for (const row of foldWords(escapeTerminal(para), chipW)) {
|
|
277
|
-
if (content.length >= USER_CHIP_ROWS) {
|
|
278
|
-
truncated = true;
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
content.push(row);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
// R2 (law 1.6's recorded reversal): the band is FULL WIDTH. It was
|
|
285
|
-
// sized to its longest row, on the argument that a one-word turn
|
|
286
|
-
// like `/think` would otherwise paint a bar across the terminal —
|
|
287
|
-
// which optimises the degenerate case at the cost of every real
|
|
288
|
-
// message. The human's words are the one surface that gets the
|
|
289
|
-
// whole row.
|
|
290
|
-
//
|
|
291
|
-
// displayWidth stays the padding authority (never `length`): a CJK
|
|
292
|
-
// row is two cells per character and pads by cells.
|
|
293
|
-
const inner = chipW;
|
|
294
|
-
for (const row of content) {
|
|
295
|
-
rows.push(`${p.rv}${CHIP_PAD}${row}${" ".repeat(Math.max(0, inner - displayWidth(row)))}${CHIP_PAD}${p.rvEnd}`);
|
|
296
|
-
}
|
|
321
|
+
// Graphite §5 (G6, R2b): the person's words render as markdown, the
|
|
322
|
+
// line breaks they typed kept. REL-0152-D13: only as far as the
|
|
323
|
+
// bound needs — a pasted file has thousands of lines and twelve rows
|
|
324
|
+
// are shown, so a prefix of the source is rendered (a few lines past
|
|
325
|
+
// the bound, so a block that closes there still closes).
|
|
326
|
+
const source = paras.slice(0, USER_CHIP_ROWS + 4).join("\n");
|
|
327
|
+
const rendered = renderMarkdown(escapeTerminal(source), chipW, { hardBreaks: true });
|
|
328
|
+
const truncated = rendered.length > USER_CHIP_ROWS || paras.length > USER_CHIP_ROWS + 4;
|
|
329
|
+
const content = rendered.slice(0, USER_CHIP_ROWS);
|
|
330
|
+
const fill = (row) => " ".repeat(Math.max(0, chipW - visibleWidth(row) + CHIP_RIGHT));
|
|
331
|
+
const padRow = `${p.humanEdge} ${p.human}${" ".repeat(Math.max(0, W - 1))}${p.washEnd}`;
|
|
332
|
+
// every reset inside a row re-opens the block's ground and ink, so a
|
|
333
|
+
// styled span never strands the rest of the row off the block
|
|
334
|
+
const onGround = (row) => row.replaceAll("\x1b[0m", `\x1b[0m${p.human}${p.humanInk}`).replaceAll("\x1b[49m", p.human).replaceAll("\x1b[39m", p.humanInk);
|
|
335
|
+
const out = painted
|
|
336
|
+
? [padRow, ...content.map((row) => `${p.humanEdge} ${p.human}${p.humanInk} ${onGround(row)}${fill(row)}${p.fgEnd}${p.washEnd}`), padRow]
|
|
337
|
+
: content.map((row) => `\u258c${p.rv} ${row.replaceAll("\x1b[0m", `\x1b[0m${p.rv}`)}${fill(row)}${p.rvEnd}`);
|
|
297
338
|
if (!truncated)
|
|
298
|
-
return
|
|
299
|
-
// The notice is OUTSIDE the
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
// make that difference visible or it reads as data loss.
|
|
304
|
-
const shown = rows.length;
|
|
305
|
-
const total = paras.length;
|
|
306
|
-
const more = Math.max(0, total - shown);
|
|
307
|
-
// DC-45: THE NOTICE FOLDS TOO. It was written at a fixed 30 columns
|
|
308
|
-
// and emitted verbatim at every width, so a paste of thirteen lines
|
|
309
|
-
// in a terminal narrower than the sentence tripped the compositor's
|
|
310
|
-
// invariant ① and killed the session. The tiers are TUI2-R1.5 ⑤'s
|
|
311
|
-
// discipline: `sent in full` is the SEMANTICS — the whole reason
|
|
312
|
-
// the row exists — so the count gives way before it, and `cutLine`
|
|
313
|
-
// is the backstop that holds at any width there is.
|
|
339
|
+
return out;
|
|
340
|
+
// The notice is OUTSIDE the block, in the cut-row vocabulary, and it
|
|
341
|
+
// says what matters: the model got all of it (DC-45: it folds too —
|
|
342
|
+
// `sent in full` is the semantics and gives way last).
|
|
343
|
+
const more = Math.max(0, paras.length - content.length);
|
|
314
344
|
const count = more > 0 ? `+${more} more line${more === 1 ? "" : "s"}` : "cut here";
|
|
315
345
|
const short = more > 0 ? `+${more}` : "cut";
|
|
316
|
-
|
|
317
|
-
|
|
346
|
+
// on a very narrow terminal the edge gives way before the notice does
|
|
347
|
+
const lead = W - PERSON_COL >= 8 ? " ".repeat(PERSON_COL) : "";
|
|
348
|
+
out.push(cutLine(`${lead}${p.dim}\u2514 ${pickTier([`${count} \u00b7 sent in full`, `${short} \u00b7 sent in full`, "sent in full", count], Math.max(1, W - lead.length - 2))}${p.reset}`, W));
|
|
349
|
+
return out;
|
|
318
350
|
}
|
|
319
351
|
}
|
|
320
|
-
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Graphite §8.7, the main-sync round (owner, 2026-09-30) — a STEER that
|
|
354
|
+
* has not landed is ONE row in the live zone:
|
|
355
|
+
*
|
|
356
|
+
* ◇ <the message's first line> next step · ↑ takes back
|
|
357
|
+
*
|
|
358
|
+
* ADR-0057: Enter while a run works steers it — there is no queue on a
|
|
359
|
+
* terminal — and the steer lands inside the same run at its next quiet
|
|
360
|
+
* step, or after an open approval, which it never answers. The gold `◇`
|
|
361
|
+
* (a message the person sent that has not landed, §4) in the mark column
|
|
362
|
+
* and the text at the content edge; no label word (owner: the mark says
|
|
363
|
+
* it). The keys at the right say what happens next and give way first
|
|
364
|
+
* when the row is short. DECLARED REVERSAL of the `◇ queued … after this
|
|
365
|
+
* turn · ↑ edit` row, whose queue main retired in 0.45.0.
|
|
366
|
+
*/
|
|
367
|
+
export function pendingQueueRows(lines, W, waiting = "step") {
|
|
329
368
|
const p = palette();
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
369
|
+
const mark = p.goldMark === "" ? "\u25c7" : `${p.goldMark}\u25c7${p.fgEnd}`;
|
|
370
|
+
const keys = `${waiting === "approval" ? "after the approval" : "next step"} \u00b7 \u2191 takes back`;
|
|
371
|
+
return lines.map((line) => {
|
|
372
|
+
const lead = `${mark} `;
|
|
373
|
+
const leadW = visibleWidth(lead);
|
|
374
|
+
const text = escapeTerminal(line);
|
|
375
|
+
const withKeys = W - leadW - visibleWidth(keys) - 2;
|
|
376
|
+
if (withKeys >= Math.min(12, visibleWidth(text))) {
|
|
377
|
+
const shown = visibleWidth(text) <= withKeys ? text : `${widthCut(text, Math.max(1, withKeys - 1))}\u2026`;
|
|
378
|
+
return `${lead}${shown}${" ".repeat(Math.max(2, W - leadW - visibleWidth(shown) - visibleWidth(keys)))}${p.dim}${keys}${p.reset}`;
|
|
334
379
|
}
|
|
335
|
-
|
|
336
|
-
|
|
380
|
+
const room = Math.max(1, W - leadW);
|
|
381
|
+
return cutLine(`${lead}${visibleWidth(text) <= room ? text : `${widthCut(text, Math.max(1, room - 1))}\u2026`}`, W);
|
|
382
|
+
});
|
|
337
383
|
}
|
|
338
384
|
/**
|
|
339
|
-
*
|
|
385
|
+
* Graphite §7.2 — THINKING IS WORDS, under its label.
|
|
386
|
+
*
|
|
387
|
+
* `THINK <seconds>` in blue at the content edge, then the model's
|
|
388
|
+
* reasoning as `dim` italic paragraphs, shown in full and never folded
|
|
389
|
+
* (R7, the owner: "I cannot see what it was thinking" was the complaint
|
|
390
|
+
* through four rounds of folding). While the block streams, the label
|
|
391
|
+
* carries the twinkle, hanging in the mark column; the seconds appear
|
|
392
|
+
* when it settles, and only if they were measured — a block replayed from
|
|
393
|
+
* the log has no clock.
|
|
340
394
|
*
|
|
341
|
-
* The
|
|
342
|
-
* the
|
|
343
|
-
*
|
|
344
|
-
* path's `foldThinking` is what remains of it) stood for it, with a
|
|
345
|
-
* key; four rounds
|
|
346
|
-
* of machinery were then built to hand the rest back, and the owner's
|
|
347
|
-
* complaint through all of them was the same sentence: I cannot see
|
|
348
|
-
* what it was thinking.
|
|
395
|
+
* The LABEL is what tells thinking from the answer once the escapes are
|
|
396
|
+
* stripped (§1.2): the answer carries none. A pipe never sees a thinking
|
|
397
|
+
* paragraph — the inactive path writes `foldThinking`'s one line.
|
|
349
398
|
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* pipe strips and `COLOR_OFF` empties outright. Stripped, an italic
|
|
353
|
-
* paragraph and the model's ANSWER are the same bytes, and the reader
|
|
354
|
-
* cannot tell the reasoning from the reply. The two-space indent is the
|
|
355
|
-
* byte that survives: reasoning sits in, the answer stands at the
|
|
356
|
-
* margin. The owner chose this over the un-indented form for exactly
|
|
357
|
-
* that reason, with the cost stated.
|
|
399
|
+
* `ctrl+t` hides every block to its label line (`· hidden · ctrl+t`), and
|
|
400
|
+
* the text is never drawn while hidden, live or settled.
|
|
358
401
|
*
|
|
359
|
-
* Paragraphs are
|
|
360
|
-
*
|
|
361
|
-
* the
|
|
402
|
+
* Paragraphs are kept (a blank row between them); newlines INSIDE one
|
|
403
|
+
* collapse, because a hard-wrapped source line is the model's width, not
|
|
404
|
+
* the reader's.
|
|
405
|
+
*/
|
|
406
|
+
/**
|
|
407
|
+
* Graphite §7.2 — THINKING: the model's reasoning as it streams, `dim`
|
|
408
|
+
* italic at the content edge, folded by WORD like every prose surface.
|
|
409
|
+
* No label (owner, 2026-09-29): the grey italic is what tells it from the
|
|
410
|
+
* answer. While it streams the twinkle hangs in the mark column of its
|
|
411
|
+
* first row; settled, the column is empty (§4.2). Hidden (ctrl+t), it is
|
|
412
|
+
* one dim row that says so. With colour off there is no grey and no
|
|
413
|
+
* italic to tell it by, so there — and only there — its first row opens
|
|
414
|
+
* with the plain word `thinking:`.
|
|
362
415
|
*/
|
|
363
416
|
class ThinkingBlock {
|
|
364
417
|
cell;
|
|
365
418
|
constructor(cell) {
|
|
366
419
|
this.cell = cell;
|
|
367
420
|
}
|
|
368
|
-
render(W,
|
|
421
|
+
render(W, ctx) {
|
|
369
422
|
const p = palette();
|
|
370
|
-
const
|
|
423
|
+
const c = this.cell;
|
|
424
|
+
const text = escapeTerminal(c.text).trim();
|
|
371
425
|
if (text === "")
|
|
372
426
|
return [];
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
// pipe keeps its own one-line fold (render.ts foldThinking).
|
|
379
|
-
if (this.cell.folded)
|
|
380
|
-
return [`${THINK_COL}${p.dim}${p.italic}${this.cell.done ? "thinking… · /think" : "thinking…"}${p.italicEnd}${p.reset}`];
|
|
381
|
-
// DC-47 — THINKING GOES ONE LEVEL DEEPER THAN PROSE, and the
|
|
382
|
-
// reason is a law rather than a taste.
|
|
383
|
-
//
|
|
384
|
-
// §7.2: the indent is the price of §1.2 — italic and dim are
|
|
385
|
-
// escape sequences, so a rendered frame with its colour stripped
|
|
386
|
-
// (a terminal capture, a paste out of the scrollback, a log of
|
|
387
|
-
// what was drawn) would lose the line between the model's
|
|
388
|
-
// reasoning and its answer. R13's E3 moved PROSE to column 2,
|
|
389
|
-
// which is where the thinking already was, so after
|
|
390
|
-
// `sed 's/\x1b\[[0-9;]*m//g'` the two became the same row.
|
|
391
|
-
// Measured, not reasoned: both rendered
|
|
392
|
-
// `" Weighing the two shapes."` exactly.
|
|
393
|
-
//
|
|
394
|
-
// NOT a pipe, though §7.2 used to say so: `thinkingEnd`'s inactive
|
|
395
|
-
// path writes `foldThinking` — one dim line — so a pipe never sees
|
|
396
|
-
// a thinking paragraph to confuse with prose.
|
|
397
|
-
//
|
|
398
|
-
// So the thinking takes the next column in. It is still the only
|
|
399
|
-
// carrier that survives a pipe, and it is still one indent step —
|
|
400
|
-
// what moved is which step, because prose took the one it had.
|
|
401
|
-
const room = Math.max(1, W - THINK_COL.length);
|
|
427
|
+
const mark = c.done ? EDGE : `${p.gold}${TWINKLE[ctx.spinnerI % TWINKLE.length]}${p.gold === "" ? "" : p.fgEnd} `;
|
|
428
|
+
if (c.folded)
|
|
429
|
+
return [cutLine(`${mark}${p.dim}${p.italic}thinking \u00b7 hidden \u00b7 ctrl+t${p.italicEnd}${p.reset}`, W)];
|
|
430
|
+
const room = proseRoom(W);
|
|
431
|
+
const plainWord = p.italic === "" && p.dim === "" ? "thinking: " : "";
|
|
402
432
|
const rows = [];
|
|
403
433
|
for (const para of text.split(/\n\s*\n/)) {
|
|
404
434
|
const flat = para.replace(/\s+/g, " ").trim();
|
|
@@ -406,11 +436,9 @@ class ThinkingBlock {
|
|
|
406
436
|
continue;
|
|
407
437
|
if (rows.length > 0)
|
|
408
438
|
rows.push("");
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
for (const line of foldLine(flat, room))
|
|
413
|
-
rows.push(`${THINK_COL}${p.dim}${p.italic}${line}${p.italicEnd}${p.reset}`);
|
|
439
|
+
const lead = rows.length === 0 ? `${plainWord}${flat}` : flat;
|
|
440
|
+
for (const line of foldWords(lead, room))
|
|
441
|
+
rows.push(`${rows.length === 0 ? mark : THINK_COL}${p.dim}${p.italic}${line}${p.italicEnd}${p.reset}`);
|
|
414
442
|
}
|
|
415
443
|
return rows;
|
|
416
444
|
}
|
|
@@ -427,6 +455,22 @@ function spansOpenAfter(text, before) {
|
|
|
427
455
|
}
|
|
428
456
|
return open;
|
|
429
457
|
}
|
|
458
|
+
/** A line of text folded to `width` cells, breaking after the last space
|
|
459
|
+
* that fits (the space is the break), or hard at the width when a run has
|
|
460
|
+
* none. Every other character is kept, in order. */
|
|
461
|
+
export function foldAtSpaces(text, width) {
|
|
462
|
+
const rows = [];
|
|
463
|
+
let rest = text;
|
|
464
|
+
while (visibleWidth(rest) > width) {
|
|
465
|
+
const head = widthCut(rest, width);
|
|
466
|
+
const space = head.lastIndexOf(" ");
|
|
467
|
+
const cut = space > 0 ? space + 1 : Math.max(1, head.length);
|
|
468
|
+
rows.push(rest.slice(0, cut).trimEnd());
|
|
469
|
+
rest = rest.slice(cut);
|
|
470
|
+
}
|
|
471
|
+
rows.push(rest);
|
|
472
|
+
return rows;
|
|
473
|
+
}
|
|
430
474
|
/**
|
|
431
475
|
* TUI2-R1.5 ⑨ (VD-10) — the WORD-aware fold, for text a human reads.
|
|
432
476
|
*
|
|
@@ -454,44 +498,72 @@ export function foldWords(line, W) {
|
|
|
454
498
|
out.push(para);
|
|
455
499
|
continue;
|
|
456
500
|
}
|
|
457
|
-
|
|
501
|
+
// LINEAR (the thinking-freeze fix, 2026-09-30): the row's remainder is
|
|
502
|
+
// never re-measured or rebuilt as a string. It was — `visibleWidth` of
|
|
503
|
+
// the whole rest and a new rest string per row — which is O(n²/W) for
|
|
504
|
+
// one long paragraph, and a thinking block is ONE flattened paragraph
|
|
505
|
+
// re-folded every frame: 61 ms a render at 40k characters, long enough
|
|
506
|
+
// to starve the working row's timer (the twinkle froze while the model
|
|
507
|
+
// thought). The rows are byte for byte what they were: `rest` is the
|
|
508
|
+
// same `open` prefix + the same tail, read through a cursor.
|
|
458
509
|
// the spans open at the cut point, so each emitted row closes them
|
|
459
510
|
// and the next row reopens them — foldLine's own discipline, applied
|
|
460
511
|
// across the segments this function creates.
|
|
512
|
+
let pos = 0;
|
|
461
513
|
let open = [];
|
|
462
|
-
while (
|
|
514
|
+
while (widerFrom(para, pos, W)) {
|
|
463
515
|
// the widest prefix that fits, then back up to the last space in
|
|
464
516
|
// it — the SGR-aware cut keeps the spans intact
|
|
465
|
-
const
|
|
517
|
+
const pre = open.join("");
|
|
518
|
+
const head = headOf(pre, para, pos, W);
|
|
466
519
|
const at = head.lastIndexOf(" ");
|
|
467
520
|
if (at <= 0)
|
|
468
521
|
break; // one long word (or no space at all) — hard-break it
|
|
469
522
|
const cut = head.slice(0, at);
|
|
470
523
|
out.push(`${cut}${open.length > 0 || /\x1b\[[0-9;]*m/.test(cut) ? "\x1b[0m" : ""}`);
|
|
471
524
|
open = spansOpenAfter(cut, open);
|
|
472
|
-
|
|
525
|
+
// the space the row broke at is consumed; the prefix was never in `para`
|
|
526
|
+
pos += at + 1 - pre.length;
|
|
473
527
|
}
|
|
474
|
-
out.push(...foldLine(
|
|
528
|
+
out.push(...foldLine(`${open.join("")}${para.slice(pos)}`, W));
|
|
475
529
|
}
|
|
476
530
|
return out.length > 0 ? out : [""];
|
|
477
531
|
}
|
|
532
|
+
/** `visibleWidth(s.slice(from)) > W`, answered by reading at most the
|
|
533
|
+
* first W + 1 visible cells (and the escapes among them) — never the
|
|
534
|
+
* whole remainder. The same skip rule as visibleWidth. */
|
|
535
|
+
function widerFrom(s, from, W) {
|
|
536
|
+
let w = 0;
|
|
537
|
+
for (let i = from; i < s.length;) {
|
|
538
|
+
if (s[i] === "\x1b") {
|
|
539
|
+
const m = /^\x1b\[[0-9;?]*[A-Za-z]/.exec(s.slice(i, i + 256));
|
|
540
|
+
i += m !== null ? m[0].length : 1;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
// per UTF-16 unit, exactly as visibleWidth counts
|
|
544
|
+
w += displayWidth(s[i]);
|
|
545
|
+
if (w > W)
|
|
546
|
+
return true;
|
|
547
|
+
i += 1;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
/** `widthCut(pre + s.slice(from), W)` without building the whole string:
|
|
552
|
+
* widthCut stops within its first W cells, so a window of the tail is
|
|
553
|
+
* enough — widened (doubling) only when the cut ran to the window's end
|
|
554
|
+
* before the tail's (a run of zero-width code points). */
|
|
555
|
+
function headOf(pre, s, from, W) {
|
|
556
|
+
for (let span = 2 * W + 256;; span *= 2) {
|
|
557
|
+
const probe = `${pre}${s.slice(from, from + span)}`;
|
|
558
|
+
const head = widthCut(probe, W);
|
|
559
|
+
if (head.length < probe.length || from + span >= s.length)
|
|
560
|
+
return head;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
478
563
|
export function gutterFold(gutter, line, W) {
|
|
479
564
|
const textW = Math.max(1, W - 2);
|
|
480
565
|
return foldLine(line, textW).map((r) => `${gutter}${r}`);
|
|
481
566
|
}
|
|
482
|
-
/** A6: the tool-header variant — ONE cut row, never a fold. A wide
|
|
483
|
-
* header (a long target path, a wordy denial reason) used to wrap
|
|
484
|
-
* through foldLine — every wrapped row repeated the gutter, the
|
|
485
|
-
* settled row grew past its previewed height. The header names the
|
|
486
|
-
* call — the ellipsis marks the cut, the body below still carries the
|
|
487
|
-
* full content. The budget: the gutter's own visible width + the
|
|
488
|
-
* ellipsis ride the row (the invariant ① cap holds). */
|
|
489
|
-
export function gutterCut(gutter, line, W) {
|
|
490
|
-
const gutterW = visibleWidth(gutter);
|
|
491
|
-
const textW = Math.max(1, W - gutterW - 1);
|
|
492
|
-
const cut = widthCut(line, textW);
|
|
493
|
-
return [`${gutter}${cut}${visibleWidth(line) > textW ? "…" : ""}`];
|
|
494
|
-
}
|
|
495
567
|
/** Lines without the phantom empty line after a trailing newline. */
|
|
496
568
|
function countLines(text) {
|
|
497
569
|
if (text === "")
|
|
@@ -519,7 +591,9 @@ function settledMeta(c) {
|
|
|
519
591
|
// any other error.
|
|
520
592
|
if (c.name === "shell" && /^exit \d+/.test(c.resultText))
|
|
521
593
|
return `exit ${/^exit (\d+)/.exec(c.resultText)[1]}`;
|
|
522
|
-
|
|
594
|
+
// Graphite §7.5: the outcome is a WORD on the head row — the error's
|
|
595
|
+
// own text is the card's body, where it has the room to be read
|
|
596
|
+
return "failed";
|
|
523
597
|
}
|
|
524
598
|
if (c.name === "read_file") {
|
|
525
599
|
const noteAt = c.resultText.lastIndexOf("\n… ");
|
|
@@ -533,6 +607,11 @@ function settledMeta(c) {
|
|
|
533
607
|
return `${k(shown)} line${shown === 1 ? "" : "s"}`;
|
|
534
608
|
}
|
|
535
609
|
if (c.name === "write_file" || c.name === "edit_file") {
|
|
610
|
+
// R2a: the call's own diff speaks for it — a batch's hunks counted
|
|
611
|
+
// together, a write as a new file or a rewrite
|
|
612
|
+
const edit = editDiffOf(c);
|
|
613
|
+
if (edit !== null)
|
|
614
|
+
return edit.meta;
|
|
536
615
|
if (c.added + c.removed > 0)
|
|
537
616
|
return `+${c.added} -${c.removed}`;
|
|
538
617
|
// no approval diff (an auto-allowed write): the input summary may
|
|
@@ -590,18 +669,27 @@ function toolTargetOf(c) {
|
|
|
590
669
|
}
|
|
591
670
|
return toolTarget(c.name, input);
|
|
592
671
|
}
|
|
593
|
-
/**
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
672
|
+
/**
|
|
673
|
+
* Graphite §7.4 — THE CARD: one per call (R13), one skeleton in every
|
|
674
|
+
* state —
|
|
675
|
+
*
|
|
676
|
+
* head · body · foot
|
|
677
|
+
*
|
|
678
|
+
* The head carries the verb, the target (a command folds under itself,
|
|
679
|
+
* at most three rows collapsed), and at its right end the outcome, a
|
|
680
|
+
* running or waiting card's mark in front of it (§7.5). The body is the
|
|
681
|
+
* preview, aligned under the verb; when it cut rows away, its cut note
|
|
682
|
+
* carries the `ctrl+o` key at the right margin. The foot is the way back
|
|
683
|
+
* on an expanded card; a card with no body carries the key at the end of
|
|
684
|
+
* its head row instead (the card round, owner 2026-10-05). The ground is
|
|
685
|
+
* the call's STATE (§1.6): the machine's blue while it runs and once it
|
|
686
|
+
* has run, red failed or refused, gold waiting for the person. No pad
|
|
687
|
+
* rows and no side bar: surfaces are backgrounds only (§1.5).
|
|
688
|
+
*
|
|
689
|
+
* On an unknown ground nothing is painted (§3.1): the head at the content
|
|
690
|
+
* edge, the body indented under it and opened by `└`, the foot dim. The
|
|
691
|
+
* CONTENT is the same either way.
|
|
692
|
+
*/
|
|
605
693
|
class ToolExecution {
|
|
606
694
|
cell;
|
|
607
695
|
constructor(cell) {
|
|
@@ -610,254 +698,122 @@ class ToolExecution {
|
|
|
610
698
|
render(W, ctx) {
|
|
611
699
|
const p = palette();
|
|
612
700
|
const c = this.cell;
|
|
613
|
-
const verb = escapeTerminal(displayVerb(c.name));
|
|
614
|
-
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
// call could be; both retired with the `rolled` field they read.
|
|
701
|
+
const verb = escapeTerminal(displayVerb(c.name)).toUpperCase();
|
|
702
|
+
// the card round: a command folds whole (three rows collapsed, all of
|
|
703
|
+
// it expanded); every other target is one row
|
|
704
|
+
const fold = c.name === "shell" ? (c.expanded ? Number.POSITIVE_INFINITY : HEAD_ROWS) : 0;
|
|
618
705
|
if (c.state === "done") {
|
|
619
|
-
// R3i phase 5: an answered (or declined) ask_user renders its
|
|
620
|
-
//
|
|
621
|
-
// it replaces was ` ask_user (3 lines, 41.2s)`: an empty
|
|
622
|
-
// target and the answers thrown away, though the result
|
|
623
|
-
// already carried them. `askedBlock` returns [] for anything
|
|
624
|
-
// that is not the ask's own JSON, so a payload this renderer
|
|
625
|
-
// did not write can never be guessed at.
|
|
706
|
+
// R3i phase 5: an answered (or declined) ask_user renders its OWN
|
|
707
|
+
// block — the questions and what the human said.
|
|
626
708
|
if (c.name === "ask_user" && c.reason === null && !c.isError) {
|
|
627
709
|
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);
|
|
628
710
|
if (asked.length > 0)
|
|
629
711
|
return asked;
|
|
630
712
|
}
|
|
631
|
-
// W19: the
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
//
|
|
713
|
+
// W19: the refused call — a failed card whose outcome is the
|
|
714
|
+
// refusal and its reason; the [result] body still rides below
|
|
715
|
+
// (never hide information). A5: an extension's denial names it.
|
|
716
|
+
// Graphite §7.5 (R3b): a call still open when the person stopped the
|
|
717
|
+
// turn was not refused by anyone — it says `interrupted`, on the
|
|
718
|
+
// machine's ground (it was running), its output so far below. It
|
|
719
|
+
// used to read `denied · interrupted` on the failure ground.
|
|
720
|
+
if (c.reason === "interrupted") {
|
|
721
|
+
const body = toolBlockParts(c, W, ctx);
|
|
722
|
+
return card("run", " ", verb, escapeTerminal(toolTargetOf(c)), ["interrupted"], body.rows, null, W, false, fold);
|
|
723
|
+
}
|
|
638
724
|
if (c.reason !== null) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
725
|
+
// VD-11: a PERSON's refusal is worth saying (they were asked
|
|
726
|
+
// and answered); a policy's is ambient. The outcome word says
|
|
727
|
+
// which, once.
|
|
728
|
+
const word = c.verdict !== null && c.verdict.decision === "denied" && c.verdict.decidedBy === undefined ? "denied by you" : "denied";
|
|
729
|
+
const outcome = [`${word} \u00b7 ${escapeTerminal(c.reason)}`, word];
|
|
730
|
+
const body = toolBlockParts(c, W, ctx);
|
|
731
|
+
return card("fail", " ", verb, escapeTerminal(toolTargetOf(c)), outcome, body.rows, null, W, true, fold);
|
|
643
732
|
}
|
|
644
|
-
// 4c: a card settled from the durable log carries no clock at
|
|
645
|
-
//
|
|
646
|
-
// time rather than `?s`, which reads as a measurement that failed.
|
|
733
|
+
// 4c: a card settled from the durable log carries no clock at
|
|
734
|
+
// all — it says nothing about time rather than `?s`.
|
|
647
735
|
const elapsed = c.startedAt !== null && c.doneAt !== null ? settledLabel((c.doneAt - c.startedAt) / 1000) : c.startedAt === null && c.doneAt === null ? "" : "?s";
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
736
|
+
// The tasks round (owner, 2026-10-06): a BACKGROUND delegation
|
|
737
|
+
// returns at once, so its card settles in a tenth of a second with
|
|
738
|
+
// the work only just begun. It names its children — the roles on
|
|
739
|
+
// the head, `N in the background`, a row per child with the task id
|
|
740
|
+
// `/tasks` and the notice use — instead of the foreground summary it
|
|
741
|
+
// never writes (the card read `DELEGATE … 1 line` with no target).
|
|
742
|
+
const children = c.name === "delegate" && !c.isError ? backgroundChildren(c) : null;
|
|
743
|
+
if (children !== null) {
|
|
744
|
+
const p2 = palette();
|
|
745
|
+
const rw = Math.max(...children.map((x) => x.role.length));
|
|
746
|
+
const room = bodyRowRoom(W);
|
|
747
|
+
const rows = children.map((x) => cutLine(`${bodyRow()}${x.id} ${p2.ink2}${x.role.padEnd(rw)}${p2.ink2 === "" ? "" : p2.fgEnd} ${p2.dim}${x.task}${p2.reset}`, room));
|
|
748
|
+
const outcome = `${children.length} in the background${elapsed === "" ? "" : ` \u00b7 ${elapsed}`}`;
|
|
749
|
+
return card("done", " ", verb, children.map((x) => x.role).join(" \u00b7 "), [outcome], slabPaints() ? rows : openBlock(rows), null, W, false);
|
|
750
|
+
}
|
|
657
751
|
const rawMeta = settledMeta(c);
|
|
658
|
-
// VD-6's suppression RETIRED with the thing it was suppressing.
|
|
659
|
-
// A read card said `(2 lines, 0.0s) · 2 lines · ctrl+o expands`
|
|
660
|
-
// because the parens and the suffix were written by different
|
|
661
|
-
// rounds, each unaware the other was counting; the fix blanked
|
|
662
|
-
// the meta whenever the suffix would repeat it. R13 takes the
|
|
663
|
-
// count OUT of the suffix, so there is one place again and the
|
|
664
|
-
// meta is it — blanking it now would drop the fact entirely.
|
|
665
|
-
// "Stated exactly once" is unchanged and is what `countedHead`
|
|
666
|
-
// below preserves: a meta that already counts lines gets no
|
|
667
|
-
// second count beside it.
|
|
668
752
|
const meta = escapeTerminal(rawMeta);
|
|
669
|
-
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
// went fine. The gutter is two spaces; the OUTCOME lives in the
|
|
695
|
-
// metadata, in words, which is also the only form that survives
|
|
696
|
-
// a pipe with the colour stripped. A failure keeps its colour
|
|
697
|
-
// AND its words — see settledMeta.
|
|
698
|
-
const body = toolBlockParts(c, W, ctx).rows;
|
|
699
|
-
if (body.length > 0) {
|
|
700
|
-
// R13 — THE CARD, when there is something to preview: the head
|
|
701
|
-
// row names the call, the preview sits inside, and the outcome
|
|
702
|
-
// CLOSES it on its own line (§7.5's words, moved off the head
|
|
703
|
-
// row because the head row is no longer the only row). D6: the
|
|
704
|
-
// target is bold there — the head row's job is to say WHAT was
|
|
705
|
-
// run, and the metadata has its own row now.
|
|
706
|
-
//
|
|
707
|
-
// A failure takes NO tint on the head row (R9): only the
|
|
708
|
-
// outcome word is coloured, which is §1.2 exactly — the
|
|
709
|
-
// colour rides the fact, not the object that carries it.
|
|
710
|
-
const target = escapeTerminal(toolTargetOf(c));
|
|
711
|
-
const head = cutLine(` ${verbCol} ${p.bold}${target}${p.reset}`, W);
|
|
712
|
-
// §7.5's outcome, in words, on its own row: what happened, how
|
|
713
|
-
// much of it there was, how long it took. The count is stated
|
|
714
|
-
// exactly ONCE (VD-6) — a meta that already counts lines does
|
|
715
|
-
// not get a second count beside it.
|
|
716
|
-
const n = countLines(c.resultText);
|
|
717
|
-
const counted = n > 0 && !/^\d+( of \d+)? lines?$/.test(rawMeta) ? `${n} line${n === 1 ? "" : "s"}` : "";
|
|
718
|
-
// pin 4's order, on the row that carries the core now: the
|
|
719
|
-
// ATTRIBUTION drops first, then the count, and the core —
|
|
720
|
-
// what happened and how long it took — is never cut open.
|
|
721
|
-
const join = (...xs) => xs.filter((x) => x !== "").join(" · ");
|
|
722
|
-
const attr = approvedBy.replace(/^ · /, "");
|
|
723
|
-
// DC-50 / R14: the key is RESERVED (§7.5), so its width comes
|
|
724
|
-
// out of the budget BEFORE the tiers are picked — not
|
|
725
|
-
// appended after, which is what the first build did and
|
|
726
|
-
// which let a narrow row cut the affordance off entirely.
|
|
727
|
-
// The tier ladder is what gives way; the key never is.
|
|
728
|
-
const keySuffix = c.expanded ? ` · ${COLLAPSE_ROW}` : "";
|
|
729
|
-
const words = pickTier([join(meta, counted, elapsed, attr), join(meta, counted, elapsed), join(meta, elapsed), meta], W - visibleWidth(noteIndent()) - keySuffix.length);
|
|
730
|
-
// DC-50 / R14 — ONE CARD, ONE SKELETON, expanded or not.
|
|
731
|
-
//
|
|
732
|
-
// The expanded card used to take a DIFFERENT shape: its head
|
|
733
|
-
// row carried the outcome inline (`shell npm test · exit 0 ·
|
|
734
|
-
// 40 lines · 0.1s`), it had no outcome row at all, and the
|
|
735
|
-
// `ctrl+o collapses` affordance was a row of its own at the
|
|
736
|
-
// end of the body. That was defensible while an expanded card
|
|
737
|
-
// was rare — reachable only for a live or approval-parked
|
|
738
|
-
// cell — and it stopped being defensible the moment ctrl+o
|
|
739
|
-
// became a switch that expands EVERY settled card at once: a
|
|
740
|
-
// page where the same call has two skeletons depending on a
|
|
741
|
-
// global toggle is the instability this round is named for.
|
|
742
|
-
//
|
|
743
|
-
// So both states are pad · head · blank · body · blank ·
|
|
744
|
-
// outcome · pad. The head row says WHAT was run and nothing
|
|
745
|
-
// else; the outcome row says what happened, how much, how
|
|
746
|
-
// long — and carries the affordance, because the affordance
|
|
747
|
-
// is a fact about this card's state and the outcome row is
|
|
748
|
-
// where this card's facts live.
|
|
749
|
-
const withKey = `${words}${keySuffix}`;
|
|
750
|
-
const outcome = c.isError ? `${p.red}${withKey}${p.reset}` : withKey;
|
|
751
|
-
return slabBlock(head, body, outcome, W);
|
|
753
|
+
const attr = attribution(c).replace(/^ · /, "");
|
|
754
|
+
// VD-6: the line count is stated exactly ONCE — a meta that
|
|
755
|
+
// already counts lines gets no second count beside it.
|
|
756
|
+
// R2a: an edit's or a write's result is kiso's receipt (`edited
|
|
757
|
+
// <path>` and its revision), not something it made: its diff is
|
|
758
|
+
// the body, and the receipt's line count is no fact about it
|
|
759
|
+
const n = editDiffOf(c) === null ? countLines(c.resultText) : 0;
|
|
760
|
+
const counted = n > 0 && !/^\d+( of \d+)? lines?$/.test(rawMeta) ? `${n} line${n === 1 ? "" : "s"}` : "";
|
|
761
|
+
const join = (...xs) => xs.filter((x) => x !== "").join(" \u00b7 ");
|
|
762
|
+
// pin 4's order: the ATTRIBUTION gives way first, then the count;
|
|
763
|
+
// what happened and how long it took is never cut open.
|
|
764
|
+
// The last tier is the CORE — what happened and how long it took;
|
|
765
|
+
// below it the target elides (headCore), never the core.
|
|
766
|
+
const tiers = [join(meta, counted, elapsed, attr), join(meta, counted, elapsed), join(meta, elapsed)];
|
|
767
|
+
const body = toolBlockParts(c, W, ctx);
|
|
768
|
+
const state = c.isError ? "fail" : "done";
|
|
769
|
+
if (body.rows.length > 0) {
|
|
770
|
+
// the foot is the way back: an expanded card's, when collapsing
|
|
771
|
+
// would hide something again (a card whose whole body always
|
|
772
|
+
// shows has no way back to offer). A collapsed card's key stands
|
|
773
|
+
// on its cut note's row (the card round, `cutNote`).
|
|
774
|
+
const collapsed = { ...c, expanded: false };
|
|
775
|
+
const behind = c.expanded && (toolBlockParts(collapsed, W, ctx).cut || hiddenLines(collapsed, W) !== null);
|
|
776
|
+
const foot = behind ? COLLAPSE_ROW : null;
|
|
777
|
+
return card(state, " ", verb, escapeTerminal(toolTargetOf(c)), tiers, body.rows, foot, W, c.isError, fold);
|
|
752
778
|
}
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
// ruling of 2026-09-03 supersedes it: the surface says WORK, not
|
|
761
|
-
// VERBATIM, and a page where some calls are cards and others are
|
|
762
|
-
// loose rows is the instability the owner was pointing at. §1.6
|
|
763
|
-
// moves with it.
|
|
764
|
-
const bare = c.isError ? ` ${p.red}${text}${p.reset}` : ` ${text}`;
|
|
765
|
-
return slabBlock(appendSuffix(bare, expandSuffix(hidden, W - visibleWidth(bare))), [], null, W);
|
|
779
|
+
// no body: the head row between its pads, and the key — when the
|
|
780
|
+
// result sits behind it (a read) — at the end of the head row
|
|
781
|
+
const hidden = hiddenLines(c, W);
|
|
782
|
+
// the key is RESERVED (TUI2-R1.5 ⑤): its words shorten only after
|
|
783
|
+
// the attribution and the count have given way
|
|
784
|
+
const keyed = hidden === null ? tiers : [...tiers.map((t) => `${t} \u00b7 ${EXPAND_ROW}`), `${tiers[tiers.length - 1]} \u00b7 ctrl+o`];
|
|
785
|
+
return card(state, " ", verb, escapeTerminal(toolTargetOf(c)), keyed, [], null, W, c.isError, fold);
|
|
766
786
|
}
|
|
767
787
|
if (c.state === "approval") {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
return out;
|
|
788
|
+
const body = toolBlockParts(c, W, ctx);
|
|
789
|
+
// a foot only where there is a body above it (§1.3)
|
|
790
|
+
return card("ask", `${p.gold}\u276f${p.gold === "" ? "" : p.fgEnd} `, verb, liveTarget(c), ["needs you"], body.rows, c.expanded && body.rows.length > 0 ? COLLAPSE_ROW : null, W, false, fold);
|
|
772
791
|
}
|
|
773
792
|
if (c.state === "running") {
|
|
774
|
-
//
|
|
775
|
-
// rides the
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
// welded to the last surviving characters of a cut word
|
|
780
|
-
// ("compil 1s"). The head is cut against the room the duration
|
|
781
|
-
// leaves; the duration then rides the row, always legible.
|
|
793
|
+
// R3 (design §5.2): a running call BREATHES in its mark cell; its
|
|
794
|
+
// duration rides the head row's right end, where the settled
|
|
795
|
+
// card's outcome will stand — the settle changes the words and
|
|
796
|
+
// the ground, never the height (DC-46: the settle may add only
|
|
797
|
+
// the foot).
|
|
782
798
|
const elapsed = c.startedAt !== null ? Math.max(1, Math.round((ctx.now - c.startedAt) / 1000)) : 1;
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
// SETTLED card's height from its first frame. The settle then
|
|
793
|
-
// changes CONTENT and never position: the spinner becomes two
|
|
794
|
-
// spaces in the same two columns, the live window gives back
|
|
795
|
-
// the rows the result did not need, and the metadata row that
|
|
796
|
-
// said `3s` says `exit 0 · 90 lines · 3.2s`.
|
|
797
|
-
//
|
|
798
|
-
// The elapsed moves OFF the head row onto that metadata row,
|
|
799
|
-
// which is where the settled card keeps it — the head row's job
|
|
800
|
-
// is to say what is running, and it says the same thing before
|
|
801
|
-
// and after.
|
|
802
|
-
//
|
|
803
|
-
// DC-43: with too little room for the seven-row skeleton (two
|
|
804
|
-
// pads, the head, two blanks, one preview row and the metadata)
|
|
805
|
-
// there is no card — the call keeps its head row until it
|
|
806
|
-
// commits, which is the one form that fits anywhere.
|
|
799
|
+
const el = elapsedLabel(elapsed);
|
|
800
|
+
const gestures = c.name === "shell" ? " \u00b7 esc stops \u00b7 alt+\u23ce redirects" : "";
|
|
801
|
+
// the gestures give way first (the live row names the same keys);
|
|
802
|
+
// `running · Ns` is the core, and the narrow ladder keeps the
|
|
803
|
+
// elapsed alone at the very end
|
|
804
|
+
const tiers = [`running \u00b7 ${el}${gestures}`, `running \u00b7 ${el}`];
|
|
805
|
+
const mark = `${breathFrame(ctx.spinnerI)} `;
|
|
806
|
+
// DC-43: with too little room for a card the call keeps its head
|
|
807
|
+
// row alone until it commits — the one form that fits anywhere.
|
|
807
808
|
const liveRows = ctx.liveWindow ?? CAP_PREVIEW;
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
// against the room the duration leaves (VD-4: the duration
|
|
813
|
-
// is its own segment, never welded to a cut word).
|
|
814
|
-
const bare = gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, Math.max(4, W - dur.length));
|
|
815
|
-
return [`${bare[0]}${p.dim}${dur}${p.reset}`];
|
|
816
|
-
}
|
|
817
|
-
// DC-46 — the two GESTURES ride the status row, where they cost
|
|
818
|
-
// nothing. They were a footer INSIDE the window, spending one of
|
|
819
|
-
// its rows on a sentence that is not output; with the window
|
|
820
|
-
// grown from its content, that row was the difference between a
|
|
821
|
-
// settle that swaps content and one that changes height. The
|
|
822
|
-
// row's shape is the settled outcome row's, so the settle
|
|
823
|
-
// rewrites it in place: `3s · esc stops` → `exit 0 · 90 lines ·
|
|
824
|
-
// 3.2s`.
|
|
825
|
-
const gestures = c.name === "shell" ? " · esc stops · alt+⏎ redirects" : "";
|
|
826
|
-
const status = pickTier([`${elapsedLabel(elapsed)}${gestures}`, elapsedLabel(elapsed)], Math.max(1, W - visibleWidth(noteIndent())));
|
|
827
|
-
const live = toolBlockBody(c, W, ctx);
|
|
828
|
-
if (live.length > 0)
|
|
829
|
-
return slabBlock(gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, W)[0], live, status, W);
|
|
830
|
-
// DC-48 — THE THREE-ROW CARD IS ONE ROW, so it is assembled here
|
|
831
|
-
// against the room it actually has.
|
|
832
|
-
//
|
|
833
|
-
// This branch used to cut the head to `W` and hand it to
|
|
834
|
-
// `slabBlock`, which joins head and outcome and cut nothing —
|
|
835
|
-
// so the row came out `W` wide PLUS the whole status, and the
|
|
836
|
-
// compositor did what it promises: it threw. On the owner's
|
|
837
|
-
// 80-column terminal a long `find` produced a 113-column row on
|
|
838
|
-
// its FIRST FRAME, which is the first second of every command.
|
|
839
|
-
//
|
|
840
|
-
// Pin 4's order: the command is the cuttable span and the
|
|
841
|
-
// elapsed is never cut open, so the command takes what the
|
|
842
|
-
// status leaves — the `· ` between them and the two-column
|
|
843
|
-
// gutter included.
|
|
844
|
-
// the status takes its own tier against the room the row has, not
|
|
845
|
-
// against a whole width: on a narrow terminal the gestures give
|
|
846
|
-
// way so the COMMAND keeps something to say, and the elapsed —
|
|
847
|
-
// pin 4's core — never does.
|
|
848
|
-
const MIN_TARGET = 10; // the gutter, the verb column, a character of command
|
|
849
|
-
const oneRow = pickTier([`${elapsedLabel(elapsed)}${gestures}`, elapsedLabel(elapsed)], Math.max(1, W - MIN_TARGET - 3));
|
|
850
|
-
const room = Math.max(4, W - visibleWidth(oneRow) - 3);
|
|
851
|
-
const only = gutterCut(gutter, `${verbCol} ${liveTarget(c)}`, room)[0];
|
|
852
|
-
return slabBlock(`${only}${p.dim} · ${oneRow}${p.reset}`, [], null, W);
|
|
809
|
+
if (liveRows <= 0)
|
|
810
|
+
return [cardHeadRow(mark, verb, liveTarget(c), tiers, W, false, false)];
|
|
811
|
+
const body = toolBlockParts(c, W, ctx);
|
|
812
|
+
return card("run", mark, verb, liveTarget(c), tiers, body.rows, c.expanded && body.rows.length > 0 ? COLLAPSE_ROW : null, W, false, fold);
|
|
853
813
|
}
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
|
|
857
|
-
// The `◦` carried "not started yet" ALONE, and a marker is not a
|
|
858
|
-
// word: on a screen where running and queued rows sit together, a
|
|
859
|
-
// reader has to already know the glyph. The suffix says it.
|
|
860
|
-
return gutterCut(`${p.dim}◦${p.reset} `, `${verbCol} ${liveTarget(c)}${p.dim} · queued${p.reset}`, W);
|
|
814
|
+
// not started yet (queued behind an exclusive call): the same card,
|
|
815
|
+
// its outcome says so.
|
|
816
|
+
return card("run", " ", verb, liveTarget(c), ["queued"], [], null, W, false, fold);
|
|
861
817
|
}
|
|
862
818
|
}
|
|
863
819
|
// ---- TUI2-R1 (A): the self-naming expand affordance ----
|
|
@@ -887,7 +843,7 @@ function hiddenLines(c, W) {
|
|
|
887
843
|
if (n === 0)
|
|
888
844
|
return null;
|
|
889
845
|
if (c.isError)
|
|
890
|
-
return null; //
|
|
846
|
+
return null; // an error previews its text, and the foot carries the key
|
|
891
847
|
// R13: a call that PREVIEWS carries the key on its own note row when
|
|
892
848
|
// something is cut, and needs no affordance at all when nothing is.
|
|
893
849
|
// A head-row suffix as well would be TUI2-R1's two affordances for
|
|
@@ -897,90 +853,6 @@ function hiddenLines(c, W) {
|
|
|
897
853
|
return null;
|
|
898
854
|
return n;
|
|
899
855
|
}
|
|
900
|
-
/**
|
|
901
|
-
* TUI2-R1 (A) — the suffix, in the width that is LEFT.
|
|
902
|
-
*
|
|
903
|
-
* Three tiers, degrading: the full form teaches the key AND what it
|
|
904
|
-
* does, the terse form keeps the count and the key, the bare form keeps
|
|
905
|
-
* the key alone. Below that the row is left exactly as it is today — the
|
|
906
|
-
* affordance is worth a suffix, never worth cutting the path the row
|
|
907
|
-
* exists to name (invariant ① holds by construction: the tier is chosen
|
|
908
|
-
* against the room the row actually has).
|
|
909
|
-
*/
|
|
910
|
-
/** TUI2-R1.5 ⑤ — the cells a settled head row reserves for its
|
|
911
|
-
* affordance: exactly the shortest tier, " · ctrl+o". The suffix used
|
|
912
|
-
* to take whatever width happened to be left, so a long target spent it
|
|
913
|
-
* all and the card said nothing about the lines behind the key. Every
|
|
914
|
-
* row that fitted its head before still fits it; only a head that would
|
|
915
|
-
* have eaten the whole row gives up its last nine cells. */
|
|
916
|
-
const SUFFIX_MIN = " · ctrl+o".length;
|
|
917
|
-
/**
|
|
918
|
-
* TUI2-R1.5 pin 4 — the settled head row's text, with a PINNED cut
|
|
919
|
-
* order.
|
|
920
|
-
*
|
|
921
|
-
* The row carries four things of very different value, and until now a
|
|
922
|
-
* single trailing widthCut decided between them by position: the parens
|
|
923
|
-
* came last, so the parens were what got cut. The walkthrough caught
|
|
924
|
-
* both consequences —
|
|
925
|
-
*
|
|
926
|
-
* ✓ shell printf '…' 1 2 … 12 (exit 0 · approv… · ctrl+o
|
|
927
|
-
* ✓ shell for i in 1 2 3 … … · ctrl+o
|
|
928
|
-
*
|
|
929
|
-
* — an UNCLOSED parenthesis, and a row that lost the exit code and the
|
|
930
|
-
* duration to a command string that had no claim on them. A cut that
|
|
931
|
-
* leaves `(exit 0 · approv…` has not shortened a fact, it has broken
|
|
932
|
-
* one: the reader is left holding the beginning of a sentence.
|
|
933
|
-
*
|
|
934
|
-
* The order, tightest last:
|
|
935
|
-
* 1. the affordance is already reserved by the caller (⑤);
|
|
936
|
-
* 2. the RESULT CORE — `(exit 0, 3.0s)`, `(+1 -1, 0.2s)` — renders
|
|
937
|
-
* whole, closing paren included, or not at all;
|
|
938
|
-
* 3. the ATTRIBUTION segment drops before the core is touched: it is
|
|
939
|
-
* a note about who decided, and the result is what happened;
|
|
940
|
-
* 4. the COMMAND/target truncates with `…`. It is the most
|
|
941
|
-
* compressible thing on the row — a reader recognises a command
|
|
942
|
-
* from its head — and it is the only part with a natural ellipsis.
|
|
943
|
-
*/
|
|
944
|
-
function settledHeadText(verbCol, target, meta, attr, elapsed, room, counted = "") {
|
|
945
|
-
// R13 — ONE GRAMMAR FOR BOTH CARDS. This row used to close with W4's
|
|
946
|
-
// parentheses — `read a.ts (0.1s) · 10 lines · ctrl+o expands` —
|
|
947
|
-
// while the bodied card's outcome row said `exit 0 · 90 lines · 0.4s`
|
|
948
|
-
// in a `·` chain. Two shapes for the same facts, and which one a call
|
|
949
|
-
// got depended on whether it happened to have a preview. The chain
|
|
950
|
-
// wins: it is the one the outcome row already uses, it reads in one
|
|
951
|
-
// direction, and it puts the elapsed where the other card puts it.
|
|
952
|
-
//
|
|
953
|
-
// The giving-way order is pin 4's, unchanged: the ATTRIBUTION drops
|
|
954
|
-
// first, then the count, and the core — what happened and how long it
|
|
955
|
-
// took — is never cut open; below that the target itself truncates.
|
|
956
|
-
const join = (...xs) => xs.filter((x) => x !== "").join(" · ");
|
|
957
|
-
const core = join(meta, counted, elapsed);
|
|
958
|
-
const withAttr = join(meta, counted, elapsed, attr.replace(" · ", ""));
|
|
959
|
-
const lead = `${verbCol} `;
|
|
960
|
-
const row = (tail) => `${lead}${target}${tail === "" ? "" : ` · ${tail}`}`;
|
|
961
|
-
// 1. everything; 2. the attribution gives way; 2b. the COUNT gives
|
|
962
|
-
// way next (pin 4), where the suffix is not already carrying it —
|
|
963
|
-
// the target whole through all three
|
|
964
|
-
const whole = firstFit([row(withAttr), row(core), row(join(meta, elapsed))], room);
|
|
965
|
-
if (whole !== null)
|
|
966
|
-
return whole;
|
|
967
|
-
// 3. the target truncates, the core stays whole
|
|
968
|
-
const stem = join(meta, elapsed);
|
|
969
|
-
const budget = room - visibleWidth(lead) - visibleWidth(stem) - 4; // the ellipsis + " · "
|
|
970
|
-
if (budget >= 1)
|
|
971
|
-
return `${lead}${widthCut(target, budget)}… · ${stem}`;
|
|
972
|
-
// 4. DC-48 — the ELAPSED still rides, and the target takes what is
|
|
973
|
-
// left. This used to return the target alone, on the argument that
|
|
974
|
-
// a cut core would leave a half-open parenthesis; R13's chain has
|
|
975
|
-
// no bracket to leave open, so the reason retired with the
|
|
976
|
-
// parentheses and pin 4's own rule applies at every width: what
|
|
977
|
-
// happened and how long it took is never cut away.
|
|
978
|
-
const floor = elapsed;
|
|
979
|
-
const left = room - visibleWidth(lead) - visibleWidth(floor) - 4; // the ellipsis + " · "
|
|
980
|
-
if (left >= 1)
|
|
981
|
-
return `${lead}${widthCut(target, left)}… · ${floor}`;
|
|
982
|
-
return `${lead}${widthCut(target, Math.max(1, room - visibleWidth(lead)))}`;
|
|
983
|
-
}
|
|
984
856
|
/**
|
|
985
857
|
* TUI2-R1.5 ⑤ (VD-11) — approval attribution, about humans.
|
|
986
858
|
*
|
|
@@ -1001,33 +873,15 @@ function attribution(c) {
|
|
|
1001
873
|
return "";
|
|
1002
874
|
return c.verdict.decision === "denied" ? " · denied" : " · approved";
|
|
1003
875
|
}
|
|
1004
|
-
/** The expand key as a row's tail, in two tiers; empty when there is
|
|
1005
|
-
* nothing hidden. R13 moved the line COUNT off this suffix onto the
|
|
1006
|
-
* head row's own `·` chain (VD-6: stated exactly once). */
|
|
1007
|
-
function expandSuffix(lines, room) {
|
|
1008
|
-
if (lines === null)
|
|
1009
|
-
return "";
|
|
1010
|
-
for (const tier of [" · ctrl+o expands", " · ctrl+o"]) {
|
|
1011
|
-
if (tier.length <= room)
|
|
1012
|
-
return tier;
|
|
1013
|
-
}
|
|
1014
|
-
return "";
|
|
1015
|
-
}
|
|
1016
|
-
/** The suffix as the row's dim tail (the empty suffix leaves the row's
|
|
1017
|
-
* bytes untouched — a caller never has to branch). */
|
|
1018
|
-
function appendSuffix(row, suffix) {
|
|
1019
|
-
if (suffix === "")
|
|
1020
|
-
return row;
|
|
1021
|
-
const p = palette();
|
|
1022
|
-
return `${row}${p.dim}${suffix}${p.reset}`;
|
|
1023
|
-
}
|
|
1024
876
|
/* DECLARED REVERSAL (D-S2-1, owner-ruled 2026-09-06): `focusToken` and
|
|
1025
877
|
`EXPAND_KEY` stood here — TUI2-R2 ⑤'s bright ctrl+o token on the
|
|
1026
878
|
newest live card, "exactly one bright token per frame". DC-50 made
|
|
1027
879
|
ctrl+o a global switch, so there was no target left for a marker to
|
|
1028
880
|
name; the status row's idle hint names the switch (`idleHint`). */
|
|
1029
|
-
/** TUI2-R1 (A) — the expanded card's key
|
|
881
|
+
/** TUI2-R1 (A) — the expanded card's key: the way back. */
|
|
1030
882
|
const COLLAPSE_ROW = "ctrl+o collapses";
|
|
883
|
+
/** Graphite §7.4 — the key on a card with rows behind it. */
|
|
884
|
+
const EXPAND_ROW = "ctrl+o expands";
|
|
1031
885
|
/* DECLARED REVERSAL (R13, owner-ruled 2026-09-03): the W13 rollup's
|
|
1032
886
|
noun table (`ROLLUP_NOUN`) and TUI2-R1 (B)'s exploration-run set
|
|
1033
887
|
(`EXPLORE_NOUN`, `isExploreTool`) stood here. Both collapses retired
|
|
@@ -1113,8 +967,8 @@ export function askedBlock(resultText, seconds, W) {
|
|
|
1113
967
|
return [];
|
|
1114
968
|
const asked = parsed;
|
|
1115
969
|
const p = palette();
|
|
1116
|
-
const head = (n, outcome) => cutLine(
|
|
1117
|
-
const row = (body) => cutLine(
|
|
970
|
+
const head = (n, outcome) => cutLine(`${EDGE}${p.bold}asked${p.reset} ${n} ${n === 1 ? "question" : "questions"} ${p.dim}(${seconds === null ? outcome : `${outcome}, ${seconds.toFixed(1)}s`})${p.reset}`, W);
|
|
971
|
+
const row = (body) => cutLine(`${EDGE}${p.dim}│${p.reset} ${body}`, W);
|
|
1118
972
|
if (Array.isArray(asked.declined) && asked.declined.length > 0) {
|
|
1119
973
|
// the honest decline record: WHAT went unanswered, and what the
|
|
1120
974
|
// choices had been — the panel already computes both.
|
|
@@ -1159,72 +1013,68 @@ const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
|
|
|
1159
1013
|
* `└` survives as the mark that OPENS the block, once, on its first
|
|
1160
1014
|
* row (see openBlock). In-block notes take the same indent,
|
|
1161
1015
|
* no glyph — because a second `└` inside one block would be the same
|
|
1162
|
-
* mark meaning two things (§4.1).
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
* rows are output". Where nothing paints, it is exactly that, and R8a
|
|
1168
|
-
* stands unchanged. */
|
|
1016
|
+
* mark meaning two things (§4.1). */
|
|
1017
|
+
/** Graphite §7.4 — off the surface (unknown ground), a card's body sits
|
|
1018
|
+
* two columns under its head: the head at the content edge (2), the body
|
|
1019
|
+
* at 4, opened by `└` (R8a's indent, which is what says "these rows are
|
|
1020
|
+
* output" once nothing is painted). */
|
|
1169
1021
|
const BODY_ROW_FLAT = " ";
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
*
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
*
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1022
|
+
/** Inside a painted card the body sits at the content edge, under the
|
|
1023
|
+
* verb — the head and what it printed line up (owner, 2026-09-29, the
|
|
1024
|
+
* 0.44 card's alignment). */
|
|
1025
|
+
const CARD_BODY = "";
|
|
1026
|
+
/** A card's inner right margin. */
|
|
1027
|
+
const CARD_RIGHT = 1;
|
|
1028
|
+
/** The cells a painted card has for its content: after its edge cell and
|
|
1029
|
+
* its mark cell, from the content edge to the right edge. */
|
|
1030
|
+
const cardInner = (W) => Math.max(1, W - EDGE.length);
|
|
1031
|
+
/** Graphite §1.8 — the model's words and its thinking begin at the
|
|
1032
|
+
* content edge, like every other block. */
|
|
1033
|
+
const PROSE_COL = EDGE;
|
|
1034
|
+
const THINK_COL = EDGE;
|
|
1035
|
+
const bodyRow = () => (slabPaints() ? CARD_BODY : BODY_ROW_FLAT);
|
|
1036
|
+
const noteIndent = bodyRow;
|
|
1037
|
+
/** The cells a body row's text has: under the target inside a card, under
|
|
1038
|
+
* the flat indent outside one. */
|
|
1039
|
+
const bodyTextWidth = (W) => Math.max(1, slabPaints() ? cardInner(W) - CARD_BODY.length - CARD_RIGHT : W - BODY_ROW_FLAT.length);
|
|
1040
|
+
/** The cells a whole body row has (its indent included). */
|
|
1041
|
+
const bodyRowRoom = (W) => Math.max(1, slabPaints() ? cardInner(W) - CARD_RIGHT : W);
|
|
1189
1042
|
/**
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
* the outcome closing it. `└` does not open a slab — the surface is the
|
|
1196
|
-
* container, and a corner inside it is §1.3's empty mark one scale up.
|
|
1197
|
-
*
|
|
1198
|
-
* THE DEGRADATION IS THE POINT OF THE PREDICATE. `wash` is a chosen
|
|
1199
|
-
* background on the two KNOWN grounds and reverse video on the third
|
|
1200
|
-
* (§3's last rung). A chip inverting for one row is the design working; eight
|
|
1201
|
-
* output rows inverting is a blackboard in the middle of the transcript.
|
|
1202
|
-
* So a slab paints only where the wash is a real background, and where
|
|
1203
|
-
* it is not the block degrades to what it has always been — the R8a
|
|
1204
|
-
* four-column indent with a dim tail. Never to reverse video.
|
|
1205
|
-
*
|
|
1206
|
-
* The content shape does NOT change with the surface: the note row and
|
|
1207
|
-
* the outcome row exist either way, in `washDim` on the slab and in the
|
|
1208
|
-
* ordinary dim off it. Only the surface and its two blank rows are
|
|
1209
|
-
* contingent, because an unpainted blank row is §1.3's empty mark at the
|
|
1210
|
-
* scale of a row.
|
|
1043
|
+
* Graphite §7.4 / §3.1 — a card paints only where the ground is known:
|
|
1044
|
+
* there its surfaces are real backgrounds. On the unknown ground they
|
|
1045
|
+
* would be reverse video, and eight inverted output rows are a black slab
|
|
1046
|
+
* in the middle of the transcript, so the card keeps its content and
|
|
1047
|
+
* loses its surface.
|
|
1211
1048
|
*/
|
|
1212
1049
|
function slabPaints() {
|
|
1050
|
+
return palette().washDone !== "" && currentGround() !== "unknown";
|
|
1051
|
+
}
|
|
1052
|
+
function cardPaint(state) {
|
|
1213
1053
|
const p = palette();
|
|
1214
|
-
|
|
1054
|
+
if (state === "fail")
|
|
1055
|
+
return { bg: p.washFail, edge: p.failEdge };
|
|
1056
|
+
if (state === "ask")
|
|
1057
|
+
return { bg: p.washAsk, edge: p.askEdge };
|
|
1058
|
+
return { bg: p.washRun, edge: p.runEdge };
|
|
1215
1059
|
}
|
|
1216
|
-
/** One
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
|
|
1060
|
+
/** One painted card row, the full width: the edge cell in column 0, the
|
|
1061
|
+
* mark cell in column 1, the content from the content edge, the state's
|
|
1062
|
+
* ground to the right edge, padded by DISPLAY width. A reset inside the
|
|
1063
|
+
* content would strand the ground for the rest of the row, so every
|
|
1064
|
+
* reset re-opens it (the selection bar's discipline). */
|
|
1065
|
+
function slabRow(inner, W, paint, mark = " ") {
|
|
1221
1066
|
const p = palette();
|
|
1222
|
-
const
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1067
|
+
const room = cardInner(W);
|
|
1068
|
+
const fitted = visibleWidth(inner) > room ? cutLine(inner, room) : inner;
|
|
1069
|
+
const body = fitted.replaceAll(p.reset, `${p.reset}${paint.bg}`);
|
|
1070
|
+
const pad = Math.max(0, room - visibleWidth(fitted));
|
|
1071
|
+
return `${paint.edge} ${paint.bg}${mark.replaceAll(p.reset, `${p.reset}${paint.bg}`)}${body}${paint.bg}${" ".repeat(pad)}${p.washEnd}`;
|
|
1072
|
+
}
|
|
1073
|
+
/** A card's pad: a whole row of its ground (§1.5), its edge cell too. */
|
|
1074
|
+
function padRow(paint, W) {
|
|
1075
|
+
const p = palette();
|
|
1076
|
+
return `${paint.edge} ${paint.bg}${" ".repeat(Math.max(0, W - 1))}${p.washEnd}`;
|
|
1225
1077
|
}
|
|
1226
|
-
/** A metadata row inside (or under) a block: `washDim` on the slab,
|
|
1227
|
-
* the ordinary dim off it. §2.1 is why there are two. */
|
|
1228
1078
|
/** The widest form that fits the row, or the last one — the head row's
|
|
1229
1079
|
* own discipline (TUI2-R1.5 ⑤, pin 4) applied to the slab's two
|
|
1230
1080
|
* metadata rows: the parts give way in a PINNED ORDER, and the part
|
|
@@ -1232,65 +1082,177 @@ function slabRow(inner, W) {
|
|
|
1232
1082
|
function pickTier(tiers, room) {
|
|
1233
1083
|
return firstFit(tiers, room) ?? tiers[tiers.length - 1];
|
|
1234
1084
|
}
|
|
1235
|
-
/** The widest form that fits the row, or null when none does
|
|
1236
|
-
* settled head row asks this with its target whole, and only then
|
|
1237
|
-
* cuts the target. The three rows that give way in a pinned order
|
|
1238
|
-
* (the settled head, the running head, the outcome row) share this
|
|
1239
|
-
* walk; their ORDERS differ by ruling and stay their own. */
|
|
1085
|
+
/** The widest form that fits the row, or null when none does. */
|
|
1240
1086
|
function firstFit(tiers, room) {
|
|
1241
1087
|
for (const t of tiers)
|
|
1242
1088
|
if (visibleWidth(t) <= room)
|
|
1243
1089
|
return t;
|
|
1244
1090
|
return null;
|
|
1245
1091
|
}
|
|
1092
|
+
/** A metadata row inside a card's body (a cut note, the tool's own cap):
|
|
1093
|
+
* cut, never folded — a metadata row that wraps costs the block a row it
|
|
1094
|
+
* did not budget. */
|
|
1246
1095
|
function noteRow(text, W, tone) {
|
|
1247
1096
|
const p = palette();
|
|
1248
|
-
// CUT, never folded — A6's rule for the tool header, and for the same
|
|
1249
|
-
// reason: a metadata row that wraps costs the block a row it did not
|
|
1250
|
-
// budget, and at 30 columns the fold put "expands" alone on a line of
|
|
1251
|
-
// its own. The row names a fact; a cut names it shorter.
|
|
1252
1097
|
const open = tone === "body" ? p.washDim : p.dim;
|
|
1253
1098
|
const close = tone === "body" ? p.washDimEnd : p.reset;
|
|
1254
|
-
return [cutLine(`${open}${noteIndent()}${text}${close}`, W)];
|
|
1099
|
+
return [cutLine(`${open}${noteIndent()}${text}${close}`, bodyRowRoom(W))];
|
|
1100
|
+
}
|
|
1101
|
+
/** `text` elided in its MIDDLE to `room` cells — a path keeps its head
|
|
1102
|
+
* and its file name, which is how a reader recognises it (§7.5). */
|
|
1103
|
+
function elideMiddle(text, room) {
|
|
1104
|
+
if (visibleWidth(text) <= room)
|
|
1105
|
+
return text;
|
|
1106
|
+
if (room <= 1)
|
|
1107
|
+
return "\u2026".slice(0, Math.max(0, room));
|
|
1108
|
+
const right = Math.floor((room - 1) / 2);
|
|
1109
|
+
const left = room - 1 - right;
|
|
1110
|
+
const chars = Array.from(text);
|
|
1111
|
+
let tail = "";
|
|
1112
|
+
for (let i = chars.length - 1; i >= 0; i -= 1) {
|
|
1113
|
+
if (displayWidth(chars[i] + tail) > right)
|
|
1114
|
+
break;
|
|
1115
|
+
tail = chars[i] + tail;
|
|
1116
|
+
}
|
|
1117
|
+
return `${widthCut(text, left)}\u2026${tail}`;
|
|
1118
|
+
}
|
|
1119
|
+
/** §7.5 — only the outcome WORD takes colour: the first segment of the
|
|
1120
|
+
* outcome — `exit 0` in the success colour, a failure's word in the
|
|
1121
|
+
* failure colour, `needs you` in gold (the card round: it stands with
|
|
1122
|
+
* its `❯` now) — and the rest is `dim`. */
|
|
1123
|
+
function outcomeStyled(text, error) {
|
|
1124
|
+
const p = palette();
|
|
1125
|
+
if (text === "")
|
|
1126
|
+
return "";
|
|
1127
|
+
const at = text.indexOf(" \u00b7 ");
|
|
1128
|
+
const first = at < 0 ? text : text.slice(0, at);
|
|
1129
|
+
const rest = at < 0 ? "" : text.slice(at);
|
|
1130
|
+
const tone = error ? p.red : /^exit 0$/.test(first) ? p.green : first === "needs you" ? p.gold : "";
|
|
1131
|
+
return tone === "" ? `${p.dim}${text}${p.reset}` : `${tone}${first}${p.reset}${rest === "" ? "" : `${p.dim}${rest}${p.reset}`}`;
|
|
1255
1132
|
}
|
|
1256
1133
|
/**
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1259
|
-
* `
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1134
|
+
* §7.4 / §7.5 — a card's head row, without the card's edge cell: the verb
|
|
1135
|
+
* (upper case, `dim`), the target, and the outcome right-aligned in
|
|
1136
|
+
* `room` cells, `mark` (one styled cell, or "") standing in front of the
|
|
1137
|
+
* outcome's words. It gives way in a pinned order: the outcome's tiers
|
|
1138
|
+
* first (the attribution, then the count), then the target elides in its
|
|
1139
|
+
* middle; the outcome word is never cut. The row never folds; a command
|
|
1140
|
+
* folds in `headRows`.
|
|
1141
|
+
*/
|
|
1142
|
+
function headCore(verb, target, tiers, room, error, mark = "") {
|
|
1143
|
+
const p = palette();
|
|
1144
|
+
// the verb and ONE space, then the target (owner, 2026-09-29: the verb
|
|
1145
|
+
// column padded to seven left the target stranded between the verb and
|
|
1146
|
+
// the body under it — 0.44's `shell pwd && ls -la` reads as one line)
|
|
1147
|
+
const lead = `${p.dim}${verb}${p.reset} `;
|
|
1148
|
+
const avail = Math.max(1, room - (verb.length + 1));
|
|
1149
|
+
// the card round (owner, 2026-10-05): the running `●` and the waiting
|
|
1150
|
+
// `❯` stand where the settled outcome will, so the state is read in one
|
|
1151
|
+
// place and nothing sits against the verb
|
|
1152
|
+
const marked = (out) => `${mark === "" ? "" : `${mark} `}${outcomeStyled(out, error)}`;
|
|
1153
|
+
const mw = mark === "" ? 0 : 2;
|
|
1154
|
+
const compose = (tg, out) => `${lead}${tg}${" ".repeat(Math.max(2, avail - visibleWidth(tg) - visibleWidth(out) - mw))}${marked(out)}`;
|
|
1155
|
+
for (const t of tiers)
|
|
1156
|
+
if (visibleWidth(target) + 2 + visibleWidth(t) + mw <= avail)
|
|
1157
|
+
return compose(target, t);
|
|
1158
|
+
const last = tiers[tiers.length - 1] ?? "";
|
|
1159
|
+
const tRoom = avail - 2 - visibleWidth(last) - mw;
|
|
1160
|
+
if (tRoom >= 4)
|
|
1161
|
+
return compose(elideMiddle(target, tRoom), last);
|
|
1162
|
+
// A NARROW row: the target gives way entirely, then the verb's padding
|
|
1163
|
+
// and the verb, then everything but the outcome — and the outcome's
|
|
1164
|
+
// last word, how long it took, is the one thing kept to the end (pin 4:
|
|
1165
|
+
// what happened and how long it took is never cut open).
|
|
1166
|
+
const bareLead = `${p.dim}${verb}${p.reset} `;
|
|
1167
|
+
// the outcome's segments give way from the FRONT — the count, then
|
|
1168
|
+
// the outcome word (and the mark with it) — so how long it took, and
|
|
1169
|
+
// the key where there is one, are the last to go
|
|
1170
|
+
const segs = last.split(" \u00b7 ");
|
|
1171
|
+
const suffixes = segs.map((_, k) => segs.slice(k).join(" \u00b7 "));
|
|
1172
|
+
for (const row of [`${lead}${marked(last)}`, `${bareLead}${marked(last)}`, ...suffixes.map((x, k) => (k === 0 ? marked(x) : `${p.dim}${x}${p.reset}`))]) {
|
|
1173
|
+
if (visibleWidth(row) <= room)
|
|
1174
|
+
return row;
|
|
1175
|
+
}
|
|
1176
|
+
return cutLine(`${p.dim}${suffixes[suffixes.length - 1]}${p.reset}`, room);
|
|
1177
|
+
}
|
|
1178
|
+
/** The card round — the cells the head's first row keeps for the outcome
|
|
1179
|
+
* when a command folds, whatever the outcome says now: `● running · 59m
|
|
1180
|
+
* 59s` and `exit 127 · 59m 59s` both fit. The fold is measured against
|
|
1181
|
+
* this, never against the words of the moment, so a card neither
|
|
1182
|
+
* re-folds while it runs nor changes height when it settles (DC-46: a
|
|
1183
|
+
* live card never shrinks). */
|
|
1184
|
+
const OUTCOME_ROOM = 20;
|
|
1185
|
+
/** The least room a command's first row keeps; narrower, the head is one
|
|
1186
|
+
* row and the command elides (the narrow ladder, `headCore`). */
|
|
1187
|
+
const FOLD_MIN = 12;
|
|
1188
|
+
/** A collapsed card's command shows at most this many rows. */
|
|
1189
|
+
const HEAD_ROWS = 3;
|
|
1190
|
+
/**
|
|
1191
|
+
* The card round (owner, 2026-10-05) — the head's rows. A shell command
|
|
1192
|
+
* is code the person approves and audits, so it is never cut in its
|
|
1193
|
+
* middle: when it does not fit beside the outcome it folds at its spaces,
|
|
1194
|
+
* each further row hanging under its own first character, for at most
|
|
1195
|
+
* `max` rows — past that the last row ends in `…` and ctrl+o shows the
|
|
1196
|
+
* rest. Every other target (a path keeps its head and its file name) is
|
|
1197
|
+
* one row, `headCore`'s. DECLARED REVERSAL of §7.5's "the row never
|
|
1198
|
+
* folds" for a command.
|
|
1199
|
+
*/
|
|
1200
|
+
function headRows(verb, target, tiers, room, error, mark, max) {
|
|
1201
|
+
const single = [headCore(verb, target, tiers, room, error, mark)];
|
|
1202
|
+
if (max <= 1)
|
|
1203
|
+
return single;
|
|
1204
|
+
const lead = verb.length + 1;
|
|
1205
|
+
const avail = room - lead;
|
|
1206
|
+
const last = tiers[tiers.length - 1] ?? "";
|
|
1207
|
+
const first = avail - 2 - Math.max(OUTCOME_ROOM, visibleWidth(last) + (mark === "" ? 0 : 2));
|
|
1208
|
+
if (first < FOLD_MIN || visibleWidth(target) <= first)
|
|
1209
|
+
return single;
|
|
1210
|
+
const head = widthCut(target, first);
|
|
1211
|
+
const space = head.lastIndexOf(" ");
|
|
1212
|
+
const cut = space > 0 ? space + 1 : Math.max(1, head.length);
|
|
1213
|
+
const rest = target.slice(cut).trimStart();
|
|
1214
|
+
const rows = [target.slice(0, cut).trimEnd(), ...(rest === "" ? [] : foldAtSpaces(rest, avail))];
|
|
1215
|
+
const shown = rows.length <= max ? rows : [...rows.slice(0, max - 1), `${widthCut(rows.slice(max - 1).join(" "), avail - 1)}\u2026`];
|
|
1216
|
+
return [headCore(verb, shown[0], tiers, room, error, mark), ...shown.slice(1).map((r) => `${" ".repeat(lead)}${r}`)];
|
|
1217
|
+
}
|
|
1218
|
+
/** DC-43 — the head row ALONE, its mark in the mark column: a running
|
|
1219
|
+
* call's form when the room left is too small for a card. `mark` is two
|
|
1220
|
+
* cells (the glyph and a space, or two spaces). */
|
|
1221
|
+
function cardHeadRow(mark, verb, target, tiers, W, _painted, error) {
|
|
1222
|
+
return cutLine(`${mark}${headCore(verb, target, tiers, Math.max(1, W - EDGE.length), error)}`, W);
|
|
1223
|
+
}
|
|
1224
|
+
/** The card's foot: the key, right-aligned. */
|
|
1225
|
+
function footRow(key, room) {
|
|
1226
|
+
const p = palette();
|
|
1227
|
+
return `${" ".repeat(Math.max(0, room - visibleWidth(key)))}${p.dim}${key}${p.reset}`;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Graphite §7.4 — assemble a card: pad · head · body · foot · pad on the
|
|
1231
|
+
* state's ground, full width, its edge cell in column 0, column 1 blank;
|
|
1232
|
+
* or, off the surface, the same content without it — the mark in the
|
|
1233
|
+
* mark column, the head at the content edge, the body under it, the foot
|
|
1234
|
+
* dim. `mark` is two cells: the glyph and a space, or two spaces. Inside
|
|
1235
|
+
* a card the glyph stands in front of the outcome's words (the card
|
|
1236
|
+
* round: in column 1 it sat against the verb). `fold` is how many rows
|
|
1237
|
+
* the head's command may take (0: the target never folds).
|
|
1263
1238
|
*/
|
|
1264
|
-
function
|
|
1239
|
+
function card(state, mark, verb, target, tiers, body, foot, W, error, fold = 0) {
|
|
1265
1240
|
if (!slabPaints()) {
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1241
|
+
const [first, ...more] = headRows(verb, target, tiers, Math.max(1, W - EDGE.length), error, "", fold);
|
|
1242
|
+
const out = [cutLine(`${mark}${first}`, W), ...more.map((r) => cutLine(`${EDGE}${r}`, W)), ...body];
|
|
1243
|
+
if (foot !== null)
|
|
1244
|
+
out.push(cutLine(footRow(foot, W), W));
|
|
1269
1245
|
return out;
|
|
1270
1246
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
// The callers above size their parts against the room they have;
|
|
1281
|
-
// this is the backstop that makes invariant ① hold whatever they
|
|
1282
|
-
// do, and it is what was missing when a running card's head was
|
|
1283
|
-
// cut to W and then had a whole status appended to it.
|
|
1284
|
-
const only = cutLine(outcome === null ? head : `${head} ${outcome}`, W);
|
|
1285
|
-
return [slabRow("", W), slabRow(only, W), slabRow("", W)];
|
|
1286
|
-
}
|
|
1287
|
-
const top = [slabRow("", W), slabRow(head, W), slabRow("", W), ...body.map((r) => slabRow(r, W))];
|
|
1288
|
-
// An EXPANDED block closes with its own footer and passes no outcome;
|
|
1289
|
-
// a blank row and an empty metadata row under it would be §1.3's
|
|
1290
|
-
// empty mark twice over.
|
|
1291
|
-
if (outcome === null)
|
|
1292
|
-
return [...top, slabRow("", W)];
|
|
1293
|
-
return [...top, slabRow("", W), ...noteRow(outcome, W, "body").map((r) => slabRow(r, W)), slabRow("", W)];
|
|
1247
|
+
const paint = cardPaint(state);
|
|
1248
|
+
const inner = cardInner(W);
|
|
1249
|
+
const glyph = mark.replace(/ $/, "");
|
|
1250
|
+
const head = headRows(verb, target, tiers, inner - CARD_RIGHT, error, glyph.trim() === "" ? "" : glyph, fold);
|
|
1251
|
+
const rows = [padRow(paint, W), ...head.map((r) => slabRow(r, W, paint)), ...body.map((r) => slabRow(r, W, paint))];
|
|
1252
|
+
if (foot !== null)
|
|
1253
|
+
rows.push(slabRow(footRow(foot, inner - CARD_RIGHT), W, paint));
|
|
1254
|
+
rows.push(padRow(paint, W));
|
|
1255
|
+
return rows;
|
|
1294
1256
|
}
|
|
1295
1257
|
/*
|
|
1296
1258
|
* RETIRED (DC-50 / R14, 2026-09-05) — `expandedCard`.
|
|
@@ -1307,7 +1269,7 @@ function slabBlock(head, body, outcome, W) {
|
|
|
1307
1269
|
* `r13-rhythm-surface.test.ts`.
|
|
1308
1270
|
*/
|
|
1309
1271
|
/** 0.24.2 ② — the live region's `thinking…` placeholder: dim italic at
|
|
1310
|
-
*
|
|
1272
|
+
* the content edge, no glyph, the SAME shape a thinking paragraph takes so that
|
|
1311
1273
|
* whatever arrives replaces it in place. Never committed — see the
|
|
1312
1274
|
* compositor's #project for why that is what makes it allowed. */
|
|
1313
1275
|
export function thinkingRow() {
|
|
@@ -1331,10 +1293,12 @@ function openBlock(rows) {
|
|
|
1331
1293
|
return rows;
|
|
1332
1294
|
// the corner REPLACES two of the four indent columns, so the text
|
|
1333
1295
|
// stays in the same column as every other row of the block.
|
|
1334
|
-
return [...rows.slice(0, i), `${first.slice(0, at)}
|
|
1296
|
+
return [...rows.slice(0, i), `${first.slice(0, at)}${" ".repeat(BODY_ROW_FLAT.length - 2)}\u2514 ${first.slice(at + BODY_ROW_FLAT.length)}`, ...rows.slice(i + 1)];
|
|
1335
1297
|
}
|
|
1336
1298
|
const blockMemo = new WeakMap();
|
|
1337
|
-
/** The block's body rows below the header (memoized, W9)
|
|
1299
|
+
/** The block's body rows below the header (memoized, W9), and whether
|
|
1300
|
+
* the preview CUT anything — which is what puts the key on a card's
|
|
1301
|
+
* foot (§7.4). */
|
|
1338
1302
|
function toolBlockParts(c, W, ctx) {
|
|
1339
1303
|
const memo = blockMemo.get(c);
|
|
1340
1304
|
// the SURFACE is part of the key: the same cell renders different rows
|
|
@@ -1342,80 +1306,111 @@ function toolBlockParts(c, W, ctx) {
|
|
|
1342
1306
|
// would otherwise be served the pre-ground shape forever.
|
|
1343
1307
|
const liveRows = ctx.liveWindow ?? CAP_PREVIEW;
|
|
1344
1308
|
const state = `${c.state}:${c.isError}:${c.name}:${c.expanded ? "x" : ""}:${slabPaints() ? "slab" : "flat"}:${liveRows}`;
|
|
1345
|
-
|
|
1309
|
+
// R2a: an edit or a write shows what it changed while it runs and once
|
|
1310
|
+
// it has — never after a refusal (nothing changed) or an error
|
|
1311
|
+
const edit = !c.isError && c.reason === null && (c.state === "running" || c.state === "done") ? editDiffOf(c) : null;
|
|
1312
|
+
const content = c.state === "approval" ? (c.diff ?? null) : edit !== null ? edit : c.resultText;
|
|
1346
1313
|
if (memo !== undefined && memo.width === W && memo.state === state && memo.content === content)
|
|
1347
1314
|
return memo;
|
|
1348
1315
|
const p = palette();
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
:
|
|
1386
|
-
: c.state === "
|
|
1387
|
-
?
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1316
|
+
const tone = slabPaints() ? "body" : "dim";
|
|
1317
|
+
let cut = false;
|
|
1318
|
+
const keyed = c.state === "done" && !c.expanded;
|
|
1319
|
+
const capped = (all, dir, starts = []) => {
|
|
1320
|
+
if (all.length <= CAP_PREVIEW)
|
|
1321
|
+
return all;
|
|
1322
|
+
cut = true;
|
|
1323
|
+
// R9 P2 / D4: FIVE output rows, and the note is a row of its own.
|
|
1324
|
+
// A shell's conclusion is at the bottom, so its note goes ABOVE the
|
|
1325
|
+
// tail; everything else answers at the top, so its note closes it.
|
|
1326
|
+
// The card round: a settled, collapsed card's key rides the note.
|
|
1327
|
+
return dir === "tail"
|
|
1328
|
+
? [...cutNote(all.length - CAP_PREVIEW, "earlier", W, tone, keyed), ...tailWindow(all, CAP_PREVIEW, starts)]
|
|
1329
|
+
: [...all.slice(0, CAP_PREVIEW), ...cutNote(all.length - CAP_PREVIEW, "more", W, tone, keyed)];
|
|
1330
|
+
};
|
|
1331
|
+
const shellTail = (out, dir) => capped(out.rows, dir, out.starts);
|
|
1332
|
+
// a diff inside a card: its own rows, the head of them until the key
|
|
1333
|
+
const diffCapped = (lines, cap) => {
|
|
1334
|
+
const all = lines === null ? [] : diffRows(lines, W);
|
|
1335
|
+
if (c.expanded || all.length <= cap)
|
|
1336
|
+
return all;
|
|
1337
|
+
cut = true;
|
|
1338
|
+
return [...all.slice(0, cap), ...cutNote(all.length - cap, "more", W, tone, keyed)];
|
|
1339
|
+
};
|
|
1340
|
+
const rows = edit !== null
|
|
1341
|
+
? diffCapped(edit.lines, edit.cap)
|
|
1342
|
+
: c.state === "approval" && slabPaints()
|
|
1343
|
+
? diffCapped(c.diff, CAP_DIFF)
|
|
1344
|
+
: c.expanded
|
|
1345
|
+
? // W15: the WHOLE body, no cap, no cut note (nothing is cut).
|
|
1346
|
+
c.state === "approval"
|
|
1347
|
+
? diffBody(c.diff, W, true)
|
|
1348
|
+
: c.name === "delegate"
|
|
1349
|
+
? c.state === "running"
|
|
1350
|
+
? delegateRunning(c, W)
|
|
1351
|
+
: delegateSettled(c, W)
|
|
1352
|
+
: blockRows(c.resultText, W, tone)
|
|
1353
|
+
: c.state === "done"
|
|
1354
|
+
? c.isError
|
|
1355
|
+
? capped(errorRows(c, W, tone), "head")
|
|
1356
|
+
: c.name === "delegate"
|
|
1357
|
+
? delegateSettled(c, W)
|
|
1358
|
+
: // R13 — EVERY settled call previews: a shell its tail,
|
|
1359
|
+
// everything else its head. read_file is the single
|
|
1360
|
+
// exception (E1): its result is the file, and the key
|
|
1361
|
+
// opens the whole thing.
|
|
1362
|
+
noPreview(c)
|
|
1363
|
+
? []
|
|
1364
|
+
: shellTail(blockLines(c.resultText, W, tone), c.name === "shell" ? "tail" : "head")
|
|
1365
|
+
: c.state === "running"
|
|
1366
|
+
? c.name === "delegate"
|
|
1367
|
+
? delegateRunning(c, W)
|
|
1368
|
+
: // R13 E1 — the call with no preview settled has none
|
|
1369
|
+
// while it runs either.
|
|
1370
|
+
noPreview({ ...c, state: "done" })
|
|
1371
|
+
? []
|
|
1372
|
+
: liveWindow(c, W, liveCap(c, liveRows), tone)
|
|
1373
|
+
: c.state === "approval"
|
|
1374
|
+
? diffBody(c.diff, W)
|
|
1375
|
+
: [];
|
|
1376
|
+
// E1 governs the PREVIEW, not kiso's sentence about the result: a read
|
|
1377
|
+
// the TOOL capped takes one body row (`offset=201 for the rest`).
|
|
1378
|
+
const note = c.expanded || edit !== null ? null : toolCutNote(c.name, c.resultText);
|
|
1396
1379
|
if (note !== null)
|
|
1397
|
-
rows.push(...
|
|
1398
|
-
//
|
|
1399
|
-
//
|
|
1400
|
-
// marker is missing renders nothing, and a lone footer under a head
|
|
1401
|
-
// row would be an affordance for an empty block.
|
|
1402
|
-
//
|
|
1403
|
-
// DC-50 / R14: a SETTLED card carries the affordance on its OUTCOME
|
|
1404
|
-
// row instead, so that both states have one skeleton. This footer is
|
|
1405
|
-
// for the states that have no outcome row to carry it — a card parked
|
|
1406
|
-
// for approval, or one still running that was expanded before it
|
|
1407
|
-
// settled.
|
|
1408
|
-
if (c.expanded && rows.length > 0 && c.state !== "done")
|
|
1409
|
-
rows.push(...foldLine(`${p.dim}${noteIndent()}${COLLAPSE_ROW}${p.reset}`, W));
|
|
1410
|
-
// R9 P2: `└` opens a block that has no surface. Inside a slab the
|
|
1411
|
-
// surface IS the container, and a corner in it is §1.3's empty mark
|
|
1412
|
-
// one scale up — so the corner and the slab are alternatives, never
|
|
1413
|
-
// both.
|
|
1380
|
+
rows.push(...noteRow(note, W, "dim"));
|
|
1381
|
+
// R8a: `└` opens a block that has no surface; inside a card the surface
|
|
1382
|
+
// IS the container.
|
|
1414
1383
|
const opened = slabPaints() ? rows : openBlock(rows);
|
|
1415
|
-
const parts = { width: W, state, content, rows: opened };
|
|
1384
|
+
const parts = { width: W, state, content, rows: opened, cut };
|
|
1416
1385
|
blockMemo.set(c, parts);
|
|
1417
1386
|
return parts;
|
|
1418
1387
|
}
|
|
1388
|
+
/** The tasks round — a background delegation's children, from what the
|
|
1389
|
+
* call returned (`started 2 background children: t1 explorer (session …),
|
|
1390
|
+
* t2 reviewer (session …). They read …`) and what it was asked (each
|
|
1391
|
+
* task's role and words): the task id, the role, the task's first line.
|
|
1392
|
+
* Null for anything else — a foreground delegation, a refusal. */
|
|
1393
|
+
function backgroundChildren(c) {
|
|
1394
|
+
const m = /^started \d+ background (?:child|children): (.*?)\. They read the workspace/s.exec(c.resultText);
|
|
1395
|
+
if (m === null)
|
|
1396
|
+
return null;
|
|
1397
|
+
let asked = {};
|
|
1398
|
+
try {
|
|
1399
|
+
asked = JSON.parse(c.inputFull);
|
|
1400
|
+
}
|
|
1401
|
+
catch {
|
|
1402
|
+
// the full input is always JSON (stringified at toolStart)
|
|
1403
|
+
}
|
|
1404
|
+
const pool = (asked.tasks ?? []).map((t) => ({ role: String(t.role ?? ""), task: oneRow(escapeTerminal(String(t.task ?? "").split("\n")[0] ?? "")), used: false }));
|
|
1405
|
+
const out = [];
|
|
1406
|
+
for (const s of m[1].matchAll(/(\S+) (\S+) \(session [^)]*\)/g)) {
|
|
1407
|
+
const at = pool.findIndex((t) => !t.used && t.role === s[2]);
|
|
1408
|
+
if (at >= 0)
|
|
1409
|
+
pool[at].used = true;
|
|
1410
|
+
out.push({ id: escapeTerminal(s[1]), role: escapeTerminal(s[2]), task: at >= 0 ? pool[at].task : "" });
|
|
1411
|
+
}
|
|
1412
|
+
return out.length === 0 ? null : out;
|
|
1413
|
+
}
|
|
1419
1414
|
/** R13 E1 — the one settled call that previews NOTHING. A read's result
|
|
1420
1415
|
* IS the file; five lines of it tell a reader less than the head row
|
|
1421
1416
|
* already does, and the key opens the whole thing. (The reference
|
|
@@ -1424,96 +1419,110 @@ function toolBlockParts(c, W, ctx) {
|
|
|
1424
1419
|
function noPreview(c) {
|
|
1425
1420
|
return c.state === "done" && !c.expanded && !c.isError && c.reason === null && c.name === "read_file";
|
|
1426
1421
|
}
|
|
1427
|
-
/**
|
|
1428
|
-
|
|
1429
|
-
return toolBlockParts(c, W, ctx).rows;
|
|
1430
|
-
}
|
|
1422
|
+
/** How far a wrapped output line's continuation rows stand in. */
|
|
1423
|
+
const HANG = 2;
|
|
1431
1424
|
/** Fold result text into body rows (the block's own indent): escape,
|
|
1432
|
-
* split, fold each line
|
|
1433
|
-
* final newline) drop.
|
|
1425
|
+
* split, fold each line in the body's text width; trailing empty rows
|
|
1426
|
+
* (the result's final newline) drop. Inside a card the output is
|
|
1427
|
+
* `ink2` (§2: tool output); off the surface it is dim. */
|
|
1434
1428
|
function blockRows(text, W, tone = "dim") {
|
|
1429
|
+
return blockLines(text, W, tone).rows;
|
|
1430
|
+
}
|
|
1431
|
+
/** `blockRows`, with which rows begin a line of output (a wrapped line's
|
|
1432
|
+
* continuation rows do not) — what a tail window needs to start on a
|
|
1433
|
+
* whole line. */
|
|
1434
|
+
function blockLines(text, W, tone = "dim") {
|
|
1435
1435
|
const p = palette();
|
|
1436
|
-
const textW =
|
|
1436
|
+
const textW = bodyTextWidth(W);
|
|
1437
1437
|
const rows = [];
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
// rows around them take `washDim`, which was chosen for that ground.
|
|
1442
|
-
const open = tone === "dim" ? p.dim : "";
|
|
1443
|
-
const close = tone === "dim" ? p.reset : "";
|
|
1438
|
+
const starts = [];
|
|
1439
|
+
const open = tone === "dim" ? p.dim : p.ink2;
|
|
1440
|
+
const close = tone === "dim" ? p.reset : p.ink2 === "" ? "" : p.fgEnd;
|
|
1444
1441
|
// 0.40.0: the output's own styling is dropped whole before the escape
|
|
1442
|
+
// The card round (owner, 2026-10-05): a line too long for the row
|
|
1443
|
+
// continues HANG cells in, so one line of output reads as one.
|
|
1444
|
+
const hang = textW > HANG * 4 ? HANG : 0;
|
|
1445
1445
|
for (const raw of escapeTerminal(stripAnsi(text)).split("\n")) {
|
|
1446
|
-
|
|
1447
|
-
|
|
1446
|
+
const [first = "", ...more] = foldLine(raw, textW);
|
|
1447
|
+
rows.push(`${bodyRow()}${open}${first}${close}`);
|
|
1448
|
+
starts.push(true);
|
|
1449
|
+
if (more.length === 0)
|
|
1450
|
+
continue;
|
|
1451
|
+
for (const row of foldLine(more.join(""), textW - hang)) {
|
|
1452
|
+
rows.push(`${bodyRow()}${" ".repeat(hang)}${open}${row}${close}`);
|
|
1453
|
+
starts.push(false);
|
|
1454
|
+
}
|
|
1448
1455
|
}
|
|
1449
|
-
while (rows.length > 0 && visibleWidth(rows[rows.length - 1]) === visibleWidth(bodyRow()))
|
|
1456
|
+
while (rows.length > 0 && visibleWidth(rows[rows.length - 1]) === visibleWidth(bodyRow())) {
|
|
1450
1457
|
rows.pop();
|
|
1451
|
-
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
* renderer cut at the block's bottom ("earlier rows" — the conclusion
|
|
1455
|
-
* is at the end, the reference implementation's truncateToVisualLines
|
|
1456
|
-
* direction). */
|
|
1457
|
-
function shellTail(text, W, tone = "dim") {
|
|
1458
|
-
const rows = blockRows(text, W, tone);
|
|
1459
|
-
if (rows.length <= CAP_PREVIEW)
|
|
1460
|
-
return rows;
|
|
1461
|
-
// R9 P2 / D4: FIVE output rows, and the note is a row of its own. The
|
|
1462
|
-
// pre-slab arithmetic spent one of the five on the cut note, because
|
|
1463
|
-
// the note had nowhere else to live; the slab's metadata rows are not
|
|
1464
|
-
// output and are not counted against the output's cap.
|
|
1465
|
-
const kept = CAP_PREVIEW;
|
|
1466
|
-
// The note goes ABOVE the tail: it says what was cut, and what was
|
|
1467
|
-
// cut is what came BEFORE these rows. One position on both surfaces —
|
|
1468
|
-
// the surface degrades, the content shape does not.
|
|
1469
|
-
return [...cutNote(rows.length - kept, "earlier", W, tone), ...rows.slice(rows.length - kept)];
|
|
1470
|
-
}
|
|
1471
|
-
/** R13 — the preview every OTHER settled call gets: the FIRST rows,
|
|
1472
|
-
* capped at five, the cut note BELOW them. A shell's conclusion is at
|
|
1473
|
-
* the bottom of its output, so its preview is the tail and its note
|
|
1474
|
-
* opens the block (shellTail above); a list, a search, a fetch all
|
|
1475
|
-
* answer at the top, so the note closes it. One rule, two directions,
|
|
1476
|
-
* and the direction follows where the answer is.
|
|
1477
|
-
*
|
|
1478
|
-
* This is the DECLARED REVERSAL of VD-5 for every tool but the shell
|
|
1479
|
-
* (0.22.0 reversed the shell alone): VD-5 collapsed a settled call to
|
|
1480
|
-
* its head row because ungrounded output rows owned the screen, and
|
|
1481
|
-
* the card is what changes that arithmetic — the rows are inside a
|
|
1482
|
-
* surface that says where the call begins and ends. */
|
|
1483
|
-
function previewHead(text, W, tone) {
|
|
1484
|
-
const rows = blockRows(text, W, tone);
|
|
1485
|
-
if (rows.length <= CAP_PREVIEW)
|
|
1486
|
-
return rows;
|
|
1487
|
-
return [...rows.slice(0, CAP_PREVIEW), ...cutNote(rows.length - CAP_PREVIEW, "more", W, tone)];
|
|
1458
|
+
starts.pop();
|
|
1459
|
+
}
|
|
1460
|
+
return { rows, starts };
|
|
1488
1461
|
}
|
|
1489
|
-
/** The preview's cut note
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1493
|
-
|
|
1462
|
+
/** The preview's cut note — how much was cut and in which direction.
|
|
1463
|
+
* With `key` (a settled card, collapsed) the key that shows the rest
|
|
1464
|
+
* stands at the same row's right margin: what was cut at the left, how
|
|
1465
|
+
* to see it at the right (the card round, owner 2026-10-05). DECLARED
|
|
1466
|
+
* REVERSAL of §7.4's foot for a collapsed card — the note and the key
|
|
1467
|
+
* were two rows for one fact. The key gives way after the count's word,
|
|
1468
|
+
* the count never. */
|
|
1469
|
+
function cutNote(cut, word, W, tone, key = false) {
|
|
1494
1470
|
const n = `${cut} ${word} line${cut === 1 ? "" : "s"}`;
|
|
1495
|
-
|
|
1471
|
+
const room = bodyTextWidth(W);
|
|
1472
|
+
if (key) {
|
|
1473
|
+
for (const [left, k] of [[`\u2026 ${n}`, EXPAND_ROW], [`\u2026 ${cut}`, EXPAND_ROW], [`\u2026 ${cut}`, "ctrl+o"]]) {
|
|
1474
|
+
const gap = room - visibleWidth(left) - visibleWidth(k);
|
|
1475
|
+
if (gap >= 2)
|
|
1476
|
+
return noteRow(`${left}${" ".repeat(gap)}${k}`, W, tone);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return noteRow(pickTier([`\u2026 ${n}`, `\u2026 ${cut}`], room), W, tone);
|
|
1496
1480
|
}
|
|
1497
|
-
/** The
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1481
|
+
/** The card round — a shell's tail never opens on a blank row or in the
|
|
1482
|
+
* middle of a wrapped line, and keeps its height (DC-46: a live window
|
|
1483
|
+
* never shrinks, and the settle moves nothing). When the window's top row
|
|
1484
|
+
* would be either, the window starts at an earlier line's first row and
|
|
1485
|
+
* blank rows inside it give way to make the room, the top-most first: a
|
|
1486
|
+
* blank row carries no fact, and the conclusion is at the bottom. It
|
|
1487
|
+
* looks back at most `TAIL_REACH` rows (a bounded walk, never a scan of
|
|
1488
|
+
* the whole output); when no line start can be reached, the window is
|
|
1489
|
+
* the plain tail. The rows it hides are counted in the note. `starts`
|
|
1490
|
+
* marks each row that begins a line of output. */
|
|
1491
|
+
const TAIL_REACH = 12;
|
|
1492
|
+
function tailWindow(rows, cap, starts) {
|
|
1493
|
+
const n = rows.length;
|
|
1494
|
+
if (n <= cap)
|
|
1495
|
+
return [...rows];
|
|
1496
|
+
const blank = (i) => visibleWidth(rows[i]) <= visibleWidth(bodyRow());
|
|
1497
|
+
let blanks = 0;
|
|
1498
|
+
for (let i = n - cap; i < n; i += 1)
|
|
1499
|
+
if (blank(i))
|
|
1500
|
+
blanks += 1;
|
|
1501
|
+
// s stops at 1: the window reaches only while a row above it stays
|
|
1502
|
+
// cut, so the note never counts nothing but the blanks it skipped
|
|
1503
|
+
for (let s = n - cap; s >= Math.max(1, n - cap - TAIL_REACH); s -= 1) {
|
|
1504
|
+
if (s < n - cap && blank(s))
|
|
1505
|
+
blanks += 1;
|
|
1506
|
+
const extra = n - s - cap;
|
|
1507
|
+
if (blank(s) || starts[s] === false || blanks < extra)
|
|
1508
|
+
continue;
|
|
1509
|
+
const out = [];
|
|
1510
|
+
let dropped = 0;
|
|
1511
|
+
for (let i = s; i < n; i += 1) {
|
|
1512
|
+
if (dropped < extra && blank(i))
|
|
1513
|
+
dropped += 1;
|
|
1514
|
+
else
|
|
1515
|
+
out.push(rows[i]);
|
|
1516
|
+
}
|
|
1517
|
+
return out;
|
|
1518
|
+
}
|
|
1519
|
+
return rows.slice(n - cap);
|
|
1520
|
+
}
|
|
1521
|
+
/** The error text, uncapped: the answer is at the start, and the whole
|
|
1522
|
+
* of it is the body — the head row carries only the outcome word
|
|
1523
|
+
* (W4, W19: never hide information). */
|
|
1524
|
+
function errorRows(c, W, tone) {
|
|
1525
|
+
return blockRows(c.resultText, W, tone);
|
|
1517
1526
|
}
|
|
1518
1527
|
/**
|
|
1519
1528
|
* DC-46 — THE RUNNING WINDOW GROWS, and nothing pads it.
|
|
@@ -1544,7 +1553,7 @@ function liveWindow(c, W, cap, tone) {
|
|
|
1544
1553
|
// TUI2-R1.5 ④(b) (VD-4): leading empty lines in the sidecar (a
|
|
1545
1554
|
// 4096-byte tail can begin on a line boundary) are skipped, so the
|
|
1546
1555
|
// output starts under its own header.
|
|
1547
|
-
const all =
|
|
1556
|
+
const { rows: all, starts } = blockLines(c.resultText, W, tone);
|
|
1548
1557
|
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1549
1558
|
// DC-46, derived — NOTHING YET IS NO WINDOW AT ALL, so a running call
|
|
1550
1559
|
// with no output is the same THREE-ROW card as a settled one with
|
|
@@ -1563,7 +1572,7 @@ function liveWindow(c, W, cap, tone) {
|
|
|
1563
1572
|
if (rows.length <= cap)
|
|
1564
1573
|
return rows;
|
|
1565
1574
|
return c.name === "shell"
|
|
1566
|
-
? [...cutNote(rows.length - cap, "earlier", W, tone), ...rows.slice(
|
|
1575
|
+
? [...cutNote(rows.length - cap, "earlier", W, tone), ...tailWindow(rows, cap, starts.slice(from))]
|
|
1567
1576
|
: [...rows.slice(0, cap), ...cutNote(rows.length - cap, "more", W, tone)];
|
|
1568
1577
|
}
|
|
1569
1578
|
/** DC-46 — the window's HIGH-WATER, per cell: the room a frame leaves
|
|
@@ -1600,24 +1609,31 @@ function delegateRunning(c, W) {
|
|
|
1600
1609
|
const p = palette();
|
|
1601
1610
|
const n = c.childRoles.length;
|
|
1602
1611
|
const text = n === 0 ? "children running…" : `${n === 1 ? "1 child" : `${n} children`} · ${c.childRoles.join(" · ")}`;
|
|
1603
|
-
return [
|
|
1612
|
+
return [bodyLineRow(p, text, W)];
|
|
1604
1613
|
}
|
|
1605
1614
|
function delegateSettled(c, W) {
|
|
1606
1615
|
const p = palette();
|
|
1607
1616
|
const m = /^summary: (.+)$/m.exec(c.resultText);
|
|
1608
1617
|
if (m === null)
|
|
1609
1618
|
return [];
|
|
1610
|
-
return [
|
|
1619
|
+
return [bodyLineRow(p, `${m[1]} · /last for the report`, W)];
|
|
1611
1620
|
}
|
|
1612
1621
|
/** ONE row at the left gutter, truncated to fit the width — never a
|
|
1613
1622
|
* fold (a fold would wrap into TWO rows and break the one-line height
|
|
1614
1623
|
* contract). */
|
|
1615
1624
|
function oneLineRow(p, text, W) {
|
|
1616
1625
|
const esc = escapeTerminal(text);
|
|
1617
|
-
if (visibleWidth(`${p.dim}${
|
|
1618
|
-
return `${p.dim}${
|
|
1619
|
-
const w = Math.max(1, W -
|
|
1620
|
-
return `${p.dim}${
|
|
1626
|
+
if (visibleWidth(`${p.dim}\u2514 ${esc}${p.reset}`) <= W)
|
|
1627
|
+
return `${p.dim}\u2514 ${esc}${p.reset}`;
|
|
1628
|
+
const w = Math.max(1, W - 2);
|
|
1629
|
+
return `${p.dim}\u2514 ${esc.slice(0, w - 1)}\u2026${p.reset}`;
|
|
1630
|
+
}
|
|
1631
|
+
/** Graphite §7.4 — a card body's ONE row (the delegate's): at the body
|
|
1632
|
+
* column like every body row, dim, cut to its room. */
|
|
1633
|
+
function bodyLineRow(p, text, W) {
|
|
1634
|
+
const esc = escapeTerminal(text);
|
|
1635
|
+
const room = bodyTextWidth(W);
|
|
1636
|
+
return `${bodyRow()}${p.dim}${visibleWidth(esc) <= room ? esc : `${widthCut(esc, Math.max(1, room - 1))}\u2026`}${p.reset}`;
|
|
1621
1637
|
}
|
|
1622
1638
|
/** The approval mini-diff (W7): capped at 12 folded rows — the head +
|
|
1623
1639
|
* the named middle (the renderer cut — what was cut, how to expand) +
|
|
@@ -1676,6 +1692,102 @@ export function diffBody(diff, W, expanded = false) {
|
|
|
1676
1692
|
return [...rows.slice(0, CAP_DIFF - 1), cut(rows.length - (CAP_DIFF - 1))];
|
|
1677
1693
|
return [...rows.slice(0, head), cut(rows.length - head - tail), ...rows.slice(rows.length - tail)];
|
|
1678
1694
|
}
|
|
1695
|
+
/** Graphite §6 (R2a) — the rows a card shows for a diff: a `-` / `+` line
|
|
1696
|
+
* on its tint (a background, §1.5) from the content edge to the card's
|
|
1697
|
+
* inner margin, the marker in its colour, the changed words on a deeper
|
|
1698
|
+
* tint; a kept line `dim`; kiso's own notes (`···` between hunks, a miss)
|
|
1699
|
+
* `dim` in the marker column. A long line folds under its text with the
|
|
1700
|
+
* tint on every row. Off a painted card the lines keep the flat form:
|
|
1701
|
+
* marker and text in the line's colour, the changed words underlined. */
|
|
1702
|
+
function diffRows(lines, W) {
|
|
1703
|
+
const p = palette();
|
|
1704
|
+
const painted = slabPaints();
|
|
1705
|
+
const room = bodyRowRoom(W) - bodyRow().length;
|
|
1706
|
+
const textW = Math.max(1, bodyTextWidth(W) - 2);
|
|
1707
|
+
const out = [];
|
|
1708
|
+
for (const d of lines) {
|
|
1709
|
+
const text = escapeTerminal(d.text);
|
|
1710
|
+
if (d.kind === "note") {
|
|
1711
|
+
out.push(...noteRow(text, W, "dim"));
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
if (d.kind === " ") {
|
|
1715
|
+
for (const r of foldLine(text, textW))
|
|
1716
|
+
out.push(`${bodyRow()}${p.dim} ${r}${p.reset}`);
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
const del = d.kind === "-";
|
|
1720
|
+
const rows = foldLine(text, textW);
|
|
1721
|
+
// the marks index the line's own text: they hold only where the
|
|
1722
|
+
// escape left it as it was and the fold cut it without loss
|
|
1723
|
+
const marks = text === d.text && rows.join("") === text ? (d.marks ?? []) : [];
|
|
1724
|
+
const base = del ? p.del : p.add;
|
|
1725
|
+
const [on, off] = painted ? [del ? p.delWord : p.addWord, base] : [p.underline, p.underlineEnd];
|
|
1726
|
+
let at = 0;
|
|
1727
|
+
for (const [i, r] of rows.entries()) {
|
|
1728
|
+
let body = "";
|
|
1729
|
+
let k = 0;
|
|
1730
|
+
for (const [s0, e0] of marks) {
|
|
1731
|
+
const s1 = Math.max(s0, at) - at;
|
|
1732
|
+
const e1 = Math.min(e0, at + r.length) - at;
|
|
1733
|
+
if (e1 <= s1)
|
|
1734
|
+
continue;
|
|
1735
|
+
body += `${r.slice(k, s1)}${on}${r.slice(s1, e1)}${off}`;
|
|
1736
|
+
k = e1;
|
|
1737
|
+
}
|
|
1738
|
+
body += r.slice(k);
|
|
1739
|
+
at += r.length;
|
|
1740
|
+
const lead = i === 0 ? d.kind : " ";
|
|
1741
|
+
if (painted) {
|
|
1742
|
+
const pad = " ".repeat(Math.max(0, room - 2 - visibleWidth(r)));
|
|
1743
|
+
out.push(`${bodyRow()}${base}${del ? p.fail : p.ok}${lead}${p.fgEnd} ${body}${pad}`);
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
out.push(`${bodyRow()}${del ? p.red : p.green}${lead} ${body}${p.reset}`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return out;
|
|
1751
|
+
}
|
|
1752
|
+
/** How many of a write's lines its card shows before the key. */
|
|
1753
|
+
const WRITE_HEAD = 5;
|
|
1754
|
+
const editDiffMemo = new WeakMap();
|
|
1755
|
+
function editDiffOf(c) {
|
|
1756
|
+
if (c.name !== "edit_file" && c.name !== "write_file")
|
|
1757
|
+
return null;
|
|
1758
|
+
const known = editDiffMemo.get(c);
|
|
1759
|
+
if (known !== undefined)
|
|
1760
|
+
return known;
|
|
1761
|
+
let input = null;
|
|
1762
|
+
try {
|
|
1763
|
+
const v = JSON.parse(c.inputFull);
|
|
1764
|
+
input = typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
|
|
1765
|
+
}
|
|
1766
|
+
catch {
|
|
1767
|
+
input = null;
|
|
1768
|
+
}
|
|
1769
|
+
let out = null;
|
|
1770
|
+
if (input !== null && c.name === "edit_file") {
|
|
1771
|
+
const hunks = hunksOf(input);
|
|
1772
|
+
const d = hunks === null ? null : hunksDiff(hunks);
|
|
1773
|
+
if (hunks !== null && d !== null && d.lines.length > 0) {
|
|
1774
|
+
const counts = `+${d.added} -${d.removed}`;
|
|
1775
|
+
out = { lines: d.lines, meta: hunks.length > 1 ? `${counts} \u00b7 ${hunks.length} hunks` : counts, cap: CAP_DIFF };
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
else if (input !== null && typeof input.content === "string") {
|
|
1779
|
+
const content = input.content.replace(/\n$/, "");
|
|
1780
|
+
const all = content === "" ? [] : content.split("\n");
|
|
1781
|
+
// a write that cites "absent" creates the file; any other rewrites
|
|
1782
|
+
// one whose old text is not in the log, so its lines are shown as
|
|
1783
|
+
// they now read, not as additions
|
|
1784
|
+
const created = input.expectedRevision === "absent";
|
|
1785
|
+
const n = `${all.length} line${all.length === 1 ? "" : "s"}`;
|
|
1786
|
+
out = { lines: all.map((text) => ({ kind: created ? "+" : " ", text })), meta: created ? `new file \u00b7 ${n}` : `rewrote \u00b7 ${n}`, cap: WRITE_HEAD };
|
|
1787
|
+
}
|
|
1788
|
+
editDiffMemo.set(c, out);
|
|
1789
|
+
return out;
|
|
1790
|
+
}
|
|
1679
1791
|
/** The TOOL's OWN truncation note (W10) — a different fact from the
|
|
1680
1792
|
* renderer's cut: the tools truncate and append a continuation note
|
|
1681
1793
|
* (packages/tools-node/src/index.ts — read_file's "call again with
|
|
@@ -1722,18 +1834,133 @@ class MarkdownBlock {
|
|
|
1722
1834
|
// holds by construction. A block's own leading blank (its `gap`)
|
|
1723
1835
|
// stays EMPTY: an indented blank row is trailing whitespace, and
|
|
1724
1836
|
// §1.3 forbids a mark on a row with nothing to mark.
|
|
1725
|
-
|
|
1837
|
+
// Graphite §1.8: at the content edge, 92 columns at most (§7.15).
|
|
1838
|
+
const rows = renderBlock(this.cell.block, proseRoom(W)).map((r) => (r === "" ? r : `${PROSE_COL}${r}`));
|
|
1839
|
+
// Graphite §5 (R2b): a `##` heading carries `§` in the mark column
|
|
1840
|
+
// (`rail`), outside the block, the way the prototype hangs it
|
|
1841
|
+
const p = palette();
|
|
1842
|
+
if (headingLevel(this.cell.block) === 2 && p.rail !== "") {
|
|
1843
|
+
const at = rows.findIndex((r) => r !== "");
|
|
1844
|
+
if (at >= 0)
|
|
1845
|
+
rows[at] = `${p.rail}\u00a7${p.fgEnd} ${rows[at].slice(PROSE_COL.length)}`;
|
|
1846
|
+
}
|
|
1847
|
+
return rows;
|
|
1726
1848
|
}
|
|
1727
1849
|
}
|
|
1728
|
-
/**
|
|
1850
|
+
/** Graphite §7.12 — the label column of a meta row: the longest label
|
|
1851
|
+
* (`INTERRUPTED`) and one space, so the sentences line up with a card's
|
|
1852
|
+
* body (column 16). */
|
|
1853
|
+
const META_LABEL = 12;
|
|
1854
|
+
/** The labels that name an outcome, and so take its colour (§1.2). */
|
|
1855
|
+
const META_FAIL = new Set(["FAILED", "UNCERTAIN"]);
|
|
1856
|
+
/**
|
|
1857
|
+
* Graphite §7.12 — kiso's own sentences are META ROWS: a bold label at
|
|
1858
|
+
* the content edge and the sentence beside it, folded by word under
|
|
1859
|
+
* itself (VD-10: a notice is a sentence addressed to a human). No card
|
|
1860
|
+
* and no ground — they are not the machine's work. A notice with no
|
|
1861
|
+
* kind of its own (a command's confirmation) has no label and stays
|
|
1862
|
+
* whole at the content edge.
|
|
1863
|
+
*/
|
|
1729
1864
|
class ErrorLine {
|
|
1730
1865
|
cell;
|
|
1731
1866
|
constructor(cell) {
|
|
1732
1867
|
this.cell = cell;
|
|
1733
1868
|
}
|
|
1734
1869
|
render(W, _ctx) {
|
|
1735
|
-
|
|
1736
|
-
|
|
1870
|
+
const p = palette();
|
|
1871
|
+
const c = this.cell;
|
|
1872
|
+
// a sentence with no kind of its own stays whole at the content edge
|
|
1873
|
+
if (c.label === undefined)
|
|
1874
|
+
return foldWords(escapeTerminal(c.sentence ?? c.text), Math.max(1, W - EDGE.length)).map((r) => `${EDGE}${r}`);
|
|
1875
|
+
const sentence = escapeTerminal(c.sentence ?? "");
|
|
1876
|
+
const room = Math.max(1, W - EDGE.length - META_LABEL);
|
|
1877
|
+
const tone = META_FAIL.has(c.label) ? p.red : p.dim;
|
|
1878
|
+
const head = `${EDGE}${p.bold}${tone}${c.label.padEnd(META_LABEL - 1)}${p.reset} `;
|
|
1879
|
+
// a label wider than the row's room is cut like any row (invariant ①)
|
|
1880
|
+
if (sentence === "")
|
|
1881
|
+
return [cutLine(head.trimEnd(), W)];
|
|
1882
|
+
const folded = foldWords(sentence, room);
|
|
1883
|
+
// Graphite R3e (the MODE row): one word of the sentence carries the
|
|
1884
|
+
// weight — the mode switched to, bold, bypass in the failure colour
|
|
1885
|
+
const m = c.mark;
|
|
1886
|
+
const lit = (r) => {
|
|
1887
|
+
if (m === undefined || !r.includes(m.text))
|
|
1888
|
+
return `${p.dim}${r}${p.reset}`;
|
|
1889
|
+
const tone = m.tone === "fail" ? p.red : m.tone === "blue" ? p.blue : m.tone === "ok" ? p.green : m.tone === "gold" ? p.gold : "";
|
|
1890
|
+
// the word after the arrow (the mode switched TO), not an earlier one
|
|
1891
|
+
const arrow = r.indexOf(`\u2192 ${m.text}`);
|
|
1892
|
+
const at = arrow >= 0 ? arrow + 2 : r.indexOf(m.text);
|
|
1893
|
+
return `${p.dim}${r.slice(0, at)}${p.reset}${p.bold}${tone}${m.text}${p.reset}${p.dim}${r.slice(at + m.text.length)}${p.reset}`;
|
|
1894
|
+
};
|
|
1895
|
+
// Graphite R3e (owner, 2026-09-29): the MODE switch is the person's own
|
|
1896
|
+
// choice, so its label is GOLD — the colour of what the person says —
|
|
1897
|
+
// and its short `from → to` follows two spaces after it on the same
|
|
1898
|
+
// row, not the meta rows' twelve-column label column
|
|
1899
|
+
if (c.stacked === true) {
|
|
1900
|
+
const label = `${EDGE}${p.bold}${p.gold}${c.label}${p.reset} `;
|
|
1901
|
+
const lead = visibleWidth(label);
|
|
1902
|
+
return foldWords(sentence, Math.max(1, W - lead)).map((r, i) => cutLine(i === 0 ? `${label}${lit(r)}` : `${" ".repeat(lead)}${lit(r)}`, W));
|
|
1903
|
+
}
|
|
1904
|
+
// the tasks round: a row that names a thing (a task, a project) is ONE
|
|
1905
|
+
// row — what ran is cut with an ellipsis, never folded under itself
|
|
1906
|
+
if (c.oneRow === true) {
|
|
1907
|
+
const rows = [cutLine(`${head}${lit(sentence)}`, W)];
|
|
1908
|
+
for (const r of c.also ?? [])
|
|
1909
|
+
rows.push(...new ErrorLine({ text: c.text, label: r.label, sentence: r.sentence, ...(r.mark !== undefined ? { mark: r.mark } : {}), oneRow: true }).render(W, _ctx));
|
|
1910
|
+
return rows;
|
|
1911
|
+
}
|
|
1912
|
+
return folded.map((r, i) => (i === 0 ? cutLine(`${head}${lit(r)}`, W) : `${EDGE}${" ".repeat(META_LABEL)}${p.dim}${r}${p.reset}`));
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
/** The last sweep (owner, 2026-10-06) — `/think` and `/last` on the
|
|
1916
|
+
* terminal. They printed at column 0 under `--- … ---` rules, a blank row
|
|
1917
|
+
* between every part; now the row says what came back (`THINKING the last
|
|
1918
|
+
* block · 3 lines`, `LAST CALL LIST (root) · 2 lines`) and each part
|
|
1919
|
+
* follows in its own look: thinking as the thinking block draws it, a
|
|
1920
|
+
* call's input and output under dim titles, in `ink2`, hung by two. One
|
|
1921
|
+
* cell, so no blank row splits it. */
|
|
1922
|
+
class RecallBlock {
|
|
1923
|
+
cell;
|
|
1924
|
+
constructor(cell) {
|
|
1925
|
+
this.cell = cell;
|
|
1926
|
+
}
|
|
1927
|
+
render(W, ctx) {
|
|
1928
|
+
const p = palette();
|
|
1929
|
+
const rows = new ErrorLine({ text: "", label: this.cell.label, sentence: this.cell.sentence, oneRow: true }).render(W, ctx);
|
|
1930
|
+
for (const s of this.cell.sections) {
|
|
1931
|
+
if (s.title !== undefined)
|
|
1932
|
+
rows.push(cutLine(`${EDGE}${p.dim}${s.title}${p.reset}`, W));
|
|
1933
|
+
if (s.style === "thinking") {
|
|
1934
|
+
rows.push(...new ThinkingBlock({ text: s.text, done: true }).render(W, ctx));
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
const room = Math.max(1, W - EDGE.length - HANG);
|
|
1938
|
+
const lines = escapeTerminal(stripAnsi(s.text)).replace(/\n+$/, "").split("\n").flatMap((l) => foldLine(l, room));
|
|
1939
|
+
for (const l of lines)
|
|
1940
|
+
rows.push(`${EDGE}${" ".repeat(HANG)}${p.ink2}${l}${p.fgEnd}`);
|
|
1941
|
+
}
|
|
1942
|
+
return rows;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Graphite §7.11 — THE SEAL: the turn's line — `✦` in gold hanging in
|
|
1947
|
+
* the mark column, the words `dim` at the content edge. Its forms come widest first (the cold
|
|
1948
|
+
* cache sheds its label before anything is cut, R3g); the widest that
|
|
1949
|
+
* fits is drawn, and the last is cut with `…` — one row at every width.
|
|
1950
|
+
*/
|
|
1951
|
+
class SealLine {
|
|
1952
|
+
cell;
|
|
1953
|
+
constructor(cell) {
|
|
1954
|
+
this.cell = cell;
|
|
1955
|
+
}
|
|
1956
|
+
render(W, _ctx) {
|
|
1957
|
+
const p = palette();
|
|
1958
|
+
const mark = p.goldMark === "" ? `${p.bold}\u2726${p.reset}` : `${p.goldMark}\u2726${p.fgEnd}`;
|
|
1959
|
+
// the mark HANGS in the mark column (§1.8), the words at the edge
|
|
1960
|
+
const room = Math.max(1, W - EDGE.length);
|
|
1961
|
+
const text = pickTier(this.cell.tiers, room);
|
|
1962
|
+
const fitted = visibleWidth(text) <= room ? text : `${widthCut(text, Math.max(1, room - 1))}\u2026`;
|
|
1963
|
+
return [cutLine(`${mark} ${p.dim}${fitted}${p.reset}`, W)];
|
|
1737
1964
|
}
|
|
1738
1965
|
}
|
|
1739
1966
|
/** 4c — the folded history's ONE row. Dim, like every row that is about
|
|
@@ -1746,7 +1973,7 @@ class FoldRow {
|
|
|
1746
1973
|
}
|
|
1747
1974
|
render(W, _ctx) {
|
|
1748
1975
|
const p = palette();
|
|
1749
|
-
return [cutLine(
|
|
1976
|
+
return [cutLine(`${EDGE}${p.dim}${escapeTerminal(this.cell.label)}${p.reset}`, W)];
|
|
1750
1977
|
}
|
|
1751
1978
|
}
|
|
1752
1979
|
/** The CLI's pre-rendered blocks (the banner, the recap, slash-command
|
|
@@ -1880,20 +2107,28 @@ export function idleHint(room, expand = null) {
|
|
|
1880
2107
|
}
|
|
1881
2108
|
export function statusLine(status, tail, W, hint, expand = null) {
|
|
1882
2109
|
const p = palette();
|
|
1883
|
-
|
|
2110
|
+
// Graphite §8.7 (R3b): a status that waits for the person (`❯ …`, a
|
|
2111
|
+
// panel's own words) says so first — `❯ needs you · run paused` — the
|
|
2112
|
+
// `❯` gold where there is colour, the words as they were after it. The
|
|
2113
|
+
// window title already says `needs you` (§8.10); now the screen does.
|
|
2114
|
+
const waiting = status.startsWith("\u276f ");
|
|
2115
|
+
const said = waiting ? `\u276f needs you \u00b7 ${status.slice(2)}` : status;
|
|
2116
|
+
const text = `${said}${tail === "" ? "" : ` · ${tail}`}`;
|
|
2117
|
+
// the gold goes on the `❯` only while the cut row still starts with it
|
|
2118
|
+
const lead = (s) => (waiting && p.gold !== "" && s.startsWith("\u276f") ? `${p.gold}\u276f${p.fgEnd}${p.dim}${s.slice(1)}` : `${p.dim}${s}`);
|
|
1884
2119
|
// W18: the hint is a parameter — the compacting row right-aligns its
|
|
1885
2120
|
// "esc to cancel" (the same one-line-bounded shape as W12's delegate
|
|
1886
2121
|
// row; the #16g rule still cuts the HINT first, then the status with
|
|
1887
2122
|
// a "…" — never a fold).
|
|
1888
2123
|
const statusW = visibleWidth(text);
|
|
1889
2124
|
if (statusW > W) {
|
|
1890
|
-
return `${
|
|
2125
|
+
return `${lead(widthCut(text, W - 1))}…${p.reset}`;
|
|
1891
2126
|
}
|
|
1892
2127
|
const hintText = hint ?? idleHint(Math.max(0, W - statusW), expand);
|
|
1893
2128
|
const hintW = visibleWidth(hintText);
|
|
1894
2129
|
if (hintW === 0 || statusW + hintW > W)
|
|
1895
|
-
return `${
|
|
1896
|
-
return `${
|
|
2130
|
+
return `${lead(text)}${p.reset}`;
|
|
2131
|
+
return `${lead(text)}${" ".repeat(Math.max(0, W - statusW - hintW))}${hintText}${p.reset}`;
|
|
1897
2132
|
}
|
|
1898
2133
|
/**
|
|
1899
2134
|
* TUI2-R3v2 ① — THE selection bar. One engine, every selection surface.
|
|
@@ -1924,6 +2159,19 @@ export function statusLine(status, tail, W, hint, expand = null) {
|
|
|
1924
2159
|
*/
|
|
1925
2160
|
export function selectionBar(styled, visible, W) {
|
|
1926
2161
|
const p = palette();
|
|
2162
|
+
// Graphite §8.2 (R3a) — on a known ground the selected row is a card's
|
|
2163
|
+
// head in the colour that waits for the person: its `askEdge` cell in
|
|
2164
|
+
// column 0, the row on `washAsk` to the right edge, and a row's `→`
|
|
2165
|
+
// marker (the approval, question, pick and command lists carry one in
|
|
2166
|
+
// column 1) drawn as a gold `›`. DECLARED REVERSAL of the reverse-video
|
|
2167
|
+
// bar and its `→` there; `dim` stays (it meets the floor on washAsk).
|
|
2168
|
+
// Off a known ground the reverse-video bar below stays — one row, so no
|
|
2169
|
+
// seam.
|
|
2170
|
+
if (slabPaints()) {
|
|
2171
|
+
const marked = styled.replace(/^((?:\x1b\[[0-9;]*m)*)\u2192/, `$1${p.gold}\u203a${p.fgEnd}`);
|
|
2172
|
+
const inner = marked.replaceAll(p.reset, `${p.reset}${p.washAsk}`);
|
|
2173
|
+
return `${p.askEdge} ${p.washAsk}${inner}${" ".repeat(Math.max(0, W - 1 - visible))}${p.washEnd}`;
|
|
2174
|
+
}
|
|
1927
2175
|
// R2 (design §2.1 — nothing dim ever sits on the wash): the bar IS a
|
|
1928
2176
|
// wash. A dim span inside it renders grey-on-grey — 3.91:1 on the
|
|
1929
2177
|
// light ground, under the 4.5 floor — and the dim spans are exactly
|
|
@@ -1955,17 +2203,33 @@ export function selectionBar(styled, visible, W) {
|
|
|
1955
2203
|
* columns the walls were taking.
|
|
1956
2204
|
*/
|
|
1957
2205
|
export function boxTop(W) {
|
|
1958
|
-
//
|
|
1959
|
-
//
|
|
1960
|
-
|
|
2206
|
+
// Graphite §7.8: the one rule that carries colour — gold at its left
|
|
2207
|
+
// end, where the person's edge is, fading to the hairline.
|
|
2208
|
+
return fadeRule(W);
|
|
2209
|
+
}
|
|
2210
|
+
/** Graphite R3e — a read-only sheet over the input (`/status`): the band's
|
|
2211
|
+
* named hairline, one fact per row — its label dim in a column, its value
|
|
2212
|
+
* folded by word under itself — and the row that says how it closes.
|
|
2213
|
+
* It closes like the keys sheet, but what is typed after it is typed. */
|
|
2214
|
+
export function infoSheetRows(title, facts, W) {
|
|
1961
2215
|
const p = palette();
|
|
1962
|
-
|
|
2216
|
+
const labelW = Math.max(0, ...facts.map((f) => f.label.length)) + 2;
|
|
2217
|
+
const room = Math.max(1, W - 2 - labelW);
|
|
2218
|
+
const rows = [bandHeader(title, W)];
|
|
2219
|
+
for (const f of facts) {
|
|
2220
|
+
const folded = foldWords(escapeTerminal(f.value), room);
|
|
2221
|
+
for (const [i, line] of folded.entries())
|
|
2222
|
+
rows.push(` ${i === 0 ? `${p.dim}${f.label.padEnd(labelW)}${p.reset}` : " ".repeat(labelW)}${line}`);
|
|
2223
|
+
}
|
|
2224
|
+
rows.push(` ${p.dim}${SHEET_CLOSE}${p.reset}`);
|
|
2225
|
+
return rows.map((r) => cutLine(r, W));
|
|
1963
2226
|
}
|
|
1964
|
-
/** R2 — the same rule below
|
|
1965
|
-
* the compositor's two call sites did not
|
|
2227
|
+
/** R2 — the same rule below, in the hairline colour (§1.1). Named for its
|
|
2228
|
+
* POSITION, not its shape, so the compositor's two call sites did not
|
|
2229
|
+
* have to move. */
|
|
1966
2230
|
export function boxBottom(W) {
|
|
1967
2231
|
const p = palette();
|
|
1968
|
-
return `${p.dim}${"\u2500".repeat(Math.max(0, W))}${p.reset}`;
|
|
2232
|
+
return `${p.line === "" ? p.dim : p.line}${"\u2500".repeat(Math.max(0, W))}${p.line === "" ? p.reset : p.fgEnd}`;
|
|
1969
2233
|
}
|
|
1970
2234
|
/** The terminal label + rhythm gap (the pipe path's v2c bytes — the
|
|
1971
2235
|
* exact render the passthrough needs). */
|
|
@@ -1974,3 +2238,108 @@ export function terminalPipe(label, statusLineText) {
|
|
|
1974
2238
|
}
|
|
1975
2239
|
/** The pipe-path pieces the passthrough reuses (byte-identical). */
|
|
1976
2240
|
export { foldThinking, foldResult, renderToolSummary, TOOL_SUMMARY_MAX };
|
|
2241
|
+
// ── Graphite §8.2 — what every list band shares ─────────────────────
|
|
2242
|
+
// Moved from the @ picker (P3) so the pick panels, which draw in this
|
|
2243
|
+
// package, read the same window, marks, key row and matcher.
|
|
2244
|
+
/**
|
|
2245
|
+
* The subsequence embedding, TIGHTENED — the two-pass walk every good
|
|
2246
|
+
* fuzzy finder uses, and the reason `@ra` emboldens the "ra" of
|
|
2247
|
+
* "src/range.js" rather than the r of "src" and the a of "range".
|
|
2248
|
+
*
|
|
2249
|
+
* Pass 1 walks forward and stops at the EARLIEST index that completes
|
|
2250
|
+
* the query — this both answers "does it match at all" and fixes the
|
|
2251
|
+
* right-hand edge. Pass 2 walks backward from that edge, taking the
|
|
2252
|
+
* LATEST position for each query character in turn, which slides every
|
|
2253
|
+
* matched character as far right as it can go without crossing the
|
|
2254
|
+
* next one. The result is the most clustered embedding that ends where
|
|
2255
|
+
* the earliest match ends.
|
|
2256
|
+
*
|
|
2257
|
+
* Case-insensitive: both sides are lowercased by the caller once per
|
|
2258
|
+
* query rather than once per character.
|
|
2259
|
+
*
|
|
2260
|
+
* Returns the ascending match indices, or null when the query is not a
|
|
2261
|
+
* subsequence of the path at all.
|
|
2262
|
+
*/
|
|
2263
|
+
export function atEmbed(lowerPath, lowerQuery) {
|
|
2264
|
+
if (lowerQuery === "")
|
|
2265
|
+
return [];
|
|
2266
|
+
// pass 1 — forward, to the earliest completing index
|
|
2267
|
+
let qi = 0;
|
|
2268
|
+
let end = -1;
|
|
2269
|
+
for (let pi = 0; pi < lowerPath.length; pi += 1) {
|
|
2270
|
+
if (lowerPath[pi] === lowerQuery[qi]) {
|
|
2271
|
+
qi += 1;
|
|
2272
|
+
if (qi === lowerQuery.length) {
|
|
2273
|
+
end = pi;
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
if (end === -1)
|
|
2279
|
+
return null; // not a subsequence — no embedding exists
|
|
2280
|
+
// pass 2 — backward from that edge, sliding each character right
|
|
2281
|
+
const hit = new Array(lowerQuery.length);
|
|
2282
|
+
let pi = end;
|
|
2283
|
+
for (let j = lowerQuery.length - 1; j >= 0; j -= 1) {
|
|
2284
|
+
while (lowerPath[pi] !== lowerQuery[j])
|
|
2285
|
+
pi -= 1;
|
|
2286
|
+
hit[j] = pi;
|
|
2287
|
+
pi -= 1;
|
|
2288
|
+
}
|
|
2289
|
+
return hit;
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Graphite P2 (owner, 2026-10-03) — the band's window, shared by the file
|
|
2293
|
+
* picker, the command list and the session picker: EIGHT rows on a
|
|
2294
|
+
* terminal 30 rows or taller, five below.
|
|
2295
|
+
*/
|
|
2296
|
+
export function bandVisible(height) {
|
|
2297
|
+
return height >= 30 ? 8 : 5;
|
|
2298
|
+
}
|
|
2299
|
+
/** The window over a list, with a scroll-off of one: while more lies past
|
|
2300
|
+
* an edge the cursor stays a row inside it, so the edge row that carries
|
|
2301
|
+
* a more-mark is never the selected one. Stateless, like bandWindow. */
|
|
2302
|
+
export function bandWindow(total, selected, visible) {
|
|
2303
|
+
const count = Math.min(total, visible);
|
|
2304
|
+
return { first: Math.max(0, Math.min(selected - count + 2, total - count)), count };
|
|
2305
|
+
}
|
|
2306
|
+
/** The more-mark for row `i` of a window: `↑` on its first row when the
|
|
2307
|
+
* list goes on above, `↓` on its last when it goes on below. */
|
|
2308
|
+
export function moreMark(i, first, count, total) {
|
|
2309
|
+
return i === first && first > 0 ? "↑" : i === first + count - 1 && first + count < total ? "↓" : null;
|
|
2310
|
+
}
|
|
2311
|
+
/** The key row that closes a band: what the keys do, then the selection's
|
|
2312
|
+
* place in the list at the right edge. The keys give way from the least
|
|
2313
|
+
* needed (the first in `keys`); the counter stays. */
|
|
2314
|
+
export function bandKeyRow(keys, selected, total, W) {
|
|
2315
|
+
const p = palette();
|
|
2316
|
+
const count = total === 0 ? "0/0" : `${selected + 1}/${total}`;
|
|
2317
|
+
let kept = [...keys];
|
|
2318
|
+
while (kept.length > 0 && 2 + visibleWidth(kept.join(" · ")) + 2 + count.length + 1 > W)
|
|
2319
|
+
kept = kept.slice(1);
|
|
2320
|
+
const text = kept.join(" · ");
|
|
2321
|
+
const gap = Math.max(1, W - 1 - 2 - visibleWidth(text) - count.length);
|
|
2322
|
+
return `${p.dim}${widthCut(` ${text}${" ".repeat(gap)}${count}`, Math.max(0, W))}${p.reset}`;
|
|
2323
|
+
}
|
|
2324
|
+
/** Text with the given code-unit positions in bold gold — what the person
|
|
2325
|
+
* typed, in the colour of what the person says — and the rest in `base`. */
|
|
2326
|
+
export function goldHits(text, hits, base) {
|
|
2327
|
+
const p = palette();
|
|
2328
|
+
// off a known ground there is no gold: the warn tint carries it (as
|
|
2329
|
+
// /resume's title does), so a selected row — bold throughout — still
|
|
2330
|
+
// shows which letters matched
|
|
2331
|
+
const gold = p.gold !== "" ? p.gold : p.warn;
|
|
2332
|
+
// one span per RUN of hits, not per letter: a typed prefix is a run
|
|
2333
|
+
let out = "";
|
|
2334
|
+
let inHit = null;
|
|
2335
|
+
let i = 0;
|
|
2336
|
+
for (const ch of text) {
|
|
2337
|
+
const hit = hits.has(i);
|
|
2338
|
+
if (hit !== inHit)
|
|
2339
|
+
out += hit ? `${p.reset}${p.bold}${gold}` : `${inHit === null ? "" : p.reset}${base}`;
|
|
2340
|
+
inHit = hit;
|
|
2341
|
+
out += ch;
|
|
2342
|
+
i += ch.length;
|
|
2343
|
+
}
|
|
2344
|
+
return `${out}${p.reset}`;
|
|
2345
|
+
}
|