@vincemakes/kiso-tui-cells 0.46.2 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1242 -825
- package/dist/diff.d.ts +40 -3
- package/dist/diff.js +142 -5
- package/dist/graphite.d.ts +143 -0
- package/dist/graphite.js +311 -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,131 @@ 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
|
+
// 0.47.1 (finding 0470-F4, owner 2026-10-07): said once. The
|
|
730
|
+
// runtime's reason when the person gave none is the words
|
|
731
|
+
// "denied by user" — `denied by you · denied by user` said the
|
|
732
|
+
// refusal twice on one row, and the body (`[Permission denied]
|
|
733
|
+
// denied by user`, what the model was handed) a third time. So
|
|
734
|
+
// that refusal is `denied by you` alone, with no body. A refusal
|
|
735
|
+
// WITH a reason keeps both: the head gives its reason way on a
|
|
736
|
+
// narrow row (the pinned order), and the body still says it.
|
|
737
|
+
const quiet = word === "denied by you" && c.reason === DEFAULT_REFUSAL;
|
|
738
|
+
const outcome = quiet ? [word] : [`${word} \u00b7 ${escapeTerminal(c.reason)}`, word];
|
|
739
|
+
const body = quiet && c.resultText.trim() === `[Permission denied] ${c.reason}` ? { rows: [] } : toolBlockParts(c, W, ctx);
|
|
740
|
+
return card("fail", " ", verb, escapeTerminal(toolTargetOf(c)), outcome, body.rows, null, W, true, fold);
|
|
643
741
|
}
|
|
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.
|
|
742
|
+
// 4c: a card settled from the durable log carries no clock at
|
|
743
|
+
// all — it says nothing about time rather than `?s`.
|
|
647
744
|
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
|
-
|
|
745
|
+
// The tasks round (owner, 2026-10-06): a BACKGROUND delegation
|
|
746
|
+
// returns at once, so its card settles in a tenth of a second with
|
|
747
|
+
// the work only just begun. It names its children — the roles on
|
|
748
|
+
// the head, `N in the background`, a row per child with the task id
|
|
749
|
+
// `/tasks` and the notice use — instead of the foreground summary it
|
|
750
|
+
// never writes (the card read `DELEGATE … 1 line` with no target).
|
|
751
|
+
const children = c.name === "delegate" && !c.isError ? backgroundChildren(c) : null;
|
|
752
|
+
if (children !== null) {
|
|
753
|
+
const p2 = palette();
|
|
754
|
+
const rw = Math.max(...children.map((x) => x.role.length));
|
|
755
|
+
const room = bodyRowRoom(W);
|
|
756
|
+
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));
|
|
757
|
+
const outcome = `${children.length} in the background${elapsed === "" ? "" : ` \u00b7 ${elapsed}`}`;
|
|
758
|
+
return card("done", " ", verb, children.map((x) => x.role).join(" \u00b7 "), [outcome], slabPaints() ? rows : openBlock(rows), null, W, false);
|
|
759
|
+
}
|
|
657
760
|
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
761
|
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);
|
|
762
|
+
const attr = attribution(c).replace(/^ · /, "");
|
|
763
|
+
// VD-6: the line count is stated exactly ONCE — a meta that
|
|
764
|
+
// already counts lines gets no second count beside it.
|
|
765
|
+
// R2a: an edit's or a write's result is kiso's receipt (`edited
|
|
766
|
+
// <path>` and its revision), not something it made: its diff is
|
|
767
|
+
// the body, and the receipt's line count is no fact about it
|
|
768
|
+
const n = editDiffOf(c) === null ? countLines(c.resultText) : 0;
|
|
769
|
+
const counted = n > 0 && !/^\d+( of \d+)? lines?$/.test(rawMeta) ? `${n} line${n === 1 ? "" : "s"}` : "";
|
|
770
|
+
const join = (...xs) => xs.filter((x) => x !== "").join(" \u00b7 ");
|
|
771
|
+
// pin 4's order: the ATTRIBUTION gives way first, then the count;
|
|
772
|
+
// what happened and how long it took is never cut open.
|
|
773
|
+
// The last tier is the CORE — what happened and how long it took;
|
|
774
|
+
// below it the target elides (headCore), never the core.
|
|
775
|
+
const tiers = [join(meta, counted, elapsed, attr), join(meta, counted, elapsed), join(meta, elapsed)];
|
|
776
|
+
const body = toolBlockParts(c, W, ctx);
|
|
777
|
+
const state = c.isError ? "fail" : "done";
|
|
778
|
+
if (body.rows.length > 0) {
|
|
779
|
+
// the foot is the way back: an expanded card's, when collapsing
|
|
780
|
+
// would hide something again (a card whose whole body always
|
|
781
|
+
// shows has no way back to offer). A collapsed card's key stands
|
|
782
|
+
// on its cut note's row (the card round, `cutNote`).
|
|
783
|
+
const collapsed = { ...c, expanded: false };
|
|
784
|
+
const behind = c.expanded && (toolBlockParts(collapsed, W, ctx).cut || hiddenLines(collapsed, W) !== null);
|
|
785
|
+
const foot = behind ? COLLAPSE_ROW : null;
|
|
786
|
+
return card(state, " ", verb, escapeTerminal(toolTargetOf(c)), tiers, body.rows, foot, W, c.isError, fold);
|
|
752
787
|
}
|
|
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);
|
|
788
|
+
// no body: the head row between its pads, and the key — when the
|
|
789
|
+
// result sits behind it (a read) — at the end of the head row
|
|
790
|
+
const hidden = hiddenLines(c, W);
|
|
791
|
+
// the key is RESERVED (TUI2-R1.5 ⑤): its words shorten only after
|
|
792
|
+
// the attribution and the count have given way
|
|
793
|
+
const keyed = hidden === null ? tiers : [...tiers.map((t) => `${t} \u00b7 ${EXPAND_ROW}`), `${tiers[tiers.length - 1]} \u00b7 ctrl+o`];
|
|
794
|
+
return card(state, " ", verb, escapeTerminal(toolTargetOf(c)), keyed, [], null, W, c.isError, fold);
|
|
766
795
|
}
|
|
767
796
|
if (c.state === "approval") {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
return out;
|
|
797
|
+
const body = toolBlockParts(c, W, ctx);
|
|
798
|
+
// a foot only where there is a body above it (§1.3)
|
|
799
|
+
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
800
|
}
|
|
773
801
|
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.
|
|
802
|
+
// R3 (design §5.2): a running call BREATHES in its mark cell; its
|
|
803
|
+
// duration rides the head row's right end, where the settled
|
|
804
|
+
// card's outcome will stand — the settle changes the words and
|
|
805
|
+
// the ground, never the height (DC-46: the settle may add only
|
|
806
|
+
// the foot).
|
|
782
807
|
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.
|
|
808
|
+
const el = elapsedLabel(elapsed);
|
|
809
|
+
const gestures = c.name === "shell" ? " \u00b7 esc stops \u00b7 alt+\u23ce redirects" : "";
|
|
810
|
+
// the gestures give way first (the live row names the same keys);
|
|
811
|
+
// `running · Ns` is the core, and the narrow ladder keeps the
|
|
812
|
+
// elapsed alone at the very end
|
|
813
|
+
const tiers = [`running \u00b7 ${el}${gestures}`, `running \u00b7 ${el}`];
|
|
814
|
+
const mark = `${breathFrame(ctx.spinnerI)} `;
|
|
815
|
+
// DC-43: with too little room for a card the call keeps its head
|
|
816
|
+
// row alone until it commits — the one form that fits anywhere.
|
|
807
817
|
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);
|
|
818
|
+
if (liveRows <= 0)
|
|
819
|
+
return [cardHeadRow(mark, verb, liveTarget(c), tiers, W, false, false)];
|
|
820
|
+
const body = toolBlockParts(c, W, ctx);
|
|
821
|
+
return card("run", mark, verb, liveTarget(c), tiers, body.rows, c.expanded && body.rows.length > 0 ? COLLAPSE_ROW : null, W, false, fold);
|
|
853
822
|
}
|
|
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);
|
|
823
|
+
// not started yet (queued behind an exclusive call): the same card,
|
|
824
|
+
// its outcome says so.
|
|
825
|
+
return card("run", " ", verb, liveTarget(c), ["queued"], [], null, W, false, fold);
|
|
861
826
|
}
|
|
862
827
|
}
|
|
863
828
|
// ---- TUI2-R1 (A): the self-naming expand affordance ----
|
|
@@ -887,7 +852,7 @@ function hiddenLines(c, W) {
|
|
|
887
852
|
if (n === 0)
|
|
888
853
|
return null;
|
|
889
854
|
if (c.isError)
|
|
890
|
-
return null; //
|
|
855
|
+
return null; // an error previews its text, and the foot carries the key
|
|
891
856
|
// R13: a call that PREVIEWS carries the key on its own note row when
|
|
892
857
|
// something is cut, and needs no affordance at all when nothing is.
|
|
893
858
|
// A head-row suffix as well would be TUI2-R1's two affordances for
|
|
@@ -897,90 +862,6 @@ function hiddenLines(c, W) {
|
|
|
897
862
|
return null;
|
|
898
863
|
return n;
|
|
899
864
|
}
|
|
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
865
|
/**
|
|
985
866
|
* TUI2-R1.5 ⑤ (VD-11) — approval attribution, about humans.
|
|
986
867
|
*
|
|
@@ -1001,33 +882,15 @@ function attribution(c) {
|
|
|
1001
882
|
return "";
|
|
1002
883
|
return c.verdict.decision === "denied" ? " · denied" : " · approved";
|
|
1003
884
|
}
|
|
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
885
|
/* DECLARED REVERSAL (D-S2-1, owner-ruled 2026-09-06): `focusToken` and
|
|
1025
886
|
`EXPAND_KEY` stood here — TUI2-R2 ⑤'s bright ctrl+o token on the
|
|
1026
887
|
newest live card, "exactly one bright token per frame". DC-50 made
|
|
1027
888
|
ctrl+o a global switch, so there was no target left for a marker to
|
|
1028
889
|
name; the status row's idle hint names the switch (`idleHint`). */
|
|
1029
|
-
/** TUI2-R1 (A) — the expanded card's key
|
|
890
|
+
/** TUI2-R1 (A) — the expanded card's key: the way back. */
|
|
1030
891
|
const COLLAPSE_ROW = "ctrl+o collapses";
|
|
892
|
+
/** Graphite §7.4 — the key on a card with rows behind it. */
|
|
893
|
+
const EXPAND_ROW = "ctrl+o expands";
|
|
1031
894
|
/* DECLARED REVERSAL (R13, owner-ruled 2026-09-03): the W13 rollup's
|
|
1032
895
|
noun table (`ROLLUP_NOUN`) and TUI2-R1 (B)'s exploration-run set
|
|
1033
896
|
(`EXPLORE_NOUN`, `isExploreTool`) stood here. Both collapses retired
|
|
@@ -1113,8 +976,8 @@ export function askedBlock(resultText, seconds, W) {
|
|
|
1113
976
|
return [];
|
|
1114
977
|
const asked = parsed;
|
|
1115
978
|
const p = palette();
|
|
1116
|
-
const head = (n, outcome) => cutLine(
|
|
1117
|
-
const row = (body) => cutLine(
|
|
979
|
+
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);
|
|
980
|
+
const row = (body) => cutLine(`${EDGE}${p.dim}│${p.reset} ${body}`, W);
|
|
1118
981
|
if (Array.isArray(asked.declined) && asked.declined.length > 0) {
|
|
1119
982
|
// the honest decline record: WHAT went unanswered, and what the
|
|
1120
983
|
// choices had been — the panel already computes both.
|
|
@@ -1159,72 +1022,68 @@ const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
|
|
|
1159
1022
|
* `└` survives as the mark that OPENS the block, once, on its first
|
|
1160
1023
|
* row (see openBlock). In-block notes take the same indent,
|
|
1161
1024
|
* 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. */
|
|
1025
|
+
* mark meaning two things (§4.1). */
|
|
1026
|
+
/** Graphite §7.4 — off the surface (unknown ground), a card's body sits
|
|
1027
|
+
* two columns under its head: the head at the content edge (2), the body
|
|
1028
|
+
* at 4, opened by `└` (R8a's indent, which is what says "these rows are
|
|
1029
|
+
* output" once nothing is painted). */
|
|
1169
1030
|
const BODY_ROW_FLAT = " ";
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
*
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
*
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1031
|
+
/** Inside a painted card the body sits at the content edge, under the
|
|
1032
|
+
* verb — the head and what it printed line up (owner, 2026-09-29, the
|
|
1033
|
+
* 0.44 card's alignment). */
|
|
1034
|
+
const CARD_BODY = "";
|
|
1035
|
+
/** A card's inner right margin. */
|
|
1036
|
+
const CARD_RIGHT = 1;
|
|
1037
|
+
/** The cells a painted card has for its content: after its edge cell and
|
|
1038
|
+
* its mark cell, from the content edge to the right edge. */
|
|
1039
|
+
const cardInner = (W) => Math.max(1, W - EDGE.length);
|
|
1040
|
+
/** Graphite §1.8 — the model's words and its thinking begin at the
|
|
1041
|
+
* content edge, like every other block. */
|
|
1042
|
+
const PROSE_COL = EDGE;
|
|
1043
|
+
const THINK_COL = EDGE;
|
|
1044
|
+
const bodyRow = () => (slabPaints() ? CARD_BODY : BODY_ROW_FLAT);
|
|
1045
|
+
const noteIndent = bodyRow;
|
|
1046
|
+
/** The cells a body row's text has: under the target inside a card, under
|
|
1047
|
+
* the flat indent outside one. */
|
|
1048
|
+
const bodyTextWidth = (W) => Math.max(1, slabPaints() ? cardInner(W) - CARD_BODY.length - CARD_RIGHT : W - BODY_ROW_FLAT.length);
|
|
1049
|
+
/** The cells a whole body row has (its indent included). */
|
|
1050
|
+
const bodyRowRoom = (W) => Math.max(1, slabPaints() ? cardInner(W) - CARD_RIGHT : W);
|
|
1189
1051
|
/**
|
|
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.
|
|
1052
|
+
* Graphite §7.4 / §3.1 — a card paints only where the ground is known:
|
|
1053
|
+
* there its surfaces are real backgrounds. On the unknown ground they
|
|
1054
|
+
* would be reverse video, and eight inverted output rows are a black slab
|
|
1055
|
+
* in the middle of the transcript, so the card keeps its content and
|
|
1056
|
+
* loses its surface.
|
|
1211
1057
|
*/
|
|
1212
1058
|
function slabPaints() {
|
|
1059
|
+
return palette().washDone !== "" && currentGround() !== "unknown";
|
|
1060
|
+
}
|
|
1061
|
+
function cardPaint(state) {
|
|
1062
|
+
const p = palette();
|
|
1063
|
+
if (state === "fail")
|
|
1064
|
+
return { bg: p.washFail, edge: p.failEdge };
|
|
1065
|
+
if (state === "ask")
|
|
1066
|
+
return { bg: p.washAsk, edge: p.askEdge };
|
|
1067
|
+
return { bg: p.washRun, edge: p.runEdge };
|
|
1068
|
+
}
|
|
1069
|
+
/** One painted card row, the full width: the edge cell in column 0, the
|
|
1070
|
+
* mark cell in column 1, the content from the content edge, the state's
|
|
1071
|
+
* ground to the right edge, padded by DISPLAY width. A reset inside the
|
|
1072
|
+
* content would strand the ground for the rest of the row, so every
|
|
1073
|
+
* reset re-opens it (the selection bar's discipline). */
|
|
1074
|
+
function slabRow(inner, W, paint, mark = " ") {
|
|
1213
1075
|
const p = palette();
|
|
1214
|
-
|
|
1076
|
+
const room = cardInner(W);
|
|
1077
|
+
const fitted = visibleWidth(inner) > room ? cutLine(inner, room) : inner;
|
|
1078
|
+
const body = fitted.replaceAll(p.reset, `${p.reset}${paint.bg}`);
|
|
1079
|
+
const pad = Math.max(0, room - visibleWidth(fitted));
|
|
1080
|
+
return `${paint.edge} ${paint.bg}${mark.replaceAll(p.reset, `${p.reset}${paint.bg}`)}${body}${paint.bg}${" ".repeat(pad)}${p.washEnd}`;
|
|
1215
1081
|
}
|
|
1216
|
-
/**
|
|
1217
|
-
|
|
1218
|
-
* row, so every reset re-opens it — the selection bar's discipline,
|
|
1219
|
-
* applied to a surface that spans many rows instead of one. */
|
|
1220
|
-
function slabRow(inner, W) {
|
|
1082
|
+
/** A card's pad: a whole row of its ground (§1.5), its edge cell too. */
|
|
1083
|
+
function padRow(paint, W) {
|
|
1221
1084
|
const p = palette();
|
|
1222
|
-
|
|
1223
|
-
const pad = Math.max(0, W - visibleWidth(inner));
|
|
1224
|
-
return `${p.wash}${body}${" ".repeat(pad)}${p.washEnd}`;
|
|
1085
|
+
return `${paint.edge} ${paint.bg}${" ".repeat(Math.max(0, W - 1))}${p.washEnd}`;
|
|
1225
1086
|
}
|
|
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
1087
|
/** The widest form that fits the row, or the last one — the head row's
|
|
1229
1088
|
* own discipline (TUI2-R1.5 ⑤, pin 4) applied to the slab's two
|
|
1230
1089
|
* metadata rows: the parts give way in a PINNED ORDER, and the part
|
|
@@ -1232,65 +1091,177 @@ function slabRow(inner, W) {
|
|
|
1232
1091
|
function pickTier(tiers, room) {
|
|
1233
1092
|
return firstFit(tiers, room) ?? tiers[tiers.length - 1];
|
|
1234
1093
|
}
|
|
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. */
|
|
1094
|
+
/** The widest form that fits the row, or null when none does. */
|
|
1240
1095
|
function firstFit(tiers, room) {
|
|
1241
1096
|
for (const t of tiers)
|
|
1242
1097
|
if (visibleWidth(t) <= room)
|
|
1243
1098
|
return t;
|
|
1244
1099
|
return null;
|
|
1245
1100
|
}
|
|
1101
|
+
/** A metadata row inside a card's body (a cut note, the tool's own cap):
|
|
1102
|
+
* cut, never folded — a metadata row that wraps costs the block a row it
|
|
1103
|
+
* did not budget. */
|
|
1246
1104
|
function noteRow(text, W, tone) {
|
|
1247
1105
|
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
1106
|
const open = tone === "body" ? p.washDim : p.dim;
|
|
1253
1107
|
const close = tone === "body" ? p.washDimEnd : p.reset;
|
|
1254
|
-
return [cutLine(`${open}${noteIndent()}${text}${close}`, W)];
|
|
1108
|
+
return [cutLine(`${open}${noteIndent()}${text}${close}`, bodyRowRoom(W))];
|
|
1109
|
+
}
|
|
1110
|
+
/** `text` elided in its MIDDLE to `room` cells — a path keeps its head
|
|
1111
|
+
* and its file name, which is how a reader recognises it (§7.5). */
|
|
1112
|
+
function elideMiddle(text, room) {
|
|
1113
|
+
if (visibleWidth(text) <= room)
|
|
1114
|
+
return text;
|
|
1115
|
+
if (room <= 1)
|
|
1116
|
+
return "\u2026".slice(0, Math.max(0, room));
|
|
1117
|
+
const right = Math.floor((room - 1) / 2);
|
|
1118
|
+
const left = room - 1 - right;
|
|
1119
|
+
const chars = Array.from(text);
|
|
1120
|
+
let tail = "";
|
|
1121
|
+
for (let i = chars.length - 1; i >= 0; i -= 1) {
|
|
1122
|
+
if (displayWidth(chars[i] + tail) > right)
|
|
1123
|
+
break;
|
|
1124
|
+
tail = chars[i] + tail;
|
|
1125
|
+
}
|
|
1126
|
+
return `${widthCut(text, left)}\u2026${tail}`;
|
|
1127
|
+
}
|
|
1128
|
+
/** §7.5 — only the outcome WORD takes colour: the first segment of the
|
|
1129
|
+
* outcome — `exit 0` in the success colour, a failure's word in the
|
|
1130
|
+
* failure colour, `needs you` in gold (the card round: it stands with
|
|
1131
|
+
* its `❯` now) — and the rest is `dim`. */
|
|
1132
|
+
function outcomeStyled(text, error) {
|
|
1133
|
+
const p = palette();
|
|
1134
|
+
if (text === "")
|
|
1135
|
+
return "";
|
|
1136
|
+
const at = text.indexOf(" \u00b7 ");
|
|
1137
|
+
const first = at < 0 ? text : text.slice(0, at);
|
|
1138
|
+
const rest = at < 0 ? "" : text.slice(at);
|
|
1139
|
+
const tone = error ? p.red : /^exit 0$/.test(first) ? p.green : first === "needs you" ? p.gold : "";
|
|
1140
|
+
return tone === "" ? `${p.dim}${text}${p.reset}` : `${tone}${first}${p.reset}${rest === "" ? "" : `${p.dim}${rest}${p.reset}`}`;
|
|
1255
1141
|
}
|
|
1256
1142
|
/**
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1259
|
-
* `
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1143
|
+
* §7.4 / §7.5 — a card's head row, without the card's edge cell: the verb
|
|
1144
|
+
* (upper case, `dim`), the target, and the outcome right-aligned in
|
|
1145
|
+
* `room` cells, `mark` (one styled cell, or "") standing in front of the
|
|
1146
|
+
* outcome's words. It gives way in a pinned order: the outcome's tiers
|
|
1147
|
+
* first (the attribution, then the count), then the target elides in its
|
|
1148
|
+
* middle; the outcome word is never cut. The row never folds; a command
|
|
1149
|
+
* folds in `headRows`.
|
|
1150
|
+
*/
|
|
1151
|
+
function headCore(verb, target, tiers, room, error, mark = "") {
|
|
1152
|
+
const p = palette();
|
|
1153
|
+
// the verb and ONE space, then the target (owner, 2026-09-29: the verb
|
|
1154
|
+
// column padded to seven left the target stranded between the verb and
|
|
1155
|
+
// the body under it — 0.44's `shell pwd && ls -la` reads as one line)
|
|
1156
|
+
const lead = `${p.dim}${verb}${p.reset} `;
|
|
1157
|
+
const avail = Math.max(1, room - (verb.length + 1));
|
|
1158
|
+
// the card round (owner, 2026-10-05): the running `●` and the waiting
|
|
1159
|
+
// `❯` stand where the settled outcome will, so the state is read in one
|
|
1160
|
+
// place and nothing sits against the verb
|
|
1161
|
+
const marked = (out) => `${mark === "" ? "" : `${mark} `}${outcomeStyled(out, error)}`;
|
|
1162
|
+
const mw = mark === "" ? 0 : 2;
|
|
1163
|
+
const compose = (tg, out) => `${lead}${tg}${" ".repeat(Math.max(2, avail - visibleWidth(tg) - visibleWidth(out) - mw))}${marked(out)}`;
|
|
1164
|
+
for (const t of tiers)
|
|
1165
|
+
if (visibleWidth(target) + 2 + visibleWidth(t) + mw <= avail)
|
|
1166
|
+
return compose(target, t);
|
|
1167
|
+
const last = tiers[tiers.length - 1] ?? "";
|
|
1168
|
+
const tRoom = avail - 2 - visibleWidth(last) - mw;
|
|
1169
|
+
if (tRoom >= 4)
|
|
1170
|
+
return compose(elideMiddle(target, tRoom), last);
|
|
1171
|
+
// A NARROW row: the target gives way entirely, then the verb's padding
|
|
1172
|
+
// and the verb, then everything but the outcome — and the outcome's
|
|
1173
|
+
// last word, how long it took, is the one thing kept to the end (pin 4:
|
|
1174
|
+
// what happened and how long it took is never cut open).
|
|
1175
|
+
const bareLead = `${p.dim}${verb}${p.reset} `;
|
|
1176
|
+
// the outcome's segments give way from the FRONT — the count, then
|
|
1177
|
+
// the outcome word (and the mark with it) — so how long it took, and
|
|
1178
|
+
// the key where there is one, are the last to go
|
|
1179
|
+
const segs = last.split(" \u00b7 ");
|
|
1180
|
+
const suffixes = segs.map((_, k) => segs.slice(k).join(" \u00b7 "));
|
|
1181
|
+
for (const row of [`${lead}${marked(last)}`, `${bareLead}${marked(last)}`, ...suffixes.map((x, k) => (k === 0 ? marked(x) : `${p.dim}${x}${p.reset}`))]) {
|
|
1182
|
+
if (visibleWidth(row) <= room)
|
|
1183
|
+
return row;
|
|
1184
|
+
}
|
|
1185
|
+
return cutLine(`${p.dim}${suffixes[suffixes.length - 1]}${p.reset}`, room);
|
|
1186
|
+
}
|
|
1187
|
+
/** The card round — the cells the head's first row keeps for the outcome
|
|
1188
|
+
* when a command folds, whatever the outcome says now: `● running · 59m
|
|
1189
|
+
* 59s` and `exit 127 · 59m 59s` both fit. The fold is measured against
|
|
1190
|
+
* this, never against the words of the moment, so a card neither
|
|
1191
|
+
* re-folds while it runs nor changes height when it settles (DC-46: a
|
|
1192
|
+
* live card never shrinks). */
|
|
1193
|
+
const OUTCOME_ROOM = 20;
|
|
1194
|
+
/** The least room a command's first row keeps; narrower, the head is one
|
|
1195
|
+
* row and the command elides (the narrow ladder, `headCore`). */
|
|
1196
|
+
const FOLD_MIN = 12;
|
|
1197
|
+
/** A collapsed card's command shows at most this many rows. */
|
|
1198
|
+
const HEAD_ROWS = 3;
|
|
1199
|
+
/**
|
|
1200
|
+
* The card round (owner, 2026-10-05) — the head's rows. A shell command
|
|
1201
|
+
* is code the person approves and audits, so it is never cut in its
|
|
1202
|
+
* middle: when it does not fit beside the outcome it folds at its spaces,
|
|
1203
|
+
* each further row hanging under its own first character, for at most
|
|
1204
|
+
* `max` rows — past that the last row ends in `…` and ctrl+o shows the
|
|
1205
|
+
* rest. Every other target (a path keeps its head and its file name) is
|
|
1206
|
+
* one row, `headCore`'s. DECLARED REVERSAL of §7.5's "the row never
|
|
1207
|
+
* folds" for a command.
|
|
1208
|
+
*/
|
|
1209
|
+
function headRows(verb, target, tiers, room, error, mark, max) {
|
|
1210
|
+
const single = [headCore(verb, target, tiers, room, error, mark)];
|
|
1211
|
+
if (max <= 1)
|
|
1212
|
+
return single;
|
|
1213
|
+
const lead = verb.length + 1;
|
|
1214
|
+
const avail = room - lead;
|
|
1215
|
+
const last = tiers[tiers.length - 1] ?? "";
|
|
1216
|
+
const first = avail - 2 - Math.max(OUTCOME_ROOM, visibleWidth(last) + (mark === "" ? 0 : 2));
|
|
1217
|
+
if (first < FOLD_MIN || visibleWidth(target) <= first)
|
|
1218
|
+
return single;
|
|
1219
|
+
const head = widthCut(target, first);
|
|
1220
|
+
const space = head.lastIndexOf(" ");
|
|
1221
|
+
const cut = space > 0 ? space + 1 : Math.max(1, head.length);
|
|
1222
|
+
const rest = target.slice(cut).trimStart();
|
|
1223
|
+
const rows = [target.slice(0, cut).trimEnd(), ...(rest === "" ? [] : foldAtSpaces(rest, avail))];
|
|
1224
|
+
const shown = rows.length <= max ? rows : [...rows.slice(0, max - 1), `${widthCut(rows.slice(max - 1).join(" "), avail - 1)}\u2026`];
|
|
1225
|
+
return [headCore(verb, shown[0], tiers, room, error, mark), ...shown.slice(1).map((r) => `${" ".repeat(lead)}${r}`)];
|
|
1226
|
+
}
|
|
1227
|
+
/** DC-43 — the head row ALONE, its mark in the mark column: a running
|
|
1228
|
+
* call's form when the room left is too small for a card. `mark` is two
|
|
1229
|
+
* cells (the glyph and a space, or two spaces). */
|
|
1230
|
+
function cardHeadRow(mark, verb, target, tiers, W, _painted, error) {
|
|
1231
|
+
return cutLine(`${mark}${headCore(verb, target, tiers, Math.max(1, W - EDGE.length), error)}`, W);
|
|
1232
|
+
}
|
|
1233
|
+
/** The card's foot: the key, right-aligned. */
|
|
1234
|
+
function footRow(key, room) {
|
|
1235
|
+
const p = palette();
|
|
1236
|
+
return `${" ".repeat(Math.max(0, room - visibleWidth(key)))}${p.dim}${key}${p.reset}`;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Graphite §7.4 — assemble a card: pad · head · body · foot · pad on the
|
|
1240
|
+
* state's ground, full width, its edge cell in column 0, column 1 blank;
|
|
1241
|
+
* or, off the surface, the same content without it — the mark in the
|
|
1242
|
+
* mark column, the head at the content edge, the body under it, the foot
|
|
1243
|
+
* dim. `mark` is two cells: the glyph and a space, or two spaces. Inside
|
|
1244
|
+
* a card the glyph stands in front of the outcome's words (the card
|
|
1245
|
+
* round: in column 1 it sat against the verb). `fold` is how many rows
|
|
1246
|
+
* the head's command may take (0: the target never folds).
|
|
1263
1247
|
*/
|
|
1264
|
-
function
|
|
1248
|
+
function card(state, mark, verb, target, tiers, body, foot, W, error, fold = 0) {
|
|
1265
1249
|
if (!slabPaints()) {
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1250
|
+
const [first, ...more] = headRows(verb, target, tiers, Math.max(1, W - EDGE.length), error, "", fold);
|
|
1251
|
+
const out = [cutLine(`${mark}${first}`, W), ...more.map((r) => cutLine(`${EDGE}${r}`, W)), ...body];
|
|
1252
|
+
if (foot !== null)
|
|
1253
|
+
out.push(cutLine(footRow(foot, W), W));
|
|
1269
1254
|
return out;
|
|
1270
1255
|
}
|
|
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)];
|
|
1256
|
+
const paint = cardPaint(state);
|
|
1257
|
+
const inner = cardInner(W);
|
|
1258
|
+
const glyph = mark.replace(/ $/, "");
|
|
1259
|
+
const head = headRows(verb, target, tiers, inner - CARD_RIGHT, error, glyph.trim() === "" ? "" : glyph, fold);
|
|
1260
|
+
const rows = [padRow(paint, W), ...head.map((r) => slabRow(r, W, paint)), ...body.map((r) => slabRow(r, W, paint))];
|
|
1261
|
+
if (foot !== null)
|
|
1262
|
+
rows.push(slabRow(footRow(foot, inner - CARD_RIGHT), W, paint));
|
|
1263
|
+
rows.push(padRow(paint, W));
|
|
1264
|
+
return rows;
|
|
1294
1265
|
}
|
|
1295
1266
|
/*
|
|
1296
1267
|
* RETIRED (DC-50 / R14, 2026-09-05) — `expandedCard`.
|
|
@@ -1307,7 +1278,7 @@ function slabBlock(head, body, outcome, W) {
|
|
|
1307
1278
|
* `r13-rhythm-surface.test.ts`.
|
|
1308
1279
|
*/
|
|
1309
1280
|
/** 0.24.2 ② — the live region's `thinking…` placeholder: dim italic at
|
|
1310
|
-
*
|
|
1281
|
+
* the content edge, no glyph, the SAME shape a thinking paragraph takes so that
|
|
1311
1282
|
* whatever arrives replaces it in place. Never committed — see the
|
|
1312
1283
|
* compositor's #project for why that is what makes it allowed. */
|
|
1313
1284
|
export function thinkingRow() {
|
|
@@ -1331,10 +1302,12 @@ function openBlock(rows) {
|
|
|
1331
1302
|
return rows;
|
|
1332
1303
|
// the corner REPLACES two of the four indent columns, so the text
|
|
1333
1304
|
// stays in the same column as every other row of the block.
|
|
1334
|
-
return [...rows.slice(0, i), `${first.slice(0, at)}
|
|
1305
|
+
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
1306
|
}
|
|
1336
1307
|
const blockMemo = new WeakMap();
|
|
1337
|
-
/** The block's body rows below the header (memoized, W9)
|
|
1308
|
+
/** The block's body rows below the header (memoized, W9), and whether
|
|
1309
|
+
* the preview CUT anything — which is what puts the key on a card's
|
|
1310
|
+
* foot (§7.4). */
|
|
1338
1311
|
function toolBlockParts(c, W, ctx) {
|
|
1339
1312
|
const memo = blockMemo.get(c);
|
|
1340
1313
|
// the SURFACE is part of the key: the same cell renders different rows
|
|
@@ -1342,80 +1315,114 @@ function toolBlockParts(c, W, ctx) {
|
|
|
1342
1315
|
// would otherwise be served the pre-ground shape forever.
|
|
1343
1316
|
const liveRows = ctx.liveWindow ?? CAP_PREVIEW;
|
|
1344
1317
|
const state = `${c.state}:${c.isError}:${c.name}:${c.expanded ? "x" : ""}:${slabPaints() ? "slab" : "flat"}:${liveRows}`;
|
|
1345
|
-
|
|
1318
|
+
// R2a: an edit or a write shows what it changed while it runs and once
|
|
1319
|
+
// it has — never after a refusal (nothing changed) or an error
|
|
1320
|
+
const edit = !c.isError && c.reason === null && (c.state === "running" || c.state === "done") ? editDiffOf(c) : null;
|
|
1321
|
+
const content = c.state === "approval" ? (c.diff ?? null) : edit !== null ? edit : c.resultText;
|
|
1346
1322
|
if (memo !== undefined && memo.width === W && memo.state === state && memo.content === content)
|
|
1347
1323
|
return memo;
|
|
1348
1324
|
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
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1325
|
+
const tone = slabPaints() ? "body" : "dim";
|
|
1326
|
+
let cut = false;
|
|
1327
|
+
const keyed = c.state === "done" && !c.expanded;
|
|
1328
|
+
const capped = (all, dir, starts = []) => {
|
|
1329
|
+
if (all.length <= CAP_PREVIEW)
|
|
1330
|
+
return all;
|
|
1331
|
+
cut = true;
|
|
1332
|
+
// R9 P2 / D4: FIVE output rows, and the note is a row of its own.
|
|
1333
|
+
// A shell's conclusion is at the bottom, so its note goes ABOVE the
|
|
1334
|
+
// tail; everything else answers at the top, so its note closes it.
|
|
1335
|
+
// The card round: a settled, collapsed card's key rides the note.
|
|
1336
|
+
if (dir === "tail") {
|
|
1337
|
+
const w = tailWindow(all, CAP_PREVIEW, starts);
|
|
1338
|
+
return [...cutNote(w.lines, "earlier", W, tone, keyed, w.partial), ...w.rows];
|
|
1339
|
+
}
|
|
1340
|
+
const h = headCut(all, CAP_PREVIEW, starts);
|
|
1341
|
+
return [...h.rows, ...cutNote(h.lines, "more", W, tone, keyed, h.partial)];
|
|
1342
|
+
};
|
|
1343
|
+
const shellTail = (out, dir) => capped(out.rows, dir, out.starts);
|
|
1344
|
+
// a diff inside a card: its own rows, the head of them until the key
|
|
1345
|
+
const diffCapped = (lines, cap) => {
|
|
1346
|
+
const all = lines === null ? [] : diffRows(lines, W);
|
|
1347
|
+
if (c.expanded || all.length <= cap)
|
|
1348
|
+
return all;
|
|
1349
|
+
cut = true;
|
|
1350
|
+
return [...all.slice(0, cap), ...cutNote(all.length - cap, "more", W, tone, keyed)];
|
|
1351
|
+
};
|
|
1352
|
+
const rows = edit !== null
|
|
1353
|
+
? diffCapped(edit.lines, edit.cap)
|
|
1354
|
+
: c.state === "approval" && slabPaints()
|
|
1355
|
+
? diffCapped(c.diff, CAP_DIFF)
|
|
1356
|
+
: c.expanded
|
|
1357
|
+
? // W15: the WHOLE body, no cap, no cut note (nothing is cut).
|
|
1358
|
+
c.state === "approval"
|
|
1359
|
+
? diffBody(c.diff, W, true)
|
|
1360
|
+
: c.name === "delegate"
|
|
1361
|
+
? c.state === "running"
|
|
1362
|
+
? delegateRunning(c, W)
|
|
1363
|
+
: delegateSettled(c, W)
|
|
1364
|
+
: blockRows(c.resultText, W, tone)
|
|
1365
|
+
: c.state === "done"
|
|
1366
|
+
? c.isError
|
|
1367
|
+
? capped(errorRows(c, W, tone), "head")
|
|
1368
|
+
: c.name === "delegate"
|
|
1369
|
+
? delegateSettled(c, W)
|
|
1370
|
+
: // R13 — EVERY settled call previews: a shell its tail,
|
|
1371
|
+
// everything else its head. read_file is the single
|
|
1372
|
+
// exception (E1): its result is the file, and the key
|
|
1373
|
+
// opens the whole thing.
|
|
1374
|
+
noPreview(c)
|
|
1375
|
+
? []
|
|
1376
|
+
: shellTail(blockLines(c.resultText, W, tone), c.name === "shell" ? "tail" : "head")
|
|
1377
|
+
: c.state === "running"
|
|
1378
|
+
? c.name === "delegate"
|
|
1379
|
+
? delegateRunning(c, W)
|
|
1380
|
+
: // R13 E1 — the call with no preview settled has none
|
|
1381
|
+
// while it runs either.
|
|
1382
|
+
noPreview({ ...c, state: "done" })
|
|
1383
|
+
? []
|
|
1384
|
+
: liveWindow(c, W, liveCap(c, liveRows), tone)
|
|
1385
|
+
: c.state === "approval"
|
|
1386
|
+
? diffBody(c.diff, W)
|
|
1387
|
+
: [];
|
|
1388
|
+
// E1 governs the PREVIEW, not kiso's sentence about the result: a read
|
|
1389
|
+
// the TOOL capped takes one body row (`offset=201 for the rest`).
|
|
1390
|
+
const note = c.expanded || edit !== null ? null : toolCutNote(c.name, c.resultText);
|
|
1396
1391
|
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.
|
|
1392
|
+
rows.push(...noteRow(note, W, "dim"));
|
|
1393
|
+
// R8a: `└` opens a block that has no surface; inside a card the surface
|
|
1394
|
+
// IS the container.
|
|
1414
1395
|
const opened = slabPaints() ? rows : openBlock(rows);
|
|
1415
|
-
const parts = { width: W, state, content, rows: opened };
|
|
1396
|
+
const parts = { width: W, state, content, rows: opened, cut };
|
|
1416
1397
|
blockMemo.set(c, parts);
|
|
1417
1398
|
return parts;
|
|
1418
1399
|
}
|
|
1400
|
+
/** The tasks round — a background delegation's children, from what the
|
|
1401
|
+
* call returned (`started 2 background children: t1 explorer (session …),
|
|
1402
|
+
* t2 reviewer (session …). They read …`) and what it was asked (each
|
|
1403
|
+
* task's role and words): the task id, the role, the task's first line.
|
|
1404
|
+
* Null for anything else — a foreground delegation, a refusal. */
|
|
1405
|
+
function backgroundChildren(c) {
|
|
1406
|
+
const m = /^started \d+ background (?:child|children): (.*?)\. They read the workspace/s.exec(c.resultText);
|
|
1407
|
+
if (m === null)
|
|
1408
|
+
return null;
|
|
1409
|
+
let asked = {};
|
|
1410
|
+
try {
|
|
1411
|
+
asked = JSON.parse(c.inputFull);
|
|
1412
|
+
}
|
|
1413
|
+
catch {
|
|
1414
|
+
// the full input is always JSON (stringified at toolStart)
|
|
1415
|
+
}
|
|
1416
|
+
const pool = (asked.tasks ?? []).map((t) => ({ role: String(t.role ?? ""), task: oneRow(escapeTerminal(String(t.task ?? "").split("\n")[0] ?? "")), used: false }));
|
|
1417
|
+
const out = [];
|
|
1418
|
+
for (const s of m[1].matchAll(/(\S+) (\S+) \(session [^)]*\)/g)) {
|
|
1419
|
+
const at = pool.findIndex((t) => !t.used && t.role === s[2]);
|
|
1420
|
+
if (at >= 0)
|
|
1421
|
+
pool[at].used = true;
|
|
1422
|
+
out.push({ id: escapeTerminal(s[1]), role: escapeTerminal(s[2]), task: at >= 0 ? pool[at].task : "" });
|
|
1423
|
+
}
|
|
1424
|
+
return out.length === 0 ? null : out;
|
|
1425
|
+
}
|
|
1419
1426
|
/** R13 E1 — the one settled call that previews NOTHING. A read's result
|
|
1420
1427
|
* IS the file; five lines of it tell a reader less than the head row
|
|
1421
1428
|
* already does, and the key opens the whole thing. (The reference
|
|
@@ -1424,96 +1431,143 @@ function toolBlockParts(c, W, ctx) {
|
|
|
1424
1431
|
function noPreview(c) {
|
|
1425
1432
|
return c.state === "done" && !c.expanded && !c.isError && c.reason === null && c.name === "read_file";
|
|
1426
1433
|
}
|
|
1427
|
-
/**
|
|
1428
|
-
|
|
1429
|
-
return toolBlockParts(c, W, ctx).rows;
|
|
1430
|
-
}
|
|
1434
|
+
/** How far a wrapped output line's continuation rows stand in. */
|
|
1435
|
+
const HANG = 2;
|
|
1431
1436
|
/** Fold result text into body rows (the block's own indent): escape,
|
|
1432
|
-
* split, fold each line
|
|
1433
|
-
* final newline) drop.
|
|
1437
|
+
* split, fold each line in the body's text width; trailing empty rows
|
|
1438
|
+
* (the result's final newline) drop. Inside a card the output is
|
|
1439
|
+
* `ink2` (§2: tool output); off the surface it is dim. */
|
|
1434
1440
|
function blockRows(text, W, tone = "dim") {
|
|
1441
|
+
return blockLines(text, W, tone).rows;
|
|
1442
|
+
}
|
|
1443
|
+
/** `blockRows`, with which rows begin a line of output (a wrapped line's
|
|
1444
|
+
* continuation rows do not) — what a tail window needs to start on a
|
|
1445
|
+
* whole line. */
|
|
1446
|
+
function blockLines(text, W, tone = "dim") {
|
|
1435
1447
|
const p = palette();
|
|
1436
|
-
const textW =
|
|
1448
|
+
const textW = bodyTextWidth(W);
|
|
1437
1449
|
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 : "";
|
|
1450
|
+
const starts = [];
|
|
1451
|
+
const open = tone === "dim" ? p.dim : p.ink2;
|
|
1452
|
+
const close = tone === "dim" ? p.reset : p.ink2 === "" ? "" : p.fgEnd;
|
|
1444
1453
|
// 0.40.0: the output's own styling is dropped whole before the escape
|
|
1454
|
+
// The card round (owner, 2026-10-05): a line too long for the row
|
|
1455
|
+
// continues HANG cells in, so one line of output reads as one.
|
|
1456
|
+
const hang = textW > HANG * 4 ? HANG : 0;
|
|
1445
1457
|
for (const raw of escapeTerminal(stripAnsi(text)).split("\n")) {
|
|
1446
|
-
|
|
1447
|
-
|
|
1458
|
+
const [first = "", ...more] = foldLine(raw, textW);
|
|
1459
|
+
rows.push(`${bodyRow()}${open}${first}${close}`);
|
|
1460
|
+
starts.push(true);
|
|
1461
|
+
if (more.length === 0)
|
|
1462
|
+
continue;
|
|
1463
|
+
for (const row of foldLine(more.join(""), textW - hang)) {
|
|
1464
|
+
rows.push(`${bodyRow()}${" ".repeat(hang)}${open}${row}${close}`);
|
|
1465
|
+
starts.push(false);
|
|
1466
|
+
}
|
|
1448
1467
|
}
|
|
1449
|
-
while (rows.length > 0 && visibleWidth(rows[rows.length - 1]) === visibleWidth(bodyRow()))
|
|
1468
|
+
while (rows.length > 0 && visibleWidth(rows[rows.length - 1]) === visibleWidth(bodyRow())) {
|
|
1450
1469
|
rows.pop();
|
|
1451
|
-
|
|
1470
|
+
starts.pop();
|
|
1471
|
+
}
|
|
1472
|
+
return { rows, starts };
|
|
1452
1473
|
}
|
|
1453
|
-
/** The
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
//
|
|
1462
|
-
//
|
|
1463
|
-
//
|
|
1464
|
-
//
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1474
|
+
/** The preview's cut note — how much was cut and in which direction.
|
|
1475
|
+
* With `key` (a settled card, collapsed) the key that shows the rest
|
|
1476
|
+
* stands at the same row's right margin: what was cut at the left, how
|
|
1477
|
+
* to see it at the right (the card round, owner 2026-10-05). DECLARED
|
|
1478
|
+
* REVERSAL of §7.4's foot for a collapsed card — the note and the key
|
|
1479
|
+
* were two rows for one fact. The key gives way after the count's word,
|
|
1480
|
+
* the count never. */
|
|
1481
|
+
function cutNote(cut, word, W, tone, key = false, partial = false) {
|
|
1482
|
+
// 0.47.1 (finding 0470-F1, owner 2026-10-07): `cut` counts output
|
|
1483
|
+
// LINES, the unit the note names (it counted rows: a wrapped line hidden
|
|
1484
|
+
// whole was two or more "lines"). `partial`: the line at the cut is
|
|
1485
|
+
// shown only in part — its start above a tail, its rest below a head —
|
|
1486
|
+
// and the note says so rather than counting it.
|
|
1487
|
+
const part = word === "earlier" ? "start" : "rest";
|
|
1488
|
+
const lines = `${cut} ${word} line${cut === 1 ? "" : "s"}`;
|
|
1489
|
+
const n = !partial ? lines : cut === 0 ? `the ${part} of this line` : `${lines} and the ${part} of this one`;
|
|
1490
|
+
const short = !partial ? `\u2026 ${cut}` : cut === 0 ? "\u2026" : `\u2026 ${cut}+`;
|
|
1491
|
+
// the part's clause gives way before the count's word does
|
|
1492
|
+
const tiers = partial && cut > 0 ? [`\u2026 ${n}`, `\u2026 ${lines}`, short] : [`\u2026 ${n}`, short];
|
|
1493
|
+
const room = bodyTextWidth(W);
|
|
1494
|
+
if (key) {
|
|
1495
|
+
for (const [left, k] of [...tiers.map((l) => [l, EXPAND_ROW]), [short, "ctrl+o"]]) {
|
|
1496
|
+
const gap = room - visibleWidth(left) - visibleWidth(k);
|
|
1497
|
+
if (gap >= 2)
|
|
1498
|
+
return noteRow(`${left}${" ".repeat(gap)}${k}`, W, tone);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return noteRow(pickTier(tiers, room), W, tone);
|
|
1502
|
+
}
|
|
1503
|
+
/** What a cut window hides, for its note (0.47.1, finding 0470-F1): the
|
|
1504
|
+
* output lines among the hidden rows, and whether the line at the cut is
|
|
1505
|
+
* shown in part. `starts` marks each row that begins a line; where it is
|
|
1506
|
+
* unknown (empty) every row counts as a line, as before. A tail that opens
|
|
1507
|
+
* on a continuation row shows the end of a line whose start is hidden —
|
|
1508
|
+
* that line is not counted, it is the note's `partial`. */
|
|
1509
|
+
function tailCount(shown, hidden, starts, first) {
|
|
1510
|
+
const partial = starts[first] === false;
|
|
1511
|
+
return { rows: shown, lines: hidden.filter((i) => starts[i] !== false).length - (partial ? 1 : 0), partial };
|
|
1512
|
+
}
|
|
1513
|
+
function headCut(rows, cap, starts) {
|
|
1514
|
+
let lines = 0;
|
|
1515
|
+
for (let i = cap; i < rows.length; i += 1)
|
|
1516
|
+
if (starts[i] !== false)
|
|
1517
|
+
lines += 1;
|
|
1518
|
+
return { rows: rows.slice(0, cap), lines, partial: starts[cap] === false };
|
|
1519
|
+
}
|
|
1520
|
+
/** The card round — a shell's tail never opens on a blank row or in the
|
|
1521
|
+
* middle of a wrapped line, and keeps its height (DC-46: a live window
|
|
1522
|
+
* never shrinks, and the settle moves nothing). When the window's top row
|
|
1523
|
+
* would be either, the window starts at an earlier line's first row and
|
|
1524
|
+
* blank rows inside it give way to make the room, the top-most first: a
|
|
1525
|
+
* blank row carries no fact, and the conclusion is at the bottom. It
|
|
1526
|
+
* looks back at most `TAIL_REACH` rows (a bounded walk, never a scan of
|
|
1527
|
+
* the whole output); when no line start can be reached, the window is
|
|
1528
|
+
* the plain tail. The rows it hides are counted in the note. `starts`
|
|
1529
|
+
* marks each row that begins a line of output. */
|
|
1530
|
+
const TAIL_REACH = 12;
|
|
1531
|
+
/** The runtime's reason for a refusal the person gave no words for
|
|
1532
|
+
* (session.approve(id, false)) — a card says `denied by you` alone then. */
|
|
1533
|
+
const DEFAULT_REFUSAL = "denied by user";
|
|
1534
|
+
function tailWindow(rows, cap, starts) {
|
|
1535
|
+
const n = rows.length;
|
|
1536
|
+
if (n <= cap)
|
|
1537
|
+
return { rows: [...rows], lines: 0, partial: false };
|
|
1538
|
+
const blank = (i) => visibleWidth(rows[i]) <= visibleWidth(bodyRow());
|
|
1539
|
+
let blanks = 0;
|
|
1540
|
+
for (let i = n - cap; i < n; i += 1)
|
|
1541
|
+
if (blank(i))
|
|
1542
|
+
blanks += 1;
|
|
1543
|
+
// s stops at 1: the window reaches only while a row above it stays
|
|
1544
|
+
// cut, so the note never counts nothing but the blanks it skipped
|
|
1545
|
+
for (let s = n - cap; s >= Math.max(1, n - cap - TAIL_REACH); s -= 1) {
|
|
1546
|
+
if (s < n - cap && blank(s))
|
|
1547
|
+
blanks += 1;
|
|
1548
|
+
const extra = n - s - cap;
|
|
1549
|
+
if (blank(s) || starts[s] === false || blanks < extra)
|
|
1550
|
+
continue;
|
|
1551
|
+
const out = [];
|
|
1552
|
+
const hidden = Array.from({ length: s }, (_, i) => i);
|
|
1553
|
+
let dropped = 0;
|
|
1554
|
+
for (let i = s; i < n; i += 1) {
|
|
1555
|
+
if (dropped < extra && blank(i)) {
|
|
1556
|
+
dropped += 1;
|
|
1557
|
+
hidden.push(i);
|
|
1558
|
+
}
|
|
1559
|
+
else
|
|
1560
|
+
out.push(rows[i]);
|
|
1561
|
+
}
|
|
1562
|
+
return tailCount(out, hidden, starts, s);
|
|
1563
|
+
}
|
|
1564
|
+
return tailCount(rows.slice(n - cap), Array.from({ length: n - cap }, (_, i) => i), starts, n - cap);
|
|
1565
|
+
}
|
|
1566
|
+
/** The error text, uncapped: the answer is at the start, and the whole
|
|
1567
|
+
* of it is the body — the head row carries only the outcome word
|
|
1568
|
+
* (W4, W19: never hide information). */
|
|
1569
|
+
function errorRows(c, W, tone) {
|
|
1570
|
+
return blockRows(c.resultText, W, tone);
|
|
1517
1571
|
}
|
|
1518
1572
|
/**
|
|
1519
1573
|
* DC-46 — THE RUNNING WINDOW GROWS, and nothing pads it.
|
|
@@ -1544,7 +1598,7 @@ function liveWindow(c, W, cap, tone) {
|
|
|
1544
1598
|
// TUI2-R1.5 ④(b) (VD-4): leading empty lines in the sidecar (a
|
|
1545
1599
|
// 4096-byte tail can begin on a line boundary) are skipped, so the
|
|
1546
1600
|
// output starts under its own header.
|
|
1547
|
-
const all =
|
|
1601
|
+
const { rows: all, starts } = blockLines(c.resultText, W, tone);
|
|
1548
1602
|
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(bodyRow()));
|
|
1549
1603
|
// DC-46, derived — NOTHING YET IS NO WINDOW AT ALL, so a running call
|
|
1550
1604
|
// with no output is the same THREE-ROW card as a settled one with
|
|
@@ -1562,9 +1616,12 @@ function liveWindow(c, W, cap, tone) {
|
|
|
1562
1616
|
const rows = all.slice(from);
|
|
1563
1617
|
if (rows.length <= cap)
|
|
1564
1618
|
return rows;
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1619
|
+
if (c.name === "shell") {
|
|
1620
|
+
const w = tailWindow(rows, cap, starts.slice(from));
|
|
1621
|
+
return [...cutNote(w.lines, "earlier", W, tone, false, w.partial), ...w.rows];
|
|
1622
|
+
}
|
|
1623
|
+
const h = headCut(rows, cap, starts.slice(from));
|
|
1624
|
+
return [...h.rows, ...cutNote(h.lines, "more", W, tone, false, h.partial)];
|
|
1568
1625
|
}
|
|
1569
1626
|
/** DC-46 — the window's HIGH-WATER, per cell: the room a frame leaves
|
|
1570
1627
|
* caps how far a window may GROW, and never shrinks one that already
|
|
@@ -1600,24 +1657,31 @@ function delegateRunning(c, W) {
|
|
|
1600
1657
|
const p = palette();
|
|
1601
1658
|
const n = c.childRoles.length;
|
|
1602
1659
|
const text = n === 0 ? "children running…" : `${n === 1 ? "1 child" : `${n} children`} · ${c.childRoles.join(" · ")}`;
|
|
1603
|
-
return [
|
|
1660
|
+
return [bodyLineRow(p, text, W)];
|
|
1604
1661
|
}
|
|
1605
1662
|
function delegateSettled(c, W) {
|
|
1606
1663
|
const p = palette();
|
|
1607
1664
|
const m = /^summary: (.+)$/m.exec(c.resultText);
|
|
1608
1665
|
if (m === null)
|
|
1609
1666
|
return [];
|
|
1610
|
-
return [
|
|
1667
|
+
return [bodyLineRow(p, `${m[1]} · /last for the report`, W)];
|
|
1611
1668
|
}
|
|
1612
1669
|
/** ONE row at the left gutter, truncated to fit the width — never a
|
|
1613
1670
|
* fold (a fold would wrap into TWO rows and break the one-line height
|
|
1614
1671
|
* contract). */
|
|
1615
1672
|
function oneLineRow(p, text, W) {
|
|
1616
1673
|
const esc = escapeTerminal(text);
|
|
1617
|
-
if (visibleWidth(`${p.dim}${
|
|
1618
|
-
return `${p.dim}${
|
|
1619
|
-
const w = Math.max(1, W -
|
|
1620
|
-
return `${p.dim}${
|
|
1674
|
+
if (visibleWidth(`${p.dim}\u2514 ${esc}${p.reset}`) <= W)
|
|
1675
|
+
return `${p.dim}\u2514 ${esc}${p.reset}`;
|
|
1676
|
+
const w = Math.max(1, W - 2);
|
|
1677
|
+
return `${p.dim}\u2514 ${esc.slice(0, w - 1)}\u2026${p.reset}`;
|
|
1678
|
+
}
|
|
1679
|
+
/** Graphite §7.4 — a card body's ONE row (the delegate's): at the body
|
|
1680
|
+
* column like every body row, dim, cut to its room. */
|
|
1681
|
+
function bodyLineRow(p, text, W) {
|
|
1682
|
+
const esc = escapeTerminal(text);
|
|
1683
|
+
const room = bodyTextWidth(W);
|
|
1684
|
+
return `${bodyRow()}${p.dim}${visibleWidth(esc) <= room ? esc : `${widthCut(esc, Math.max(1, room - 1))}\u2026`}${p.reset}`;
|
|
1621
1685
|
}
|
|
1622
1686
|
/** The approval mini-diff (W7): capped at 12 folded rows — the head +
|
|
1623
1687
|
* the named middle (the renderer cut — what was cut, how to expand) +
|
|
@@ -1676,6 +1740,102 @@ export function diffBody(diff, W, expanded = false) {
|
|
|
1676
1740
|
return [...rows.slice(0, CAP_DIFF - 1), cut(rows.length - (CAP_DIFF - 1))];
|
|
1677
1741
|
return [...rows.slice(0, head), cut(rows.length - head - tail), ...rows.slice(rows.length - tail)];
|
|
1678
1742
|
}
|
|
1743
|
+
/** Graphite §6 (R2a) — the rows a card shows for a diff: a `-` / `+` line
|
|
1744
|
+
* on its tint (a background, §1.5) from the content edge to the card's
|
|
1745
|
+
* inner margin, the marker in its colour, the changed words on a deeper
|
|
1746
|
+
* tint; a kept line `dim`; kiso's own notes (`···` between hunks, a miss)
|
|
1747
|
+
* `dim` in the marker column. A long line folds under its text with the
|
|
1748
|
+
* tint on every row. Off a painted card the lines keep the flat form:
|
|
1749
|
+
* marker and text in the line's colour, the changed words underlined. */
|
|
1750
|
+
function diffRows(lines, W) {
|
|
1751
|
+
const p = palette();
|
|
1752
|
+
const painted = slabPaints();
|
|
1753
|
+
const room = bodyRowRoom(W) - bodyRow().length;
|
|
1754
|
+
const textW = Math.max(1, bodyTextWidth(W) - 2);
|
|
1755
|
+
const out = [];
|
|
1756
|
+
for (const d of lines) {
|
|
1757
|
+
const text = escapeTerminal(d.text);
|
|
1758
|
+
if (d.kind === "note") {
|
|
1759
|
+
out.push(...noteRow(text, W, "dim"));
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (d.kind === " ") {
|
|
1763
|
+
for (const r of foldLine(text, textW))
|
|
1764
|
+
out.push(`${bodyRow()}${p.dim} ${r}${p.reset}`);
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
const del = d.kind === "-";
|
|
1768
|
+
const rows = foldLine(text, textW);
|
|
1769
|
+
// the marks index the line's own text: they hold only where the
|
|
1770
|
+
// escape left it as it was and the fold cut it without loss
|
|
1771
|
+
const marks = text === d.text && rows.join("") === text ? (d.marks ?? []) : [];
|
|
1772
|
+
const base = del ? p.del : p.add;
|
|
1773
|
+
const [on, off] = painted ? [del ? p.delWord : p.addWord, base] : [p.underline, p.underlineEnd];
|
|
1774
|
+
let at = 0;
|
|
1775
|
+
for (const [i, r] of rows.entries()) {
|
|
1776
|
+
let body = "";
|
|
1777
|
+
let k = 0;
|
|
1778
|
+
for (const [s0, e0] of marks) {
|
|
1779
|
+
const s1 = Math.max(s0, at) - at;
|
|
1780
|
+
const e1 = Math.min(e0, at + r.length) - at;
|
|
1781
|
+
if (e1 <= s1)
|
|
1782
|
+
continue;
|
|
1783
|
+
body += `${r.slice(k, s1)}${on}${r.slice(s1, e1)}${off}`;
|
|
1784
|
+
k = e1;
|
|
1785
|
+
}
|
|
1786
|
+
body += r.slice(k);
|
|
1787
|
+
at += r.length;
|
|
1788
|
+
const lead = i === 0 ? d.kind : " ";
|
|
1789
|
+
if (painted) {
|
|
1790
|
+
const pad = " ".repeat(Math.max(0, room - 2 - visibleWidth(r)));
|
|
1791
|
+
out.push(`${bodyRow()}${base}${del ? p.fail : p.ok}${lead}${p.fgEnd} ${body}${pad}`);
|
|
1792
|
+
}
|
|
1793
|
+
else {
|
|
1794
|
+
out.push(`${bodyRow()}${del ? p.red : p.green}${lead} ${body}${p.reset}`);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return out;
|
|
1799
|
+
}
|
|
1800
|
+
/** How many of a write's lines its card shows before the key. */
|
|
1801
|
+
const WRITE_HEAD = 5;
|
|
1802
|
+
const editDiffMemo = new WeakMap();
|
|
1803
|
+
function editDiffOf(c) {
|
|
1804
|
+
if (c.name !== "edit_file" && c.name !== "write_file")
|
|
1805
|
+
return null;
|
|
1806
|
+
const known = editDiffMemo.get(c);
|
|
1807
|
+
if (known !== undefined)
|
|
1808
|
+
return known;
|
|
1809
|
+
let input = null;
|
|
1810
|
+
try {
|
|
1811
|
+
const v = JSON.parse(c.inputFull);
|
|
1812
|
+
input = typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
|
|
1813
|
+
}
|
|
1814
|
+
catch {
|
|
1815
|
+
input = null;
|
|
1816
|
+
}
|
|
1817
|
+
let out = null;
|
|
1818
|
+
if (input !== null && c.name === "edit_file") {
|
|
1819
|
+
const hunks = hunksOf(input);
|
|
1820
|
+
const d = hunks === null ? null : hunksDiff(hunks);
|
|
1821
|
+
if (hunks !== null && d !== null && d.lines.length > 0) {
|
|
1822
|
+
const counts = `+${d.added} -${d.removed}`;
|
|
1823
|
+
out = { lines: d.lines, meta: hunks.length > 1 ? `${counts} \u00b7 ${hunks.length} hunks` : counts, cap: CAP_DIFF };
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
else if (input !== null && typeof input.content === "string") {
|
|
1827
|
+
const content = input.content.replace(/\n$/, "");
|
|
1828
|
+
const all = content === "" ? [] : content.split("\n");
|
|
1829
|
+
// a write that cites "absent" creates the file; any other rewrites
|
|
1830
|
+
// one whose old text is not in the log, so its lines are shown as
|
|
1831
|
+
// they now read, not as additions
|
|
1832
|
+
const created = input.expectedRevision === "absent";
|
|
1833
|
+
const n = `${all.length} line${all.length === 1 ? "" : "s"}`;
|
|
1834
|
+
out = { lines: all.map((text) => ({ kind: created ? "+" : " ", text })), meta: created ? `new file \u00b7 ${n}` : `rewrote \u00b7 ${n}`, cap: WRITE_HEAD };
|
|
1835
|
+
}
|
|
1836
|
+
editDiffMemo.set(c, out);
|
|
1837
|
+
return out;
|
|
1838
|
+
}
|
|
1679
1839
|
/** The TOOL's OWN truncation note (W10) — a different fact from the
|
|
1680
1840
|
* renderer's cut: the tools truncate and append a continuation note
|
|
1681
1841
|
* (packages/tools-node/src/index.ts — read_file's "call again with
|
|
@@ -1722,18 +1882,133 @@ class MarkdownBlock {
|
|
|
1722
1882
|
// holds by construction. A block's own leading blank (its `gap`)
|
|
1723
1883
|
// stays EMPTY: an indented blank row is trailing whitespace, and
|
|
1724
1884
|
// §1.3 forbids a mark on a row with nothing to mark.
|
|
1725
|
-
|
|
1885
|
+
// Graphite §1.8: at the content edge, 92 columns at most (§7.15).
|
|
1886
|
+
const rows = renderBlock(this.cell.block, proseRoom(W)).map((r) => (r === "" ? r : `${PROSE_COL}${r}`));
|
|
1887
|
+
// Graphite §5 (R2b): a `##` heading carries `§` in the mark column
|
|
1888
|
+
// (`rail`), outside the block, the way the prototype hangs it
|
|
1889
|
+
const p = palette();
|
|
1890
|
+
if (headingLevel(this.cell.block) === 2 && p.rail !== "") {
|
|
1891
|
+
const at = rows.findIndex((r) => r !== "");
|
|
1892
|
+
if (at >= 0)
|
|
1893
|
+
rows[at] = `${p.rail}\u00a7${p.fgEnd} ${rows[at].slice(PROSE_COL.length)}`;
|
|
1894
|
+
}
|
|
1895
|
+
return rows;
|
|
1726
1896
|
}
|
|
1727
1897
|
}
|
|
1728
|
-
/**
|
|
1898
|
+
/** Graphite §7.12 — the label column of a meta row: the longest label
|
|
1899
|
+
* (`INTERRUPTED`) and one space, so the sentences line up with a card's
|
|
1900
|
+
* body (column 16). */
|
|
1901
|
+
const META_LABEL = 12;
|
|
1902
|
+
/** The labels that name an outcome, and so take its colour (§1.2). */
|
|
1903
|
+
const META_FAIL = new Set(["FAILED", "UNCERTAIN"]);
|
|
1904
|
+
/**
|
|
1905
|
+
* Graphite §7.12 — kiso's own sentences are META ROWS: a bold label at
|
|
1906
|
+
* the content edge and the sentence beside it, folded by word under
|
|
1907
|
+
* itself (VD-10: a notice is a sentence addressed to a human). No card
|
|
1908
|
+
* and no ground — they are not the machine's work. A notice with no
|
|
1909
|
+
* kind of its own (a command's confirmation) has no label and stays
|
|
1910
|
+
* whole at the content edge.
|
|
1911
|
+
*/
|
|
1729
1912
|
class ErrorLine {
|
|
1730
1913
|
cell;
|
|
1731
1914
|
constructor(cell) {
|
|
1732
1915
|
this.cell = cell;
|
|
1733
1916
|
}
|
|
1734
1917
|
render(W, _ctx) {
|
|
1735
|
-
|
|
1736
|
-
|
|
1918
|
+
const p = palette();
|
|
1919
|
+
const c = this.cell;
|
|
1920
|
+
// a sentence with no kind of its own stays whole at the content edge
|
|
1921
|
+
if (c.label === undefined)
|
|
1922
|
+
return foldWords(escapeTerminal(c.sentence ?? c.text), Math.max(1, W - EDGE.length)).map((r) => `${EDGE}${r}`);
|
|
1923
|
+
const sentence = escapeTerminal(c.sentence ?? "");
|
|
1924
|
+
const room = Math.max(1, W - EDGE.length - META_LABEL);
|
|
1925
|
+
const tone = META_FAIL.has(c.label) ? p.red : p.dim;
|
|
1926
|
+
const head = `${EDGE}${p.bold}${tone}${c.label.padEnd(META_LABEL - 1)}${p.reset} `;
|
|
1927
|
+
// a label wider than the row's room is cut like any row (invariant ①)
|
|
1928
|
+
if (sentence === "")
|
|
1929
|
+
return [cutLine(head.trimEnd(), W)];
|
|
1930
|
+
const folded = foldWords(sentence, room);
|
|
1931
|
+
// Graphite R3e (the MODE row): one word of the sentence carries the
|
|
1932
|
+
// weight — the mode switched to, bold, bypass in the failure colour
|
|
1933
|
+
const m = c.mark;
|
|
1934
|
+
const lit = (r) => {
|
|
1935
|
+
if (m === undefined || !r.includes(m.text))
|
|
1936
|
+
return `${p.dim}${r}${p.reset}`;
|
|
1937
|
+
const tone = m.tone === "fail" ? p.red : m.tone === "blue" ? p.blue : m.tone === "ok" ? p.green : m.tone === "gold" ? p.gold : "";
|
|
1938
|
+
// the word after the arrow (the mode switched TO), not an earlier one
|
|
1939
|
+
const arrow = r.indexOf(`\u2192 ${m.text}`);
|
|
1940
|
+
const at = arrow >= 0 ? arrow + 2 : r.indexOf(m.text);
|
|
1941
|
+
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}`;
|
|
1942
|
+
};
|
|
1943
|
+
// Graphite R3e (owner, 2026-09-29): the MODE switch is the person's own
|
|
1944
|
+
// choice, so its label is GOLD — the colour of what the person says —
|
|
1945
|
+
// and its short `from → to` follows two spaces after it on the same
|
|
1946
|
+
// row, not the meta rows' twelve-column label column
|
|
1947
|
+
if (c.stacked === true) {
|
|
1948
|
+
const label = `${EDGE}${p.bold}${p.gold}${c.label}${p.reset} `;
|
|
1949
|
+
const lead = visibleWidth(label);
|
|
1950
|
+
return foldWords(sentence, Math.max(1, W - lead)).map((r, i) => cutLine(i === 0 ? `${label}${lit(r)}` : `${" ".repeat(lead)}${lit(r)}`, W));
|
|
1951
|
+
}
|
|
1952
|
+
// the tasks round: a row that names a thing (a task, a project) is ONE
|
|
1953
|
+
// row — what ran is cut with an ellipsis, never folded under itself
|
|
1954
|
+
if (c.oneRow === true) {
|
|
1955
|
+
const rows = [cutLine(`${head}${lit(sentence)}`, W)];
|
|
1956
|
+
for (const r of c.also ?? [])
|
|
1957
|
+
rows.push(...new ErrorLine({ text: c.text, label: r.label, sentence: r.sentence, ...(r.mark !== undefined ? { mark: r.mark } : {}), oneRow: true }).render(W, _ctx));
|
|
1958
|
+
return rows;
|
|
1959
|
+
}
|
|
1960
|
+
return folded.map((r, i) => (i === 0 ? cutLine(`${head}${lit(r)}`, W) : `${EDGE}${" ".repeat(META_LABEL)}${p.dim}${r}${p.reset}`));
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
/** The last sweep (owner, 2026-10-06) — `/think` and `/last` on the
|
|
1964
|
+
* terminal. They printed at column 0 under `--- … ---` rules, a blank row
|
|
1965
|
+
* between every part; now the row says what came back (`THINKING the last
|
|
1966
|
+
* block · 3 lines`, `LAST CALL LIST (root) · 2 lines`) and each part
|
|
1967
|
+
* follows in its own look: thinking as the thinking block draws it, a
|
|
1968
|
+
* call's input and output under dim titles, in `ink2`, hung by two. One
|
|
1969
|
+
* cell, so no blank row splits it. */
|
|
1970
|
+
class RecallBlock {
|
|
1971
|
+
cell;
|
|
1972
|
+
constructor(cell) {
|
|
1973
|
+
this.cell = cell;
|
|
1974
|
+
}
|
|
1975
|
+
render(W, ctx) {
|
|
1976
|
+
const p = palette();
|
|
1977
|
+
const rows = new ErrorLine({ text: "", label: this.cell.label, sentence: this.cell.sentence, oneRow: true }).render(W, ctx);
|
|
1978
|
+
for (const s of this.cell.sections) {
|
|
1979
|
+
if (s.title !== undefined)
|
|
1980
|
+
rows.push(cutLine(`${EDGE}${p.dim}${s.title}${p.reset}`, W));
|
|
1981
|
+
if (s.style === "thinking") {
|
|
1982
|
+
rows.push(...new ThinkingBlock({ text: s.text, done: true }).render(W, ctx));
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
const room = Math.max(1, W - EDGE.length - HANG);
|
|
1986
|
+
const lines = escapeTerminal(stripAnsi(s.text)).replace(/\n+$/, "").split("\n").flatMap((l) => foldLine(l, room));
|
|
1987
|
+
for (const l of lines)
|
|
1988
|
+
rows.push(`${EDGE}${" ".repeat(HANG)}${p.ink2}${l}${p.fgEnd}`);
|
|
1989
|
+
}
|
|
1990
|
+
return rows;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Graphite §7.11 — THE SEAL: the turn's line — `✦` in gold hanging in
|
|
1995
|
+
* the mark column, the words `dim` at the content edge. Its forms come widest first (the cold
|
|
1996
|
+
* cache sheds its label before anything is cut, R3g); the widest that
|
|
1997
|
+
* fits is drawn, and the last is cut with `…` — one row at every width.
|
|
1998
|
+
*/
|
|
1999
|
+
class SealLine {
|
|
2000
|
+
cell;
|
|
2001
|
+
constructor(cell) {
|
|
2002
|
+
this.cell = cell;
|
|
2003
|
+
}
|
|
2004
|
+
render(W, _ctx) {
|
|
2005
|
+
const p = palette();
|
|
2006
|
+
const mark = p.goldMark === "" ? `${p.bold}\u2726${p.reset}` : `${p.goldMark}\u2726${p.fgEnd}`;
|
|
2007
|
+
// the mark HANGS in the mark column (§1.8), the words at the edge
|
|
2008
|
+
const room = Math.max(1, W - EDGE.length);
|
|
2009
|
+
const text = pickTier(this.cell.tiers, room);
|
|
2010
|
+
const fitted = visibleWidth(text) <= room ? text : `${widthCut(text, Math.max(1, room - 1))}\u2026`;
|
|
2011
|
+
return [cutLine(`${mark} ${p.dim}${fitted}${p.reset}`, W)];
|
|
1737
2012
|
}
|
|
1738
2013
|
}
|
|
1739
2014
|
/** 4c — the folded history's ONE row. Dim, like every row that is about
|
|
@@ -1746,7 +2021,7 @@ class FoldRow {
|
|
|
1746
2021
|
}
|
|
1747
2022
|
render(W, _ctx) {
|
|
1748
2023
|
const p = palette();
|
|
1749
|
-
return [cutLine(
|
|
2024
|
+
return [cutLine(`${EDGE}${p.dim}${escapeTerminal(this.cell.label)}${p.reset}`, W)];
|
|
1750
2025
|
}
|
|
1751
2026
|
}
|
|
1752
2027
|
/** The CLI's pre-rendered blocks (the banner, the recap, slash-command
|
|
@@ -1880,20 +2155,28 @@ export function idleHint(room, expand = null) {
|
|
|
1880
2155
|
}
|
|
1881
2156
|
export function statusLine(status, tail, W, hint, expand = null) {
|
|
1882
2157
|
const p = palette();
|
|
1883
|
-
|
|
2158
|
+
// Graphite §8.7 (R3b): a status that waits for the person (`❯ …`, a
|
|
2159
|
+
// panel's own words) says so first — `❯ needs you · run paused` — the
|
|
2160
|
+
// `❯` gold where there is colour, the words as they were after it. The
|
|
2161
|
+
// window title already says `needs you` (§8.10); now the screen does.
|
|
2162
|
+
const waiting = status.startsWith("\u276f ");
|
|
2163
|
+
const said = waiting ? `\u276f needs you \u00b7 ${status.slice(2)}` : status;
|
|
2164
|
+
const text = `${said}${tail === "" ? "" : ` · ${tail}`}`;
|
|
2165
|
+
// the gold goes on the `❯` only while the cut row still starts with it
|
|
2166
|
+
const lead = (s) => (waiting && p.gold !== "" && s.startsWith("\u276f") ? `${p.gold}\u276f${p.fgEnd}${p.dim}${s.slice(1)}` : `${p.dim}${s}`);
|
|
1884
2167
|
// W18: the hint is a parameter — the compacting row right-aligns its
|
|
1885
2168
|
// "esc to cancel" (the same one-line-bounded shape as W12's delegate
|
|
1886
2169
|
// row; the #16g rule still cuts the HINT first, then the status with
|
|
1887
2170
|
// a "…" — never a fold).
|
|
1888
2171
|
const statusW = visibleWidth(text);
|
|
1889
2172
|
if (statusW > W) {
|
|
1890
|
-
return `${
|
|
2173
|
+
return `${lead(widthCut(text, W - 1))}…${p.reset}`;
|
|
1891
2174
|
}
|
|
1892
2175
|
const hintText = hint ?? idleHint(Math.max(0, W - statusW), expand);
|
|
1893
2176
|
const hintW = visibleWidth(hintText);
|
|
1894
2177
|
if (hintW === 0 || statusW + hintW > W)
|
|
1895
|
-
return `${
|
|
1896
|
-
return `${
|
|
2178
|
+
return `${lead(text)}${p.reset}`;
|
|
2179
|
+
return `${lead(text)}${" ".repeat(Math.max(0, W - statusW - hintW))}${hintText}${p.reset}`;
|
|
1897
2180
|
}
|
|
1898
2181
|
/**
|
|
1899
2182
|
* TUI2-R3v2 ① — THE selection bar. One engine, every selection surface.
|
|
@@ -1924,6 +2207,19 @@ export function statusLine(status, tail, W, hint, expand = null) {
|
|
|
1924
2207
|
*/
|
|
1925
2208
|
export function selectionBar(styled, visible, W) {
|
|
1926
2209
|
const p = palette();
|
|
2210
|
+
// Graphite §8.2 (R3a) — on a known ground the selected row is a card's
|
|
2211
|
+
// head in the colour that waits for the person: its `askEdge` cell in
|
|
2212
|
+
// column 0, the row on `washAsk` to the right edge, and a row's `→`
|
|
2213
|
+
// marker (the approval, question, pick and command lists carry one in
|
|
2214
|
+
// column 1) drawn as a gold `›`. DECLARED REVERSAL of the reverse-video
|
|
2215
|
+
// bar and its `→` there; `dim` stays (it meets the floor on washAsk).
|
|
2216
|
+
// Off a known ground the reverse-video bar below stays — one row, so no
|
|
2217
|
+
// seam.
|
|
2218
|
+
if (slabPaints()) {
|
|
2219
|
+
const marked = styled.replace(/^((?:\x1b\[[0-9;]*m)*)\u2192/, `$1${p.gold}\u203a${p.fgEnd}`);
|
|
2220
|
+
const inner = marked.replaceAll(p.reset, `${p.reset}${p.washAsk}`);
|
|
2221
|
+
return `${p.askEdge} ${p.washAsk}${inner}${" ".repeat(Math.max(0, W - 1 - visible))}${p.washEnd}`;
|
|
2222
|
+
}
|
|
1927
2223
|
// R2 (design §2.1 — nothing dim ever sits on the wash): the bar IS a
|
|
1928
2224
|
// wash. A dim span inside it renders grey-on-grey — 3.91:1 on the
|
|
1929
2225
|
// light ground, under the 4.5 floor — and the dim spans are exactly
|
|
@@ -1955,17 +2251,33 @@ export function selectionBar(styled, visible, W) {
|
|
|
1955
2251
|
* columns the walls were taking.
|
|
1956
2252
|
*/
|
|
1957
2253
|
export function boxTop(W) {
|
|
1958
|
-
//
|
|
1959
|
-
//
|
|
1960
|
-
|
|
2254
|
+
// Graphite §7.8: the one rule that carries colour — gold at its left
|
|
2255
|
+
// end, where the person's edge is, fading to the hairline.
|
|
2256
|
+
return fadeRule(W);
|
|
2257
|
+
}
|
|
2258
|
+
/** Graphite R3e — a read-only sheet over the input (`/status`): the band's
|
|
2259
|
+
* named hairline, one fact per row — its label dim in a column, its value
|
|
2260
|
+
* folded by word under itself — and the row that says how it closes.
|
|
2261
|
+
* It closes like the keys sheet, but what is typed after it is typed. */
|
|
2262
|
+
export function infoSheetRows(title, facts, W) {
|
|
1961
2263
|
const p = palette();
|
|
1962
|
-
|
|
2264
|
+
const labelW = Math.max(0, ...facts.map((f) => f.label.length)) + 2;
|
|
2265
|
+
const room = Math.max(1, W - 2 - labelW);
|
|
2266
|
+
const rows = [bandHeader(title, W)];
|
|
2267
|
+
for (const f of facts) {
|
|
2268
|
+
const folded = foldWords(escapeTerminal(f.value), room);
|
|
2269
|
+
for (const [i, line] of folded.entries())
|
|
2270
|
+
rows.push(` ${i === 0 ? `${p.dim}${f.label.padEnd(labelW)}${p.reset}` : " ".repeat(labelW)}${line}`);
|
|
2271
|
+
}
|
|
2272
|
+
rows.push(` ${p.dim}${SHEET_CLOSE}${p.reset}`);
|
|
2273
|
+
return rows.map((r) => cutLine(r, W));
|
|
1963
2274
|
}
|
|
1964
|
-
/** R2 — the same rule below
|
|
1965
|
-
* the compositor's two call sites did not
|
|
2275
|
+
/** R2 — the same rule below, in the hairline colour (§1.1). Named for its
|
|
2276
|
+
* POSITION, not its shape, so the compositor's two call sites did not
|
|
2277
|
+
* have to move. */
|
|
1966
2278
|
export function boxBottom(W) {
|
|
1967
2279
|
const p = palette();
|
|
1968
|
-
return `${p.dim}${"\u2500".repeat(Math.max(0, W))}${p.reset}`;
|
|
2280
|
+
return `${p.line === "" ? p.dim : p.line}${"\u2500".repeat(Math.max(0, W))}${p.line === "" ? p.reset : p.fgEnd}`;
|
|
1969
2281
|
}
|
|
1970
2282
|
/** The terminal label + rhythm gap (the pipe path's v2c bytes — the
|
|
1971
2283
|
* exact render the passthrough needs). */
|
|
@@ -1974,3 +2286,108 @@ export function terminalPipe(label, statusLineText) {
|
|
|
1974
2286
|
}
|
|
1975
2287
|
/** The pipe-path pieces the passthrough reuses (byte-identical). */
|
|
1976
2288
|
export { foldThinking, foldResult, renderToolSummary, TOOL_SUMMARY_MAX };
|
|
2289
|
+
// ── Graphite §8.2 — what every list band shares ─────────────────────
|
|
2290
|
+
// Moved from the @ picker (P3) so the pick panels, which draw in this
|
|
2291
|
+
// package, read the same window, marks, key row and matcher.
|
|
2292
|
+
/**
|
|
2293
|
+
* The subsequence embedding, TIGHTENED — the two-pass walk every good
|
|
2294
|
+
* fuzzy finder uses, and the reason `@ra` emboldens the "ra" of
|
|
2295
|
+
* "src/range.js" rather than the r of "src" and the a of "range".
|
|
2296
|
+
*
|
|
2297
|
+
* Pass 1 walks forward and stops at the EARLIEST index that completes
|
|
2298
|
+
* the query — this both answers "does it match at all" and fixes the
|
|
2299
|
+
* right-hand edge. Pass 2 walks backward from that edge, taking the
|
|
2300
|
+
* LATEST position for each query character in turn, which slides every
|
|
2301
|
+
* matched character as far right as it can go without crossing the
|
|
2302
|
+
* next one. The result is the most clustered embedding that ends where
|
|
2303
|
+
* the earliest match ends.
|
|
2304
|
+
*
|
|
2305
|
+
* Case-insensitive: both sides are lowercased by the caller once per
|
|
2306
|
+
* query rather than once per character.
|
|
2307
|
+
*
|
|
2308
|
+
* Returns the ascending match indices, or null when the query is not a
|
|
2309
|
+
* subsequence of the path at all.
|
|
2310
|
+
*/
|
|
2311
|
+
export function atEmbed(lowerPath, lowerQuery) {
|
|
2312
|
+
if (lowerQuery === "")
|
|
2313
|
+
return [];
|
|
2314
|
+
// pass 1 — forward, to the earliest completing index
|
|
2315
|
+
let qi = 0;
|
|
2316
|
+
let end = -1;
|
|
2317
|
+
for (let pi = 0; pi < lowerPath.length; pi += 1) {
|
|
2318
|
+
if (lowerPath[pi] === lowerQuery[qi]) {
|
|
2319
|
+
qi += 1;
|
|
2320
|
+
if (qi === lowerQuery.length) {
|
|
2321
|
+
end = pi;
|
|
2322
|
+
break;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
if (end === -1)
|
|
2327
|
+
return null; // not a subsequence — no embedding exists
|
|
2328
|
+
// pass 2 — backward from that edge, sliding each character right
|
|
2329
|
+
const hit = new Array(lowerQuery.length);
|
|
2330
|
+
let pi = end;
|
|
2331
|
+
for (let j = lowerQuery.length - 1; j >= 0; j -= 1) {
|
|
2332
|
+
while (lowerPath[pi] !== lowerQuery[j])
|
|
2333
|
+
pi -= 1;
|
|
2334
|
+
hit[j] = pi;
|
|
2335
|
+
pi -= 1;
|
|
2336
|
+
}
|
|
2337
|
+
return hit;
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Graphite P2 (owner, 2026-10-03) — the band's window, shared by the file
|
|
2341
|
+
* picker, the command list and the session picker: EIGHT rows on a
|
|
2342
|
+
* terminal 30 rows or taller, five below.
|
|
2343
|
+
*/
|
|
2344
|
+
export function bandVisible(height) {
|
|
2345
|
+
return height >= 30 ? 8 : 5;
|
|
2346
|
+
}
|
|
2347
|
+
/** The window over a list, with a scroll-off of one: while more lies past
|
|
2348
|
+
* an edge the cursor stays a row inside it, so the edge row that carries
|
|
2349
|
+
* a more-mark is never the selected one. Stateless, like bandWindow. */
|
|
2350
|
+
export function bandWindow(total, selected, visible) {
|
|
2351
|
+
const count = Math.min(total, visible);
|
|
2352
|
+
return { first: Math.max(0, Math.min(selected - count + 2, total - count)), count };
|
|
2353
|
+
}
|
|
2354
|
+
/** The more-mark for row `i` of a window: `↑` on its first row when the
|
|
2355
|
+
* list goes on above, `↓` on its last when it goes on below. */
|
|
2356
|
+
export function moreMark(i, first, count, total) {
|
|
2357
|
+
return i === first && first > 0 ? "↑" : i === first + count - 1 && first + count < total ? "↓" : null;
|
|
2358
|
+
}
|
|
2359
|
+
/** The key row that closes a band: what the keys do, then the selection's
|
|
2360
|
+
* place in the list at the right edge. The keys give way from the least
|
|
2361
|
+
* needed (the first in `keys`); the counter stays. */
|
|
2362
|
+
export function bandKeyRow(keys, selected, total, W) {
|
|
2363
|
+
const p = palette();
|
|
2364
|
+
const count = total === 0 ? "0/0" : `${selected + 1}/${total}`;
|
|
2365
|
+
let kept = [...keys];
|
|
2366
|
+
while (kept.length > 0 && 2 + visibleWidth(kept.join(" · ")) + 2 + count.length + 1 > W)
|
|
2367
|
+
kept = kept.slice(1);
|
|
2368
|
+
const text = kept.join(" · ");
|
|
2369
|
+
const gap = Math.max(1, W - 1 - 2 - visibleWidth(text) - count.length);
|
|
2370
|
+
return `${p.dim}${widthCut(` ${text}${" ".repeat(gap)}${count}`, Math.max(0, W))}${p.reset}`;
|
|
2371
|
+
}
|
|
2372
|
+
/** Text with the given code-unit positions in bold gold — what the person
|
|
2373
|
+
* typed, in the colour of what the person says — and the rest in `base`. */
|
|
2374
|
+
export function goldHits(text, hits, base) {
|
|
2375
|
+
const p = palette();
|
|
2376
|
+
// off a known ground there is no gold: the warn tint carries it (as
|
|
2377
|
+
// /resume's title does), so a selected row — bold throughout — still
|
|
2378
|
+
// shows which letters matched
|
|
2379
|
+
const gold = p.gold !== "" ? p.gold : p.warn;
|
|
2380
|
+
// one span per RUN of hits, not per letter: a typed prefix is a run
|
|
2381
|
+
let out = "";
|
|
2382
|
+
let inHit = null;
|
|
2383
|
+
let i = 0;
|
|
2384
|
+
for (const ch of text) {
|
|
2385
|
+
const hit = hits.has(i);
|
|
2386
|
+
if (hit !== inHit)
|
|
2387
|
+
out += hit ? `${p.reset}${p.bold}${gold}` : `${inHit === null ? "" : p.reset}${base}`;
|
|
2388
|
+
inHit = hit;
|
|
2389
|
+
out += ch;
|
|
2390
|
+
i += ch.length;
|
|
2391
|
+
}
|
|
2392
|
+
return `${out}${p.reset}`;
|
|
2393
|
+
}
|