@vincemakes/kiso-tui-cells 0.16.2 → 0.16.4
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 +4 -3
- package/dist/approval-panel.js +136 -39
- package/dist/components.d.ts +21 -8
- package/dist/components.js +144 -48
- 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 +73 -31
- package/dist/render.d.ts +47 -11
- package/dist/render.js +175 -20
- package/dist/strings.js +72 -28
- package/package.json +2 -2
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,23 @@ 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 block's own ``` RAILS already say "this
|
|
286
|
+
// is verbatim" (E2 replaced the `│` gutter this comment used to
|
|
287
|
+
// name with them); saying it twice cost legibility and bought
|
|
288
|
+
// nothing.
|
|
289
|
+
return foldLineWidth(src.slice(indent.length), W - visibleWidth(gutter), indent).map((r) => `${gutter}${r}`);
|
|
258
290
|
}
|
|
259
291
|
case "quote": {
|
|
260
292
|
const text = b.lines.map((l) => QUOTE.exec(l)?.[1] ?? l).join(" ");
|
|
261
|
-
|
|
293
|
+
// R2: one gutter glyph. A quote and a fenced block both say "this
|
|
294
|
+
// text is not mine", and the screen was saying it two ways — ▏
|
|
295
|
+
// here and │ for code. The fences took their own ``` rails, so │
|
|
296
|
+
// is free and the quote takes it.
|
|
297
|
+
const gutter = `${p.dim}\u2502${p.reset} `;
|
|
262
298
|
return wrap(`${p.dim}${inlineSpans(text, p.dim)}${p.reset}`, W - visibleWidth(gutter), "", "").map((r) => `${gutter}${r}`);
|
|
263
299
|
}
|
|
264
300
|
case "list":
|
|
@@ -310,7 +346,10 @@ export function inlineSpans(text, base) {
|
|
|
310
346
|
if (end > i) {
|
|
311
347
|
// a code span's content is LITERAL — no markers inside it mean
|
|
312
348
|
// anything, which is what makes `x | y` survive a table split
|
|
313
|
-
|
|
349
|
+
// DC-3: inline code is a SURFACE (`wash`), closed with washEnd
|
|
350
|
+
// rather than a reset so the span composes inside a heading's
|
|
351
|
+
// or a quote's own style.
|
|
352
|
+
out += `${p.wash}${text.slice(i + 1, end)}${p.washEnd}${base}`;
|
|
314
353
|
i = end + 1;
|
|
315
354
|
continue;
|
|
316
355
|
}
|
|
@@ -456,9 +495,12 @@ function listRows(b, W) {
|
|
|
456
495
|
}
|
|
457
496
|
flush();
|
|
458
497
|
const depth = Math.min(5, Math.floor(m[1].length / 2));
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
|
|
498
|
+
// E1: normalization stays — `-`, `*` and `+` all render as ONE
|
|
499
|
+
// marker, so the model's arbitrary choice never leaks onto the
|
|
500
|
+
// screen — but the marker is `- ` rather than `•`, so a copied list
|
|
501
|
+
// is still a list. A numbered list KEEPS its numbers (they are the
|
|
502
|
+
// author's meaning, not decoration).
|
|
503
|
+
const marker = /^\d/.test(m[2]) ? `${m[2]} ` : "- ";
|
|
462
504
|
lead = `${" ".repeat(depth + 1)}${marker}`;
|
|
463
505
|
text = m[3];
|
|
464
506
|
}
|
|
@@ -484,17 +526,17 @@ function tableRows(b, W) {
|
|
|
484
526
|
if (t === null)
|
|
485
527
|
return b.lines.flatMap((l) => wrap(l, W, "", ""));
|
|
486
528
|
const cols = t.header.map((h, i) => Math.max(cellWidth(h), ...t.rows.map((r) => cellWidth(r[i] ?? ""))));
|
|
487
|
-
//
|
|
488
|
-
|
|
529
|
+
// R2: no rails. The drawn width is two columns of inset plus the
|
|
530
|
+
// columns and their two-space gutters — a table is bounded by the
|
|
531
|
+
// blank lines above and below it, exactly as every other block on the
|
|
532
|
+
// screen is, and it was the last box left on a screen that has decided
|
|
533
|
+
// not to have boxes. Alignment does the work the rails were doing, and
|
|
534
|
+
// a copied table is closer to markdown without them.
|
|
535
|
+
const total = cols.reduce((n, w) => n + w + 2, 2);
|
|
489
536
|
if (total > W)
|
|
490
537
|
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
|
-
];
|
|
538
|
+
const row = (cells, bold) => ` ${cells.map((c, i) => pad(c, cols[i], t.align[i], bold)).join(" ")}`.replace(/\s+$/, "");
|
|
539
|
+
return [row(t.header, true), ...t.rows.map((r) => row(r, false))];
|
|
498
540
|
}
|
|
499
541
|
/** A cell's column count: what a human sees, styling removed. */
|
|
500
542
|
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
|
|
@@ -102,17 +141,14 @@ export declare function kUnit(value: number | null): string;
|
|
|
102
141
|
* this verbatim; the render tests pin the sequence.
|
|
103
142
|
*/
|
|
104
143
|
export declare function renderTerminalGap(statusLine: string | null): string;
|
|
105
|
-
/**
|
|
106
|
-
* v3 §01 (V6-2) — the banner, block-split. The logo is THREE BRICK rows
|
|
107
|
-
* (the logo.svg pixel form — K I S O), then a BLANK, then the info rows:
|
|
108
|
-
* "kiso vX — tagline" + extensions. The tagline rides the version line
|
|
109
|
-
* (the old logo MIDDLE row was the tagline — a text row masquerading as
|
|
110
|
-
* the logo's centre). Every row truncates at the terminal width with a
|
|
111
|
-
* " (+N)" marker (N = the hidden display width); a window narrower than
|
|
112
|
-
* 40 columns skips the logo + the blank entirely — only the info rows.
|
|
113
|
-
* Pure.
|
|
114
|
-
*/
|
|
115
144
|
export declare const TAGLINE = "the coding agent that survives kill -9";
|
|
145
|
+
/** R2 — what the opening knows about the session. Optional because the
|
|
146
|
+
* off-TTY caller prints a banner before a model is bound. */
|
|
147
|
+
export interface BannerMeta {
|
|
148
|
+
readonly model: string;
|
|
149
|
+
readonly mode: string;
|
|
150
|
+
readonly cwd: string;
|
|
151
|
+
}
|
|
116
152
|
/** v3 §01 (W1): truncate a row at `width`, marking the hidden span
|
|
117
153
|
* " (+N)". W1: the width math is the charWidth authority (the banner's
|
|
118
154
|
* brick glyphs are 1 cell — the art's 38 columns clear 40), and the
|
|
@@ -129,7 +165,7 @@ export declare function truncateRow(row: string, width: number): string;
|
|
|
129
165
|
* text row does not repeat the name — then extensions — then the W5
|
|
130
166
|
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
131
167
|
* with a " (+N)" marker. Pure. */
|
|
132
|
-
export declare function bannerLines(W: number, H: number, version: string, extensionsText: string, resume?: readonly ResumeMeta[], now?: number): string[];
|
|
168
|
+
export declare function bannerLines(W: number, H: number, version: string, extensionsText: string, resume?: readonly ResumeMeta[], now?: number, meta?: BannerMeta | undefined): string[];
|
|
133
169
|
/** W5 — the opening-screen resume list. Every field already exists
|
|
134
170
|
* behind renderSessionLine / `kiso sessions`: the relative time, the
|
|
135
171
|
* title, then the right-aligned "N events · M runs". The columns are
|
package/dist/render.js
CHANGED
|
@@ -6,8 +6,56 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* DC-9 (design §2.3) — the failure colour is theme-resolved.
|
|
12
|
+
*
|
|
13
|
+
* ANSI 31 is 5.89:1 on a white ground and 2.83:1 on a dark one: the one
|
|
14
|
+
* token in the alphabet whose whole job is "this went wrong" was the
|
|
15
|
+
* least readable thing on the screen exactly where a dark-terminal user
|
|
16
|
+
* reads it. A failure is CONTENT (law 1.2 admits colour there), so it
|
|
17
|
+
* cannot degrade to an attribute the way `dim` does — it needs a value
|
|
18
|
+
* per ground, and the ground is what §3's ladder is for.
|
|
19
|
+
*
|
|
20
|
+
* 256-cube indices, never truecolor (§2). Measured against the grounds
|
|
21
|
+
* §2 measures against — white, and #1E1E1E:
|
|
22
|
+
*
|
|
23
|
+
* light 124 `#af0000` 7.44:1
|
|
24
|
+
* dark 173 `#d7875f` 5.97:1
|
|
25
|
+
*
|
|
26
|
+
* With NO ground established the token stays ANSI 31 — the TERMINAL's
|
|
27
|
+
* own red, which its theme picked for its own background. That is rung
|
|
28
|
+
* 4's principle exactly: when the ground is unknown, use the thing that
|
|
29
|
+
* is correct on any ground rather than guessing one.
|
|
30
|
+
*/
|
|
31
|
+
const withWash = (wash, washEnd, red = BASE.red) => ({ ...BASE, red, wash, washEnd, code: wash });
|
|
32
|
+
/**
|
|
33
|
+
* DC-3 — one table per ground.
|
|
34
|
+
*
|
|
35
|
+
* `dim` is the same in all three ON tables and that is the point: SGR 2
|
|
36
|
+
* is an ATTRIBUTE, it dims whatever the terminal's own foreground is, so
|
|
37
|
+
* it adapts to the ground instead of asserting one. Only the background
|
|
38
|
+
* genuinely needs to know, which is why `wash` is the only member that
|
|
39
|
+
* varies.
|
|
40
|
+
*/
|
|
41
|
+
export const COLOR_NEUTRAL = withWash("\x1b[7m", "\x1b[27m");
|
|
42
|
+
export const COLOR_LIGHT = withWash("\x1b[48;5;255m", "\x1b[49m", "\x1b[38;5;124m");
|
|
43
|
+
export const COLOR_DARK = withWash("\x1b[48;5;236m", "\x1b[49m", "\x1b[38;5;173m");
|
|
44
|
+
/** The historical name — the palette for a colour TTY whose ground has
|
|
45
|
+
* not been established. Unchanged in every byte except `code`, which
|
|
46
|
+
* was the defect. */
|
|
47
|
+
export const COLOR_ON = COLOR_NEUTRAL;
|
|
48
|
+
export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", reset: "" };
|
|
49
|
+
/** DC-3 — the resolved ground, set once at startup when the terminal
|
|
50
|
+
* answers (see `ground.ts`). It starts UNKNOWN and may stay that way
|
|
51
|
+
* forever; that is a supported state, not a failure. */
|
|
52
|
+
let ground = "unknown";
|
|
53
|
+
export function setGround(g) {
|
|
54
|
+
ground = g;
|
|
55
|
+
}
|
|
56
|
+
export function currentGround() {
|
|
57
|
+
return ground;
|
|
58
|
+
}
|
|
11
59
|
export function palette() {
|
|
12
60
|
// PH-1a (finding PH-F5): the no-color.org contract is "present AND
|
|
13
61
|
// non-empty" — the old `=== undefined` check let an EMPTY `NO_COLOR=`
|
|
@@ -16,7 +64,9 @@ export function palette() {
|
|
|
16
64
|
// UI with them. The v4-round plan recorded this as debugging pitfall ①;
|
|
17
65
|
// it was a bug.
|
|
18
66
|
const noColor = process.env.NO_COLOR;
|
|
19
|
-
|
|
67
|
+
if (!((noColor === undefined || noColor === "") && process.stdout.isTTY))
|
|
68
|
+
return COLOR_OFF;
|
|
69
|
+
return ground === "light" ? COLOR_LIGHT : ground === "dark" ? COLOR_DARK : COLOR_NEUTRAL;
|
|
20
70
|
}
|
|
21
71
|
/**
|
|
22
72
|
* E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
@@ -161,14 +211,49 @@ export function renderTerminalGap(statusLine) {
|
|
|
161
211
|
* 40 columns skips the logo + the blank entirely — only the info rows.
|
|
162
212
|
* Pure.
|
|
163
213
|
*/
|
|
214
|
+
/** DC-18: the display-width prefix of PLAIN text. `widthCut` lives in
|
|
215
|
+
* components.ts, which imports this module — the dependency runs one
|
|
216
|
+
* way, so the four lines live here rather than inverting it. */
|
|
217
|
+
function plainCut(text, max) {
|
|
218
|
+
let w = 0;
|
|
219
|
+
let i = 0;
|
|
220
|
+
for (; i < text.length; i += 1) {
|
|
221
|
+
const cw = charWidth(text.codePointAt(i));
|
|
222
|
+
if (w + cw > max)
|
|
223
|
+
break;
|
|
224
|
+
w += cw;
|
|
225
|
+
}
|
|
226
|
+
return text.slice(0, i);
|
|
227
|
+
}
|
|
164
228
|
export const TAGLINE = "the coding agent that survives kill -9";
|
|
165
|
-
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
|
|
229
|
+
/**
|
|
230
|
+
* R2 — the wordmark is retired (2026-08-27, the nineteen-screen review).
|
|
231
|
+
*
|
|
232
|
+
* TT-1B had already cut the 36x6 pixel art down to two rows because a
|
|
233
|
+
* tall banner's mid-scroll cut state renders as glyph garbage. The
|
|
234
|
+
* remaining two rows go now for a different reason: they say the word
|
|
235
|
+
* `kiso` in fifteen columns of block glyphs, and the word `kiso` says it
|
|
236
|
+
* in four. A rendered clover mark was tried first, at 4x2, 10x5, 14x7
|
|
237
|
+
* and 16x8, and rejected on measurement — below fourteen columns the
|
|
238
|
+
* centre star closes and the mark reads as a domino, and at fourteen it
|
|
239
|
+
* costs seven rows.
|
|
240
|
+
*
|
|
241
|
+
* What takes the room is not decoration. A first screen is asked three
|
|
242
|
+
* questions — what model, where am I, what is loaded — and it now
|
|
243
|
+
* answers them in one aligned column.
|
|
244
|
+
*/
|
|
245
|
+
/** R2 — the keys a first screen teaches. One dim row, and deliberately
|
|
246
|
+
* NOT derived from KEY_BINDINGS: the sheet is the complete list and
|
|
247
|
+
* this is the opening's five, chosen rather than generated. */
|
|
248
|
+
// R2: the keys row names bindings the product ACTUALLY has. The first
|
|
249
|
+
// draft advertised `! bash` — there is no bang passthrough in kiso and
|
|
250
|
+
// KEY_BINDINGS never had one, so the opening screen was teaching a key
|
|
251
|
+
// that does nothing. A first screen that lies is worse than a short one.
|
|
252
|
+
const BANNER_KEYS = "esc interrupt · ctrl+c exit · / commands · @ files · ? keys";
|
|
253
|
+
/** R2 — the labels. Uppercase mono, dim, letter-spaced by the column
|
|
254
|
+
* rather than by SGR: they mark sections and are never content. */
|
|
255
|
+
const BANNER_LABELS = ["MODEL", "WORKSPACE", "EXTENSIONS"];
|
|
256
|
+
const LABEL_STOP = Math.max(...BANNER_LABELS.map((l) => l.length)) + 2;
|
|
172
257
|
/** v3 §01 (W1): truncate a row at `width`, marking the hidden span
|
|
173
258
|
* " (+N)". W1: the width math is the charWidth authority (the banner's
|
|
174
259
|
* brick glyphs are 1 cell — the art's 38 columns clear 40), and the
|
|
@@ -179,6 +264,13 @@ export function truncateRow(row, width) {
|
|
|
179
264
|
const total = displayWidth(row);
|
|
180
265
|
if (total <= width)
|
|
181
266
|
return row;
|
|
267
|
+
// DC-18: a width too narrow to HOLD the marker gets a hard cut. The
|
|
268
|
+
// fixpoint below floors `cut` at 0 and then appends a 6-cell marker
|
|
269
|
+
// regardless, so every width ≤ 6 returned a row WIDER than the
|
|
270
|
+
// terminal — and invariant ① throws rather than truncating. A marker
|
|
271
|
+
// wider than the row it marks is not a marker.
|
|
272
|
+
if (width < 7)
|
|
273
|
+
return plainCut(row, Math.max(0, width));
|
|
182
274
|
// iterate the marker to a fixpoint: the marker's width changes the
|
|
183
275
|
// cut, the cut changes the hidden count the marker reports
|
|
184
276
|
let marker = " (+0)";
|
|
@@ -209,17 +301,80 @@ export function truncateRow(row, width) {
|
|
|
209
301
|
* text row does not repeat the name — then extensions — then the W5
|
|
210
302
|
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
211
303
|
* with a " (+N)" marker. Pure. */
|
|
212
|
-
export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now()) {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
304
|
+
export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now(), meta) {
|
|
305
|
+
const p = palette();
|
|
306
|
+
// R2: the banner styles itself per span. It used to be wrapped in one
|
|
307
|
+
// blanket dim by its component, which made the answers as faint as the
|
|
308
|
+
// labels asking the questions — the labels are the quiet half, the
|
|
309
|
+
// values are what a human came to read.
|
|
310
|
+
//
|
|
311
|
+
// Every width decision below is taken on PLAIN text and the styling is
|
|
312
|
+
// applied after, because truncateRow measures with displayWidth, which
|
|
313
|
+
// counts SGR bytes as columns. Style then measure is a bug waiting.
|
|
314
|
+
// DC-18: the name row is CUT like every other row here. It was the one
|
|
315
|
+
// row in this function pushed unguarded, so at W ≤ 10 `kiso 0.16.4`
|
|
316
|
+
// measured 11 cells and invariant ① threw AT STARTUP — the function
|
|
317
|
+
// whose own comment preaches "invariant ① holds at every width".
|
|
318
|
+
// The cut is taken on the plain text, per the note above.
|
|
319
|
+
const namePlain = plainCut(`kiso ${version}`, Math.max(1, W));
|
|
320
|
+
const nameCut = namePlain.slice(0, 4); // "kiso", or its surviving prefix
|
|
321
|
+
const verCut = namePlain.slice(5); // the version, if the width left room for it
|
|
322
|
+
const rows = [`${p.bold}${nameCut}${p.reset}${verCut === "" ? "" : `${p.dim} ${verCut}${p.reset}`}`];
|
|
323
|
+
const facts = [];
|
|
324
|
+
if (meta !== undefined) {
|
|
325
|
+
facts.push([BANNER_LABELS[0], `${meta.model}${meta.mode === "" ? "" : ` · ${meta.mode}`}`], [BANNER_LABELS[1], meta.cwd]);
|
|
217
326
|
}
|
|
218
|
-
if (rows.length > 0)
|
|
219
|
-
rows.push("");
|
|
220
|
-
rows.push(truncateRow(`v${version} — ${TAGLINE}`, W));
|
|
221
327
|
if (extensionsText !== "")
|
|
222
|
-
|
|
328
|
+
facts.push([BANNER_LABELS[2], extensionsText]);
|
|
329
|
+
if (facts.length > 0) {
|
|
330
|
+
rows.push("");
|
|
331
|
+
// The value column HANGS rather than truncating. The label costs
|
|
332
|
+
// columns the value used to have, and an extension list cut at the
|
|
333
|
+
// width would hide which extensions loaded — on the one screen whose
|
|
334
|
+
// job is to say what is loaded.
|
|
335
|
+
const indent = 2 + LABEL_STOP;
|
|
336
|
+
// a terminal too narrow to hold the label column at all: the room is
|
|
337
|
+
// what is left, floored at one column, and the assembled row is
|
|
338
|
+
// truncated as a unit so invariant ① holds at every width.
|
|
339
|
+
const room = Math.max(1, W - indent);
|
|
340
|
+
for (const [label, value] of facts) {
|
|
341
|
+
const lead = ` ${p.dim}${label}${p.reset}${" ".repeat(LABEL_STOP - label.length)}`;
|
|
342
|
+
const hang = " ".repeat(indent);
|
|
343
|
+
const lines = [];
|
|
344
|
+
let line = "";
|
|
345
|
+
for (const word of value.split(" ")) {
|
|
346
|
+
if (line === "")
|
|
347
|
+
line = word;
|
|
348
|
+
else if (displayWidth(`${line} ${word}`) <= room)
|
|
349
|
+
line += ` ${word}`;
|
|
350
|
+
else {
|
|
351
|
+
lines.push(line);
|
|
352
|
+
line = word;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (line !== "")
|
|
356
|
+
lines.push(line);
|
|
357
|
+
for (const [i, l] of lines.entries()) {
|
|
358
|
+
const styled = `${i === 0 ? lead : hang}${truncateRow(l, room)}`;
|
|
359
|
+
rows.push(displayWidth(styled) - (i === 0 ? p.dim.length + p.reset.length : 0) <= W ? styled : truncateRow(`${hang}${l}`, W));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (meta !== undefined && W >= 40) {
|
|
364
|
+
// R2/DC-2's device: the keys row is a list of independent clauses,
|
|
365
|
+
// so a narrow terminal drops whole clauses from the end rather than
|
|
366
|
+
// cutting one in half. `ctrl+r ex (+8)` teaches nothing.
|
|
367
|
+
const clauses = BANNER_KEYS.split(" \u00b7 ");
|
|
368
|
+
let keys = clauses[0];
|
|
369
|
+
for (let n = clauses.length; n > 1; n -= 1) {
|
|
370
|
+
const row = clauses.slice(0, n).join(" \u00b7 ");
|
|
371
|
+
if (displayWidth(row) <= W - 2) {
|
|
372
|
+
keys = row;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
rows.push("", ` ${p.dim}${truncateRow(keys, W - 2)}${p.reset}`);
|
|
377
|
+
}
|
|
223
378
|
if (W >= 40 && H >= 20 && resume.length > 0) {
|
|
224
379
|
rows.push("", ...renderResumeList(resume, W, now));
|
|
225
380
|
}
|
|
@@ -259,7 +414,7 @@ function titleCut(text, max) {
|
|
|
259
414
|
export function renderResumeList(metas, W, now) {
|
|
260
415
|
if (metas.length === 0)
|
|
261
416
|
return [];
|
|
262
|
-
const rows = ["
|
|
417
|
+
const rows = [" ✦ resume"]; // R2: the ONE fold/segment mark (§4.2)
|
|
263
418
|
const whens = metas.map((m) => relativeTime(m.updatedAt, now));
|
|
264
419
|
const metaTexts = metas.map((m) => `${m.events} events · ${m.runs} runs`);
|
|
265
420
|
const metaW = Math.max(...metaTexts.map((t) => t.length));
|