@vincemakes/kiso-tui 0.1.35 → 0.1.37

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/render.js CHANGED
@@ -7,8 +7,9 @@
7
7
  * kiso-core's Event — the CLI translates Event → RenderInput. The tui
8
8
  * package has ZERO kiso-core imports: input is data, output is bytes.
9
9
  */
10
- export const COLOR_ON = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", code: "\x1b[38;5;110m", reset: "\x1b[0m" };
11
- export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", code: "", reset: "" };
10
+ import { charWidth, displayWidth } from "./width.js";
11
+ export const COLOR_ON = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", code: "\x1b[38;5;110m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
12
+ export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", code: "", rv: "", rvEnd: "", reset: "" };
12
13
  export function palette() {
13
14
  return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
14
15
  }
@@ -293,43 +294,81 @@ export function renderTerminalGap(statusLine) {
293
294
  * Pure.
294
295
  */
295
296
  export const TAGLINE = "the coding agent that survives kill -9";
297
+ /** v6's existing logo — W1's COMPACT tier, byte-identical, no redraw. */
296
298
  const LOGO_ROWS = ["█ █ ▀█▀ █▀▀ █▀█", "█▀▄ █ ▀▀█ █ █", "▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀"];
297
- /** Display width of a row (1-cell ASCII; 2-cell CJK/wide). */
298
- function displayW(row) {
299
- let w = 0;
300
- for (let i = 0; i < row.length; i += 1) {
301
- const cp = row.codePointAt(i);
302
- w += cp > 0xff ? 2 : 1;
303
- }
304
- return w;
305
- }
306
- /** v3 §01: truncate a row at `width`, marking the hidden span " (+N)". */
299
+ /** W1's BIG tier (36x6, `█` and space only — no half-blocks, so there is
300
+ * no tile seam to lose in a font that renders ▀ ▄ at the wrong height).
301
+ * Each pixel is two cells wide on purpose (a terminal cell is ~1:2);
302
+ * the render indents two 38 columns total, clears 40. */
303
+ const BIG_LOGO_ROWS = [
304
+ "██ ██ ██████ ████████ ████████",
305
+ "██ ██ ██ ██ ██ ██",
306
+ "████ ██ ████████ ██ ██",
307
+ "████ ██ ██ ██ ██",
308
+ "██ ██ ██ ██ ██ ██",
309
+ "██ ██ ██████ ████████ ████████",
310
+ ];
311
+ /** v3 §01 (W1): truncate a row at `width`, marking the hidden span
312
+ * " (+N)". W1: the width math is the charWidth authority (the banner's
313
+ * brick glyphs are 1 cell — the art's 38 columns clear 40), and the
314
+ * marker's own cells are part of the row — the visible cut leaves room
315
+ * for it, so a truncated row never exceeds W (a cut row carries the
316
+ * marker INSIDE the width; a row that fits is returned untouched). */
307
317
  export function truncateRow(row, width) {
308
- if (displayW(row) <= width)
318
+ const total = displayWidth(row);
319
+ if (total <= width)
309
320
  return row;
310
- const cut = Math.max(0, width - 4);
311
- let w = 0;
312
- let i = 0;
313
- for (; i < row.length; i += 1) {
314
- const cw = row.codePointAt(i) > 0xff ? 2 : 1;
315
- if (w + cw > cut)
316
- break;
317
- w += cw;
321
+ // iterate the marker to a fixpoint: the marker's width changes the
322
+ // cut, the cut changes the hidden count the marker reports
323
+ let marker = " (+0)";
324
+ for (;;) {
325
+ const cut = Math.max(0, width - displayWidth(marker));
326
+ let w = 0;
327
+ let i = 0;
328
+ while (i < row.length) {
329
+ const cp = row.codePointAt(i);
330
+ const cw = charWidth(cp);
331
+ if (w + cw > cut)
332
+ break;
333
+ w += cw;
334
+ i += cp > 0xffff ? 2 : 1; // code-point stepping — never split a pair
335
+ }
336
+ const next = ` (+${total - w})`;
337
+ if (next === marker)
338
+ return `${row.slice(0, i)}${next}`;
339
+ marker = next;
318
340
  }
319
- return `${row.slice(0, i)} (+${displayW(row) - w})`;
320
341
  }
321
- /** v3 §01 (V6-2): the banner lines for a width W logo (skipped under
322
- * 40 columns) + blank + "kiso vX tagline" + extensions. */
323
- export function bannerLines(W, version, extensionsText) {
342
+ /** v3 §01 (V6-2) + W1: the banner lines for a width W and height H
343
+ * the tier table (extends the existing "under 40 columns, skip the
344
+ * logo" rule with a HEIGHT input):
345
+ * W ≥ 40 and H ≥ 20 → BIG (the 36x6 wordmark, 2-column indent)
346
+ * W ≥ 40 and 14–19 rows → COMPACT (v6's LOGO_ROWS, byte-identical)
347
+ * anything smaller → text rows only
348
+ * then the blank, then "vX — tagline" — the art IS the wordmark, so the
349
+ * text row does not repeat the name — then extensions — then the W5
350
+ * resume list (BIG only, W5). Every row truncates at the terminal width
351
+ * with a " (+N)" marker. Pure. */
352
+ export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now()) {
324
353
  const rows = [];
325
354
  if (W >= 40) {
326
- for (const r of LOGO_ROWS)
327
- rows.push(truncateRow(r, W));
328
- rows.push("");
355
+ if (H >= 20) {
356
+ for (const r of BIG_LOGO_ROWS)
357
+ rows.push(truncateRow(` ${r}`, W));
358
+ }
359
+ else if (H >= 14) {
360
+ for (const r of LOGO_ROWS)
361
+ rows.push(truncateRow(r, W));
362
+ }
329
363
  }
330
- rows.push(truncateRow(`kiso v${version} — ${TAGLINE}`, W));
364
+ if (rows.length > 0)
365
+ rows.push("");
366
+ rows.push(truncateRow(`v${version} — ${TAGLINE}`, W));
331
367
  if (extensionsText !== "")
332
368
  rows.push(truncateRow(extensionsText, W));
369
+ if (W >= 40 && H >= 20 && resume.length > 0) {
370
+ rows.push("", ...renderResumeList(resume, W, now));
371
+ }
333
372
  return rows;
334
373
  }
335
374
  export function renderRecap(s) {
@@ -353,3 +392,50 @@ export function renderSessionLine(meta) {
353
392
  // round 8: the title is the user's first prompt — model/user text, escaped.
354
393
  return `${meta.id.padEnd(24)} ${meta.runs} runs ${String(meta.events).padStart(5)} events ${when} ${escapeTerminal(meta.title)}`;
355
394
  }
395
+ export function relativeTime(updatedAt, now) {
396
+ const s = Math.max(0, now - updatedAt) / 1000;
397
+ if (s < 60)
398
+ return "now";
399
+ const m = Math.floor(s / 60);
400
+ if (m < 60)
401
+ return `${m}m ago`;
402
+ const h = Math.floor(m / 60);
403
+ if (h < 24)
404
+ return `${h}h ago`;
405
+ const d = Math.floor(h / 24);
406
+ if (d < 7)
407
+ return `${d}d ago`;
408
+ return `${Math.floor(d / 7)}w ago`;
409
+ }
410
+ function titleCut(text, max) {
411
+ if (displayWidth(text) <= max)
412
+ return text;
413
+ const room = max - displayWidth("…");
414
+ let w = 0;
415
+ let i = 0;
416
+ while (i < text.length) {
417
+ const cp = text.codePointAt(i);
418
+ const cw = charWidth(cp);
419
+ if (w + cw > room)
420
+ break;
421
+ w += cw;
422
+ i += cp > 0xffff ? 2 : 1;
423
+ }
424
+ return text.slice(0, i) + "…";
425
+ }
426
+ export function renderResumeList(metas, W, now) {
427
+ if (metas.length === 0)
428
+ return [];
429
+ const rows = [" ▞ resume"];
430
+ const whens = metas.map((m) => relativeTime(m.updatedAt, now));
431
+ const metaTexts = metas.map((m) => `${m.events} events · ${m.runs} runs`);
432
+ const metaW = Math.max(...metaTexts.map((t) => t.length));
433
+ const titleW = Math.max(1, W - 13 - metaW);
434
+ for (let i = 0; i < metas.length; i += 1) {
435
+ const title = escapeTerminal(metas[i].title);
436
+ const shown = titleCut(title, titleW);
437
+ const pad = titleW - displayWidth(shown);
438
+ rows.push(` ${whens[i].padEnd(7)} ${shown}${" ".repeat(pad)} ${metaTexts[i].padStart(metaW)}`);
439
+ }
440
+ return rows;
441
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The display-width primitives — the SINGLE width authority (TUI v5
3
+ * #16e: "charWidth is the width authority"). The eastAsianWidth table
4
+ * is a ~40-line subset (CJK ideographs/kana/hangul/fullwidth/common
5
+ * wide symbols = 2, everything else = 1 — the box-drawing/brick glyphs
6
+ * █▀▄▞▸ are narrow). Known limitation, documented in the README: emoji
7
+ * ZWJ clusters are not guaranteed perfect — each code point counts as
8
+ * its width. Zero dependencies (importable from any module).
9
+ */
10
+ /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
11
+ export declare function charWidth(cp: number): number;
12
+ /** Display width of a code-point array (cursor math, scrolling). */
13
+ export declare function widthOf(chars: readonly number[]): number;
14
+ /** Display width of a string. */
15
+ export declare function displayWidth(text: string): number;
package/dist/width.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The display-width primitives — the SINGLE width authority (TUI v5
3
+ * #16e: "charWidth is the width authority"). The eastAsianWidth table
4
+ * is a ~40-line subset (CJK ideographs/kana/hangul/fullwidth/common
5
+ * wide symbols = 2, everything else = 1 — the box-drawing/brick glyphs
6
+ * █▀▄▞▸ are narrow). Known limitation, documented in the README: emoji
7
+ * ZWJ clusters are not guaranteed perfect — each code point counts as
8
+ * its width. Zero dependencies (importable from any module).
9
+ */
10
+ /** A code point's display width: 2 for the wide ranges, 1 otherwise. */
11
+ export function charWidth(cp) {
12
+ if (cp >= 0x1100 && cp <= 0x115f)
13
+ return 2; // hangul jamo
14
+ if (cp >= 0x2e80 && cp <= 0x303e)
15
+ return 2; // radicals .. CJK punctuation
16
+ if (cp >= 0x3041 && cp <= 0x33ff)
17
+ return 2; // kana, CJK compat
18
+ if (cp >= 0x3400 && cp <= 0x4dbf)
19
+ return 2; // CJK ext A
20
+ if (cp >= 0x4e00 && cp <= 0x9fff)
21
+ return 2; // CJK unified
22
+ if (cp >= 0xa000 && cp <= 0xa4cf)
23
+ return 2; // yi
24
+ if (cp >= 0xa960 && cp <= 0xa97f)
25
+ return 2; // hangul jamo ext
26
+ if (cp >= 0xac00 && cp <= 0xd7a3)
27
+ return 2; // hangul syllables
28
+ if (cp >= 0xf900 && cp <= 0xfaff)
29
+ return 2; // CJK compat ideographs
30
+ if (cp >= 0xfe10 && cp <= 0xfe19)
31
+ return 2; // vertical forms
32
+ if (cp >= 0xfe30 && cp <= 0xfe6f)
33
+ return 2; // CJK compat forms
34
+ if (cp >= 0xff00 && cp <= 0xff60)
35
+ return 2; // fullwidth forms
36
+ if (cp >= 0xffe0 && cp <= 0xffe6)
37
+ return 2; // fullwidth signs
38
+ if (cp >= 0x1f300 && cp <= 0x1f64f)
39
+ return 2; // emoji (misc + emoticons)
40
+ if (cp >= 0x1f900 && cp <= 0x1f9ff)
41
+ return 2; // supplemental emoji
42
+ if (cp >= 0x20000 && cp <= 0x3fffd)
43
+ return 2; // CJK ext B..G
44
+ return 1;
45
+ }
46
+ /** Display width of a code-point array (cursor math, scrolling). */
47
+ export function widthOf(chars) {
48
+ let w = 0;
49
+ for (const cp of chars)
50
+ w += charWidth(cp);
51
+ return w;
52
+ }
53
+ /** Display width of a string. */
54
+ export function displayWidth(text) {
55
+ let w = 0;
56
+ for (const ch of text)
57
+ w += charWidth(ch.codePointAt(0));
58
+ return w;
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "kiso tui — the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",