@vincemakes/kiso-tui 0.1.36 → 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.d.ts +113 -7
- package/dist/components.js +471 -66
- package/dist/compositor.d.ts +58 -5
- package/dist/compositor.js +475 -58
- package/dist/editor.d.ts +3 -6
- package/dist/editor.js +27 -54
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/render.d.ts +46 -5
- package/dist/render.js +150 -30
- package/dist/width.d.ts +15 -0
- package/dist/width.js +59 -0
- package/package.json +1 -1
package/dist/editor.d.ts
CHANGED
|
@@ -16,12 +16,8 @@
|
|
|
16
16
|
* The editor is a SINGLE line: bracketed paste (?2004h) unwraps and
|
|
17
17
|
* inserts, internal newlines become spaces.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
-
/** Display width of a code-point array (cursor math, scrolling). */
|
|
22
|
-
export declare function widthOf(chars: readonly number[]): number;
|
|
23
|
-
/** Display width of a string. */
|
|
24
|
-
export declare function displayWidth(text: string): number;
|
|
19
|
+
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
20
|
+
export { charWidth, displayWidth, widthOf };
|
|
25
21
|
export declare const PROMPT = "\u258C ";
|
|
26
22
|
export declare const PROMPT_WIDTH: number;
|
|
27
23
|
/** v3 §04 — the slash-command menu's command table (English one-liners). */
|
|
@@ -45,6 +41,7 @@ export declare class Editor {
|
|
|
45
41
|
onSigint(cb: () => void): void;
|
|
46
42
|
onEot(cb: () => void): void;
|
|
47
43
|
onEscape(cb: () => void): void;
|
|
44
|
+
onExpand(cb: () => void): void;
|
|
48
45
|
/** The whole buffer as text (the CLI's line()/clearLine()). */
|
|
49
46
|
line(): string;
|
|
50
47
|
clearLine(): void;
|
package/dist/editor.js
CHANGED
|
@@ -16,56 +16,10 @@
|
|
|
16
16
|
* The editor is a SINGLE line: bracketed paste (?2004h) unwraps and
|
|
17
17
|
* inserts, internal newlines become spaces.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (cp >= 0x2e80 && cp <= 0x303e)
|
|
24
|
-
return 2; // radicals .. CJK punctuation
|
|
25
|
-
if (cp >= 0x3041 && cp <= 0x33ff)
|
|
26
|
-
return 2; // kana, CJK compat
|
|
27
|
-
if (cp >= 0x3400 && cp <= 0x4dbf)
|
|
28
|
-
return 2; // CJK ext A
|
|
29
|
-
if (cp >= 0x4e00 && cp <= 0x9fff)
|
|
30
|
-
return 2; // CJK unified
|
|
31
|
-
if (cp >= 0xa000 && cp <= 0xa4cf)
|
|
32
|
-
return 2; // yi
|
|
33
|
-
if (cp >= 0xa960 && cp <= 0xa97f)
|
|
34
|
-
return 2; // hangul jamo ext
|
|
35
|
-
if (cp >= 0xac00 && cp <= 0xd7a3)
|
|
36
|
-
return 2; // hangul syllables
|
|
37
|
-
if (cp >= 0xf900 && cp <= 0xfaff)
|
|
38
|
-
return 2; // CJK compat ideographs
|
|
39
|
-
if (cp >= 0xfe10 && cp <= 0xfe19)
|
|
40
|
-
return 2; // vertical forms
|
|
41
|
-
if (cp >= 0xfe30 && cp <= 0xfe6f)
|
|
42
|
-
return 2; // CJK compat forms
|
|
43
|
-
if (cp >= 0xff00 && cp <= 0xff60)
|
|
44
|
-
return 2; // fullwidth forms
|
|
45
|
-
if (cp >= 0xffe0 && cp <= 0xffe6)
|
|
46
|
-
return 2; // fullwidth signs
|
|
47
|
-
if (cp >= 0x1f300 && cp <= 0x1f64f)
|
|
48
|
-
return 2; // emoji (misc + emoticons)
|
|
49
|
-
if (cp >= 0x1f900 && cp <= 0x1f9ff)
|
|
50
|
-
return 2; // supplemental emoji
|
|
51
|
-
if (cp >= 0x20000 && cp <= 0x3fffd)
|
|
52
|
-
return 2; // CJK ext B..G
|
|
53
|
-
return 1;
|
|
54
|
-
}
|
|
55
|
-
/** Display width of a code-point array (cursor math, scrolling). */
|
|
56
|
-
export function widthOf(chars) {
|
|
57
|
-
let w = 0;
|
|
58
|
-
for (const cp of chars)
|
|
59
|
-
w += charWidth(cp);
|
|
60
|
-
return w;
|
|
61
|
-
}
|
|
62
|
-
/** Display width of a string. */
|
|
63
|
-
export function displayWidth(text) {
|
|
64
|
-
let w = 0;
|
|
65
|
-
for (const ch of text)
|
|
66
|
-
w += charWidth(ch.codePointAt(0));
|
|
67
|
-
return w;
|
|
68
|
-
}
|
|
19
|
+
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
20
|
+
// the width primitives moved to width.ts (W1, the single width
|
|
21
|
+
// authority) — re-exported so the editor's public surface is unchanged.
|
|
22
|
+
export { charWidth, displayWidth, widthOf };
|
|
69
23
|
import { palette } from "./render.js";
|
|
70
24
|
// TUI v4 #16d: the input row is the blue brick + the edit area — the
|
|
71
25
|
// "you>" text is gone (the brick IS the prompt; the pipe path's readline
|
|
@@ -98,7 +52,15 @@ export class Editor {
|
|
|
98
52
|
#pendingLines = []; // submits before onLine is wired (startup) — never dropped
|
|
99
53
|
#sigintCb = null;
|
|
100
54
|
#eotCb = null;
|
|
101
|
-
|
|
55
|
+
// W18: the escape LIST — the run-abort (chat) and the /compact cancel
|
|
56
|
+
// (dispatch) coexist; a listener removes itself via an unarmed guard
|
|
57
|
+
// (the compact's handler no-ops after its abort has fired).
|
|
58
|
+
#escapeCbs = [];
|
|
59
|
+
// W15: the expand-key list (ctrl+r) — the CLI's dispatch decides the
|
|
60
|
+
// target (a live cell toggles in place; a committed cell appends the
|
|
61
|
+
// expanded block). Mirrors the escape list: multiple listeners can
|
|
62
|
+
// coexist; the editor never interprets the key itself.
|
|
63
|
+
#expandCbs = [];
|
|
102
64
|
#onRender;
|
|
103
65
|
#menuOpen = false; // v3 §04: the slash-command menu
|
|
104
66
|
#menuSel = 0;
|
|
@@ -136,7 +98,10 @@ export class Editor {
|
|
|
136
98
|
this.#eotCb = cb;
|
|
137
99
|
}
|
|
138
100
|
onEscape(cb) {
|
|
139
|
-
this.#
|
|
101
|
+
this.#escapeCbs.push(cb);
|
|
102
|
+
}
|
|
103
|
+
onExpand(cb) {
|
|
104
|
+
this.#expandCbs.push(cb);
|
|
140
105
|
}
|
|
141
106
|
/** The whole buffer as text (the CLI's line()/clearLine()). */
|
|
142
107
|
line() {
|
|
@@ -253,7 +218,8 @@ export class Editor {
|
|
|
253
218
|
this.#onRender();
|
|
254
219
|
}
|
|
255
220
|
else {
|
|
256
|
-
this.#
|
|
221
|
+
for (const cb of [...this.#escapeCbs])
|
|
222
|
+
cb();
|
|
257
223
|
i += 1;
|
|
258
224
|
}
|
|
259
225
|
}
|
|
@@ -314,6 +280,13 @@ export class Editor {
|
|
|
314
280
|
}
|
|
315
281
|
i += 1;
|
|
316
282
|
}
|
|
283
|
+
else if (c === "\x12") {
|
|
284
|
+
// W15: the expand key (ctrl+r) — rides the chain like a
|
|
285
|
+
// command, the editor just forwards it.
|
|
286
|
+
for (const cb of [...this.#expandCbs])
|
|
287
|
+
cb();
|
|
288
|
+
i += 1;
|
|
289
|
+
}
|
|
317
290
|
else if (c !== undefined && c < " ") {
|
|
318
291
|
i += 1; // other control — ignored
|
|
319
292
|
}
|
|
@@ -497,7 +470,7 @@ export class Editor {
|
|
|
497
470
|
// ---- width-based horizontal scroll ----
|
|
498
471
|
#reflow() {
|
|
499
472
|
const W = (process.stdout.columns ?? 0) || 80; // degenerate 0 falls back to 80
|
|
500
|
-
const maxW = Math.max(1, W - PROMPT_WIDTH -
|
|
473
|
+
const maxW = Math.max(1, W - PROMPT_WIDTH - 4); // W6: the box's walls (2+2) — the visible line fits the box's inner width; the "…" rides inside
|
|
501
474
|
const curCol = widthOf(this.#chars.slice(0, this.#cursor));
|
|
502
475
|
const scrolledW = widthOf(this.#chars.slice(0, this.#scroll));
|
|
503
476
|
if (curCol < scrolledW) {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
export { Body, Dock, CURSOR_MARKER, type BodyOptions } from "./compositor.js";
|
|
9
9
|
export { Container, foldLine, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
|
|
10
10
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
|
|
11
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderSessionLine, renderStatusLine, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, type Palette, type PathResolver, type RecapStats, type RenderInput, type RenderResult, type RunUsage, } from "./render.js";
|
|
11
|
+
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, type Palette, type PathResolver, type RecapStats, type ResumeMeta, type RenderInput, type RenderResult, type RunUsage, } from "./render.js";
|
|
12
12
|
export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
|
package/dist/index.js
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
export { Body, Dock, CURSOR_MARKER } from "./compositor.js";
|
|
9
9
|
export { Container, foldLine, visibleWidth, SPINNER } from "./components.js";
|
|
10
10
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
|
|
11
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderSessionLine, renderStatusLine, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, } from "./render.js";
|
|
11
|
+
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, } from "./render.js";
|
|
12
12
|
export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
|
package/dist/render.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface Palette {
|
|
|
25
25
|
readonly red: string;
|
|
26
26
|
readonly green: string;
|
|
27
27
|
readonly code: string;
|
|
28
|
+
readonly rv: string;
|
|
29
|
+
readonly rvEnd: string;
|
|
28
30
|
readonly reset: string;
|
|
29
31
|
}
|
|
30
32
|
export declare const COLOR_ON: Palette;
|
|
@@ -175,7 +177,12 @@ export declare function renderEvent(ev: RenderInput, prevThinking?: boolean, res
|
|
|
175
177
|
export declare function renderToolSummary(name: string, input: Record<string, unknown>, result: {
|
|
176
178
|
content: string;
|
|
177
179
|
isError: boolean;
|
|
178
|
-
}): string;
|
|
180
|
+
}, reason?: string | null): string;
|
|
181
|
+
/** W15 — the expand header's target: the tool call's subject (the path
|
|
182
|
+
* for the *_file tools, the command for shell) — the same extraction
|
|
183
|
+
* the summary detail uses, WITHOUT the counts (the header names what
|
|
184
|
+
* was expanded, not its size). */
|
|
185
|
+
export declare function toolTarget(name: string, input: Record<string, unknown>): string;
|
|
179
186
|
/** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
|
|
180
187
|
export declare function kUnit(value: number | null): string;
|
|
181
188
|
/** B area: usage data gathered from the run's usage events. */
|
|
@@ -212,11 +219,24 @@ export declare function renderTerminalGap(statusLine: string | null): string;
|
|
|
212
219
|
* Pure.
|
|
213
220
|
*/
|
|
214
221
|
export declare const TAGLINE = "the coding agent that survives kill -9";
|
|
215
|
-
/** v3 §01: truncate a row at `width`, marking the hidden span
|
|
222
|
+
/** v3 §01 (W1): truncate a row at `width`, marking the hidden span
|
|
223
|
+
* " (+N)". W1: the width math is the charWidth authority (the banner's
|
|
224
|
+
* brick glyphs are 1 cell — the art's 38 columns clear 40), and the
|
|
225
|
+
* marker's own cells are part of the row — the visible cut leaves room
|
|
226
|
+
* for it, so a truncated row never exceeds W (a cut row carries the
|
|
227
|
+
* marker INSIDE the width; a row that fits is returned untouched). */
|
|
216
228
|
export declare function truncateRow(row: string, width: number): string;
|
|
217
|
-
/** v3 §01 (V6-2): the banner lines for a width W
|
|
218
|
-
*
|
|
219
|
-
|
|
229
|
+
/** v3 §01 (V6-2) + W1: the banner lines for a width W and height H —
|
|
230
|
+
* the tier table (extends the existing "under 40 columns, skip the
|
|
231
|
+
* logo" rule with a HEIGHT input):
|
|
232
|
+
* W ≥ 40 and H ≥ 20 → BIG (the 36x6 wordmark, 2-column indent)
|
|
233
|
+
* W ≥ 40 and 14–19 rows → COMPACT (v6's LOGO_ROWS, byte-identical)
|
|
234
|
+
* anything smaller → text rows only
|
|
235
|
+
* then the blank, then "vX — tagline" — the art IS the wordmark, so the
|
|
236
|
+
* text row does not repeat the name — then extensions — then the W5
|
|
237
|
+
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
238
|
+
* with a " (+N)" marker. Pure. */
|
|
239
|
+
export declare function bannerLines(W: number, H: number, version: string, extensionsText: string, resume?: readonly ResumeMeta[], now?: number): string[];
|
|
220
240
|
/** v3 §02 — the recap line that ends a run, replacing the "done" label +
|
|
221
241
|
* the old status line. All fields derive LOCALLY from the event stream
|
|
222
242
|
* (zero tokens): wall seconds, tool counts, usage, cache hit %, ctx left.
|
|
@@ -227,6 +247,11 @@ export interface RecapStats {
|
|
|
227
247
|
readonly edits: number;
|
|
228
248
|
readonly usage: RunUsage;
|
|
229
249
|
readonly ctxLeftPct: number | null;
|
|
250
|
+
/** W19 — the mode the turn ran under. Under "plan" the recap becomes
|
|
251
|
+
* the way-forward row (the claimed shape): a plan turn's currency is
|
|
252
|
+
* the plan, not the tool count — the timing and tool-count parts
|
|
253
|
+
* drop, and the two /mode hints replace them. */
|
|
254
|
+
readonly mode?: string;
|
|
230
255
|
}
|
|
231
256
|
export declare function renderRecap(s: RecapStats): string;
|
|
232
257
|
/** One-line summary of a session, for `kiso sessions`. */
|
|
@@ -237,3 +262,19 @@ export declare function renderSessionLine(meta: {
|
|
|
237
262
|
runs: number;
|
|
238
263
|
updatedAt: number;
|
|
239
264
|
}): string;
|
|
265
|
+
/** W5 — the opening-screen resume list. Every field already exists
|
|
266
|
+
* behind renderSessionLine / `kiso sessions`: the relative time, the
|
|
267
|
+
* title, then the right-aligned "N events · M runs". The columns are
|
|
268
|
+
* fixed per W: 4 indent + 7 when + 1 + the title (the ONLY flexible
|
|
269
|
+
* field — cut with the ellipsis marker INSIDE the width) + 1 + the meta
|
|
270
|
+
* (padStart to metaW). The done-when: the meta's right edge lands at
|
|
271
|
+
* exactly W on every row. Returns PLAIN rows — the banner's uniform dim
|
|
272
|
+
* wrap styles them (no dim+bold SGR composition). */
|
|
273
|
+
export interface ResumeMeta {
|
|
274
|
+
readonly title: string;
|
|
275
|
+
readonly events: number;
|
|
276
|
+
readonly runs: number;
|
|
277
|
+
readonly updatedAt: number;
|
|
278
|
+
}
|
|
279
|
+
export declare function relativeTime(updatedAt: number, now: number): string;
|
|
280
|
+
export declare function renderResumeList(metas: readonly ResumeMeta[], W: number, now: number): string[];
|
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
|
-
|
|
11
|
-
export const
|
|
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
|
}
|
|
@@ -188,9 +189,16 @@ function approvalDetail(name, input, resolvePath) {
|
|
|
188
189
|
* edit/write show +/- line counts, read shows lines, shell shows the exit
|
|
189
190
|
* code; failures (isError) are ✗. Pure and deterministic.
|
|
190
191
|
*/
|
|
191
|
-
export function renderToolSummary(name, input, result) {
|
|
192
|
+
export function renderToolSummary(name, input, result, reason = null) {
|
|
192
193
|
// v2a/v5: ✓ is a bold identity accent; ✗ stays red.
|
|
193
194
|
const p = palette();
|
|
195
|
+
// W19: a DENIED call (the "denied" tag) renders the pinned row — the
|
|
196
|
+
// FULL call name, the target, the reason in the W4 parentheses idiom,
|
|
197
|
+
// and NO timing metadata (the call never ran — (0.0s) would be noise).
|
|
198
|
+
// The same row in the interactive and pipe paths, byte-clean on a pipe.
|
|
199
|
+
if (reason !== null) {
|
|
200
|
+
return `${p.red}✗${p.reset} ${escapeTerminal(`${name} ${toolTarget(name, input)} (${reason})`)}`;
|
|
201
|
+
}
|
|
194
202
|
const mark = result.isError ? `${p.red}✗${p.reset}` : `${p.bold}✓${p.reset}`;
|
|
195
203
|
const shortName = name.replace("_file", "");
|
|
196
204
|
const detail = toolSummaryDetail(name, input, result);
|
|
@@ -232,6 +240,24 @@ function toolSummaryDetail(name, input, result) {
|
|
|
232
240
|
return String(input.path ?? input.command ?? "");
|
|
233
241
|
}
|
|
234
242
|
}
|
|
243
|
+
/** W15 — the expand header's target: the tool call's subject (the path
|
|
244
|
+
* for the *_file tools, the command for shell) — the same extraction
|
|
245
|
+
* the summary detail uses, WITHOUT the counts (the header names what
|
|
246
|
+
* was expanded, not its size). */
|
|
247
|
+
export function toolTarget(name, input) {
|
|
248
|
+
switch (name) {
|
|
249
|
+
case "read_file":
|
|
250
|
+
case "write_file":
|
|
251
|
+
case "edit_file":
|
|
252
|
+
return String(input.path ?? "?");
|
|
253
|
+
case "shell":
|
|
254
|
+
return String(input.command ?? "?");
|
|
255
|
+
case "list_dir":
|
|
256
|
+
return String(input.path ?? "(root)");
|
|
257
|
+
default:
|
|
258
|
+
return String(input.path ?? input.command ?? "");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
235
261
|
/** The exit code of a shell result: parsed from the failure text, 0 on success. */
|
|
236
262
|
function exitCodeOf(result) {
|
|
237
263
|
if (!result.isError)
|
|
@@ -293,47 +319,94 @@ export function renderTerminalGap(statusLine) {
|
|
|
293
319
|
* Pure.
|
|
294
320
|
*/
|
|
295
321
|
export const TAGLINE = "the coding agent that survives kill -9";
|
|
322
|
+
/** v6's existing logo — W1's COMPACT tier, byte-identical, no redraw. */
|
|
296
323
|
const LOGO_ROWS = ["█ █ ▀█▀ █▀▀ █▀█", "█▀▄ █ ▀▀█ █ █", "▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀"];
|
|
297
|
-
/**
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
324
|
+
/** W1's BIG tier (36x6, `█` and space only — no half-blocks, so there is
|
|
325
|
+
* no tile seam to lose in a font that renders ▀ ▄ at the wrong height).
|
|
326
|
+
* Each pixel is two cells wide on purpose (a terminal cell is ~1:2);
|
|
327
|
+
* the render indents two — 38 columns total, clears 40. */
|
|
328
|
+
const BIG_LOGO_ROWS = [
|
|
329
|
+
"██ ██ ██████ ████████ ████████",
|
|
330
|
+
"██ ██ ██ ██ ██ ██",
|
|
331
|
+
"████ ██ ████████ ██ ██",
|
|
332
|
+
"████ ██ ██ ██ ██",
|
|
333
|
+
"██ ██ ██ ██ ██ ██",
|
|
334
|
+
"██ ██ ██████ ████████ ████████",
|
|
335
|
+
];
|
|
336
|
+
/** v3 §01 (W1): truncate a row at `width`, marking the hidden span
|
|
337
|
+
* " (+N)". W1: the width math is the charWidth authority (the banner's
|
|
338
|
+
* brick glyphs are 1 cell — the art's 38 columns clear 40), and the
|
|
339
|
+
* marker's own cells are part of the row — the visible cut leaves room
|
|
340
|
+
* for it, so a truncated row never exceeds W (a cut row carries the
|
|
341
|
+
* marker INSIDE the width; a row that fits is returned untouched). */
|
|
307
342
|
export function truncateRow(row, width) {
|
|
308
|
-
|
|
343
|
+
const total = displayWidth(row);
|
|
344
|
+
if (total <= width)
|
|
309
345
|
return row;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
let
|
|
313
|
-
for (
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
346
|
+
// iterate the marker to a fixpoint: the marker's width changes the
|
|
347
|
+
// cut, the cut changes the hidden count the marker reports
|
|
348
|
+
let marker = " (+0)";
|
|
349
|
+
for (;;) {
|
|
350
|
+
const cut = Math.max(0, width - displayWidth(marker));
|
|
351
|
+
let w = 0;
|
|
352
|
+
let i = 0;
|
|
353
|
+
while (i < row.length) {
|
|
354
|
+
const cp = row.codePointAt(i);
|
|
355
|
+
const cw = charWidth(cp);
|
|
356
|
+
if (w + cw > cut)
|
|
357
|
+
break;
|
|
358
|
+
w += cw;
|
|
359
|
+
i += cp > 0xffff ? 2 : 1; // code-point stepping — never split a pair
|
|
360
|
+
}
|
|
361
|
+
const next = ` (+${total - w})`;
|
|
362
|
+
if (next === marker)
|
|
363
|
+
return `${row.slice(0, i)}${next}`;
|
|
364
|
+
marker = next;
|
|
318
365
|
}
|
|
319
|
-
return `${row.slice(0, i)} (+${displayW(row) - w})`;
|
|
320
366
|
}
|
|
321
|
-
/** v3 §01 (V6-2): the banner lines for a width W
|
|
322
|
-
*
|
|
323
|
-
|
|
367
|
+
/** v3 §01 (V6-2) + W1: the banner lines for a width W and height H —
|
|
368
|
+
* the tier table (extends the existing "under 40 columns, skip the
|
|
369
|
+
* logo" rule with a HEIGHT input):
|
|
370
|
+
* W ≥ 40 and H ≥ 20 → BIG (the 36x6 wordmark, 2-column indent)
|
|
371
|
+
* W ≥ 40 and 14–19 rows → COMPACT (v6's LOGO_ROWS, byte-identical)
|
|
372
|
+
* anything smaller → text rows only
|
|
373
|
+
* then the blank, then "vX — tagline" — the art IS the wordmark, so the
|
|
374
|
+
* text row does not repeat the name — then extensions — then the W5
|
|
375
|
+
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
376
|
+
* with a " (+N)" marker. Pure. */
|
|
377
|
+
export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now()) {
|
|
324
378
|
const rows = [];
|
|
325
379
|
if (W >= 40) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
380
|
+
if (H >= 20) {
|
|
381
|
+
for (const r of BIG_LOGO_ROWS)
|
|
382
|
+
rows.push(truncateRow(` ${r}`, W));
|
|
383
|
+
}
|
|
384
|
+
else if (H >= 14) {
|
|
385
|
+
for (const r of LOGO_ROWS)
|
|
386
|
+
rows.push(truncateRow(r, W));
|
|
387
|
+
}
|
|
329
388
|
}
|
|
330
|
-
rows.
|
|
389
|
+
if (rows.length > 0)
|
|
390
|
+
rows.push("");
|
|
391
|
+
rows.push(truncateRow(`v${version} — ${TAGLINE}`, W));
|
|
331
392
|
if (extensionsText !== "")
|
|
332
393
|
rows.push(truncateRow(extensionsText, W));
|
|
394
|
+
if (W >= 40 && H >= 20 && resume.length > 0) {
|
|
395
|
+
rows.push("", ...renderResumeList(resume, W, now));
|
|
396
|
+
}
|
|
333
397
|
return rows;
|
|
334
398
|
}
|
|
335
399
|
export function renderRecap(s) {
|
|
336
400
|
const p = palette();
|
|
401
|
+
// W19: under plan the recap is the way out of the mode — the header
|
|
402
|
+
// names the mode's posture, the hints name the exits (the ONLY
|
|
403
|
+
// controls — /mode is the only way to leave plan mode).
|
|
404
|
+
if (s.mode === "plan") {
|
|
405
|
+
const parts = ["plan ready", "/mode default executes", "/mode accept-edits auto-approves edits"];
|
|
406
|
+
if (s.ctxLeftPct !== null)
|
|
407
|
+
parts.push(`ctx left ~${Math.round(s.ctxLeftPct)}%`);
|
|
408
|
+
return `${p.bold}▞${p.reset} ${parts.join(" · ")}\n`;
|
|
409
|
+
}
|
|
337
410
|
const parts = [`${s.seconds}s`, `${s.tools} tool${s.tools === 1 ? "" : "s"}${s.edits > 0 ? ` (${s.edits} edit${s.edits === 1 ? "" : "s"})` : ""}`];
|
|
338
411
|
if (s.usage.known) {
|
|
339
412
|
const seg = `${s.usage.in !== null ? `in ${kUnit(s.usage.in)}` : ""}${s.usage.in !== null && s.usage.out !== null ? " " : ""}${s.usage.out !== null ? `out ${kUnit(s.usage.out)}` : ""}`;
|
|
@@ -353,3 +426,50 @@ export function renderSessionLine(meta) {
|
|
|
353
426
|
// round 8: the title is the user's first prompt — model/user text, escaped.
|
|
354
427
|
return `${meta.id.padEnd(24)} ${meta.runs} runs ${String(meta.events).padStart(5)} events ${when} ${escapeTerminal(meta.title)}`;
|
|
355
428
|
}
|
|
429
|
+
export function relativeTime(updatedAt, now) {
|
|
430
|
+
const s = Math.max(0, now - updatedAt) / 1000;
|
|
431
|
+
if (s < 60)
|
|
432
|
+
return "now";
|
|
433
|
+
const m = Math.floor(s / 60);
|
|
434
|
+
if (m < 60)
|
|
435
|
+
return `${m}m ago`;
|
|
436
|
+
const h = Math.floor(m / 60);
|
|
437
|
+
if (h < 24)
|
|
438
|
+
return `${h}h ago`;
|
|
439
|
+
const d = Math.floor(h / 24);
|
|
440
|
+
if (d < 7)
|
|
441
|
+
return `${d}d ago`;
|
|
442
|
+
return `${Math.floor(d / 7)}w ago`;
|
|
443
|
+
}
|
|
444
|
+
function titleCut(text, max) {
|
|
445
|
+
if (displayWidth(text) <= max)
|
|
446
|
+
return text;
|
|
447
|
+
const room = max - displayWidth("…");
|
|
448
|
+
let w = 0;
|
|
449
|
+
let i = 0;
|
|
450
|
+
while (i < text.length) {
|
|
451
|
+
const cp = text.codePointAt(i);
|
|
452
|
+
const cw = charWidth(cp);
|
|
453
|
+
if (w + cw > room)
|
|
454
|
+
break;
|
|
455
|
+
w += cw;
|
|
456
|
+
i += cp > 0xffff ? 2 : 1;
|
|
457
|
+
}
|
|
458
|
+
return text.slice(0, i) + "…";
|
|
459
|
+
}
|
|
460
|
+
export function renderResumeList(metas, W, now) {
|
|
461
|
+
if (metas.length === 0)
|
|
462
|
+
return [];
|
|
463
|
+
const rows = [" ▞ resume"];
|
|
464
|
+
const whens = metas.map((m) => relativeTime(m.updatedAt, now));
|
|
465
|
+
const metaTexts = metas.map((m) => `${m.events} events · ${m.runs} runs`);
|
|
466
|
+
const metaW = Math.max(...metaTexts.map((t) => t.length));
|
|
467
|
+
const titleW = Math.max(1, W - 13 - metaW);
|
|
468
|
+
for (let i = 0; i < metas.length; i += 1) {
|
|
469
|
+
const title = escapeTerminal(metas[i].title);
|
|
470
|
+
const shown = titleCut(title, titleW);
|
|
471
|
+
const pad = titleW - displayWidth(shown);
|
|
472
|
+
rows.push(` ${whens[i].padEnd(7)} ${shown}${" ".repeat(pad)} ${metaTexts[i].padStart(metaW)}`);
|
|
473
|
+
}
|
|
474
|
+
return rows;
|
|
475
|
+
}
|
package/dist/width.d.ts
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.1.38",
|
|
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",
|