@vincemakes/kiso-tui-cells 0.19.1 → 0.20.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/components.d.ts +31 -0
- package/dist/components.js +224 -43
- package/dist/render.d.ts +17 -0
- package/dist/render.js +2 -2
- package/dist/strings.d.ts +7 -0
- package/dist/strings.js +30 -2
- package/package.json +1 -1
package/dist/components.d.ts
CHANGED
|
@@ -32,6 +32,18 @@ export interface FrameCtx {
|
|
|
32
32
|
* the tier table reads H, so a resize RE-TIERS instead of
|
|
33
33
|
* re-folding frozen rows). */
|
|
34
34
|
readonly height: number;
|
|
35
|
+
/** R7a — this cell is drawn UNDER an activity header that carries the
|
|
36
|
+
* breathing mark, so its own head row wears a plain gutter.
|
|
37
|
+
*
|
|
38
|
+
* The mark belongs to the ACTIVITY, not to each call in it. A
|
|
39
|
+
* four-file burst drew four breathing marks, which is four marks
|
|
40
|
+
* distinguishing nothing (law 1.3, the same ground R2 retired the
|
|
41
|
+
* tick and cross on) — and worse, on a read that finishes in
|
|
42
|
+
* 200ms the mark is gone before the eye lands, so per-row it is
|
|
43
|
+
* motion that never resolves into meaning. On the header it is lit
|
|
44
|
+
* for the whole stretch, which is the fact it is there to carry:
|
|
45
|
+
* work is in flight. Owner-ruled 2026-08-31. */
|
|
46
|
+
readonly grouped?: boolean;
|
|
35
47
|
}
|
|
36
48
|
/** ONE screen line a component emits (raw, SGR included). */
|
|
37
49
|
export type RenderLine = string;
|
|
@@ -392,7 +404,11 @@ export interface StretchTerms {
|
|
|
392
404
|
*/
|
|
393
405
|
export declare function stretchLine(t: StretchTerms & {
|
|
394
406
|
readonly phase: "thinking" | "acting" | "settled";
|
|
407
|
+
readonly mark?: string;
|
|
395
408
|
}, W: number): string[];
|
|
409
|
+
/** R6/D3: the quiet turn's fold wears no mark either — the SECOND
|
|
410
|
+
* emission site, and the one the D3 brief did not name. Same ruling,
|
|
411
|
+
* same two-space indent; see stretchLine above for the argument. */
|
|
396
412
|
export declare function turnFold(t: {
|
|
397
413
|
words: string;
|
|
398
414
|
thoughtSeconds: number;
|
|
@@ -492,6 +508,21 @@ export declare function formatDuration(totalSeconds: number): string;
|
|
|
492
508
|
* W21: the question param is gone — the old question slot retires; a
|
|
493
509
|
* pending approval's status IS the panel's (the compositor derives
|
|
494
510
|
* it from the bound panel state). */
|
|
511
|
+
/** R8b — THE IDLE HINT GIVES WAY IN ORDER, and `ctrl+o` is on it.
|
|
512
|
+
*
|
|
513
|
+
* The transcript viewer shipped in 0.19.0 and was reachable only from
|
|
514
|
+
* the `?` sheet: not on the banner's key line, not here. A feature
|
|
515
|
+
* whose only advertisement is a screen you have to already know to
|
|
516
|
+
* open is DC-30's lesson pointing the other way.
|
|
517
|
+
*
|
|
518
|
+
* It cannot simply be appended, because this hint is dropped WHOLE
|
|
519
|
+
* when it does not fit — a longer string would take `/ commands` down
|
|
520
|
+
* with it on a narrow terminal. So the forms are a ladder, and the
|
|
521
|
+
* order says which affordance is least replaceable: `/ commands`
|
|
522
|
+
* survives longest because it is the door to everything; `ctrl+o`
|
|
523
|
+
* outranks `↑ history` because pressing up is how a person finds the
|
|
524
|
+
* history by accident, and nothing finds ctrl+o by accident. */
|
|
525
|
+
export declare function idleHint(room: number): string;
|
|
495
526
|
export declare function statusLine(status: string, tail: string, W: number, hint?: string): string;
|
|
496
527
|
/** The display-width prefix of a plain (SGR-free) text. W21: exported
|
|
497
528
|
* for the approval panel's option-2 rule-name cut. */
|
package/dist/components.js
CHANGED
|
@@ -149,7 +149,10 @@ export function cellComponent(cell) {
|
|
|
149
149
|
case "user":
|
|
150
150
|
return new UserMessage(cell);
|
|
151
151
|
case "thinking":
|
|
152
|
-
|
|
152
|
+
// R7: the committed/live surface is the BLOCK. ThinkingFold
|
|
153
|
+
// survives for the pipe path (render.ts's foldThinking), whose
|
|
154
|
+
// bytes are asserted by the --plain identity gate.
|
|
155
|
+
return new ThinkingBlock(cell);
|
|
153
156
|
case "tool":
|
|
154
157
|
return new ToolExecution(cell);
|
|
155
158
|
case "md":
|
|
@@ -281,6 +284,57 @@ export function pendingQueueRows(lines, W) {
|
|
|
281
284
|
* CJK — 2 cells per char — and tripped invariant ① on a real
|
|
282
285
|
* Chinese session). W2: the leading ⋯ is the thinking gutter — the
|
|
283
286
|
* midline mark (the state), never the text ellipsis (the truncation). */
|
|
287
|
+
/**
|
|
288
|
+
* R7 — THINKING IS A BLOCK OF WORDS.
|
|
289
|
+
*
|
|
290
|
+
* The owner's ruling, arrived at from a side-by-side with pi's screen:
|
|
291
|
+
* the model's reasoning reads as its own paragraphs — italic, dim,
|
|
292
|
+
* indented two — and is never folded away. `ThinkingFold` (below, kept
|
|
293
|
+
* for the pipe path) summarised it to one row with a key; four rounds
|
|
294
|
+
* of machinery were then built to hand the rest back, and the owner's
|
|
295
|
+
* complaint through all of them was the same sentence: I cannot see
|
|
296
|
+
* what it was thinking.
|
|
297
|
+
*
|
|
298
|
+
* THE INDENT IS NOT DECORATION. Law 1.2 is SETTLED — "strip every
|
|
299
|
+
* escape sequence and no fact is lost" — and italic is SGR 3, which a
|
|
300
|
+
* pipe strips and `COLOR_OFF` empties outright. Stripped, an italic
|
|
301
|
+
* paragraph and the model's ANSWER are the same bytes, and the reader
|
|
302
|
+
* cannot tell the reasoning from the reply. The two-space indent is the
|
|
303
|
+
* byte that survives: reasoning sits in, the answer stands at the
|
|
304
|
+
* margin. The owner chose this over the un-indented form for exactly
|
|
305
|
+
* that reason, with the cost stated.
|
|
306
|
+
*
|
|
307
|
+
* Paragraphs are preserved (a blank line between them); the newlines
|
|
308
|
+
* INSIDE a paragraph collapse, because a hard-wrapped source line is
|
|
309
|
+
* the model's line width, not the reader's.
|
|
310
|
+
*/
|
|
311
|
+
class ThinkingBlock {
|
|
312
|
+
cell;
|
|
313
|
+
constructor(cell) {
|
|
314
|
+
this.cell = cell;
|
|
315
|
+
}
|
|
316
|
+
render(W, _ctx) {
|
|
317
|
+
const p = palette();
|
|
318
|
+
const text = escapeTerminal(this.cell.text).trim();
|
|
319
|
+
if (text === "")
|
|
320
|
+
return [];
|
|
321
|
+
const room = Math.max(1, W - 2);
|
|
322
|
+
const rows = [];
|
|
323
|
+
for (const para of text.split(/\n\s*\n/)) {
|
|
324
|
+
const flat = para.replace(/\s+/g, " ").trim();
|
|
325
|
+
if (flat === "")
|
|
326
|
+
continue;
|
|
327
|
+
if (rows.length > 0)
|
|
328
|
+
rows.push("");
|
|
329
|
+
// foldLine is the ONE width authority and returns real rows —
|
|
330
|
+
// invariant ①b (a row is one physical row) holds by
|
|
331
|
+
// construction rather than by remembering to split.
|
|
332
|
+
for (const line of foldLine(flat, room))
|
|
333
|
+
rows.push(` ${p.dim}${p.italic}${line}${p.italicEnd}${p.reset}`);
|
|
334
|
+
}
|
|
335
|
+
return rows;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
284
338
|
class ThinkingFold {
|
|
285
339
|
cell;
|
|
286
340
|
constructor(cell) {
|
|
@@ -687,7 +741,7 @@ class ToolExecution {
|
|
|
687
741
|
// cannot be predicted: a turning mark implies progress the
|
|
688
742
|
// product does not have. With no ground the breath freezes to a
|
|
689
743
|
// static `●` and says the same thing more quietly.
|
|
690
|
-
const out = gutterCut(`${breathFrame(ctx.spinnerI)} `, `${verbCol} ${liveTarget(c)}`, Math.max(4, W - dur.length));
|
|
744
|
+
const out = gutterCut(ctx.grouped === true ? " " : `${breathFrame(ctx.spinnerI)} `, `${verbCol} ${liveTarget(c)}`, Math.max(4, W - dur.length));
|
|
691
745
|
out[0] = `${out[0]}${p.dim}${dur}${p.reset}`;
|
|
692
746
|
out.push(...toolBlockBody(c, W));
|
|
693
747
|
return out;
|
|
@@ -873,7 +927,7 @@ export function focusToken(row, W) {
|
|
|
873
927
|
// attribute like bold is spent everywhere. It closes with washEnd
|
|
874
928
|
// rather than a reset, so the surrounding dim survives instead of
|
|
875
929
|
// having to be re-applied.
|
|
876
|
-
return `${row.slice(0, at)}${p.
|
|
930
|
+
return `${row.slice(0, at)}${p.lift}${CTRL_R}${p.dim}${row.slice(at + CTRL_R.length)}`;
|
|
877
931
|
}
|
|
878
932
|
// A LIVE row does not carry the affordance today, and the live cell is
|
|
879
933
|
// the one ctrl+r takes FIRST (expandNext scans the live tail before
|
|
@@ -885,7 +939,7 @@ export function focusToken(row, W) {
|
|
|
885
939
|
const room = W - visibleWidth(row);
|
|
886
940
|
if (room < SUFFIX_MIN)
|
|
887
941
|
return row; // never at the cost of invariant ①
|
|
888
|
-
return `${row}${p.dim} · ${p.
|
|
942
|
+
return `${row}${p.dim} · ${p.lift}${CTRL_R}${p.reset}`;
|
|
889
943
|
}
|
|
890
944
|
const CTRL_R = "ctrl+r";
|
|
891
945
|
/** TUI2-R1 (A) — the expanded block's last row: the way back. The
|
|
@@ -964,8 +1018,10 @@ export function exploreRows(parts, W) {
|
|
|
964
1018
|
// outputs". /last shows the LAST call only — for a nine-call burst that
|
|
965
1019
|
// is one output out of nine, and a footer that sends the human to a
|
|
966
1020
|
// place the content is not is worse than a footer that says nothing.
|
|
967
|
-
|
|
968
|
-
|
|
1021
|
+
// R8a: the footer is an in-block note — the same indent, no glyph —
|
|
1022
|
+
// and the corner opens the block's first row, like every other one.
|
|
1023
|
+
rows.push(cutLine(`${p.dim}${NOTE_ROW}${COLLAPSE_ROW}${p.reset}`, W));
|
|
1024
|
+
return openBlock(rows);
|
|
969
1025
|
}
|
|
970
1026
|
/** The count term with the singular/plural forms — "no reads", "1 read",
|
|
971
1027
|
* "5 reads". The noun's singular drops the plural suffix ("dirs" → "dir",
|
|
@@ -1248,7 +1304,40 @@ function troubleClause(t) {
|
|
|
1248
1304
|
export function stretchLine(t, W) {
|
|
1249
1305
|
const p = palette();
|
|
1250
1306
|
const live = t.phase !== "settled";
|
|
1251
|
-
|
|
1307
|
+
// DECLARED SUPERSESSION (R6/D3, owner-ruled) — THE STRETCH LINE WEARS
|
|
1308
|
+
// NO MARK, in any phase.
|
|
1309
|
+
//
|
|
1310
|
+
// Law 1.3: a symbol earns its cell by carrying a fact the words do
|
|
1311
|
+
// not. When every settled fold, the live line AND the status row all
|
|
1312
|
+
// wear a star, none of them distinguishes anything — it is the tick
|
|
1313
|
+
// and the cross again (R2 retired those on exactly this ground), at
|
|
1314
|
+
// the stretch scale. design.md §7.4 had already ruled the principle
|
|
1315
|
+
// one scale down: "only the call still running carries a mark,
|
|
1316
|
+
// because only it is moving", with a settled call's mark "(none) —
|
|
1317
|
+
// the outcome is in the words. SETTLED." A settled STRETCH wearing
|
|
1318
|
+
// one contradicted a precedent the file had already ratified.
|
|
1319
|
+
//
|
|
1320
|
+
// Nothing settled is being reversed: §4 lists this mark PROPOSED and
|
|
1321
|
+
// §8 lists it OPEN. This is that proposal's ruling arriving, as a
|
|
1322
|
+
// decline, on the owner's own dogfood.
|
|
1323
|
+
//
|
|
1324
|
+
// The replacement is a two-space INDENT, not a column shift: the row
|
|
1325
|
+
// joins the settled-call family's geometry, the indent survives a
|
|
1326
|
+
// pipe as bytes (prose never starts at column 3), and both forms are
|
|
1327
|
+
// 2 cells so the width ladder below does not reflow. The status row's
|
|
1328
|
+
// twinkle survives as the ONE moving mark; `✦ took` survives as the
|
|
1329
|
+
// turn's seal.
|
|
1330
|
+
//
|
|
1331
|
+
// R7a AMENDS this by ONE case: the live ACTING line takes the
|
|
1332
|
+
// breathing mark, passed in. D3 declined a mark on the SETTLED fold,
|
|
1333
|
+
// where the words already carry the outcome; a line that means "work
|
|
1334
|
+
// is in flight RIGHT NOW" carries a fact its words do not, which is
|
|
1335
|
+
// exactly the test law 1.3 sets. It is also where the mark was
|
|
1336
|
+
// migrating TO: §7.4's "only the call still running carries one" now
|
|
1337
|
+
// applies at the stretch scale, one mark for the activity instead of
|
|
1338
|
+
// one per call. Owner-ruled 2026-08-31, on the ground that a fast
|
|
1339
|
+
// call's per-row mark is gone before the eye lands.
|
|
1340
|
+
const mark = t.mark ?? " ";
|
|
1252
1341
|
// R4a (owner ruling, 2026-08-30) — the fold row prints NO key.
|
|
1253
1342
|
//
|
|
1254
1343
|
// R4 printed `· ctrl+r 3` so the row could name its own target. The
|
|
@@ -1290,6 +1379,9 @@ export function stretchLine(t, W) {
|
|
|
1290
1379
|
const row = `${mark}${words === "" ? "" : ` ${p.rv}${words}${p.rvEnd}${p.dim} ·${p.reset}`} ${meta}${clause === "" ? "" : `${p.red}${clause}${p.reset}`}${key === "" ? "" : `${p.dim}${key}${p.reset}`}`;
|
|
1291
1380
|
return [visibleWidth(row) <= W ? row : cutLine(row, W)];
|
|
1292
1381
|
}
|
|
1382
|
+
/** R6/D3: the quiet turn's fold wears no mark either — the SECOND
|
|
1383
|
+
* emission site, and the one the D3 brief did not name. Same ruling,
|
|
1384
|
+
* same two-space indent; see stretchLine above for the argument. */
|
|
1293
1385
|
export function turnFold(t, W) {
|
|
1294
1386
|
const p = palette();
|
|
1295
1387
|
// R3b (owner, 2026-08-27): ZERO TERMS ARE DROPPED. W14 always wrote
|
|
@@ -1353,13 +1445,13 @@ export function turnFold(t, W) {
|
|
|
1353
1445
|
const keyW = KEY.length;
|
|
1354
1446
|
const key = `${p.dim}${KEY}${p.reset}`;
|
|
1355
1447
|
if (words === "") {
|
|
1356
|
-
const keyed =
|
|
1448
|
+
const keyed = ` ${meta}${key}`;
|
|
1357
1449
|
if (visibleWidth(keyed) <= W)
|
|
1358
1450
|
return [keyed];
|
|
1359
1451
|
let tightMeta = meta;
|
|
1360
1452
|
for (const [long, short] of COMPACT) {
|
|
1361
1453
|
tightMeta = tightMeta.replaceAll(long, short);
|
|
1362
|
-
const tight =
|
|
1454
|
+
const tight = ` ${tightMeta}${key}`;
|
|
1363
1455
|
if (visibleWidth(tight) <= W)
|
|
1364
1456
|
return [tight];
|
|
1365
1457
|
}
|
|
@@ -1375,22 +1467,22 @@ export function turnFold(t, W) {
|
|
|
1375
1467
|
// closed in the thinking fold, still open here. A width with no
|
|
1376
1468
|
// room for the mark is a width with nothing to say: the row is
|
|
1377
1469
|
// a hard cut of what it would have said.
|
|
1378
|
-
const cut =
|
|
1379
|
-
return [visibleWidth(cut) <= W ? cut : cutLine(
|
|
1470
|
+
const cut = ` ${widthCut(tightMeta, Math.max(1, W - 3))}…`;
|
|
1471
|
+
return [visibleWidth(cut) <= W ? cut : cutLine(` ${tightMeta}`, W)];
|
|
1380
1472
|
}
|
|
1381
|
-
return [
|
|
1473
|
+
return [` ${widthCut(tightMeta, room)}…${key}`];
|
|
1382
1474
|
}
|
|
1383
1475
|
// A9 (ruling R2, mock A): the user chip rides the fold — the human's
|
|
1384
1476
|
// words LEAD the one line, the same SGR-7 bracket as the live user
|
|
1385
1477
|
// row (#16f, side pads included). The words take the fold's width
|
|
1386
|
-
// budget: W − the gutter (
|
|
1478
|
+
// budget: W − the gutter (two spaces = 2, R6/D3) − the join (3) − the
|
|
1387
1479
|
// chip's side pads (2) − the cut-tail reserve (1, the "…") − the
|
|
1388
1480
|
// metadata's own width — the metadata survives, the words width-cut
|
|
1389
1481
|
// at the end with the honest "…" (the "…" alone is the honest floor:
|
|
1390
1482
|
// the words were there, cut).
|
|
1391
|
-
const budget = Math.max(0, W - visibleWidth(
|
|
1483
|
+
const budget = Math.max(0, W - visibleWidth(` ${meta}`) - 6 - keyW);
|
|
1392
1484
|
const cut = visibleWidth(words) > budget ? `${widthCut(words, budget)}…` : words;
|
|
1393
|
-
const row =
|
|
1485
|
+
const row = ` ${p.rv} ${cut} ${p.rvEnd} · ${meta}${key}`;
|
|
1394
1486
|
if (visibleWidth(row) <= W)
|
|
1395
1487
|
return [row];
|
|
1396
1488
|
// R3h: the same COMPACT ladder the wordless branch walks — the nouns
|
|
@@ -1398,7 +1490,7 @@ export function turnFold(t, W) {
|
|
|
1398
1490
|
let chipMeta = meta;
|
|
1399
1491
|
for (const [long, short] of COMPACT) {
|
|
1400
1492
|
chipMeta = chipMeta.replaceAll(long, short);
|
|
1401
|
-
const tighter =
|
|
1493
|
+
const tighter = ` ${p.rv} ${cut} ${p.rvEnd} · ${chipMeta}${key}`;
|
|
1402
1494
|
if (visibleWidth(tighter) <= W)
|
|
1403
1495
|
return [tighter];
|
|
1404
1496
|
}
|
|
@@ -1412,10 +1504,10 @@ export function turnFold(t, W) {
|
|
|
1412
1504
|
// bracket's pads, the join and the "…" are ten cells before a
|
|
1413
1505
|
// single character of content, so every width below ten threw
|
|
1414
1506
|
// invariant ① — see the wordless branch above for the same class.
|
|
1415
|
-
const tail =
|
|
1416
|
-
return [visibleWidth(tail) <= W ? tail : cutLine(
|
|
1507
|
+
const tail = ` ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), Math.max(1, W - 8 - visibleWidth(cut)))}…`;
|
|
1508
|
+
return [visibleWidth(tail) <= W ? tail : cutLine(` ${p.rv} ${cut} ${p.rvEnd}`, W)];
|
|
1417
1509
|
}
|
|
1418
|
-
return [
|
|
1510
|
+
return [` ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(compactAll(meta), room)}…${key}`];
|
|
1419
1511
|
}
|
|
1420
1512
|
// ---- the bounded-block flow contract (W7, W8, W10) ----
|
|
1421
1513
|
/** The caps — screen rows counted AFTER the fold, at the current width
|
|
@@ -1428,8 +1520,43 @@ const CAP_ERROR = 3; // the error text head
|
|
|
1428
1520
|
* block's body, └ the block's last row — what was cut, where the rest
|
|
1429
1521
|
* is — at the LEFT EDGE (the gutter column: the left edge alone
|
|
1430
1522
|
* distinguishes the states at --plain). Structural (constraint 1). */
|
|
1431
|
-
|
|
1523
|
+
/** R8a — A TOOL BLOCK'S ROWS ARE INDENTED, NOT GUTTERED.
|
|
1524
|
+
*
|
|
1525
|
+
* `│ ` on every row drew a bar down the left of every multi-row
|
|
1526
|
+
* output, which is what the owner kept pointing at. The fact the bar
|
|
1527
|
+
* carried — "these rows are the call's output, not prose" — is real
|
|
1528
|
+
* and law 1.2 requires it survive a pipe, so it moves into the
|
|
1529
|
+
* INDENT: four columns, one level deeper than the header (2) and than
|
|
1530
|
+
* prose (2). Bytes still tell them apart; no column of glyphs.
|
|
1531
|
+
*
|
|
1532
|
+
* `└` survives as the mark that OPENS the block, once, on its first
|
|
1533
|
+
* row (see openBlock). In-block notes take NOTE_ROW — the same indent,
|
|
1534
|
+
* no glyph — because a second `└` inside one block would be the same
|
|
1535
|
+
* mark meaning two things (§4.1). CUT_ROW is unchanged for the
|
|
1536
|
+
* surfaces that are not a tool block: the fold row's target list, the
|
|
1537
|
+
* slot's overflow count. */
|
|
1538
|
+
const BODY_ROW = " ";
|
|
1539
|
+
const NOTE_ROW = " ";
|
|
1432
1540
|
const CUT_ROW = "└ ";
|
|
1541
|
+
/** R8a — stamp `└` on a block's FIRST row, after every slice and note
|
|
1542
|
+
* has been assembled, so the mark is always on the first row actually
|
|
1543
|
+
* emitted rather than on one a cap may have dropped. */
|
|
1544
|
+
function openBlock(rows) {
|
|
1545
|
+
// the corner goes on the first row that HAS something on it. A cap
|
|
1546
|
+
// or a blank leading output line can put an empty row first, and a
|
|
1547
|
+
// corner there would be a mark on a row with nothing to mark — law
|
|
1548
|
+
// 1.3, which is the rule this whole change is serving.
|
|
1549
|
+
const i = rows.findIndex((r) => visibleWidth(r) > visibleWidth(BODY_ROW));
|
|
1550
|
+
if (i < 0)
|
|
1551
|
+
return rows;
|
|
1552
|
+
const first = rows[i];
|
|
1553
|
+
const at = first.indexOf(BODY_ROW);
|
|
1554
|
+
if (at < 0)
|
|
1555
|
+
return rows;
|
|
1556
|
+
// the corner REPLACES two of the four indent columns, so the text
|
|
1557
|
+
// stays in the same column as every other row of the block.
|
|
1558
|
+
return [...rows.slice(0, i), `${first.slice(0, at)} \u2514 ${first.slice(at + BODY_ROW.length)}`, ...rows.slice(i + 1)];
|
|
1559
|
+
}
|
|
1433
1560
|
const blockMemo = new WeakMap();
|
|
1434
1561
|
/** The block's body rows below the header (memoized, W9). */
|
|
1435
1562
|
function toolBlockBody(c, W) {
|
|
@@ -1476,15 +1603,16 @@ function toolBlockBody(c, W) {
|
|
|
1476
1603
|
: [];
|
|
1477
1604
|
const note = c.expanded ? null : toolCutNote(c.name, c.resultText);
|
|
1478
1605
|
if (note !== null)
|
|
1479
|
-
rows.push(...foldLine(`${p.dim}${
|
|
1606
|
+
rows.push(...foldLine(`${p.dim}${NOTE_ROW}${note}${p.reset}`, W));
|
|
1480
1607
|
// TUI2-R1 (A): an EXPANDED block says how to put it back. The footer
|
|
1481
1608
|
// rides a block that HAS rows — an expanded delegate whose summary
|
|
1482
1609
|
// marker is missing renders nothing, and a lone footer under a head
|
|
1483
1610
|
// row would be an affordance for an empty block.
|
|
1484
1611
|
if (c.expanded && rows.length > 0)
|
|
1485
|
-
rows.push(...foldLine(`${p.dim}${
|
|
1486
|
-
|
|
1487
|
-
|
|
1612
|
+
rows.push(...foldLine(`${p.dim}${NOTE_ROW}${COLLAPSE_ROW}${p.reset}`, W));
|
|
1613
|
+
const opened = openBlock(rows);
|
|
1614
|
+
blockMemo.set(c, { width: W, state, content, rows: opened });
|
|
1615
|
+
return opened;
|
|
1488
1616
|
}
|
|
1489
1617
|
/** Fold result text into dim body rows (the BODY_ROW prefix): escape,
|
|
1490
1618
|
* split, fold each line at W−prefix; trailing empty rows (the result's
|
|
@@ -1511,7 +1639,7 @@ function shellTail(text, W) {
|
|
|
1511
1639
|
if (rows.length <= CAP_SHELL_SETTLED)
|
|
1512
1640
|
return rows;
|
|
1513
1641
|
const kept = CAP_SHELL_SETTLED - 1;
|
|
1514
|
-
const cut = foldLine(`${p.dim}${
|
|
1642
|
+
const cut = foldLine(`${p.dim}${NOTE_ROW}+${rows.length - kept} earlier rows · ctrl+r${p.reset}`, W);
|
|
1515
1643
|
return [...rows.slice(rows.length - kept), ...cut];
|
|
1516
1644
|
}
|
|
1517
1645
|
/** The error text head: the FIRST rows, capped at 3 — the answer is at
|
|
@@ -1531,7 +1659,7 @@ function errorBody(c, W) {
|
|
|
1531
1659
|
const rows = blockRows(c.resultText.split("\n").slice(skipFirst).join("\n"), W);
|
|
1532
1660
|
if (rows.length <= CAP_ERROR)
|
|
1533
1661
|
return rows;
|
|
1534
|
-
const cut = foldLine(`${p.dim}${
|
|
1662
|
+
const cut = foldLine(`${p.dim}${NOTE_ROW}+${rows.length - (CAP_ERROR - 1)} more · ctrl+r${p.reset}`, W);
|
|
1535
1663
|
return [...rows.slice(0, CAP_ERROR - 1), ...cut];
|
|
1536
1664
|
}
|
|
1537
1665
|
/** The running tool's FIXED-height window (W8): exactly 3 rows from
|
|
@@ -1542,15 +1670,23 @@ function errorBody(c, W) {
|
|
|
1542
1670
|
function liveWindow(text, W) {
|
|
1543
1671
|
const p = palette();
|
|
1544
1672
|
if (text === "") {
|
|
1545
|
-
|
|
1673
|
+
// R7a: blank, not two bare gutters. A `│` marks a row that HAS
|
|
1674
|
+
// content; two of them above "waiting for output" drew a tall
|
|
1675
|
+
// empty bar under every command that had not printed yet — which
|
|
1676
|
+
// is most of them, for their first second.
|
|
1677
|
+
// ...and the waiting row sits DIRECTLY under its header, with the
|
|
1678
|
+
// blanks below it — VD-4's own rule ("the output starts under its
|
|
1679
|
+
// own header and grows downward"), which the gutter rows used to
|
|
1680
|
+
// satisfy by accident and blanks made visible as a two-row gap.
|
|
1681
|
+
return [`${p.dim}${NOTE_ROW}waiting for output${p.reset}`, "", ""];
|
|
1546
1682
|
}
|
|
1547
1683
|
const rows = blockRows(text, W);
|
|
1548
1684
|
if (rows.length <= CAP_LIVE_WINDOW) {
|
|
1549
1685
|
while (rows.length < CAP_LIVE_WINDOW)
|
|
1550
|
-
rows.push(
|
|
1686
|
+
rows.push(""); // R7a: blank, not a bar
|
|
1551
1687
|
return rows;
|
|
1552
1688
|
}
|
|
1553
|
-
const cut = foldLine(`${p.dim}${
|
|
1689
|
+
const cut = foldLine(`${p.dim}${NOTE_ROW}+${rows.length - (CAP_LIVE_WINDOW - 1)} earlier rows · ctrl+r${p.reset}`, W);
|
|
1554
1690
|
return [...rows.slice(rows.length - (CAP_LIVE_WINDOW - 1)), ...cut];
|
|
1555
1691
|
}
|
|
1556
1692
|
/**
|
|
@@ -1593,8 +1729,8 @@ function shellLiveTail(text, W) {
|
|
|
1593
1729
|
return liveWindow("", W);
|
|
1594
1730
|
const kept = rows.slice(Math.max(0, rows.length - (CAP_LIVE_WINDOW - 1)));
|
|
1595
1731
|
while (kept.length < CAP_LIVE_WINDOW - 1)
|
|
1596
|
-
kept.push(
|
|
1597
|
-
return [...kept, cutLine(`${p.dim}${
|
|
1732
|
+
kept.push(""); // R7a: blank, not a bar
|
|
1733
|
+
return [...kept, cutLine(`${p.dim}${NOTE_ROW}live tail · esc stop · alt+⏎ redirect${p.reset}`, W)];
|
|
1598
1734
|
}
|
|
1599
1735
|
/**
|
|
1600
1736
|
* R4 — the standing act slot.
|
|
@@ -1642,10 +1778,11 @@ export function slotTail(text, W, rows) {
|
|
|
1642
1778
|
const all = blockRows(text, W);
|
|
1643
1779
|
const from = all.findIndex((r) => visibleWidth(r) > visibleWidth(BODY_ROW));
|
|
1644
1780
|
const body = from < 0 ? [] : all.slice(from);
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1781
|
+
// R7a: no pad. The slot stopped padding (see slotPad) and this was
|
|
1782
|
+
// the same pad by another route — three blank rows under a call with
|
|
1783
|
+
// nothing to say yet, which is the hole a7's blank-run guard prices.
|
|
1784
|
+
// R8a: the corner opens whatever slice survives the cap.
|
|
1785
|
+
return openBlock(body.slice(Math.max(0, body.length - rows)));
|
|
1649
1786
|
}
|
|
1650
1787
|
/** R4 — clamp or pad assembled slot rows to EXACTLY `rows`. The padding
|
|
1651
1788
|
* is what makes the slot stand; the clamp is what keeps the slot from
|
|
@@ -1654,11 +1791,24 @@ export function slotTail(text, W, rows) {
|
|
|
1654
1791
|
export function slotPad(content, rows) {
|
|
1655
1792
|
if (rows <= 0)
|
|
1656
1793
|
return [];
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1794
|
+
// R7a — THE SLOT NO LONGER PADS. It caps, and that is all.
|
|
1795
|
+
//
|
|
1796
|
+
// R4 padded to a fixed height because the slot's content came and
|
|
1797
|
+
// went: a finished call left the block, the block shrank, and every
|
|
1798
|
+
// row above it moved. The pad bought stability with rows drawn as
|
|
1799
|
+
// `│`, which is why a tall empty gutter ran down the screen under
|
|
1800
|
+
// every short block — the owner's own screenshot, and law 1.3's
|
|
1801
|
+
// case: a mark on a row with nothing to mark.
|
|
1802
|
+
//
|
|
1803
|
+
// Blanking the gutter revealed the hole it had been covering, and
|
|
1804
|
+
// the a7 replay priced the hole: blank runs over 2 in 653 of 733
|
|
1805
|
+
// frames, the screen never durably filling. So the pad had to go —
|
|
1806
|
+
// and the height it was buying is now bought by the CONTENT, since
|
|
1807
|
+
// R7a keeps every call's row for the life of the stretch. A block
|
|
1808
|
+
// whose rows only accumulate cannot shrink, so there is nothing
|
|
1809
|
+
// left for a pad to hold up. Measured: 65 of 733 at 40x24, the
|
|
1810
|
+
// pre-R7a number exactly, with the motion gates still green.
|
|
1811
|
+
return content.slice(0, rows);
|
|
1662
1812
|
}
|
|
1663
1813
|
/** R4 — the slot's overflow row: the calls in flight beyond the head
|
|
1664
1814
|
* budget. It lives INSIDE the slot (it is one of the four rows), which
|
|
@@ -1889,7 +2039,11 @@ export function cutLine(line, W) {
|
|
|
1889
2039
|
width += cw;
|
|
1890
2040
|
i += 1;
|
|
1891
2041
|
}
|
|
1892
|
-
|
|
2042
|
+
// R8a: the reset comes from the PALETTE, not hardcoded. `\x1b[0m`
|
|
2043
|
+
// here put an escape into every cut row under NO_COLOR and behind a
|
|
2044
|
+
// pipe — the one context COLOR_OFF exists to keep clean (§1.2). A
|
|
2045
|
+
// coloured palette is byte-identical, because its reset IS `\x1b[0m`.
|
|
2046
|
+
return `${out}${palette().reset}…`;
|
|
1893
2047
|
}
|
|
1894
2048
|
/** W20 — the settled block's duration, the `2h 14m` form (the task
|
|
1895
2049
|
* narrative's long-horizon idiom): minutes+seconds under an hour,
|
|
@@ -1974,6 +2128,33 @@ class Checklist {
|
|
|
1974
2128
|
* W21: the question param is gone — the old question slot retires; a
|
|
1975
2129
|
* pending approval's status IS the panel's (the compositor derives
|
|
1976
2130
|
* it from the bound panel state). */
|
|
2131
|
+
/** R8b — THE IDLE HINT GIVES WAY IN ORDER, and `ctrl+o` is on it.
|
|
2132
|
+
*
|
|
2133
|
+
* The transcript viewer shipped in 0.19.0 and was reachable only from
|
|
2134
|
+
* the `?` sheet: not on the banner's key line, not here. A feature
|
|
2135
|
+
* whose only advertisement is a screen you have to already know to
|
|
2136
|
+
* open is DC-30's lesson pointing the other way.
|
|
2137
|
+
*
|
|
2138
|
+
* It cannot simply be appended, because this hint is dropped WHOLE
|
|
2139
|
+
* when it does not fit — a longer string would take `/ commands` down
|
|
2140
|
+
* with it on a narrow terminal. So the forms are a ladder, and the
|
|
2141
|
+
* order says which affordance is least replaceable: `/ commands`
|
|
2142
|
+
* survives longest because it is the door to everything; `ctrl+o`
|
|
2143
|
+
* outranks `↑ history` because pressing up is how a person finds the
|
|
2144
|
+
* history by accident, and nothing finds ctrl+o by accident. */
|
|
2145
|
+
export function idleHint(room) {
|
|
2146
|
+
// The third rung is today's hint, kept so that NO width loses
|
|
2147
|
+
// something that used to fit: without it, a room of 24-30 columns
|
|
2148
|
+
// fell all the way to `/ commands` even though the old form fitted.
|
|
2149
|
+
// So the ladder is not a strict ranking of the three affordances —
|
|
2150
|
+
// it is the widest honest form at each room, and ctrl+o is on the
|
|
2151
|
+
// first two rungs rather than on all of them.
|
|
2152
|
+
for (const form of [" / commands · ↑ history · ctrl+o transcript", " / commands · ctrl+o transcript", " / commands · ↑ history", " / commands"]) {
|
|
2153
|
+
if (visibleWidth(form) <= room)
|
|
2154
|
+
return form;
|
|
2155
|
+
}
|
|
2156
|
+
return "";
|
|
2157
|
+
}
|
|
1977
2158
|
export function statusLine(status, tail, W, hint) {
|
|
1978
2159
|
const p = palette();
|
|
1979
2160
|
const text = `${status}${tail === "" ? "" : ` · ${tail}`}`;
|
|
@@ -1981,13 +2162,13 @@ export function statusLine(status, tail, W, hint) {
|
|
|
1981
2162
|
// "esc to cancel" (the same one-line-bounded shape as W12's delegate
|
|
1982
2163
|
// row; the #16g rule still cuts the HINT first, then the status with
|
|
1983
2164
|
// a "…" — never a fold).
|
|
1984
|
-
const hintText = hint ?? " / commands · ↑ history";
|
|
1985
2165
|
const statusW = visibleWidth(text);
|
|
1986
2166
|
if (statusW > W) {
|
|
1987
2167
|
return `${p.dim}${widthCut(text, W - 1)}…${p.reset}`;
|
|
1988
2168
|
}
|
|
2169
|
+
const hintText = hint ?? idleHint(Math.max(0, W - statusW));
|
|
1989
2170
|
const hintW = visibleWidth(hintText);
|
|
1990
|
-
if (statusW + hintW > W)
|
|
2171
|
+
if (hintW === 0 || statusW + hintW > W)
|
|
1991
2172
|
return `${p.dim}${text}${p.reset}`;
|
|
1992
2173
|
return `${p.dim}${text}${" ".repeat(Math.max(0, W - statusW - hintW))}${hintText}${p.reset}`;
|
|
1993
2174
|
}
|
package/dist/render.d.ts
CHANGED
|
@@ -77,6 +77,23 @@ export interface Palette {
|
|
|
77
77
|
* spans and must end without stranding them. */
|
|
78
78
|
readonly wash: string;
|
|
79
79
|
readonly washEnd: string;
|
|
80
|
+
/** R7a — THE FOCUS MARKER'S EMPHASIS, and it is not a background.
|
|
81
|
+
*
|
|
82
|
+
* DC-3 gave the `ctrl+r` token the wash, which is a BACKGROUND once
|
|
83
|
+
* a ground is resolved: `48;5;236` on dark reads as a black block
|
|
84
|
+
* behind the key, on a row that is otherwise plain text. The owner
|
|
85
|
+
* asked for it gone. The invariant DC-3 was serving — exactly one
|
|
86
|
+
* bright token per frame, because the key has exactly one target —
|
|
87
|
+
* never required a background; it required CONTRAST against the dim
|
|
88
|
+
* siblings, and full-strength bold foreground is more of it than a
|
|
89
|
+
* wash was.
|
|
90
|
+
*
|
|
91
|
+
* Three escapes because "dim" has two forms here: SGR 2 in the
|
|
92
|
+
* neutral palette, a 256-colour foreground in the resolved ones.
|
|
93
|
+
* 22 cancels the attribute, 39 restores the default foreground, 1
|
|
94
|
+
* is the emphasis. It closes by re-opening the palette's own dim,
|
|
95
|
+
* like `washEnd`, so the surrounding span survives. */
|
|
96
|
+
readonly lift: string;
|
|
80
97
|
readonly reset: string;
|
|
81
98
|
}
|
|
82
99
|
/**
|
package/dist/render.js
CHANGED
|
@@ -28,7 +28,7 @@ const BASE = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32
|
|
|
28
28
|
* 4's principle exactly: when the ground is unknown, use the thing that
|
|
29
29
|
* is correct on any ground rather than guessing one.
|
|
30
30
|
*/
|
|
31
|
-
const withWash = (wash, washEnd, red = BASE.red, dim = BASE.dim) => ({ ...BASE, red, dim, wash, washEnd, code: wash });
|
|
31
|
+
const withWash = (wash, washEnd, red = BASE.red, dim = BASE.dim) => ({ ...BASE, red, dim, wash, washEnd, code: wash, lift: "\x1b[22m\x1b[39m\x1b[1m" });
|
|
32
32
|
/**
|
|
33
33
|
* DC-3 — one table per ground.
|
|
34
34
|
*
|
|
@@ -56,7 +56,7 @@ export const COLOR_DARK = withWash("\x1b[48;5;236m", "\x1b[49m", "\x1b[38;5;173m
|
|
|
56
56
|
* not been established. Unchanged in every byte except `code`, which
|
|
57
57
|
* was the defect. */
|
|
58
58
|
export const COLOR_ON = COLOR_NEUTRAL;
|
|
59
|
-
export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", reset: "" };
|
|
59
|
+
export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", lift: "", reset: "" };
|
|
60
60
|
/** DC-3 — the resolved ground, set once at startup when the terminal
|
|
61
61
|
* answers (see `ground.ts`). It starts UNKNOWN and may stay that way
|
|
62
62
|
* forever; that is a supported state, not a failure. */
|
package/dist/strings.d.ts
CHANGED
|
@@ -176,6 +176,13 @@ export declare const PANEL_KEYS_ROW = "panels: \u2191\u2193 move \u00B7 \u23CE c
|
|
|
176
176
|
* screens pretending to be one. A narrow terminal shows fewer columns
|
|
177
177
|
* of the same truth, which is the honest degradation.
|
|
178
178
|
*/
|
|
179
|
+
/** R8b — the band's own opening row: a labelled rule at full width.
|
|
180
|
+
*
|
|
181
|
+
* Moved here from the @ picker, unchanged in every byte, because the
|
|
182
|
+
* keys sheet needs it too and `components.ts` already imports this
|
|
183
|
+
* module — the dependency only runs one way. `at-picker.ts` re-exports
|
|
184
|
+
* it, so every existing import site is untouched. */
|
|
185
|
+
export declare function bandHeader(label: string, W: number): string;
|
|
179
186
|
export declare function keysSheetRows(W: number): string[];
|
|
180
187
|
/** TUI2-R1 (D) — the keys as ONE line, for /help. The same table the
|
|
181
188
|
* sheet renders, joined — so the two can disagree only by deleting a
|
package/dist/strings.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* looked up here.
|
|
19
19
|
*/
|
|
20
20
|
import { escapeTerminal, palette } from "./render.js";
|
|
21
|
-
import { displayWidth } from "./width.js";
|
|
21
|
+
import { displayWidth, visibleWidth } from "./width.js";
|
|
22
22
|
/** v2a: the interactive prompt — the identity accent. readline owns the
|
|
23
23
|
* echo of what the user types; we own the prompt's color. (v2c: the
|
|
24
24
|
* readline prompt keeps "you> " — the brick ▌ is the dock's row only;
|
|
@@ -296,6 +296,27 @@ const SHEET_STOPS = [
|
|
|
296
296
|
* screens pretending to be one. A narrow terminal shows fewer columns
|
|
297
297
|
* of the same truth, which is the honest degradation.
|
|
298
298
|
*/
|
|
299
|
+
/** R8b — the band's own opening row: a labelled rule at full width.
|
|
300
|
+
*
|
|
301
|
+
* Moved here from the @ picker, unchanged in every byte, because the
|
|
302
|
+
* keys sheet needs it too and `components.ts` already imports this
|
|
303
|
+
* module — the dependency only runs one way. `at-picker.ts` re-exports
|
|
304
|
+
* it, so every existing import site is untouched. */
|
|
305
|
+
export function bandHeader(label, W) {
|
|
306
|
+
const p = palette();
|
|
307
|
+
const head = `\u2500\u2500\u2500 ${label} `;
|
|
308
|
+
const line = `${head}${"\u2500".repeat(Math.max(1, W - head.length))}`;
|
|
309
|
+
let out = "";
|
|
310
|
+
let w = 0;
|
|
311
|
+
for (const ch of line) {
|
|
312
|
+
const cw = displayWidth(ch);
|
|
313
|
+
if (w + cw > Math.max(1, W))
|
|
314
|
+
break;
|
|
315
|
+
out += ch;
|
|
316
|
+
w += cw;
|
|
317
|
+
}
|
|
318
|
+
return `${p.dim}${out}${p.reset}`;
|
|
319
|
+
}
|
|
299
320
|
export function keysSheetRows(W) {
|
|
300
321
|
const p = palette();
|
|
301
322
|
const cell = (i) => {
|
|
@@ -309,7 +330,14 @@ export function keysSheetRows(W) {
|
|
|
309
330
|
const b = KEY_BINDINGS[i];
|
|
310
331
|
return b === undefined ? "" : `${b.keys} ${b.what}`;
|
|
311
332
|
};
|
|
312
|
-
|
|
333
|
+
// R8b — THE SHEET NAMES ITSELF, in the band vocabulary.
|
|
334
|
+
//
|
|
335
|
+
// Every other overlay does: `─── commands ───`, `─── files ───`,
|
|
336
|
+
// `─── sessions ───`, `── transcript · N folds ──`. This one opened
|
|
337
|
+
// with a bare bold word at column 0, which is the exact condition
|
|
338
|
+
// TUI2-R1.5 ⑦(b) named when it made the rule — with scrollback
|
|
339
|
+
// behind an overlay, nothing said where the surface began.
|
|
340
|
+
const rows = [bandHeader("keys", W)];
|
|
313
341
|
for (let r = 0; r < SHEET_GRID.length; r += 1) {
|
|
314
342
|
const indexes = SHEET_GRID[r];
|
|
315
343
|
let row = "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui-cells",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "kiso tui-cells — the components cell renderer (components, diff, width, the render slice). Zero runtime dependencies: input is data, output is bytes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|