@vincemakes/kiso-tui 0.1.39 → 0.1.41
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 +6 -0
- package/dist/approval-panel.js +6 -0
- package/dist/components.d.ts +4 -222
- package/dist/components.js +4 -884
- package/dist/compositor.d.ts +31 -8
- package/dist/compositor.js +371 -100
- package/dist/diff.d.ts +4 -32
- package/dist/diff.js +4 -122
- package/dist/editor.d.ts +14 -0
- package/dist/editor.js +266 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/render.d.ts +11 -122
- package/dist/render.js +15 -292
- package/dist/width.d.ts +4 -15
- package/dist/width.js +4 -59
- package/package.json +5 -2
package/dist/render.d.ts
CHANGED
|
@@ -6,49 +6,14 @@
|
|
|
6
6
|
* the ergonomics batch C5: the input is the tui's OWN data shape (RenderInput), never
|
|
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
|
+
*
|
|
10
|
+
* ADR-0043 Amendment 4: the cell-rendering slice (the twelve helpers
|
|
11
|
+
* components.ts imports) moved to tui-cells — this module re-exports
|
|
12
|
+
* it (compositor.ts and index.ts's imports stay verbatim) and keeps
|
|
13
|
+
* the event renderers (renderEvent, the status line, the recap, the
|
|
14
|
+
* session line).
|
|
9
15
|
*/
|
|
10
|
-
|
|
11
|
-
* v2a — the palette, centralized (no hard-coded codes elsewhere); v5
|
|
12
|
-
* (TUI v5 #16e, the v4.1 design): the decorative blue (38;5;75) is
|
|
13
|
-
* RETIRED — the identity accents (the you> prompt, the banner tagline,
|
|
14
|
-
* ✓ marks, slash-command names, the input brick) are bright-white BOLD
|
|
15
|
-
* (SGR 1); the user message is the SGR-7 chip (the 2026-08-09 ruling
|
|
16
|
-
* retired the ▍ rail); `code` is the content semantic tint for
|
|
17
|
-
* inline code spans in assistant text (256-color 110 — the cube color
|
|
18
|
-
* nearest the design's #8fb4d8); red for errors, dim for metadata,
|
|
19
|
-
* green for the diff additions. NO_COLOR set, or a non-TTY output →
|
|
20
|
-
* every code is empty, so pipes and CI carry ZERO ANSI (the existing
|
|
21
|
-
* byte-level e2e assertions guard it). Everything not listed is plain.
|
|
22
|
-
*/
|
|
23
|
-
export interface Palette {
|
|
24
|
-
readonly bold: string;
|
|
25
|
-
readonly dim: string;
|
|
26
|
-
readonly red: string;
|
|
27
|
-
readonly green: string;
|
|
28
|
-
readonly code: string;
|
|
29
|
-
readonly rv: string;
|
|
30
|
-
readonly rvEnd: string;
|
|
31
|
-
readonly reset: string;
|
|
32
|
-
}
|
|
33
|
-
export declare const COLOR_ON: Palette;
|
|
34
|
-
export declare const COLOR_OFF: Palette;
|
|
35
|
-
export declare function palette(): Palette;
|
|
36
|
-
/**
|
|
37
|
-
* E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
38
|
-
* reaches the terminal — ESC, C0 (except \t \n), C1, CR, backspace, and
|
|
39
|
-
* bidi overrides. The kiso colors are applied by render, not by the data.
|
|
40
|
-
* EVERY externally-sourced string must pass through this before any output.
|
|
41
|
-
*/
|
|
42
|
-
export declare function escapeTerminal(text: string): string;
|
|
43
|
-
/**
|
|
44
|
-
* TUI v5 #16e: the inline-code tint — backtick spans in ONE line of
|
|
45
|
-
* assistant body text get the `code` color. Deliberately NOT a markdown
|
|
46
|
-
* engine: single level only (`[^`]*` cannot nest), a span never matches
|
|
47
|
-
* across lines (the caller passes one line; an opener without a closer
|
|
48
|
-
* on the same line stays plain). NO_COLOR / pipes → the codes are empty
|
|
49
|
-
* strings → the line passes through byte-identical.
|
|
50
|
-
*/
|
|
51
|
-
export declare function colorInlineCode(line: string): string;
|
|
16
|
+
export * from "@vincemakes/kiso-tui-cells/render";
|
|
52
17
|
/** The canonical-path resolver for the approval detail — injected by the
|
|
53
18
|
* caller (the CLI passes the tools' own resolution). The tui package is
|
|
54
19
|
* pure terminal: input is data, output is bytes, zero runtime deps. */
|
|
@@ -157,35 +122,7 @@ export type RenderInput = {
|
|
|
157
122
|
* consumer closes the segment with a newline at the next non-thinking
|
|
158
123
|
* event.
|
|
159
124
|
*/
|
|
160
|
-
/**
|
|
161
|
-
* v2b — one thinking BLOCK folds to ONE dim line: the first 100 chars, a
|
|
162
|
-
* " (… /think shows full)" marker when the block is longer. The consumer
|
|
163
|
-
* buffers the block's deltas, renders this at the block's end, and keeps
|
|
164
|
-
* the full text for /think. Pipes get the same fold — the content
|
|
165
|
-
* strategy is presentation-independent.
|
|
166
|
-
*/
|
|
167
|
-
export declare function foldThinking(block: string): string;
|
|
168
|
-
/** v2b — the [result] echo truncates at 160 chars + a /last hint. */
|
|
169
|
-
export declare function foldResult(content: string): string;
|
|
170
125
|
export declare function renderEvent(ev: RenderInput, prevThinking?: boolean, resolvePath?: PathResolver): RenderResult;
|
|
171
|
-
/**
|
|
172
|
-
* B area: one-line summary of a completed tool call, e.g.
|
|
173
|
-
* ✓ edit src/foo.ts (+12 -3) ✓ read src/bar.ts (140 lines)
|
|
174
|
-
* ✗ shell npm test (exit 1)
|
|
175
|
-
* edit/write show +/- line counts, read shows lines, shell shows the exit
|
|
176
|
-
* code; failures (isError) are ✗. Pure and deterministic.
|
|
177
|
-
*/
|
|
178
|
-
export declare function renderToolSummary(name: string, input: Record<string, unknown>, result: {
|
|
179
|
-
content: string;
|
|
180
|
-
isError: boolean;
|
|
181
|
-
}, reason?: string | null): string;
|
|
182
|
-
/** W15 — the expand header's target: the tool call's subject (the path
|
|
183
|
-
* for the *_file tools, the command for shell) — the same extraction
|
|
184
|
-
* the summary detail uses, WITHOUT the counts (the header names what
|
|
185
|
-
* was expanded, not its size). */
|
|
186
|
-
export declare function toolTarget(name: string, input: Record<string, unknown>): string;
|
|
187
|
-
/** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
|
|
188
|
-
export declare function kUnit(value: number | null): string;
|
|
189
126
|
/** B area: usage data gathered from the run's usage events. */
|
|
190
127
|
export interface RunUsage {
|
|
191
128
|
readonly in: number | null;
|
|
@@ -202,42 +139,6 @@ export interface RunUsage {
|
|
|
202
139
|
* passed in (chars/4 vs the window), marked with ~.
|
|
203
140
|
*/
|
|
204
141
|
export declare function renderStatusLine(turn: number, usage: RunUsage, ctxRatio: number, faux?: boolean): string | null;
|
|
205
|
-
/**
|
|
206
|
-
* v2a rhythm — the exact bytes after a terminal event: the status line
|
|
207
|
-
* hugs the terminal (show what there is — omitted when there is nothing to show),
|
|
208
|
-
* then EXACTLY one blank line before the next prompt. The consumer prints
|
|
209
|
-
* this verbatim; the render tests pin the sequence.
|
|
210
|
-
*/
|
|
211
|
-
export declare function renderTerminalGap(statusLine: string | null): string;
|
|
212
|
-
/**
|
|
213
|
-
* v3 §01 (V6-2) — the banner, block-split. The logo is THREE BRICK rows
|
|
214
|
-
* (the logo.svg pixel form — K I S O), then a BLANK, then the info rows:
|
|
215
|
-
* "kiso vX — tagline" + extensions. The tagline rides the version line
|
|
216
|
-
* (the old logo MIDDLE row was the tagline — a text row masquerading as
|
|
217
|
-
* the logo's centre). Every row truncates at the terminal width with a
|
|
218
|
-
* " (+N)" marker (N = the hidden display width); a window narrower than
|
|
219
|
-
* 40 columns skips the logo + the blank entirely — only the info rows.
|
|
220
|
-
* Pure.
|
|
221
|
-
*/
|
|
222
|
-
export declare const TAGLINE = "the coding agent that survives kill -9";
|
|
223
|
-
/** v3 §01 (W1): truncate a row at `width`, marking the hidden span
|
|
224
|
-
* " (+N)". W1: the width math is the charWidth authority (the banner's
|
|
225
|
-
* brick glyphs are 1 cell — the art's 38 columns clear 40), and the
|
|
226
|
-
* marker's own cells are part of the row — the visible cut leaves room
|
|
227
|
-
* for it, so a truncated row never exceeds W (a cut row carries the
|
|
228
|
-
* marker INSIDE the width; a row that fits is returned untouched). */
|
|
229
|
-
export declare function truncateRow(row: string, width: number): string;
|
|
230
|
-
/** v3 §01 (V6-2) + W1: the banner lines for a width W and height H —
|
|
231
|
-
* the tier table (extends the existing "under 40 columns, skip the
|
|
232
|
-
* logo" rule with a HEIGHT input):
|
|
233
|
-
* W ≥ 40 and H ≥ 20 → BIG (the 36x6 wordmark, 2-column indent)
|
|
234
|
-
* W ≥ 40 and 14–19 rows → COMPACT (v6's LOGO_ROWS, byte-identical)
|
|
235
|
-
* anything smaller → text rows only
|
|
236
|
-
* then the blank, then "vX — tagline" — the art IS the wordmark, so the
|
|
237
|
-
* text row does not repeat the name — then extensions — then the W5
|
|
238
|
-
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
239
|
-
* with a " (+N)" marker. Pure. */
|
|
240
|
-
export declare function bannerLines(W: number, H: number, version: string, extensionsText: string, resume?: readonly ResumeMeta[], now?: number): string[];
|
|
241
142
|
/** v3 §02 — the recap line that ends a run, replacing the "done" label +
|
|
242
143
|
* the old status line. All fields derive LOCALLY from the event stream
|
|
243
144
|
* (zero tokens): wall seconds, tool counts, usage, cache hit %, ctx left.
|
|
@@ -247,6 +148,10 @@ export interface RecapStats {
|
|
|
247
148
|
readonly tools: number;
|
|
248
149
|
readonly edits: number;
|
|
249
150
|
readonly usage: RunUsage;
|
|
151
|
+
/** R-C item 4: the per-turn cache miss (min(prevIn, in) − cacheRead),
|
|
152
|
+
* passed only when above the noise floor — the re-sent-uncached
|
|
153
|
+
* prefix. Absent → the recap bytes stay the historical form. */
|
|
154
|
+
readonly missed?: number;
|
|
250
155
|
readonly ctxLeftPct: number | null;
|
|
251
156
|
/** W19 — the mode the turn ran under. Under "plan" the recap becomes
|
|
252
157
|
* the way-forward row (the claimed shape): a plan turn's currency is
|
|
@@ -263,19 +168,3 @@ export declare function renderSessionLine(meta: {
|
|
|
263
168
|
runs: number;
|
|
264
169
|
updatedAt: number;
|
|
265
170
|
}): string;
|
|
266
|
-
/** W5 — the opening-screen resume list. Every field already exists
|
|
267
|
-
* behind renderSessionLine / `kiso sessions`: the relative time, the
|
|
268
|
-
* title, then the right-aligned "N events · M runs". The columns are
|
|
269
|
-
* fixed per W: 4 indent + 7 when + 1 + the title (the ONLY flexible
|
|
270
|
-
* field — cut with the ellipsis marker INSIDE the width) + 1 + the meta
|
|
271
|
-
* (padStart to metaW). The done-when: the meta's right edge lands at
|
|
272
|
-
* exactly W on every row. Returns PLAIN rows — the banner's uniform dim
|
|
273
|
-
* wrap styles them (no dim+bold SGR composition). */
|
|
274
|
-
export interface ResumeMeta {
|
|
275
|
-
readonly title: string;
|
|
276
|
-
readonly events: number;
|
|
277
|
-
readonly runs: number;
|
|
278
|
-
readonly updatedAt: number;
|
|
279
|
-
}
|
|
280
|
-
export declare function relativeTime(updatedAt: number, now: number): string;
|
|
281
|
-
export declare function renderResumeList(metas: readonly ResumeMeta[], W: number, now: number): string[];
|
package/dist/render.js
CHANGED
|
@@ -6,41 +6,15 @@
|
|
|
6
6
|
* the ergonomics batch C5: the input is the tui's OWN data shape (RenderInput), never
|
|
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
|
+
*
|
|
10
|
+
* ADR-0043 Amendment 4: the cell-rendering slice (the twelve helpers
|
|
11
|
+
* components.ts imports) moved to tui-cells — this module re-exports
|
|
12
|
+
* it (compositor.ts and index.ts's imports stay verbatim) and keeps
|
|
13
|
+
* the event renderers (renderEvent, the status line, the recap, the
|
|
14
|
+
* session line).
|
|
9
15
|
*/
|
|
10
|
-
import {
|
|
11
|
-
export
|
|
12
|
-
export const COLOR_OFF = { bold: "", dim: "", red: "", green: "", code: "", rv: "", rvEnd: "", reset: "" };
|
|
13
|
-
export function palette() {
|
|
14
|
-
return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* E group/round 8: strip terminal-injection vectors from MODEL/TOOL text before it
|
|
18
|
-
* reaches the terminal — ESC, C0 (except \t \n), C1, CR, backspace, and
|
|
19
|
-
* bidi overrides. The kiso colors are applied by render, not by the data.
|
|
20
|
-
* EVERY externally-sourced string must pass through this before any output.
|
|
21
|
-
*/
|
|
22
|
-
export function escapeTerminal(text) {
|
|
23
|
-
// eslint-disable-next-line no-control-regex
|
|
24
|
-
return text
|
|
25
|
-
.replace(/[\u0000-\u0008\u000d\u000e-\u001f\u007f]/g, "") // C0 (keeps only \t and \n)
|
|
26
|
-
.replace(/\u001b/g, "") // ESC
|
|
27
|
-
.replace(/[\u0080-\u009f]/g, "") // C1
|
|
28
|
-
.replace(/[\u202a-\u202e\u2066-\u2069]/g, ""); // bidi
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* TUI v5 #16e: the inline-code tint — backtick spans in ONE line of
|
|
32
|
-
* assistant body text get the `code` color. Deliberately NOT a markdown
|
|
33
|
-
* engine: single level only (`[^`]*` cannot nest), a span never matches
|
|
34
|
-
* across lines (the caller passes one line; an opener without a closer
|
|
35
|
-
* on the same line stays plain). NO_COLOR / pipes → the codes are empty
|
|
36
|
-
* strings → the line passes through byte-identical.
|
|
37
|
-
*/
|
|
38
|
-
export function colorInlineCode(line) {
|
|
39
|
-
const p = palette();
|
|
40
|
-
if (p.code === "" || p.reset === "")
|
|
41
|
-
return line;
|
|
42
|
-
return line.replace(/`([^`]*)`/g, `${p.code}\`$1\`${p.reset}`);
|
|
43
|
-
}
|
|
16
|
+
import { escapeTerminal, foldResult, foldThinking, kUnit, palette } from "@vincemakes/kiso-tui-cells/render";
|
|
17
|
+
export * from "@vincemakes/kiso-tui-cells/render";
|
|
44
18
|
/**
|
|
45
19
|
* Render one event. `text` may be a continuation (text_delta appends to the
|
|
46
20
|
* current line); `newline` says whether the line is complete.
|
|
@@ -50,25 +24,6 @@ export function colorInlineCode(line) {
|
|
|
50
24
|
* consumer closes the segment with a newline at the next non-thinking
|
|
51
25
|
* event.
|
|
52
26
|
*/
|
|
53
|
-
/**
|
|
54
|
-
* v2b — one thinking BLOCK folds to ONE dim line: the first 100 chars, a
|
|
55
|
-
* " (… /think shows full)" marker when the block is longer. The consumer
|
|
56
|
-
* buffers the block's deltas, renders this at the block's end, and keeps
|
|
57
|
-
* the full text for /think. Pipes get the same fold — the content
|
|
58
|
-
* strategy is presentation-independent.
|
|
59
|
-
*/
|
|
60
|
-
export function foldThinking(block) {
|
|
61
|
-
const p = palette();
|
|
62
|
-
const trimmed = escapeTerminal(block.trim());
|
|
63
|
-
const truncated = trimmed.length > 100;
|
|
64
|
-
return `${p.dim}…${trimmed.slice(0, 100)}${truncated ? ` (${block.length} chars · /think)` : ""}${p.reset}\n`;
|
|
65
|
-
}
|
|
66
|
-
/** v2b — the [result] echo truncates at 160 chars + a /last hint. */
|
|
67
|
-
export function foldResult(content) {
|
|
68
|
-
const flat = content.replaceAll("\n", " ");
|
|
69
|
-
const truncated = flat.length > 160;
|
|
70
|
-
return `${escapeTerminal(flat.slice(0, 160))}${truncated ? " (/last for full)" : ""}`;
|
|
71
|
-
}
|
|
72
27
|
export function renderEvent(ev, prevThinking = false, resolvePath = (p) => p) {
|
|
73
28
|
const p = palette();
|
|
74
29
|
switch (ev.type) {
|
|
@@ -182,97 +137,6 @@ function approvalDetail(name, input, resolvePath) {
|
|
|
182
137
|
}
|
|
183
138
|
return `\n ${escapeTerminal(JSON.stringify(input))}`;
|
|
184
139
|
}
|
|
185
|
-
/**
|
|
186
|
-
* B area: one-line summary of a completed tool call, e.g.
|
|
187
|
-
* ✓ edit src/foo.ts (+12 -3) ✓ read src/bar.ts (140 lines)
|
|
188
|
-
* ✗ shell npm test (exit 1)
|
|
189
|
-
* edit/write show +/- line counts, read shows lines, shell shows the exit
|
|
190
|
-
* code; failures (isError) are ✗. Pure and deterministic.
|
|
191
|
-
*/
|
|
192
|
-
export function renderToolSummary(name, input, result, reason = null) {
|
|
193
|
-
// v2a/v5: ✓ is a bold identity accent; ✗ stays red.
|
|
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
|
-
}
|
|
202
|
-
const mark = result.isError ? `${p.red}✗${p.reset}` : `${p.bold}✓${p.reset}`;
|
|
203
|
-
const shortName = name.replace("_file", "");
|
|
204
|
-
const detail = toolSummaryDetail(name, input, result);
|
|
205
|
-
return `${mark} ${escapeTerminal(`${shortName} ${detail}`)}`;
|
|
206
|
-
}
|
|
207
|
-
function toolSummaryDetail(name, input, result) {
|
|
208
|
-
// Line count without the phantom empty line after a trailing newline.
|
|
209
|
-
const lines = (text) => {
|
|
210
|
-
if (text === "")
|
|
211
|
-
return 0;
|
|
212
|
-
const parts = text.split("\n");
|
|
213
|
-
return parts[parts.length - 1] === "" ? parts.length - 1 : parts.length;
|
|
214
|
-
};
|
|
215
|
-
switch (name) {
|
|
216
|
-
case "read_file": {
|
|
217
|
-
const path = String(input.path ?? "?");
|
|
218
|
-
const count = lines(String(result.content));
|
|
219
|
-
return `${path} (${count} line${count === 1 ? "" : "s"})`;
|
|
220
|
-
}
|
|
221
|
-
case "write_file": {
|
|
222
|
-
const path = String(input.path ?? "?");
|
|
223
|
-
const count = lines(String(input.content ?? ""));
|
|
224
|
-
return `${path} (+${count})`;
|
|
225
|
-
}
|
|
226
|
-
case "edit_file": {
|
|
227
|
-
const path = String(input.path ?? "?");
|
|
228
|
-
const removed = lines(String(input.search ?? ""));
|
|
229
|
-
const added = lines(String(input.replace ?? ""));
|
|
230
|
-
return `${path} (+${added} -${removed})`;
|
|
231
|
-
}
|
|
232
|
-
case "shell": {
|
|
233
|
-
const command = String(input.command ?? "?");
|
|
234
|
-
const exit = exitCodeOf(result);
|
|
235
|
-
return `${command} (exit ${exit})`;
|
|
236
|
-
}
|
|
237
|
-
case "list_dir":
|
|
238
|
-
return String(input.path ?? "(root)");
|
|
239
|
-
default:
|
|
240
|
-
return String(input.path ?? input.command ?? "");
|
|
241
|
-
}
|
|
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
|
-
}
|
|
261
|
-
/** The exit code of a shell result: parsed from the failure text, 0 on success. */
|
|
262
|
-
function exitCodeOf(result) {
|
|
263
|
-
if (!result.isError)
|
|
264
|
-
return 0;
|
|
265
|
-
const m = /exit (\d+)/.exec(result.content);
|
|
266
|
-
return m !== null ? Number(m[1]) : 1;
|
|
267
|
-
}
|
|
268
|
-
/** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
|
|
269
|
-
export function kUnit(value) {
|
|
270
|
-
if (value === null)
|
|
271
|
-
return "?";
|
|
272
|
-
if (value >= 1000)
|
|
273
|
-
return `${(value / 1000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
274
|
-
return String(value);
|
|
275
|
-
}
|
|
276
140
|
/**
|
|
277
141
|
* B area/v2a: the one-line status bar after a terminal, e.g.
|
|
278
142
|
* [turn 3 · in 12.4k out 1.8k · cache 9.2k · ctx ~14%]
|
|
@@ -299,112 +163,17 @@ export function renderStatusLine(turn, usage, ctxRatio, faux = false) {
|
|
|
299
163
|
return null;
|
|
300
164
|
return `[turn ${turn} · ${parts.join(" · ")}]`;
|
|
301
165
|
}
|
|
302
|
-
/**
|
|
303
|
-
* v2a rhythm — the exact bytes after a terminal event: the status line
|
|
304
|
-
* hugs the terminal (show what there is — omitted when there is nothing to show),
|
|
305
|
-
* then EXACTLY one blank line before the next prompt. The consumer prints
|
|
306
|
-
* this verbatim; the render tests pin the sequence.
|
|
307
|
-
*/
|
|
308
|
-
export function renderTerminalGap(statusLine) {
|
|
309
|
-
return `${statusLine === null ? "" : `${statusLine}\n`}\n`;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* v3 §01 (V6-2) — the banner, block-split. The logo is THREE BRICK rows
|
|
313
|
-
* (the logo.svg pixel form — K I S O), then a BLANK, then the info rows:
|
|
314
|
-
* "kiso vX — tagline" + extensions. The tagline rides the version line
|
|
315
|
-
* (the old logo MIDDLE row was the tagline — a text row masquerading as
|
|
316
|
-
* the logo's centre). Every row truncates at the terminal width with a
|
|
317
|
-
* " (+N)" marker (N = the hidden display width); a window narrower than
|
|
318
|
-
* 40 columns skips the logo + the blank entirely — only the info rows.
|
|
319
|
-
* Pure.
|
|
320
|
-
*/
|
|
321
|
-
export const TAGLINE = "the coding agent that survives kill -9";
|
|
322
|
-
/** v6's existing logo — W1's COMPACT tier, byte-identical, no redraw. */
|
|
323
|
-
const LOGO_ROWS = ["█ █ ▀█▀ █▀▀ █▀█", "█▀▄ █ ▀▀█ █ █", "▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀"];
|
|
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). */
|
|
342
|
-
export function truncateRow(row, width) {
|
|
343
|
-
const total = displayWidth(row);
|
|
344
|
-
if (total <= width)
|
|
345
|
-
return row;
|
|
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;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
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()) {
|
|
378
|
-
const rows = [];
|
|
379
|
-
if (W >= 40) {
|
|
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
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if (rows.length > 0)
|
|
390
|
-
rows.push("");
|
|
391
|
-
rows.push(truncateRow(`v${version} — ${TAGLINE}`, W));
|
|
392
|
-
if (extensionsText !== "")
|
|
393
|
-
rows.push(truncateRow(extensionsText, W));
|
|
394
|
-
if (W >= 40 && H >= 20 && resume.length > 0) {
|
|
395
|
-
rows.push("", ...renderResumeList(resume, W, now));
|
|
396
|
-
}
|
|
397
|
-
return rows;
|
|
398
|
-
}
|
|
399
166
|
export function renderRecap(s) {
|
|
400
167
|
const p = palette();
|
|
401
168
|
// W19: under plan the recap is the way out of the mode — the header
|
|
402
169
|
// names the mode's posture, the hints name the exits (the ONLY
|
|
403
|
-
// controls — /mode is the only way to leave plan mode).
|
|
170
|
+
// controls — /mode is the only way to leave plan mode). The plan
|
|
171
|
+
// branch drops the metadata segments (ctx left) BEFORE the fold: the
|
|
172
|
+
// fixed hints are 79 cols — ONE row at W=80, never the folded line —
|
|
173
|
+
// and the ctx-left hint lives where it always lived: the status
|
|
174
|
+
// row's right side.
|
|
404
175
|
if (s.mode === "plan") {
|
|
405
176
|
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
177
|
return `${p.bold}▞${p.reset} ${parts.join(" · ")}\n`;
|
|
409
178
|
}
|
|
410
179
|
const parts = [`${s.seconds}s`, `${s.tools} tool${s.tools === 1 ? "" : "s"}${s.edits > 0 ? ` (${s.edits} edit${s.edits === 1 ? "" : "s"})` : ""}`];
|
|
@@ -413,7 +182,8 @@ export function renderRecap(s) {
|
|
|
413
182
|
if (seg !== "")
|
|
414
183
|
parts.push(seg);
|
|
415
184
|
if (s.usage.cache !== null && s.usage.in !== null && s.usage.in > 0) {
|
|
416
|
-
|
|
185
|
+
const hit = `cache ${Math.round((s.usage.cache / s.usage.in) * 100)}%`;
|
|
186
|
+
parts.push(s.missed !== undefined && s.missed > 0 ? `${hit} · miss ${kUnit(s.missed)}` : hit);
|
|
417
187
|
}
|
|
418
188
|
}
|
|
419
189
|
if (s.ctxLeftPct !== null)
|
|
@@ -426,50 +196,3 @@ export function renderSessionLine(meta) {
|
|
|
426
196
|
// round 8: the title is the user's first prompt — model/user text, escaped.
|
|
427
197
|
return `${meta.id.padEnd(24)} ${meta.runs} runs ${String(meta.events).padStart(5)} events ${when} ${escapeTerminal(meta.title)}`;
|
|
428
198
|
}
|
|
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
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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;
|
|
1
|
+
/** The display-width primitives moved to tui-cells (ADR-0043
|
|
2
|
+
* Amendment 4) — this module is the re-export shim: the tui's
|
|
3
|
+
* internal imports (./width.js) stay verbatim. */
|
|
4
|
+
export * from "@vincemakes/kiso-tui-cells/width";
|
package/dist/width.js
CHANGED
|
@@ -1,59 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
}
|
|
1
|
+
/** The display-width primitives moved to tui-cells (ADR-0043
|
|
2
|
+
* Amendment 4) — this module is the re-export shim: the tui's
|
|
3
|
+
* internal imports (./width.js) stay verbatim. */
|
|
4
|
+
export * from "@vincemakes/kiso-tui-cells/width";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
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",
|
|
@@ -33,5 +33,8 @@
|
|
|
33
33
|
"bugs": {
|
|
34
34
|
"url": "https://github.com/vincemakes/kiso/issues"
|
|
35
35
|
},
|
|
36
|
-
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme"
|
|
36
|
+
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.1.41"
|
|
39
|
+
}
|
|
37
40
|
}
|