@vincemakes/kiso-tui-cells 0.16.1 → 0.16.3
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.js +2 -1
- package/dist/components.d.ts +18 -7
- package/dist/components.js +47 -19
- package/dist/ground.d.ts +46 -0
- package/dist/ground.js +76 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/md.d.ts +1 -1
- package/dist/md.js +71 -31
- package/dist/render.d.ts +47 -1
- package/dist/render.js +93 -19
- package/dist/strings.js +71 -24
- package/package.json +2 -2
package/dist/approval-panel.js
CHANGED
|
@@ -193,7 +193,8 @@ function panelRuleText(view) {
|
|
|
193
193
|
const base = view.amended === true
|
|
194
194
|
? `${head}${p.dim}needs approval · (amended) — asked by${p.reset}${tail}`
|
|
195
195
|
: `${head}${p.dim}needs approval — asked by${p.reset}${tail}`;
|
|
196
|
-
|
|
196
|
+
// DC-3: the fix hint is metadata — it borrowed the inline-code tint.
|
|
197
|
+
return hint ? `${base}${p.dim} · ${escapeTerminal(hint)}${p.reset}` : base;
|
|
197
198
|
}
|
|
198
199
|
/**
|
|
199
200
|
* TUI2-R3v2 ① — ONE ROW PER OPTION, and the cursor's row is a bar.
|
package/dist/components.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* (untouched); render.ts supplies the original text (palette, escape,
|
|
18
18
|
* tint, fold wording).
|
|
19
19
|
*/
|
|
20
|
-
import { foldThinking, foldResult, renderToolSummary, type ResumeMeta } from "./render.js";
|
|
20
|
+
import { foldThinking, foldResult, renderToolSummary, type ResumeMeta, type BannerMeta } from "./render.js";
|
|
21
21
|
import { type MdBlock } from "./md.js";
|
|
22
22
|
export { MdStream, renderBlock, renderMarkdown, type MdBlock, type MdKind } from "./md.js";
|
|
23
23
|
/** The spinner glyphs, cycled by the compositor's on-demand tick. */
|
|
@@ -168,6 +168,7 @@ export type BodyCell = {
|
|
|
168
168
|
version: string;
|
|
169
169
|
extensionsText: string;
|
|
170
170
|
resume: ResumeMeta[];
|
|
171
|
+
meta?: BannerMeta | undefined;
|
|
171
172
|
done: true;
|
|
172
173
|
} | {
|
|
173
174
|
kind: "raw";
|
|
@@ -244,7 +245,7 @@ export declare function expandSuffix(lines: number | null, room: number): string
|
|
|
244
245
|
* would put the invariant "exactly one bright token" in as many hands as
|
|
245
246
|
* there are emitters. Here it has exactly one.
|
|
246
247
|
*
|
|
247
|
-
* NO_COLOR: p.
|
|
248
|
+
* NO_COLOR: p.dim is empty, so the row's bytes are untouched.
|
|
248
249
|
*/
|
|
249
250
|
export declare function focusToken(row: string, W: number): string;
|
|
250
251
|
/** W13 — the rollup opt-in table: which tools collapse, and the count
|
|
@@ -374,11 +375,21 @@ export declare function widthCut(text: string, max: number): string;
|
|
|
374
375
|
* keeps the columns from moving as the bar walks the list.
|
|
375
376
|
*/
|
|
376
377
|
export declare function selectionBar(styled: string, visible: number, W: number): string;
|
|
377
|
-
/**
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
378
|
+
/**
|
|
379
|
+
* R2 — the composer's rails, and the ONE edge vocabulary.
|
|
380
|
+
*
|
|
381
|
+
* W6 turned two ╌ dotted rows into a rounded box, reasoning that "the
|
|
382
|
+
* box already says input lives here". That is reversed here, and the
|
|
383
|
+
* reason is not taste: a rule is a DELIMITER and a box is a CONTAINER,
|
|
384
|
+
* and the screen was carrying six edge vocabularies at once (this box,
|
|
385
|
+
* the panel's │ gutter and └── tail, the diff gutter, the quote's ▏, the
|
|
386
|
+
* table's ├─┼─┤ rails, the markdown rule's ─). One dashed rule replaces
|
|
387
|
+
* the ones that separate; the │ gutter survives where it SCOPES.
|
|
388
|
+
*
|
|
389
|
+
* Row-neutral by construction: CHROME_ROWS is still 4, so every gate
|
|
390
|
+
* keyed on H − 4 is untouched, and the input row gains the two columns
|
|
391
|
+
* the walls were taking.
|
|
392
|
+
*/
|
|
382
393
|
export declare function boxTop(W: number): string;
|
|
383
394
|
/** W6 — the box: the chrome's bottom rail. */
|
|
384
395
|
export declare function boxBottom(W: number): string;
|
package/dist/components.js
CHANGED
|
@@ -206,22 +206,32 @@ class UserMessage {
|
|
|
206
206
|
// was a 3000-line turn writing 260,298 bytes in one frame.
|
|
207
207
|
const paras = this.cell.text.split("\n");
|
|
208
208
|
let truncated = false;
|
|
209
|
+
// DC-6: the folded CONTENT first, then ONE width over all of it.
|
|
210
|
+
// The pad used to be computed per source paragraph, so a message
|
|
211
|
+
// with two lines drew as two bars of two different lengths — a
|
|
212
|
+
// ragged right edge on a block that is one block. A single
|
|
213
|
+
// paragraph was always correct, which is why it survived: the
|
|
214
|
+
// shape only appears once a message has a second line.
|
|
215
|
+
const content = [];
|
|
209
216
|
for (const para of paras) {
|
|
210
|
-
if (
|
|
217
|
+
if (content.length >= USER_CHIP_ROWS) {
|
|
211
218
|
truncated = true;
|
|
212
219
|
break;
|
|
213
220
|
}
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
for (const row of folded) {
|
|
217
|
-
if (rows.length >= USER_CHIP_ROWS) {
|
|
221
|
+
for (const row of foldLine(escapeTerminal(para), chipW)) {
|
|
222
|
+
if (content.length >= USER_CHIP_ROWS) {
|
|
218
223
|
truncated = true;
|
|
219
224
|
break;
|
|
220
225
|
}
|
|
221
|
-
|
|
222
|
-
rows.push(`${p.rv} ${row}${" ".repeat(pad)} ${p.rvEnd}`);
|
|
226
|
+
content.push(row);
|
|
223
227
|
}
|
|
224
228
|
}
|
|
229
|
+
// displayWidth stays the padding authority (never `length`): a CJK
|
|
230
|
+
// row is two cells per character and pads by cells.
|
|
231
|
+
const inner = content.length === 0 ? 0 : Math.max(...content.map((r) => displayWidth(r)));
|
|
232
|
+
for (const row of content) {
|
|
233
|
+
rows.push(`${p.rv} ${row}${" ".repeat(inner - displayWidth(row))} ${p.rvEnd}`);
|
|
234
|
+
}
|
|
225
235
|
if (!truncated)
|
|
226
236
|
return rows;
|
|
227
237
|
// The notice is OUTSIDE the chip's reverse video, in the cut-row
|
|
@@ -758,7 +768,7 @@ function appendSuffix(row, suffix) {
|
|
|
758
768
|
* would put the invariant "exactly one bright token" in as many hands as
|
|
759
769
|
* there are emitters. Here it has exactly one.
|
|
760
770
|
*
|
|
761
|
-
* NO_COLOR: p.
|
|
771
|
+
* NO_COLOR: p.dim is empty, so the row's bytes are untouched.
|
|
762
772
|
*/
|
|
763
773
|
export function focusToken(row, W) {
|
|
764
774
|
const p = palette();
|
|
@@ -766,9 +776,17 @@ export function focusToken(row, W) {
|
|
|
766
776
|
if (at !== -1) {
|
|
767
777
|
// the row already names the key — brighten the token in place, and
|
|
768
778
|
// leave every other span exactly as it was
|
|
769
|
-
if (p.
|
|
779
|
+
if (p.dim === "")
|
|
770
780
|
return row;
|
|
771
|
-
|
|
781
|
+
// DC-3: the marker takes the WASH. It used to take the inline-code
|
|
782
|
+
// tint and inherited its 1.54:1 — the cue naming the key that
|
|
783
|
+
// reveals a cell was itself the least readable thing on a white
|
|
784
|
+
// terminal. The wash is right for a second reason: this token has
|
|
785
|
+
// to be UNIQUE on the frame ("exactly one bright token"), and an
|
|
786
|
+
// attribute like bold is spent everywhere. It closes with washEnd
|
|
787
|
+
// rather than a reset, so the surrounding dim survives instead of
|
|
788
|
+
// having to be re-applied.
|
|
789
|
+
return `${row.slice(0, at)}${p.wash}${CTRL_R}${p.washEnd}${row.slice(at + CTRL_R.length)}`;
|
|
772
790
|
}
|
|
773
791
|
// A LIVE row does not carry the affordance today, and the live cell is
|
|
774
792
|
// the one ctrl+r takes FIRST (expandNext scans the live tail before
|
|
@@ -780,7 +798,7 @@ export function focusToken(row, W) {
|
|
|
780
798
|
const room = W - visibleWidth(row);
|
|
781
799
|
if (room < SUFFIX_MIN)
|
|
782
800
|
return row; // never at the cost of invariant ①
|
|
783
|
-
return `${row}${p.dim} · ${p.reset}${p.
|
|
801
|
+
return `${row}${p.dim} · ${p.reset}${p.wash}${CTRL_R}${p.washEnd}`;
|
|
784
802
|
}
|
|
785
803
|
const CTRL_R = "ctrl+r";
|
|
786
804
|
/** TUI2-R1 (A) — the expanded block's last row: the way back. The
|
|
@@ -1270,7 +1288,7 @@ class Banner {
|
|
|
1270
1288
|
}
|
|
1271
1289
|
render(W, ctx) {
|
|
1272
1290
|
const p = palette();
|
|
1273
|
-
const rows = bannerLines(W, ctx.height, this.cell.version, this.cell.extensionsText, this.cell.resume, ctx.now);
|
|
1291
|
+
const rows = bannerLines(W, ctx.height, this.cell.version, this.cell.extensionsText, this.cell.resume, ctx.now, this.cell.meta);
|
|
1274
1292
|
return rows.map((r) => `${p.dim}${r}${p.reset}`);
|
|
1275
1293
|
}
|
|
1276
1294
|
}
|
|
@@ -1460,17 +1478,27 @@ export function selectionBar(styled, visible, W) {
|
|
|
1460
1478
|
const inner = styled.replaceAll(p.reset, `${p.reset}${p.rv}`);
|
|
1461
1479
|
return `${p.rv} ${inner}${" ".repeat(Math.max(0, W - visible - 2))} ${p.rvEnd}`;
|
|
1462
1480
|
}
|
|
1463
|
-
/**
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1481
|
+
/**
|
|
1482
|
+
* R2 — the composer's rails, and the ONE edge vocabulary.
|
|
1483
|
+
*
|
|
1484
|
+
* W6 turned two ╌ dotted rows into a rounded box, reasoning that "the
|
|
1485
|
+
* box already says input lives here". That is reversed here, and the
|
|
1486
|
+
* reason is not taste: a rule is a DELIMITER and a box is a CONTAINER,
|
|
1487
|
+
* and the screen was carrying six edge vocabularies at once (this box,
|
|
1488
|
+
* the panel's │ gutter and └── tail, the diff gutter, the quote's ▏, the
|
|
1489
|
+
* table's ├─┼─┤ rails, the markdown rule's ─). One dashed rule replaces
|
|
1490
|
+
* the ones that separate; the │ gutter survives where it SCOPES.
|
|
1491
|
+
*
|
|
1492
|
+
* Row-neutral by construction: CHROME_ROWS is still 4, so every gate
|
|
1493
|
+
* keyed on H − 4 is untouched, and the input row gains the two columns
|
|
1494
|
+
* the walls were taking.
|
|
1495
|
+
*/
|
|
1468
1496
|
export function boxTop(W) {
|
|
1469
|
-
return `\x1b[2m
|
|
1497
|
+
return `\x1b[2m\u256d${"\u2500".repeat(Math.max(0, W - 2))}\u256e\x1b[0m`;
|
|
1470
1498
|
}
|
|
1471
1499
|
/** W6 — the box: the chrome's bottom rail. */
|
|
1472
1500
|
export function boxBottom(W) {
|
|
1473
|
-
return `\x1b[2m
|
|
1501
|
+
return `\x1b[2m\u2570${"\u2500".repeat(Math.max(0, W - 2))}\u256f\x1b[0m`;
|
|
1474
1502
|
}
|
|
1475
1503
|
/** The terminal label + rhythm gap (the pipe path's v2c bytes — the
|
|
1476
1504
|
* exact render the passthrough needs). */
|
package/dist/ground.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DC-3 §1 — the GROUND: is the terminal light or dark.
|
|
3
|
+
*
|
|
4
|
+
* Every colour in the palette is chosen against a background, and until
|
|
5
|
+
* now kiso had no way to know what that background was: `palette()`
|
|
6
|
+
* returned one constant, its greys were picked on a dark terminal, and
|
|
7
|
+
* on a white one the inline-code token measured 1.54:1 against a 4.5:1
|
|
8
|
+
* floor (finding DC-3).
|
|
9
|
+
*
|
|
10
|
+
* This module is the answer, and it is deliberately PURE. The terminal's
|
|
11
|
+
* reply arrives as a string and the environment arrives as fields, so
|
|
12
|
+
* the whole decision is testable without a terminal and the terminal
|
|
13
|
+
* work is reduced to plumbing.
|
|
14
|
+
*
|
|
15
|
+
* `unknown` is a real answer, not a failure. It means the caller must
|
|
16
|
+
* use the mark that is correct on ANY ground — reverse video rather than
|
|
17
|
+
* a wash, the terminal's own dim rather than a chosen grey. A design
|
|
18
|
+
* that degrades is the price of never painting light-mode paint onto a
|
|
19
|
+
* dark screen.
|
|
20
|
+
*/
|
|
21
|
+
export type Ground = "light" | "dark" | "unknown";
|
|
22
|
+
export interface Rgb {
|
|
23
|
+
readonly r: number;
|
|
24
|
+
readonly g: number;
|
|
25
|
+
readonly b: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function parseOscColor(body: string): Rgb | null;
|
|
28
|
+
/** The WCAG relative luminance — the same formula the contrast ratios in
|
|
29
|
+
* `packages/tui/design.md` §2 are computed with, so the ground and the
|
|
30
|
+
* floor cannot drift apart. */
|
|
31
|
+
export declare function relativeLuminance({ r, g, b }: Rgb): number;
|
|
32
|
+
export declare function groundFrom(rgb: Rgb): Exclude<Ground, "unknown">;
|
|
33
|
+
export interface GroundInputs {
|
|
34
|
+
/** KISO_THEME — an explicit answer from the human. */
|
|
35
|
+
readonly theme?: string | undefined;
|
|
36
|
+
/** The body of the terminal's OSC 11 answer, if one arrived. */
|
|
37
|
+
readonly osc?: string | undefined;
|
|
38
|
+
/** The COLORFGBG environment variable, if it is set. */
|
|
39
|
+
readonly colorfgbg?: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The ladder, first hit wins. Every rung that cannot answer falls
|
|
43
|
+
* through rather than guessing, and the bottom of the ladder is
|
|
44
|
+
* `unknown` — see the module comment for why that is a result.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveGround({ theme, osc, colorfgbg }: GroundInputs): Ground;
|
package/dist/ground.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DC-3 §1 — the GROUND: is the terminal light or dark.
|
|
3
|
+
*
|
|
4
|
+
* Every colour in the palette is chosen against a background, and until
|
|
5
|
+
* now kiso had no way to know what that background was: `palette()`
|
|
6
|
+
* returned one constant, its greys were picked on a dark terminal, and
|
|
7
|
+
* on a white one the inline-code token measured 1.54:1 against a 4.5:1
|
|
8
|
+
* floor (finding DC-3).
|
|
9
|
+
*
|
|
10
|
+
* This module is the answer, and it is deliberately PURE. The terminal's
|
|
11
|
+
* reply arrives as a string and the environment arrives as fields, so
|
|
12
|
+
* the whole decision is testable without a terminal and the terminal
|
|
13
|
+
* work is reduced to plumbing.
|
|
14
|
+
*
|
|
15
|
+
* `unknown` is a real answer, not a failure. It means the caller must
|
|
16
|
+
* use the mark that is correct on ANY ground — reverse video rather than
|
|
17
|
+
* a wash, the terminal's own dim rather than a chosen grey. A design
|
|
18
|
+
* that degrades is the price of never painting light-mode paint onto a
|
|
19
|
+
* dark screen.
|
|
20
|
+
*/
|
|
21
|
+
/** `<n>;rgb:R/G/B` with 1–4 hex digits per component — the body of an
|
|
22
|
+
* OSC 10/11 answer, already stripped of its introducer and terminator
|
|
23
|
+
* by the editor (DC-7). Anything else is null: a guess about the
|
|
24
|
+
* ground is worse than admitting there isn't one. */
|
|
25
|
+
const OSC_RGB = /^\d+;rgb:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})$/i;
|
|
26
|
+
export function parseOscColor(body) {
|
|
27
|
+
const m = OSC_RGB.exec(body.trim());
|
|
28
|
+
if (m === null)
|
|
29
|
+
return null;
|
|
30
|
+
// a component is n hex digits of a full-scale value, so it scales by
|
|
31
|
+
// its own maximum — `f` is full white exactly as `ffff` is.
|
|
32
|
+
const comp = (h) => Math.round((Number.parseInt(h, 16) / (16 ** h.length - 1)) * 255);
|
|
33
|
+
return { r: comp(m[1]), g: comp(m[2]), b: comp(m[3]) };
|
|
34
|
+
}
|
|
35
|
+
/** The WCAG relative luminance — the same formula the contrast ratios in
|
|
36
|
+
* `packages/tui/design.md` §2 are computed with, so the ground and the
|
|
37
|
+
* floor cannot drift apart. */
|
|
38
|
+
export function relativeLuminance({ r, g, b }) {
|
|
39
|
+
const lin = (v) => {
|
|
40
|
+
const c = v / 255;
|
|
41
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
42
|
+
};
|
|
43
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
44
|
+
}
|
|
45
|
+
export function groundFrom(rgb) {
|
|
46
|
+
return relativeLuminance(rgb) > 0.5 ? "light" : "dark";
|
|
47
|
+
}
|
|
48
|
+
/** COLORFGBG is `fg;bg` or `fg;default;bg` — the BACKGROUND is the last
|
|
49
|
+
* field, as an ANSI index. 0–6 and 8 are the dark half of the sixteen;
|
|
50
|
+
* 7 and 9–15 are the light half. Absent on most terminals, which is why
|
|
51
|
+
* it sits below the question kiso can actually ask. */
|
|
52
|
+
function fromColorFgBg(value) {
|
|
53
|
+
const fields = value.split(";");
|
|
54
|
+
const bg = Number.parseInt(fields[fields.length - 1] ?? "", 10);
|
|
55
|
+
if (!Number.isInteger(bg) || bg < 0 || bg > 15)
|
|
56
|
+
return "unknown";
|
|
57
|
+
return bg === 7 || bg >= 9 ? "light" : "dark";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The ladder, first hit wins. Every rung that cannot answer falls
|
|
61
|
+
* through rather than guessing, and the bottom of the ladder is
|
|
62
|
+
* `unknown` — see the module comment for why that is a result.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveGround({ theme, osc, colorfgbg }) {
|
|
65
|
+
const explicit = theme?.trim().toLowerCase();
|
|
66
|
+
if (explicit === "light" || explicit === "dark")
|
|
67
|
+
return explicit;
|
|
68
|
+
if (osc !== undefined && osc !== "") {
|
|
69
|
+
const rgb = parseOscColor(osc);
|
|
70
|
+
if (rgb !== null)
|
|
71
|
+
return groundFrom(rgb);
|
|
72
|
+
}
|
|
73
|
+
if (colorfgbg !== undefined && colorfgbg !== "")
|
|
74
|
+
return fromColorFgBg(colorfgbg);
|
|
75
|
+
return "unknown";
|
|
76
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,4 +14,7 @@ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWi
|
|
|
14
14
|
export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact, } from "./strings.js";
|
|
15
15
|
export { extensionsBannerText, helpRows, unansweredAskView, type BannerExtension } from "./strings.js";
|
|
16
16
|
export { displayVerb } from "./strings.js";
|
|
17
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type ResumeMeta, } from "./render.js";
|
|
17
|
+
export { bannerLines, COLOR_OFF, COLOR_DARK, COLOR_LIGHT, COLOR_NEUTRAL, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type ResumeMeta, } from "./render.js";
|
|
18
|
+
/** DC-3 — the ground: is the terminal light or dark. Pure; see the
|
|
19
|
+
* module comment for why `unknown` is a result and not a failure. */
|
|
20
|
+
export { groundFrom, parseOscColor, relativeLuminance, resolveGround, type Ground, type GroundInputs, type Rgb } from "./ground.js";
|
package/dist/index.js
CHANGED
|
@@ -28,4 +28,7 @@ export { extensionsBannerText, helpRows, unansweredAskView } from "./strings.js"
|
|
|
28
28
|
// TUI2-R2pre ④: the ONE display-verb table — the screen names the act,
|
|
29
29
|
// the tool table names the call.
|
|
30
30
|
export { displayVerb } from "./strings.js";
|
|
31
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|
|
31
|
+
export { bannerLines, COLOR_OFF, COLOR_DARK, COLOR_LIGHT, COLOR_NEUTRAL, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|
|
32
|
+
/** DC-3 — the ground: is the terminal light or dark. Pure; see the
|
|
33
|
+
* module comment for why `unknown` is a result and not a failure. */
|
|
34
|
+
export { groundFrom, parseOscColor, relativeLuminance, resolveGround } from "./ground.js";
|
package/dist/md.d.ts
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
/** The block kinds. `fence-open`/`fence-line` are separate kinds on
|
|
40
40
|
* purpose: a fence's rows must be able to freeze ONE AT A TIME. */
|
|
41
|
-
export type MdKind = "para" | "heading" | "list" | "table" | "quote" | "rule" | "fence-open" | "fence-line";
|
|
41
|
+
export type MdKind = "para" | "heading" | "list" | "table" | "quote" | "rule" | "fence-open" | "fence-line" | "fence-close";
|
|
42
42
|
/** One block: its SOURCE lines, never a rendered form. The render is a
|
|
43
43
|
* pure function of (block, width), which is what makes the freeze
|
|
44
44
|
* property a property of the scanner alone. */
|
package/dist/md.js
CHANGED
|
@@ -41,6 +41,10 @@ import { breakable, charWidth, displayWidth } from "./width.js";
|
|
|
41
41
|
import { escapeTerminal } from "./render.js";
|
|
42
42
|
import { visibleWidth } from "./components.js";
|
|
43
43
|
// ---- line classification -------------------------------------------
|
|
44
|
+
/** E2 — the rail a fenced block is drawn with. Three backticks: what
|
|
45
|
+
* the model wrote, and what a human gets back when they copy the block
|
|
46
|
+
* out of the terminal. */
|
|
47
|
+
const RAIL = "\u0060\u0060\u0060";
|
|
44
48
|
const FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
45
49
|
/** ATX only, and the space is REQUIRED: `#hashtag` is prose. */
|
|
46
50
|
const HEADING = /^ {0,3}(#{1,6}) +(\S.*)$/;
|
|
@@ -164,11 +168,16 @@ export class MdStream {
|
|
|
164
168
|
}
|
|
165
169
|
#line(line) {
|
|
166
170
|
if (this.#fence !== null) {
|
|
167
|
-
// the closer emits
|
|
168
|
-
//
|
|
169
|
-
// a
|
|
171
|
+
// E2: the closer emits its OWN block now. The rule it used to
|
|
172
|
+
// obey — "a bottom border is drawn only by an actual close, and
|
|
173
|
+
// under committed lines a phantom one would be a lie the
|
|
174
|
+
// force-commit path could freeze" — is UNCHANGED and is why this
|
|
175
|
+
// is safe: the rail appears here, on an actual close, and never
|
|
176
|
+
// before. An unterminated fence still draws no bottom, which is
|
|
177
|
+
// the truth about an unterminated fence.
|
|
170
178
|
if (closesFence(line, this.#fence)) {
|
|
171
179
|
this.#fence = null;
|
|
180
|
+
this.#push({ kind: "fence-close", lines: [line], gap: false, lang: "" });
|
|
172
181
|
return;
|
|
173
182
|
}
|
|
174
183
|
this.#push({ kind: "fence-line", lines: [line], gap: false, lang: "" });
|
|
@@ -230,22 +239,38 @@ function blockBody(b, W) {
|
|
|
230
239
|
const p = palette();
|
|
231
240
|
switch (b.kind) {
|
|
232
241
|
case "heading": {
|
|
233
|
-
// the
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
242
|
+
// DC-4: the LEVEL is information and it used to be discarded —
|
|
243
|
+
// `#`, `##` and `###` all rendered as the same bold line, so a
|
|
244
|
+
// structured answer arrived flat. Levels are NOT differentiated
|
|
245
|
+
// by colour: 1 adds an underline, 2 is bold alone, and 3 and
|
|
246
|
+
// below print their own `###`, because attributes have run out
|
|
247
|
+
// and a marker is the only carrier that survives a pipe. A
|
|
248
|
+
// `**bold**` inside a heading is still a no-op, which is the mono
|
|
249
|
+
// discipline paying for itself.
|
|
239
250
|
const m = HEADING.exec(b.lines[0] ?? "");
|
|
240
|
-
|
|
251
|
+
const level = (m?.[1] ?? "#").length;
|
|
252
|
+
const text = m?.[2] ?? b.lines[0] ?? "";
|
|
253
|
+
const style = level === 1 ? `${p.bold}${p.underline}` : p.bold;
|
|
254
|
+
const marker = level >= 3 ? `${"#".repeat(level)} ` : "";
|
|
255
|
+
return wrap(`${style}${marker}${inlineSpans(text, style)}${p.reset}`, W, "", "");
|
|
241
256
|
}
|
|
242
257
|
case "rule":
|
|
243
|
-
|
|
258
|
+
// R2: the dashed rule, at the block's own width. The 28 was a
|
|
259
|
+
// guess that read as a short line rather than a divider, and ─
|
|
260
|
+
// belonged to the box vocabulary this round is collapsing.
|
|
261
|
+
return [`${p.dim}${"\u254c".repeat(Math.max(1, W))}${p.reset}`];
|
|
244
262
|
case "fence-open":
|
|
245
|
-
// the
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
263
|
+
// E2: the RAIL, not a gutter. A block drawn with ``` is still a
|
|
264
|
+
// fenced block when a human selects it and pastes it somewhere
|
|
265
|
+
// else; a block drawn with a gutter is not. Zero highlighting —
|
|
266
|
+
// which is exactly what makes a fence body line committable on
|
|
267
|
+
// its own.
|
|
268
|
+
return [`${p.dim}${RAIL}${b.lang}${p.reset}`];
|
|
269
|
+
case "fence-close":
|
|
270
|
+
// only ever reached by an ACTUAL close (see MdStream#line): an
|
|
271
|
+
// unterminated fence draws no bottom, which is the truth about
|
|
272
|
+
// an unterminated fence.
|
|
273
|
+
return [`${p.dim}${RAIL}${p.reset}`];
|
|
249
274
|
case "fence-line": {
|
|
250
275
|
// a fence body's INDENTATION is its content. The wrapper drops
|
|
251
276
|
// leading spaces \u2014 right for prose, a lie for code \u2014 so the indent
|
|
@@ -253,12 +278,21 @@ function blockBody(b, W) {
|
|
|
253
278
|
// under it rather than returning to the gutter.
|
|
254
279
|
const src = (b.lines[0] ?? "").replace(/\t/g, " ");
|
|
255
280
|
const indent = /^ */.exec(src)[0];
|
|
256
|
-
const gutter =
|
|
257
|
-
|
|
281
|
+
const gutter = " "; // E2: the rails bound the block; the body just insets
|
|
282
|
+
// DC-3: a fenced BODY carries no colour token. It used to take
|
|
283
|
+
// `code` — 1.54:1 on a white terminal, applied to whole blocks,
|
|
284
|
+
// which made the code the model just wrote the least readable
|
|
285
|
+
// thing on screen. The `│` gutter already says "this block is
|
|
286
|
+
// verbatim"; saying it twice cost legibility and bought nothing.
|
|
287
|
+
return foldLineWidth(src.slice(indent.length), W - visibleWidth(gutter), indent).map((r) => `${gutter}${r}`);
|
|
258
288
|
}
|
|
259
289
|
case "quote": {
|
|
260
290
|
const text = b.lines.map((l) => QUOTE.exec(l)?.[1] ?? l).join(" ");
|
|
261
|
-
|
|
291
|
+
// R2: one gutter glyph. A quote and a fenced block both say "this
|
|
292
|
+
// text is not mine", and the screen was saying it two ways — ▏
|
|
293
|
+
// here and │ for code. The fences took their own ``` rails, so │
|
|
294
|
+
// is free and the quote takes it.
|
|
295
|
+
const gutter = `${p.dim}\u2502${p.reset} `;
|
|
262
296
|
return wrap(`${p.dim}${inlineSpans(text, p.dim)}${p.reset}`, W - visibleWidth(gutter), "", "").map((r) => `${gutter}${r}`);
|
|
263
297
|
}
|
|
264
298
|
case "list":
|
|
@@ -310,7 +344,10 @@ export function inlineSpans(text, base) {
|
|
|
310
344
|
if (end > i) {
|
|
311
345
|
// a code span's content is LITERAL — no markers inside it mean
|
|
312
346
|
// anything, which is what makes `x | y` survive a table split
|
|
313
|
-
|
|
347
|
+
// DC-3: inline code is a SURFACE (`wash`), closed with washEnd
|
|
348
|
+
// rather than a reset so the span composes inside a heading's
|
|
349
|
+
// or a quote's own style.
|
|
350
|
+
out += `${p.wash}${text.slice(i + 1, end)}${p.washEnd}${base}`;
|
|
314
351
|
i = end + 1;
|
|
315
352
|
continue;
|
|
316
353
|
}
|
|
@@ -456,9 +493,12 @@ function listRows(b, W) {
|
|
|
456
493
|
}
|
|
457
494
|
flush();
|
|
458
495
|
const depth = Math.min(5, Math.floor(m[1].length / 2));
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
|
|
496
|
+
// E1: normalization stays — `-`, `*` and `+` all render as ONE
|
|
497
|
+
// marker, so the model's arbitrary choice never leaks onto the
|
|
498
|
+
// screen — but the marker is `- ` rather than `•`, so a copied list
|
|
499
|
+
// is still a list. A numbered list KEEPS its numbers (they are the
|
|
500
|
+
// author's meaning, not decoration).
|
|
501
|
+
const marker = /^\d/.test(m[2]) ? `${m[2]} ` : "- ";
|
|
462
502
|
lead = `${" ".repeat(depth + 1)}${marker}`;
|
|
463
503
|
text = m[3];
|
|
464
504
|
}
|
|
@@ -484,17 +524,17 @@ function tableRows(b, W) {
|
|
|
484
524
|
if (t === null)
|
|
485
525
|
return b.lines.flatMap((l) => wrap(l, W, "", ""));
|
|
486
526
|
const cols = t.header.map((h, i) => Math.max(cellWidth(h), ...t.rows.map((r) => cellWidth(r[i] ?? ""))));
|
|
487
|
-
//
|
|
488
|
-
|
|
527
|
+
// R2: no rails. The drawn width is two columns of inset plus the
|
|
528
|
+
// columns and their two-space gutters — a table is bounded by the
|
|
529
|
+
// blank lines above and below it, exactly as every other block on the
|
|
530
|
+
// screen is, and it was the last box left on a screen that has decided
|
|
531
|
+
// not to have boxes. Alignment does the work the rails were doing, and
|
|
532
|
+
// a copied table is closer to markdown without them.
|
|
533
|
+
const total = cols.reduce((n, w) => n + w + 2, 2);
|
|
489
534
|
if (total > W)
|
|
490
535
|
return recordRows(t, W);
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
return [
|
|
494
|
-
row(t.header, true),
|
|
495
|
-
`${p.dim}├${cols.map((w) => "─".repeat(w + 2)).join("┼")}┤${p.reset}`,
|
|
496
|
-
...t.rows.map((r) => row(r, false)),
|
|
497
|
-
];
|
|
536
|
+
const row = (cells, bold) => ` ${cells.map((c, i) => pad(c, cols[i], t.align[i], bold)).join(" ")}`.replace(/\s+$/, "");
|
|
537
|
+
return [row(t.header, true), ...t.rows.map((r) => row(r, false))];
|
|
498
538
|
}
|
|
499
539
|
/** A cell's column count: what a human sees, styling removed. */
|
|
500
540
|
function cellWidth(cell) {
|
package/dist/render.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* produce the bytes a human sees. Colors are raw ANSI — zero
|
|
6
6
|
* dependencies (the tui-cells package has none).
|
|
7
7
|
*/
|
|
8
|
+
import type { Ground } from "./ground.js";
|
|
8
9
|
/**
|
|
9
10
|
* v2a — the palette, centralized (no hard-coded codes elsewhere); v5
|
|
10
11
|
* (TUI v5 #16e, the v4.1 design): the decorative blue (38;5;75) is
|
|
@@ -42,6 +43,12 @@ export interface Palette {
|
|
|
42
43
|
* addressed to the human. This is the ruling's own set gaining its
|
|
43
44
|
* missing member, not a fourth colour. */
|
|
44
45
|
readonly warn: string;
|
|
46
|
+
/** DC-3 — RETIRED as a tint; kept as an alias of `wash` so nothing
|
|
47
|
+
* reading it gets the old absolute grey. It was 256-colour index 252
|
|
48
|
+
* (#d0d0d0): 1.54:1 on a white terminal, against a 4.5:1 floor, and
|
|
49
|
+
* five call sites shared it. Inline code is a SURFACE now — never a
|
|
50
|
+
* foreground tint, and never applied to a whole fenced block, whose
|
|
51
|
+
* `│` gutter already says the same thing more cheaply. */
|
|
45
52
|
readonly code: string;
|
|
46
53
|
/** TUI2-MD (MD-1, the owner's circle) — the markdown round's ONE new
|
|
47
54
|
* member. `*italic*` needs a rendering, and under the mono discipline
|
|
@@ -53,12 +60,44 @@ export interface Palette {
|
|
|
53
60
|
* SGR-0 that would strand the heading's own style. */
|
|
54
61
|
readonly italic: string;
|
|
55
62
|
readonly italicEnd: string;
|
|
63
|
+
/** DC-4 — the heading round's ONE new member, on the italic precedent:
|
|
64
|
+
* SGR 4 is an ATTRIBUTE, so it costs the alphabet nothing chromatic
|
|
65
|
+
* and a terminal without underlines simply draws the text. It carries
|
|
66
|
+
* the level-1 heading; levels 3 and below carry their own `###`,
|
|
67
|
+
* because attributes run out and a marker survives a pipe. */
|
|
68
|
+
readonly underline: string;
|
|
69
|
+
readonly underlineEnd: string;
|
|
56
70
|
readonly rv: string;
|
|
57
71
|
readonly rvEnd: string;
|
|
72
|
+
/** DC-3 — the VERBATIM surface: the human's own words, and inline
|
|
73
|
+
* code. A background, so it needs the ground; with no ground it is
|
|
74
|
+
* reverse video, which is correct on any ground and is rung 4 of the
|
|
75
|
+
* ladder in `ground.ts`. Closed with 49 rather than SGR 0, for the
|
|
76
|
+
* reason `rv` is closed with 27: a washed span sits inside other
|
|
77
|
+
* spans and must end without stranding them. */
|
|
78
|
+
readonly wash: string;
|
|
79
|
+
readonly washEnd: string;
|
|
58
80
|
readonly reset: string;
|
|
59
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* DC-3 — one table per ground.
|
|
84
|
+
*
|
|
85
|
+
* `dim` is the same in all three ON tables and that is the point: SGR 2
|
|
86
|
+
* is an ATTRIBUTE, it dims whatever the terminal's own foreground is, so
|
|
87
|
+
* it adapts to the ground instead of asserting one. Only the background
|
|
88
|
+
* genuinely needs to know, which is why `wash` is the only member that
|
|
89
|
+
* varies.
|
|
90
|
+
*/
|
|
91
|
+
export declare const COLOR_NEUTRAL: Palette;
|
|
92
|
+
export declare const COLOR_LIGHT: Palette;
|
|
93
|
+
export declare const COLOR_DARK: Palette;
|
|
94
|
+
/** The historical name — the palette for a colour TTY whose ground has
|
|
95
|
+
* not been established. Unchanged in every byte except `code`, which
|
|
96
|
+
* was the defect. */
|
|
60
97
|
export declare const COLOR_ON: Palette;
|
|
61
98
|
export declare const COLOR_OFF: Palette;
|
|
99
|
+
export declare function setGround(g: Ground): void;
|
|
100
|
+
export declare function currentGround(): Ground;
|
|
62
101
|
export declare function palette(): Palette;
|
|
63
102
|
/**
|
|
64
103
|
* E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
@@ -113,6 +152,13 @@ export declare function renderTerminalGap(statusLine: string | null): string;
|
|
|
113
152
|
* Pure.
|
|
114
153
|
*/
|
|
115
154
|
export declare const TAGLINE = "the coding agent that survives kill -9";
|
|
155
|
+
/** R2 — what the opening knows about the session. Optional because the
|
|
156
|
+
* off-TTY caller prints a banner before a model is bound. */
|
|
157
|
+
export interface BannerMeta {
|
|
158
|
+
readonly model: string;
|
|
159
|
+
readonly mode: string;
|
|
160
|
+
readonly cwd: string;
|
|
161
|
+
}
|
|
116
162
|
/** v3 §01 (W1): truncate a row at `width`, marking the hidden span
|
|
117
163
|
* " (+N)". W1: the width math is the charWidth authority (the banner's
|
|
118
164
|
* brick glyphs are 1 cell — the art's 38 columns clear 40), and the
|
|
@@ -129,7 +175,7 @@ export declare function truncateRow(row: string, width: number): string;
|
|
|
129
175
|
* text row does not repeat the name — then extensions — then the W5
|
|
130
176
|
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
131
177
|
* with a " (+N)" marker. Pure. */
|
|
132
|
-
export declare function bannerLines(W: number, H: number, version: string, extensionsText: string, resume?: readonly ResumeMeta[], now?: number): string[];
|
|
178
|
+
export declare function bannerLines(W: number, H: number, version: string, extensionsText: string, resume?: readonly ResumeMeta[], now?: number, meta?: BannerMeta | undefined): string[];
|
|
133
179
|
/** W5 — the opening-screen resume list. Every field already exists
|
|
134
180
|
* behind renderSessionLine / `kiso sessions`: the relative time, the
|
|
135
181
|
* title, then the right-aligned "N events · M runs". The columns are
|
package/dist/render.js
CHANGED
|
@@ -6,8 +6,35 @@
|
|
|
6
6
|
* dependencies (the tui-cells package has none).
|
|
7
7
|
*/
|
|
8
8
|
import { charWidth, displayWidth } from "./width.js";
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const BASE = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", warn: "\x1b[33m", italic: "\x1b[3m", italicEnd: "\x1b[23m", underline: "\x1b[4m", underlineEnd: "\x1b[24m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
|
|
10
|
+
const withWash = (wash, washEnd) => ({ ...BASE, wash, washEnd, code: wash });
|
|
11
|
+
/**
|
|
12
|
+
* DC-3 — one table per ground.
|
|
13
|
+
*
|
|
14
|
+
* `dim` is the same in all three ON tables and that is the point: SGR 2
|
|
15
|
+
* is an ATTRIBUTE, it dims whatever the terminal's own foreground is, so
|
|
16
|
+
* it adapts to the ground instead of asserting one. Only the background
|
|
17
|
+
* genuinely needs to know, which is why `wash` is the only member that
|
|
18
|
+
* varies.
|
|
19
|
+
*/
|
|
20
|
+
export const COLOR_NEUTRAL = withWash("\x1b[7m", "\x1b[27m");
|
|
21
|
+
export const COLOR_LIGHT = withWash("\x1b[48;5;255m", "\x1b[49m");
|
|
22
|
+
export const COLOR_DARK = withWash("\x1b[48;5;236m", "\x1b[49m");
|
|
23
|
+
/** The historical name — the palette for a colour TTY whose ground has
|
|
24
|
+
* not been established. Unchanged in every byte except `code`, which
|
|
25
|
+
* was the defect. */
|
|
26
|
+
export const COLOR_ON = COLOR_NEUTRAL;
|
|
27
|
+
export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", reset: "" };
|
|
28
|
+
/** DC-3 — the resolved ground, set once at startup when the terminal
|
|
29
|
+
* answers (see `ground.ts`). It starts UNKNOWN and may stay that way
|
|
30
|
+
* forever; that is a supported state, not a failure. */
|
|
31
|
+
let ground = "unknown";
|
|
32
|
+
export function setGround(g) {
|
|
33
|
+
ground = g;
|
|
34
|
+
}
|
|
35
|
+
export function currentGround() {
|
|
36
|
+
return ground;
|
|
37
|
+
}
|
|
11
38
|
export function palette() {
|
|
12
39
|
// PH-1a (finding PH-F5): the no-color.org contract is "present AND
|
|
13
40
|
// non-empty" — the old `=== undefined` check let an EMPTY `NO_COLOR=`
|
|
@@ -16,7 +43,9 @@ export function palette() {
|
|
|
16
43
|
// UI with them. The v4-round plan recorded this as debugging pitfall ①;
|
|
17
44
|
// it was a bug.
|
|
18
45
|
const noColor = process.env.NO_COLOR;
|
|
19
|
-
|
|
46
|
+
if (!((noColor === undefined || noColor === "") && process.stdout.isTTY))
|
|
47
|
+
return COLOR_OFF;
|
|
48
|
+
return ground === "light" ? COLOR_LIGHT : ground === "dark" ? COLOR_DARK : COLOR_NEUTRAL;
|
|
20
49
|
}
|
|
21
50
|
/**
|
|
22
51
|
* E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
@@ -162,13 +191,30 @@ export function renderTerminalGap(statusLine) {
|
|
|
162
191
|
* Pure.
|
|
163
192
|
*/
|
|
164
193
|
export const TAGLINE = "the coding agent that survives kill -9";
|
|
165
|
-
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
|
|
194
|
+
/**
|
|
195
|
+
* R2 — the wordmark is retired (2026-08-27, the nineteen-screen review).
|
|
196
|
+
*
|
|
197
|
+
* TT-1B had already cut the 36x6 pixel art down to two rows because a
|
|
198
|
+
* tall banner's mid-scroll cut state renders as glyph garbage. The
|
|
199
|
+
* remaining two rows go now for a different reason: they say the word
|
|
200
|
+
* `kiso` in fifteen columns of block glyphs, and the word `kiso` says it
|
|
201
|
+
* in four. A rendered clover mark was tried first, at 4x2, 10x5, 14x7
|
|
202
|
+
* and 16x8, and rejected on measurement — below fourteen columns the
|
|
203
|
+
* centre star closes and the mark reads as a domino, and at fourteen it
|
|
204
|
+
* costs seven rows.
|
|
205
|
+
*
|
|
206
|
+
* What takes the room is not decoration. A first screen is asked three
|
|
207
|
+
* questions — what model, where am I, what is loaded — and it now
|
|
208
|
+
* answers them in one aligned column.
|
|
209
|
+
*/
|
|
210
|
+
/** R2 — the keys a first screen teaches. One dim row, and deliberately
|
|
211
|
+
* NOT derived from KEY_BINDINGS: the sheet is the complete list and
|
|
212
|
+
* this is the opening's five, chosen rather than generated. */
|
|
213
|
+
const BANNER_KEYS = "esc interrupt · ctrl+c exit · / commands · ! bash · ctrl+r expand";
|
|
214
|
+
/** R2 — the labels. Uppercase mono, dim, letter-spaced by the column
|
|
215
|
+
* rather than by SGR: they mark sections and are never content. */
|
|
216
|
+
const BANNER_LABELS = ["MODEL", "WORKSPACE", "EXTENSIONS"];
|
|
217
|
+
const LABEL_STOP = Math.max(...BANNER_LABELS.map((l) => l.length)) + 2;
|
|
172
218
|
/** v3 §01 (W1): truncate a row at `width`, marking the hidden span
|
|
173
219
|
* " (+N)". W1: the width math is the charWidth authority (the banner's
|
|
174
220
|
* brick glyphs are 1 cell — the art's 38 columns clear 40), and the
|
|
@@ -209,17 +255,45 @@ export function truncateRow(row, width) {
|
|
|
209
255
|
* text row does not repeat the name — then extensions — then the W5
|
|
210
256
|
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
211
257
|
* with a " (+N)" marker. Pure. */
|
|
212
|
-
export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now()) {
|
|
213
|
-
const rows = [];
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
258
|
+
export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now(), meta) {
|
|
259
|
+
const rows = [truncateRow(`kiso ${version}`, W)];
|
|
260
|
+
const facts = [];
|
|
261
|
+
if (meta !== undefined) {
|
|
262
|
+
facts.push([BANNER_LABELS[0], `${meta.model}${meta.mode === "" ? "" : ` · ${meta.mode}`}`], [BANNER_LABELS[1], meta.cwd]);
|
|
217
263
|
}
|
|
218
|
-
if (rows.length > 0)
|
|
219
|
-
rows.push("");
|
|
220
|
-
rows.push(truncateRow(`v${version} — ${TAGLINE}`, W));
|
|
221
264
|
if (extensionsText !== "")
|
|
222
|
-
|
|
265
|
+
facts.push([BANNER_LABELS[2], extensionsText]);
|
|
266
|
+
if (facts.length > 0) {
|
|
267
|
+
rows.push("");
|
|
268
|
+
// The value column HANGS rather than truncating. The label costs
|
|
269
|
+
// columns the value used to have, and an extension list cut at the
|
|
270
|
+
// width would hide which extensions loaded — on the one screen whose
|
|
271
|
+
// job is to say what is loaded. Words wrap; a word longer than the
|
|
272
|
+
// column still truncates, with truncateRow's honest marker.
|
|
273
|
+
const room = Math.max(8, W - 2 - LABEL_STOP);
|
|
274
|
+
for (const [label, value] of facts) {
|
|
275
|
+
const lead = ` ${label}${" ".repeat(LABEL_STOP - label.length)}`;
|
|
276
|
+
const hang = " ".repeat(displayWidth(lead));
|
|
277
|
+
let line = "";
|
|
278
|
+
const out = [];
|
|
279
|
+
for (const word of value.split(" ")) {
|
|
280
|
+
if (line === "")
|
|
281
|
+
line = word;
|
|
282
|
+
else if (displayWidth(`${line} ${word}`) <= room)
|
|
283
|
+
line += ` ${word}`;
|
|
284
|
+
else {
|
|
285
|
+
out.push(line);
|
|
286
|
+
line = word;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (line !== "")
|
|
290
|
+
out.push(line);
|
|
291
|
+
for (const [i, l] of out.entries())
|
|
292
|
+
rows.push(truncateRow(`${i === 0 ? lead : hang}${l}`, W));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (meta !== undefined && W >= 40)
|
|
296
|
+
rows.push("", truncateRow(` ${BANNER_KEYS}`, W));
|
|
223
297
|
if (W >= 40 && H >= 20 && resume.length > 0) {
|
|
224
298
|
rows.push("", ...renderResumeList(resume, W, now));
|
|
225
299
|
}
|
package/dist/strings.js
CHANGED
|
@@ -191,6 +191,7 @@ export const KEY_BINDINGS = [
|
|
|
191
191
|
{ keys: "ctrl+r", what: "expand cells" },
|
|
192
192
|
{ keys: "tab", what: "complete (menu / @)" },
|
|
193
193
|
{ keys: "?", what: "this sheet" },
|
|
194
|
+
{ keys: "ctrl+z / ctrl+y", what: "undo / redo" },
|
|
194
195
|
];
|
|
195
196
|
/**
|
|
196
197
|
* TUI2-R2pre ④ — THE display-verb table (the integrator's ruling).
|
|
@@ -254,12 +255,14 @@ const SHEET_GRID = [
|
|
|
254
255
|
[3, 4, 5],
|
|
255
256
|
[6, 7],
|
|
256
257
|
[8, 9],
|
|
258
|
+
[10], // UD-1: its own single-column row — no hand-tuned stop touched
|
|
257
259
|
];
|
|
258
260
|
const SHEET_STOPS = [
|
|
259
261
|
[16, 43],
|
|
260
262
|
[16, 43],
|
|
261
263
|
[36],
|
|
262
264
|
[36],
|
|
265
|
+
[],
|
|
263
266
|
];
|
|
264
267
|
/**
|
|
265
268
|
* TUI2-R1 (D) — the sheet, one screen, static.
|
|
@@ -273,7 +276,10 @@ export function keysSheetRows(W) {
|
|
|
273
276
|
const p = palette();
|
|
274
277
|
const cell = (i) => {
|
|
275
278
|
const b = KEY_BINDINGS[i];
|
|
276
|
-
|
|
279
|
+
// DC-3: the key NAMES are the sheet's content, not a code span —
|
|
280
|
+
// they borrowed the inline-code tint and became the least readable
|
|
281
|
+
// thing on the one screen whose whole job is being read.
|
|
282
|
+
return b === undefined ? "" : `${p.bold}${b.keys}${p.reset} ${b.what}`;
|
|
277
283
|
};
|
|
278
284
|
const plainCell = (i) => {
|
|
279
285
|
const b = KEY_BINDINGS[i];
|
|
@@ -296,13 +302,40 @@ export function keysSheetRows(W) {
|
|
|
296
302
|
}
|
|
297
303
|
rows.push(row);
|
|
298
304
|
}
|
|
299
|
-
rows.push(`${p.dim}${
|
|
305
|
+
rows.push(`${p.dim}${panelKeysRow(W)}${p.reset}`);
|
|
300
306
|
return rows.map((row) => cutRow(row, W));
|
|
301
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* DC-2 — the panel row degrades by CLAUSE.
|
|
310
|
+
*
|
|
311
|
+
* The row is 76 columns of independent clauses joined by ` · `. At 72 it
|
|
312
|
+
* used to lose the tail of the last one, so `t types` became `t`: the
|
|
313
|
+
* reader was told a key existed and not told what it did, on a row that
|
|
314
|
+
* still looked complete. Dropping a whole clause says less; it never
|
|
315
|
+
* says something false. `cutRow`'s ellipsis is the floor below this, for
|
|
316
|
+
* a width that cannot hold even the first clause.
|
|
317
|
+
*/
|
|
318
|
+
function panelKeysRow(W) {
|
|
319
|
+
const clauses = PANEL_KEYS_ROW.split(" \u00b7 ");
|
|
320
|
+
for (let n = clauses.length; n > 1; n -= 1) {
|
|
321
|
+
const row = clauses.slice(0, n).join(" \u00b7 ");
|
|
322
|
+
if (displayWidth(row) <= W)
|
|
323
|
+
return row;
|
|
324
|
+
}
|
|
325
|
+
return clauses[0];
|
|
326
|
+
}
|
|
302
327
|
/** One row, cut at the width — SGR-aware, the ellipsis after the reset
|
|
303
328
|
* (the cutLine convention; duplicated here rather than imported so the
|
|
304
329
|
* strings module keeps its no-components-dependency shape). */
|
|
305
330
|
function cutRow(row, W) {
|
|
331
|
+
// DC-2: a cut is MARKED. This returned the surviving prefix with
|
|
332
|
+
// nothing to say it was a prefix, so a row that had lost its tail read
|
|
333
|
+
// as a whole row — the one thing the tree's own fold rule forbids
|
|
334
|
+
// ("the honest …, never a silent truncate"). The mark costs a column,
|
|
335
|
+
// so the cut lands one column earlier to pay for it.
|
|
336
|
+
if (displayWidth(row.replace(/\x1b\[[0-9;]*m/g, "")) <= W)
|
|
337
|
+
return row;
|
|
338
|
+
const limit = Math.max(0, W - 1);
|
|
306
339
|
let out = "";
|
|
307
340
|
let width = 0;
|
|
308
341
|
for (let i = 0; i < row.length;) {
|
|
@@ -313,13 +346,13 @@ function cutRow(row, W) {
|
|
|
313
346
|
continue;
|
|
314
347
|
}
|
|
315
348
|
const cw = displayWidth(row[i]);
|
|
316
|
-
if (width + cw >
|
|
317
|
-
|
|
349
|
+
if (width + cw > limit)
|
|
350
|
+
break;
|
|
318
351
|
out += row[i];
|
|
319
352
|
width += cw;
|
|
320
353
|
i += 1;
|
|
321
354
|
}
|
|
322
|
-
return out
|
|
355
|
+
return `${out}${palette().reset}\u2026`;
|
|
323
356
|
}
|
|
324
357
|
/** TUI2-R1 (D) — the keys as ONE line, for /help. The same table the
|
|
325
358
|
* sheet renders, joined — so the two can disagree only by deleting a
|
|
@@ -340,24 +373,38 @@ export function keysHelpRow() {
|
|
|
340
373
|
*/
|
|
341
374
|
export function helpRows() {
|
|
342
375
|
const p = palette();
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
376
|
+
// DC-1: ONE description column. The gap used to be four spaces after
|
|
377
|
+
// the name whatever the name's length, so `/help`'s description began
|
|
378
|
+
// three columns left of `/compact`'s and the second column wandered
|
|
379
|
+
// down the list. displayWidth is the authority, as everywhere else.
|
|
380
|
+
const table = [
|
|
381
|
+
["/help", "print this list of commands"],
|
|
382
|
+
["/think", "show the last full thinking block"],
|
|
383
|
+
["/last", "show the most recent tool call's input and output"],
|
|
384
|
+
["/status", "show session id, event count, and context estimate"],
|
|
385
|
+
["/mode", "show the approval tier; /mode <name> switches (manual/default/accept-edits/plan/bypass)"],
|
|
386
|
+
["/model", "list model profiles; /model <name|provider/model> switches"],
|
|
387
|
+
["/compact", "summarize the older conversation to free context"],
|
|
388
|
+
["/clear", "start a fresh conversation (the old session stays resumable)"],
|
|
389
|
+
["/resume", "switch to another session; /resume <id> goes directly"],
|
|
390
|
+
["exit", "leave the session"],
|
|
391
|
+
// TUI2-R1 (D): the SENTENCE is deliberately unchanged. Deriving it
|
|
392
|
+
// from KEY_BINDINGS would be an improvement and it would also move
|
|
393
|
+
// an assertion outside the round's declared supersession classes,
|
|
394
|
+
// so the sheet stays the derived surface and this row keeps its
|
|
395
|
+
// words. `keysHelpRow()` exists for the round allowed to swap it;
|
|
396
|
+
// until then the drift guard is the test that every binding in the
|
|
397
|
+
// table is mentioned here. DC-1 changes the PADDING, not the words.
|
|
398
|
+
["keys", "enter sends \u00b7 ctrl+J newline (shift+enter where encoded) \u00b7 esc stops the run \u00b7 alt+\u23ce stops it and sends this instead \u00b7 @ files \u00b7 1-4 answers an ask"],
|
|
362
399
|
];
|
|
400
|
+
const stop = Math.max(...table.map(([name]) => displayWidth(name))) + 4;
|
|
401
|
+
const cmd = (name, desc) => `${p.bold}${name}${p.reset}${" ".repeat(stop - displayWidth(name))}${desc}`;
|
|
402
|
+
const rows = table.slice(0, -2).map(([name, desc]) => cmd(name, desc));
|
|
403
|
+
// the last call carries its own newline exactly as it did inline:
|
|
404
|
+
// bodyLog splits on \n, so `exit` and `keys` land as two rows from one
|
|
405
|
+
// call — the shape the KC1/KC2/KC3 gestures were added to, unchanged.
|
|
406
|
+
const [exitName, exitDesc] = table[table.length - 2];
|
|
407
|
+
const [keysName, keysDesc] = table[table.length - 1];
|
|
408
|
+
rows.push(`${cmd(exitName, exitDesc)}\n${cmd(keysName, keysDesc)}`);
|
|
409
|
+
return rows;
|
|
363
410
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui-cells",
|
|
3
|
-
"version": "0.16.
|
|
4
|
-
"description": "kiso tui-cells
|
|
3
|
+
"version": "0.16.3",
|
|
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",
|
|
7
7
|
"exports": {
|