@vincemakes/kiso-tui-cells 0.46.1 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-panel.d.ts +149 -62
- package/dist/approval-panel.js +346 -227
- package/dist/components.d.ts +151 -21
- package/dist/components.js +1190 -821
- package/dist/diff.d.ts +40 -3
- package/dist/diff.js +142 -5
- package/dist/graphite.d.ts +123 -0
- package/dist/graphite.js +257 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -3
- package/dist/md.d.ts +18 -7
- package/dist/md.js +290 -37
- package/dist/render.d.ts +136 -27
- package/dist/render.js +381 -137
- package/dist/strings.d.ts +47 -46
- package/dist/strings.js +130 -171
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* dependencies (the tui-cells package has none).
|
|
7
7
|
*/
|
|
8
8
|
import { charWidth, displayWidth, visibleWidth, widthCut } from "./width.js";
|
|
9
|
+
import { bg, breathRamp, colourTier, fg, graphiteColours, mix } from "./graphite.js";
|
|
9
10
|
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
|
/**
|
|
11
12
|
* DC-9 (design §2.3) — the failure colour is theme-resolved.
|
|
@@ -17,11 +18,8 @@ const BASE = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32
|
|
|
17
18
|
* cannot degrade to an attribute the way `dim` does — it needs a value
|
|
18
19
|
* per ground, and the ground is what §3's ladder is for.
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* light 124 `#af0000` 7.44:1
|
|
24
|
-
* dark 173 `#d7875f` 5.97:1
|
|
21
|
+
* Its values are Graphite's `fail` (design.md §2), written in the
|
|
22
|
+
* resolved tier.
|
|
25
23
|
*
|
|
26
24
|
* With NO ground established the token stays ANSI 31 — the TERMINAL's
|
|
27
25
|
* own red, which its theme picked for its own background. That is the last rung
|
|
@@ -31,8 +29,44 @@ const BASE = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32
|
|
|
31
29
|
/** `washDimEnd` is DERIVED, never passed: a grey that cannot be closed
|
|
32
30
|
* without taking the wash with it is not a usable token, and deriving
|
|
33
31
|
* the close makes the pair impossible to mis-wire at a call site. */
|
|
32
|
+
/** The Graphite members, empty — the unknown ground's and colour-off's. */
|
|
33
|
+
const NO_GRAPHITE = {
|
|
34
|
+
ink2: "",
|
|
35
|
+
rail: "",
|
|
36
|
+
line: "",
|
|
37
|
+
gold: "",
|
|
38
|
+
goldMark: "",
|
|
39
|
+
blue: "",
|
|
40
|
+
ok: "",
|
|
41
|
+
fail: "",
|
|
42
|
+
humanInk: "",
|
|
43
|
+
track: "",
|
|
44
|
+
washRun: "",
|
|
45
|
+
washDone: "",
|
|
46
|
+
washFail: "",
|
|
47
|
+
washAsk: "",
|
|
48
|
+
human: "",
|
|
49
|
+
codeBg: "",
|
|
50
|
+
add: "",
|
|
51
|
+
del: "",
|
|
52
|
+
addWord: "",
|
|
53
|
+
delWord: "",
|
|
54
|
+
edge: "",
|
|
55
|
+
quoteBar: "",
|
|
56
|
+
noteBar: "",
|
|
57
|
+
warnBar: "",
|
|
58
|
+
cautionBar: "",
|
|
59
|
+
runEdge: "",
|
|
60
|
+
failEdge: "",
|
|
61
|
+
askEdge: "",
|
|
62
|
+
humanEdge: "",
|
|
63
|
+
fgEnd: "",
|
|
64
|
+
breath: [],
|
|
65
|
+
tier: null,
|
|
66
|
+
};
|
|
34
67
|
const withWash = (wash, washEnd, red = BASE.red, dim = BASE.dim, washDim = "") => ({
|
|
35
68
|
...BASE,
|
|
69
|
+
...NO_GRAPHITE,
|
|
36
70
|
red,
|
|
37
71
|
dim,
|
|
38
72
|
wash,
|
|
@@ -41,12 +75,88 @@ const withWash = (wash, washEnd, red = BASE.red, dim = BASE.dim, washDim = "") =
|
|
|
41
75
|
washDimEnd: washDim === "" ? "" : "\x1b[39m",
|
|
42
76
|
code: wash,
|
|
43
77
|
});
|
|
78
|
+
/**
|
|
79
|
+
* Graphite (design.md §2, §3.4) — the palette for a KNOWN ground.
|
|
80
|
+
*
|
|
81
|
+
* The colours come from `graphite.ts`: the table's values for the kind,
|
|
82
|
+
* with the surfaces derived from the reported ground when there is one.
|
|
83
|
+
* The members the pre-Graphite code reads keep their names and take the
|
|
84
|
+
* Graphite value of the same job: `dim` the dim token, `red` the failure
|
|
85
|
+
* colour, `green` the success colour, `warn` gold (the uncertain badge
|
|
86
|
+
* is a question for the person, which is gold's meaning), `wash` the
|
|
87
|
+
* settled card's ground, and `washDim` the dim token — which clears the
|
|
88
|
+
* floor on every card ground (§2.1), so the separate grey retires in
|
|
89
|
+
* value while the name stays for its call sites.
|
|
90
|
+
*/
|
|
91
|
+
/** How far a card's edge cell is deepened from its ground toward the
|
|
92
|
+
* state's colour — quiet enough to read as the card's own edge, not a
|
|
93
|
+
* stripe (the owner's seam test, B6, 2026-09-29). */
|
|
94
|
+
const EDGE_DEPTH = 0.22;
|
|
95
|
+
/** The person's edge is gold, quieted toward the warm ground so a whole
|
|
96
|
+
* cell reads as an edge and not a stripe (the seam test's B4). */
|
|
97
|
+
const HUMAN_EDGE_DEPTH = 0.55;
|
|
98
|
+
/** How far a changed word's ground is deepened from its row's toward the
|
|
99
|
+
* row's colour: enough to find the word, not so much that ink stops
|
|
100
|
+
* reading on it. */
|
|
101
|
+
const WORD_DEPTH = 0.3;
|
|
102
|
+
/** How far a quote's or an alert's bar is deepened from the ground toward
|
|
103
|
+
* its colour: a bar, not a stripe (the person's edge's measure). */
|
|
104
|
+
const BAR_DEPTH = 0.5;
|
|
105
|
+
export function paletteFor(kind, ground, tier) {
|
|
106
|
+
const c = graphiteColours(kind, ground, tier);
|
|
107
|
+
const f = (x) => fg(x, tier);
|
|
108
|
+
const b = (x) => bg(x, tier);
|
|
109
|
+
return {
|
|
110
|
+
...BASE,
|
|
111
|
+
dim: f(c.dim),
|
|
112
|
+
red: f(c.fail),
|
|
113
|
+
green: f(c.ok),
|
|
114
|
+
warn: f(c.gold),
|
|
115
|
+
wash: b(c.washDone),
|
|
116
|
+
washEnd: "\x1b[49m",
|
|
117
|
+
washDim: f(c.dim),
|
|
118
|
+
washDimEnd: "\x1b[39m",
|
|
119
|
+
code: b(c.washDone),
|
|
120
|
+
ink2: f(c.ink2),
|
|
121
|
+
rail: f(c.rail),
|
|
122
|
+
line: f(c.line),
|
|
123
|
+
gold: f(c.gold),
|
|
124
|
+
goldMark: f(c.goldMark),
|
|
125
|
+
blue: f(c.blue),
|
|
126
|
+
ok: f(c.ok),
|
|
127
|
+
fail: f(c.fail),
|
|
128
|
+
humanInk: f(c.humanInk),
|
|
129
|
+
track: f(c.track),
|
|
130
|
+
washRun: b(c.washRun),
|
|
131
|
+
washDone: b(c.washDone),
|
|
132
|
+
washFail: b(c.washFail),
|
|
133
|
+
washAsk: b(c.washAsk),
|
|
134
|
+
human: b(c.human),
|
|
135
|
+
codeBg: b(c.code),
|
|
136
|
+
add: b(c.add),
|
|
137
|
+
del: b(c.del),
|
|
138
|
+
addWord: b(mix(c.add, c.ok, WORD_DEPTH)),
|
|
139
|
+
delWord: b(mix(c.del, c.fail, WORD_DEPTH)),
|
|
140
|
+
edge: f(mix(c.line, c.rail, 0.5)),
|
|
141
|
+
quoteBar: b(mix(c.ground, c.rail, BAR_DEPTH)),
|
|
142
|
+
noteBar: b(mix(c.ground, c.blue, BAR_DEPTH)),
|
|
143
|
+
warnBar: b(mix(c.ground, c.goldMark, BAR_DEPTH)),
|
|
144
|
+
cautionBar: b(mix(c.ground, c.fail, BAR_DEPTH)),
|
|
145
|
+
runEdge: b(mix(c.washRun, c.blue, EDGE_DEPTH)),
|
|
146
|
+
failEdge: b(mix(c.washFail, c.fail, EDGE_DEPTH)),
|
|
147
|
+
askEdge: b(mix(c.washAsk, c.goldMark, EDGE_DEPTH)),
|
|
148
|
+
humanEdge: b(mix(c.human, c.goldMark, HUMAN_EDGE_DEPTH)),
|
|
149
|
+
fgEnd: "\x1b[39m",
|
|
150
|
+
breath: breathRamp(c).map(f),
|
|
151
|
+
tier,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
44
154
|
/**
|
|
45
155
|
* DC-3 — one table per ground.
|
|
46
156
|
*
|
|
47
|
-
* R3 (owner, 2026-08-27) — `dim` is ABSOLUTE once the ground is known
|
|
48
|
-
*
|
|
49
|
-
*
|
|
157
|
+
* R3 (owner, 2026-08-27) — `dim` is ABSOLUTE once the ground is known:
|
|
158
|
+
* Graphite's `dim`, measured against the floor on every surface it can
|
|
159
|
+
* reach (design.md §2.1).
|
|
50
160
|
*
|
|
51
161
|
* DC-3 shipped SGR 2 instead, on the argument that an attribute adapts
|
|
52
162
|
* to the ground while an absolute grey asserts one. That argument is
|
|
@@ -62,23 +172,39 @@ const withWash = (wash, washEnd, red = BASE.red, dim = BASE.dim, washDim = "") =
|
|
|
62
172
|
* attribute is exactly the "correct on any ground" degradation there.
|
|
63
173
|
*/
|
|
64
174
|
export const COLOR_NEUTRAL = withWash("\x1b[7m", "\x1b[27m");
|
|
65
|
-
|
|
66
|
-
|
|
175
|
+
/** The two reference palettes: Graphite on its reference grounds, in the
|
|
176
|
+
* 24-bit tier. `paletteFor` is the general form. */
|
|
177
|
+
export const COLOR_LIGHT = paletteFor("light", null, "24bit");
|
|
178
|
+
export const COLOR_DARK = paletteFor("dark", null, "24bit");
|
|
67
179
|
/** The historical name — the palette for a colour TTY whose ground has
|
|
68
180
|
* not been established. Unchanged in every byte except `code`, which
|
|
69
181
|
* was the defect. */
|
|
70
182
|
export const COLOR_ON = COLOR_NEUTRAL;
|
|
71
|
-
export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", washDim: "", washDimEnd: "", reset: "" };
|
|
183
|
+
export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", warn: "", code: "", italic: "", italicEnd: "", underline: "", underlineEnd: "", rv: "", rvEnd: "", wash: "", washEnd: "", washDim: "", washDimEnd: "", reset: "", ...NO_GRAPHITE };
|
|
72
184
|
/** DC-3 — the resolved ground, set once at startup when the terminal
|
|
73
185
|
* answers (see `ground.ts`). It starts UNKNOWN and may stay that way
|
|
74
186
|
* forever; that is a supported state, not a failure. */
|
|
75
187
|
let ground = "unknown";
|
|
76
|
-
|
|
188
|
+
/** §3.4 — the colour the terminal reported, when it reported one; the
|
|
189
|
+
* surfaces are derived from it. Null when the ground was resolved
|
|
190
|
+
* without a colour. */
|
|
191
|
+
let groundRgb = null;
|
|
192
|
+
export function setGround(g, rgb = null) {
|
|
77
193
|
ground = g;
|
|
194
|
+
groundRgb = g === "unknown" ? null : rgb;
|
|
78
195
|
}
|
|
79
196
|
export function currentGround() {
|
|
80
197
|
return ground;
|
|
81
198
|
}
|
|
199
|
+
export function currentGroundRgb() {
|
|
200
|
+
return groundRgb;
|
|
201
|
+
}
|
|
202
|
+
/** One palette per (ground, reported colour, tier): `palette()` runs on
|
|
203
|
+
* every render and the derivation is not free, so the last answer is
|
|
204
|
+
* kept and reused while none of its inputs changed. The inputs are
|
|
205
|
+
* compared field by field (the render-perf pass, 2026-10-07): a key
|
|
206
|
+
* string built on every call was the hottest line of a Graphite frame. */
|
|
207
|
+
let memo = null;
|
|
82
208
|
export function palette() {
|
|
83
209
|
// PH-1a (finding PH-F5): the no-color.org contract is "present AND
|
|
84
210
|
// non-empty" — the old `=== undefined` check let an EMPTY `NO_COLOR=`
|
|
@@ -89,7 +215,15 @@ export function palette() {
|
|
|
89
215
|
const noColor = process.env.NO_COLOR;
|
|
90
216
|
if (!((noColor === undefined || noColor === "") && process.stdout.isTTY))
|
|
91
217
|
return COLOR_OFF;
|
|
92
|
-
|
|
218
|
+
if (ground === "unknown")
|
|
219
|
+
return COLOR_NEUTRAL;
|
|
220
|
+
const tier = colourTier(process.env.COLORTERM);
|
|
221
|
+
const r = groundRgb === null ? -1 : groundRgb.r;
|
|
222
|
+
const g = groundRgb === null ? -1 : groundRgb.g;
|
|
223
|
+
const b = groundRgb === null ? -1 : groundRgb.b;
|
|
224
|
+
if (memo === null || memo.ground !== ground || memo.tier !== tier || memo.r !== r || memo.g !== g || memo.b !== b)
|
|
225
|
+
memo = { ground, r, g, b, tier, p: paletteFor(ground, groundRgb, tier) };
|
|
226
|
+
return memo.p;
|
|
93
227
|
}
|
|
94
228
|
/**
|
|
95
229
|
* E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
@@ -362,23 +496,45 @@ export function renderTerminalGap(statusLine) {
|
|
|
362
496
|
* the transition. Every glyph is in Menlo and absent from Apple Color
|
|
363
497
|
* Emoji (§6.1's test, run). */
|
|
364
498
|
export const TWINKLE = ["\u2727", "\u2726", "\u2736", "\u2738", "\u273a", "\u2738", "\u2726"];
|
|
365
|
-
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Graphite §7.8 — the composer's top rule: `gold-mark` for its first eighth,
|
|
501
|
+
* fading to `line` by a third of the width, `line` after. Drawn in a
|
|
502
|
+
* handful of runs, not a colour per cell, so the row costs a few escapes
|
|
503
|
+
* rather than one per column. Off a known ground it is the plain dim rule.
|
|
504
|
+
*/
|
|
505
|
+
export function fadeRule(W) {
|
|
506
|
+
const p = palette();
|
|
507
|
+
const n = Math.max(0, W);
|
|
508
|
+
if (p.tier === null || ground === "unknown")
|
|
509
|
+
return `${p.dim}${"\u2500".repeat(n)}${p.reset}`;
|
|
510
|
+
const c = graphiteColours(ground, groundRgb, p.tier);
|
|
511
|
+
const solid = Math.max(1, Math.round(n / 8));
|
|
512
|
+
const end = Math.max(solid + 1, Math.round(n / 3));
|
|
513
|
+
const STEPS = 6;
|
|
514
|
+
let out = `${fg(c.goldMark, p.tier)}${"\u2500".repeat(Math.min(n, solid))}`;
|
|
515
|
+
let at = solid;
|
|
516
|
+
for (let k = 1; k <= STEPS && at < Math.min(n, end); k += 1) {
|
|
517
|
+
const to = k === STEPS ? Math.min(n, end) : Math.min(n, solid + Math.round(((end - solid) * k) / STEPS));
|
|
518
|
+
if (to > at)
|
|
519
|
+
out += `${fg(mix(c.goldMark, c.line, k / (STEPS + 1)), p.tier)}${"\u2500".repeat(to - at)}`;
|
|
520
|
+
at = Math.max(at, to);
|
|
521
|
+
}
|
|
522
|
+
if (at < n)
|
|
523
|
+
out += `${fg(c.line, p.tier)}${"\u2500".repeat(n - at)}`;
|
|
524
|
+
return `${out}${p.fgEnd}`;
|
|
525
|
+
}
|
|
526
|
+
/** The breath's frame: `●` at the step's brightness (§5.2 — seven steps
|
|
527
|
+
* of gold toward the running card's ground, never under the graphic
|
|
528
|
+
* floor, `graphite.ts` `breathRamp`). With no ground — or under
|
|
529
|
+
* NO_COLOR — it freezes to a static `●`, because a brightness ramp needs
|
|
530
|
+
* a background to be a ramp against and §3.1 forbids guessing one. The
|
|
531
|
+
* glyph never changes, so the freeze degrades the motion and never the
|
|
532
|
+
* meaning. */
|
|
376
533
|
export function breathFrame(step) {
|
|
377
534
|
const p = palette();
|
|
378
|
-
|
|
379
|
-
if (ramp === null || p.bold === "")
|
|
535
|
+
if (p.breath.length === 0)
|
|
380
536
|
return "\u25cf";
|
|
381
|
-
return
|
|
537
|
+
return `${p.breath[step % p.breath.length]}\u25cf${p.reset}`;
|
|
382
538
|
}
|
|
383
539
|
/** The twinkle's frame — pure glyph, no palette involved. */
|
|
384
540
|
export function twinkleFrame(step) {
|
|
@@ -389,33 +545,40 @@ export function twinkleFrame(step) {
|
|
|
389
545
|
export const MOTION_FRAMES = 7;
|
|
390
546
|
export const TAGLINE = "the coding agent that survives kill -9";
|
|
391
547
|
/**
|
|
392
|
-
*
|
|
548
|
+
* Graphite §7.10 — the opening: the wordmark, then what loaded.
|
|
393
549
|
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
550
|
+
* DECLARED REVERSAL (Graphite, owner-ruled 2026-09-28) of R2's "no logo,
|
|
551
|
+
* the name is the mark". R2 retired the wordmark because it cost the rows
|
|
552
|
+
* a first screen needed for three questions — what model, where am I, what
|
|
553
|
+
* is loaded. The model and the folder moved to the status bar (§8.9), what
|
|
554
|
+
* is loaded moved beside the wordmark, and the wordmark came back: ten
|
|
555
|
+
* rows, once, at the top of a session — including in the 80×24 window a
|
|
556
|
+
* Mac opens by default (owner, 2026-09-29: a wordmark the default window
|
|
557
|
+
* never shows is not worth drawing). Under 20 rows, on a terminal too
|
|
558
|
+
* narrow for it, and on a resume (the history is above the opening there,
|
|
559
|
+
* and ten rows of wordmark would bury its tail) the opening is one line.
|
|
402
560
|
*
|
|
403
|
-
*
|
|
404
|
-
* questions — what model, where am I, what is loaded — and it now
|
|
405
|
-
* answers them in one aligned column.
|
|
561
|
+
* The R2 keys row retires with it: `?` lists the keys (§8.5).
|
|
406
562
|
*/
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
563
|
+
export const MOTTO = "intent \u2192 effect \u2192 durable fact";
|
|
564
|
+
const WORDMARK = [
|
|
565
|
+
"\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
566
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2554\u255d\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557",
|
|
567
|
+
"\u2588\u2588\u2588\u2588\u2588\u2554\u255d \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
568
|
+
"\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2551\u255a\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
569
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d",
|
|
570
|
+
"\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d",
|
|
571
|
+
];
|
|
572
|
+
/** The wordmark's width in cells (its widest row). */
|
|
573
|
+
export const WORDMARK_W = Math.max(...WORDMARK.map((r) => displayWidth(r)));
|
|
574
|
+
/** The content edge (§1.8) the opening's rows start at. */
|
|
575
|
+
const OPENING_EDGE = 2;
|
|
576
|
+
/** §7.10: the facts sit beside the wordmark from this width, below it under. */
|
|
577
|
+
const FACTS_BESIDE_W = 96;
|
|
578
|
+
/** §7.10: under this height the opening is one line. */
|
|
579
|
+
const OPENING_TALL_H = 20;
|
|
580
|
+
/** The facts' label column: the longest label and two spaces. */
|
|
581
|
+
const FACT_LABEL_W = "EXTENSIONS".length + 2;
|
|
419
582
|
/** W20 — the ONE-ROW cut with the honest mark, SGR-aware. A line that
|
|
420
583
|
* fits (≤ W) passes through whole; an overflow cuts the content at
|
|
421
584
|
* W−1 — the ellipsis's slot — and the ellipsis rides AFTER the reset
|
|
@@ -493,105 +656,186 @@ export function truncateRow(row, width) {
|
|
|
493
656
|
marker = next;
|
|
494
657
|
}
|
|
495
658
|
}
|
|
496
|
-
/**
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
659
|
+
/**
|
|
660
|
+
* Graphite §7.10 — the opening's rows for a width W and height H. Pure;
|
|
661
|
+
* invariant ① holds at every width (each row is cut to W).
|
|
662
|
+
*
|
|
663
|
+
* - The tall form (H ≥ 30, W ≥ the wordmark at the content edge, not a
|
|
664
|
+
* resume): the wordmark, its rule, the tagline with the version, the
|
|
665
|
+
* motto. The facts sit beside it behind one hairline when W ≥ 96, and
|
|
666
|
+
* below it otherwise.
|
|
667
|
+
* - The one-line form otherwise: `✦ kiso <version> · <tagline>`, the
|
|
668
|
+
* facts below it.
|
|
669
|
+
*
|
|
670
|
+
* On a known ground the block cells take `mix(ink, dim, row / 4)` top to
|
|
671
|
+
* bottom, the box-drawing shadow `mix(rail, ground, 0.35)`, and the rule
|
|
672
|
+
* fades from `dim` to the ground; there is no gold (§1.2 — gold is the
|
|
673
|
+
* edge, and the opening has none). Off one the wordmark is the terminal's
|
|
674
|
+
* own foreground. `<version>` is the caller's, never a literal.
|
|
675
|
+
*/
|
|
505
676
|
export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now(), meta) {
|
|
506
677
|
const p = palette();
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
// measured 11 cells and invariant ① threw AT STARTUP — the function
|
|
518
|
-
// whose own comment preaches "invariant ① holds at every width".
|
|
519
|
-
// The cut is taken on the plain text, per the note above.
|
|
520
|
-
const namePlain = widthCut(`kiso ${version}`, Math.max(1, W));
|
|
521
|
-
const nameCut = namePlain.slice(0, 4); // "kiso", or its surviving prefix
|
|
522
|
-
const verCut = namePlain.slice(5); // the version, if the width left room for it
|
|
523
|
-
const rows = [`${p.bold}${nameCut}${p.reset}${verCut === "" ? "" : `${p.dim} ${verCut}${p.reset}`}`];
|
|
524
|
-
const facts = [];
|
|
525
|
-
if (meta !== undefined) {
|
|
526
|
-
facts.push([BANNER_LABELS[0], `${meta.model}${meta.mode === "" ? "" : ` · ${meta.mode}`}`], [BANNER_LABELS[1], meta.cwd]);
|
|
527
|
-
// DC-49 — ONE row under the cwd, and only when the workspace is the
|
|
528
|
-
// home directory. It STATES a fact and names the remedy; it does not
|
|
529
|
-
// warn, because the configuration is ALLOWED (owner, 2026-09-06) and
|
|
530
|
-
// a warning about an allowed thing teaches people to skip rows.
|
|
531
|
-
//
|
|
532
|
-
// An empty label puts it in the value column under `WORKSPACE`,
|
|
533
|
-
// where it reads as a note on that fact rather than a fact of its
|
|
534
|
-
// own. It must FIT at W=80 under the label indent: a cut row loses
|
|
535
|
-
// the remedy, which is the only actionable half of the sentence.
|
|
536
|
-
if (meta.homeWorkspace === true)
|
|
537
|
-
facts.push(["", "home directory as workspace — cd into a project to narrow it"]);
|
|
678
|
+
const width = Math.max(1, W);
|
|
679
|
+
const cut = (row) => cutLine(row, width);
|
|
680
|
+
const facts = meta !== undefined ? meta.facts : extensionsText !== "" ? [{ label: "EXTENSIONS", value: extensionsText }] : [];
|
|
681
|
+
const tall = H >= OPENING_TALL_H && width >= OPENING_EDGE + WORDMARK_W && meta?.resumed !== true;
|
|
682
|
+
const pad = " ".repeat(OPENING_EDGE);
|
|
683
|
+
const rows = [];
|
|
684
|
+
if (!tall) {
|
|
685
|
+
rows.push(cut(`\u2726 ${p.bold}kiso${p.reset} ${p.dim}${version} \u00b7 ${TAGLINE}${p.reset}`));
|
|
686
|
+
if (facts.length > 0)
|
|
687
|
+
rows.push("", ...factRows(facts, width - OPENING_EDGE).map((r) => cut(`${pad}${r}`)));
|
|
538
688
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const lead = ` ${p.dim}${label}${p.reset}${" ".repeat(LABEL_STOP - label.length)}`;
|
|
554
|
-
const hang = " ".repeat(indent);
|
|
555
|
-
const lines = [];
|
|
556
|
-
let line = "";
|
|
557
|
-
for (const word of value.split(" ")) {
|
|
558
|
-
if (line === "")
|
|
559
|
-
line = word;
|
|
560
|
-
else if (displayWidth(`${line} ${word}`) <= room)
|
|
561
|
-
line += ` ${word}`;
|
|
562
|
-
else {
|
|
563
|
-
lines.push(line);
|
|
564
|
-
line = word;
|
|
689
|
+
else {
|
|
690
|
+
const head = [...wordmarkRows(), openingRule(WORDMARK_W), `${p.ink2}${TAGLINE}${p.fgEnd}${p.dim} \u00b7 ${version}${p.reset}`, `${p.dim}${MOTTO}${p.reset}`];
|
|
691
|
+
// two cells after the wordmark's widest row, the hairline, two more,
|
|
692
|
+
// then the facts
|
|
693
|
+
const factsCol = OPENING_EDGE + WORDMARK_W + 5;
|
|
694
|
+
// beside the wordmark from 96 columns, as long as the facts' rows fit
|
|
695
|
+
// in its six; below it otherwise
|
|
696
|
+
const beside = width >= FACTS_BESIDE_W ? factRows(facts, width - factsCol) : [];
|
|
697
|
+
if (beside.length > 0 && beside.length <= WORDMARK.length) {
|
|
698
|
+
const rule = p.line !== "" ? `${p.line}\u2502${p.fgEnd}` : `${p.dim}\u2502${p.reset}`;
|
|
699
|
+
for (const [i, h] of head.entries()) {
|
|
700
|
+
if (i >= WORDMARK.length) {
|
|
701
|
+
rows.push(cut(`${pad}${h}`));
|
|
702
|
+
continue;
|
|
565
703
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
lines.push(line);
|
|
569
|
-
for (const [i, l] of lines.entries()) {
|
|
570
|
-
const styled = `${i === 0 ? lead : hang}${truncateRow(l, room)}`;
|
|
571
|
-
rows.push(displayWidth(styled) - (i === 0 ? p.dim.length + p.reset.length : 0) <= W ? styled : truncateRow(`${hang}${l}`, W));
|
|
704
|
+
const gap = " ".repeat(WORDMARK_W + 2 - visibleWidth(h));
|
|
705
|
+
rows.push(cut(`${pad}${h}${gap}${rule}${beside[i] === undefined ? "" : ` ${beside[i]}`}`));
|
|
572
706
|
}
|
|
573
707
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
// cutting one in half. `ctrl+o ex (+8)` teaches nothing.
|
|
579
|
-
const clauses = BANNER_KEYS.split(" \u00b7 ");
|
|
580
|
-
let keys = clauses[0];
|
|
581
|
-
for (let n = clauses.length; n > 1; n -= 1) {
|
|
582
|
-
const row = clauses.slice(0, n).join(" \u00b7 ");
|
|
583
|
-
if (displayWidth(row) <= W - 2) {
|
|
584
|
-
keys = row;
|
|
585
|
-
break;
|
|
586
|
-
}
|
|
708
|
+
else {
|
|
709
|
+
rows.push(...head.map((h) => cut(`${pad}${h}`)));
|
|
710
|
+
if (facts.length > 0)
|
|
711
|
+
rows.push("", ...factRows(facts, width - OPENING_EDGE).map((r) => cut(`${pad}${r}`)));
|
|
587
712
|
}
|
|
588
|
-
rows.push("", ` ${p.dim}${truncateRow(keys, W - 2)}${p.reset}`);
|
|
589
713
|
}
|
|
590
714
|
if (W >= 40 && H >= 20 && resume.length > 0) {
|
|
591
715
|
rows.push("", ...renderResumeList(resume, W, now));
|
|
592
716
|
}
|
|
593
717
|
return rows;
|
|
594
718
|
}
|
|
719
|
+
/**
|
|
720
|
+
* The wordmark's six rows, coloured per §7.10 on a known ground. The
|
|
721
|
+
* letters' cells are BACKGROUND, not `█` glyphs (§1.5): a glyph stops short
|
|
722
|
+
* of its cell in Apple Terminal and every row showed a white line through
|
|
723
|
+
* the letters (the owner's seam test, C1 against C2, 2026-09-29). The
|
|
724
|
+
* shadow stays in box-drawing glyphs, lighter than the letters. Off a known
|
|
725
|
+
* ground there is no background to paint, and the letters are `█`.
|
|
726
|
+
*/
|
|
727
|
+
function wordmarkRows() {
|
|
728
|
+
const p = palette();
|
|
729
|
+
const tier = p.tier;
|
|
730
|
+
if (tier === null || ground === "unknown")
|
|
731
|
+
return [...WORDMARK];
|
|
732
|
+
const c = graphiteColours(ground, groundRgb, tier);
|
|
733
|
+
const shadow = fg(mix(c.rail, c.ground, 0.35), tier);
|
|
734
|
+
return WORDMARK.map((row, i) => {
|
|
735
|
+
const block = bg(mix(c.ink, c.dim, Math.min(1, i / 4)), tier);
|
|
736
|
+
// the shadow's colour holds for the whole row (a letter cell is a
|
|
737
|
+
// space, which has no foreground to show); the letters open and
|
|
738
|
+
// close their background in runs
|
|
739
|
+
let out = shadow;
|
|
740
|
+
let inBlock = false;
|
|
741
|
+
for (const ch of row) {
|
|
742
|
+
const isBlock = ch === "\u2588";
|
|
743
|
+
if (isBlock !== inBlock)
|
|
744
|
+
out += isBlock ? block : "\x1b[49m";
|
|
745
|
+
inBlock = isBlock;
|
|
746
|
+
out += isBlock ? " " : ch;
|
|
747
|
+
}
|
|
748
|
+
return `${out}${inBlock ? "\x1b[49m" : ""}${p.fgEnd}`;
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
/** The rule under the wordmark: `dim` fading to the ground, in a few runs.
|
|
752
|
+
* Off a known ground, the plain dim rule. */
|
|
753
|
+
/** Graphite §5 (R2b) — the rule under an answer's `#` heading: `gold-mark`
|
|
754
|
+
* fading to the ground over `n` cells (the prototype's forty). Off a known
|
|
755
|
+
* ground, the plain dim rule. */
|
|
756
|
+
export function headingRule(n) {
|
|
757
|
+
const p = palette();
|
|
758
|
+
if (p.tier === null || ground === "unknown")
|
|
759
|
+
return `${p.dim}${"\u2500".repeat(n)}${p.reset}`;
|
|
760
|
+
const c = graphiteColours(ground, groundRgb, p.tier);
|
|
761
|
+
const STEPS = 7;
|
|
762
|
+
let out = "";
|
|
763
|
+
let at = 0;
|
|
764
|
+
for (let k = 0; k < STEPS && at < n; k += 1) {
|
|
765
|
+
const to = k === STEPS - 1 ? n : Math.round((n * (k + 1)) / STEPS);
|
|
766
|
+
if (to > at)
|
|
767
|
+
out += `${fg(mix(c.goldMark, c.ground, k / STEPS), p.tier)}${"\u2500".repeat(to - at)}`;
|
|
768
|
+
at = Math.max(at, to);
|
|
769
|
+
}
|
|
770
|
+
return `${out}${p.fgEnd}`;
|
|
771
|
+
}
|
|
772
|
+
function openingRule(n) {
|
|
773
|
+
const p = palette();
|
|
774
|
+
if (p.tier === null || ground === "unknown")
|
|
775
|
+
return `${p.dim}${"\u2500".repeat(n)}${p.reset}`;
|
|
776
|
+
const c = graphiteColours(ground, groundRgb, p.tier);
|
|
777
|
+
const STEPS = 7;
|
|
778
|
+
let out = "";
|
|
779
|
+
let at = 0;
|
|
780
|
+
for (let k = 0; k < STEPS && at < n; k += 1) {
|
|
781
|
+
const to = k === STEPS - 1 ? n : Math.round((n * (k + 1)) / STEPS);
|
|
782
|
+
if (to > at)
|
|
783
|
+
out += `${fg(mix(c.dim, c.ground, k / STEPS), p.tier)}${"\u2500".repeat(to - at)}`;
|
|
784
|
+
at = Math.max(at, to);
|
|
785
|
+
}
|
|
786
|
+
return `${out}${p.fgEnd}`;
|
|
787
|
+
}
|
|
788
|
+
/** The facts as rows `room` cells wide: the label dim in its column, the
|
|
789
|
+
* fact in ink, the note dim after it. A row that does not fit loses its
|
|
790
|
+
* note first — from its end, one ` · ` part at a time (the last sweep,
|
|
791
|
+
* owner 2026-10-06: `<id> · new · resumable after kill -9` keeps `new`
|
|
792
|
+
* beside the wordmark when a 24-cell session id leaves no room for the
|
|
793
|
+
* rest); a fact still too long HANGS under itself, folded by word — an
|
|
794
|
+
* extensions list cut at the width would hide which extensions loaded, on
|
|
795
|
+
* the one screen whose job is to say so. */
|
|
796
|
+
function factRows(facts, room) {
|
|
797
|
+
const p = palette();
|
|
798
|
+
const rows = [];
|
|
799
|
+
for (const f of facts) {
|
|
800
|
+
const label = `${p.dim}${f.label}${p.reset}${" ".repeat(Math.max(1, FACT_LABEL_W - f.label.length))}`;
|
|
801
|
+
const value = escapeTerminal(f.value);
|
|
802
|
+
const parts = f.note === undefined || f.note === "" ? [] : escapeTerminal(f.note).split(" \u00b7 ");
|
|
803
|
+
let fitted = null;
|
|
804
|
+
for (let n = parts.length; n >= 0 && fitted === null; n -= 1) {
|
|
805
|
+
const kept = parts.slice(0, n).join(" \u00b7 ");
|
|
806
|
+
const row = `${label}${value}${kept === "" ? "" : `${p.dim} \u00b7 ${kept}${p.reset}`}`;
|
|
807
|
+
if (visibleWidth(row) <= room)
|
|
808
|
+
fitted = row;
|
|
809
|
+
}
|
|
810
|
+
if (fitted !== null) {
|
|
811
|
+
rows.push(fitted);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const valueRoom = room - FACT_LABEL_W;
|
|
815
|
+
if (valueRoom < 8) {
|
|
816
|
+
rows.push(cutLine(`${label}${value}`, Math.max(1, room)));
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
const lines = [];
|
|
820
|
+
let line = "";
|
|
821
|
+
for (const word of value.split(" ")) {
|
|
822
|
+
if (line === "")
|
|
823
|
+
line = word;
|
|
824
|
+
else if (displayWidth(`${line} ${word}`) <= valueRoom)
|
|
825
|
+
line += ` ${word}`;
|
|
826
|
+
else {
|
|
827
|
+
lines.push(line);
|
|
828
|
+
line = word;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (line !== "")
|
|
832
|
+
lines.push(line);
|
|
833
|
+
const hang = " ".repeat(FACT_LABEL_W);
|
|
834
|
+
for (const [i, l] of lines.entries())
|
|
835
|
+
rows.push(cutLine(`${i === 0 ? label : hang}${l}`, Math.max(1, room)));
|
|
836
|
+
}
|
|
837
|
+
return rows;
|
|
838
|
+
}
|
|
595
839
|
export function relativeTime(updatedAt, now) {
|
|
596
840
|
const s = Math.max(0, now - updatedAt) / 1000;
|
|
597
841
|
if (s < 60)
|