@vincemakes/kiso-code 0.1.16 → 0.1.18
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/body.js +19 -6
- package/dist/dock.d.ts +21 -13
- package/dist/dock.js +42 -19
- package/dist/editor.d.ts +11 -0
- package/dist/editor.js +70 -4
- package/dist/index.js +96 -60
- package/dist/render.d.ts +29 -0
- package/dist/render.js +64 -4
- package/package.json +7 -7
package/dist/body.js
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
import { truncateDiff } from "./diff.js";
|
|
27
27
|
import { displayWidth } from "./editor.js";
|
|
28
28
|
import { escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, } from "./render.js";
|
|
29
|
-
/** The spinner glyphs, cycled by the heartbeat. */
|
|
30
|
-
const SPINNER = ["
|
|
29
|
+
/** The spinner glyphs, cycled by the heartbeat (v3 §05 — the working family). */
|
|
30
|
+
const SPINNER = ["▖", "▘", "▝", "▗"];
|
|
31
31
|
const TOOL_SUMMARY_MAX = 60; // the tool line's parameter summary, chars
|
|
32
32
|
const FRAME_MS = 16; // state changes coalesce to ≥16ms frames
|
|
33
33
|
const HEARTBEAT_MS = 200; // spinner / elapsed cadence
|
|
@@ -54,6 +54,16 @@ export class Body {
|
|
|
54
54
|
this.#active = opts.active();
|
|
55
55
|
if (this.#isActive()) {
|
|
56
56
|
this.#heartbeat = setInterval(() => {
|
|
57
|
+
// #14/#15: the idle heartbeat PAINTS NOTHING unless an
|
|
58
|
+
// ANIMATION advances — only a RUNNING tool's glyph/elapsed
|
|
59
|
+
// changes between beats. The #14 fix skipped an all-frozen
|
|
60
|
+
// body; #15 widened the skip to ANY no-change body: a cell
|
|
61
|
+
// that stays unfinished without animating (an unclosed text
|
|
62
|
+
// or thinking block) would otherwise re-paint the tail AND
|
|
63
|
+
// the dock every 200ms with zero change — the short-session
|
|
64
|
+
// leak (measured: 51KB / 46 beats after the recap, LF=0).
|
|
65
|
+
if (!this.#cells.some((c) => c.kind === "tool" && c.state === "running"))
|
|
66
|
+
return;
|
|
57
67
|
this.#spinnerI = (this.#spinnerI + 1) % SPINNER.length;
|
|
58
68
|
this.#dirty = true; // the running cells' glyph/elapsed advance
|
|
59
69
|
this.#scheduleFrame();
|
|
@@ -318,7 +328,7 @@ export class Body {
|
|
|
318
328
|
nextFrozen += 1;
|
|
319
329
|
const tail = this.#cells.slice(nextFrozen);
|
|
320
330
|
const tailHeight = tail.reduce((n, c) => n + this.#cellHeight(c, W), 0);
|
|
321
|
-
const tailTop = Math.max(1, H -
|
|
331
|
+
const tailTop = Math.max(1, H - 3 - tailHeight); // v3 §03: 4 dock rows below
|
|
322
332
|
const writeRow = Math.max(1, tailTop - 1); // the frozen area's bottom row
|
|
323
333
|
let scrolled = 0;
|
|
324
334
|
for (let i = this.#nextFrozen; i < nextFrozen; i += 1) {
|
|
@@ -339,7 +349,7 @@ export class Body {
|
|
|
339
349
|
// scrolls) and the current area, draw the cells at the body's bottom.
|
|
340
350
|
out.push("\x1b[?2026h");
|
|
341
351
|
const clearFrom = Math.min(this.#oldTailTop === 0 ? tailTop : this.#oldTailTop - scrolled, tailTop);
|
|
342
|
-
for (let row = clearFrom; row <= H -
|
|
352
|
+
for (let row = clearFrom; row <= H - 4; row += 1) {
|
|
343
353
|
out.push(`\x1b[${row};1H\x1b[0K`);
|
|
344
354
|
}
|
|
345
355
|
let row = tailTop;
|
|
@@ -362,13 +372,16 @@ export class Body {
|
|
|
362
372
|
const p = palette();
|
|
363
373
|
switch (cell.kind) {
|
|
364
374
|
case "user":
|
|
365
|
-
|
|
375
|
+
// v3 §02: the user message is a SGR BACKGROUND block, no
|
|
376
|
+
// prefix — every line carries the block's background
|
|
377
|
+
// (multi-line whole; resize-safe). Pipes stay plain.
|
|
378
|
+
return cell.text.split("\n").map((l) => `${p.bg}${escapeTerminal(l)}${p.reset}`);
|
|
366
379
|
case "thinking": {
|
|
367
380
|
const block = cell.text;
|
|
368
381
|
const trimmed = escapeTerminal(block.trim());
|
|
369
382
|
if (trimmed.length <= 100)
|
|
370
383
|
return [`${p.dim}…${trimmed}${p.reset}`];
|
|
371
|
-
return [`${p.dim}…${trimmed.slice(0, 100)} (
|
|
384
|
+
return [`${p.dim}…${trimmed.slice(0, 100)} (${block.length} chars · /think)${p.reset}`];
|
|
372
385
|
}
|
|
373
386
|
case "tool": {
|
|
374
387
|
const name = escapeTerminal(cell.name);
|
package/dist/dock.d.ts
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* implementation: zero dependencies, line-level ANSI, no differential
|
|
5
5
|
* renderer.
|
|
6
6
|
*
|
|
7
|
-
* Layout (H = terminal height): rows 1..H-
|
|
8
|
-
* streams and scrolls here, never touching the bottom), row H-
|
|
9
|
-
* dotted separator (╌), row H-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* Layout (H = terminal height): rows 1..H-4 = the scroll region (the body
|
|
8
|
+
* streams and scrolls here, never touching the bottom), row H-3 = the
|
|
9
|
+
* upper dim dotted separator (╌), row H-2 = the input line (the blue
|
|
10
|
+
* brick ▌you> + the v2c editor's row — readline is gone from the TTY
|
|
11
|
+
* path), row H-1 = the lower dotted separator, row H = the live status
|
|
12
|
+
* bar (v3 §03: idle "▸ <mode> · /mode to switch · …", running
|
|
13
|
+
* "▖ working Ns · …"; a takeover question replaces it). Bottom redraws
|
|
14
|
+
* are wrapped in CSI 2026 (synchronized output) to avoid flicker — the
|
|
15
|
+
* pi trick. The visual identity is the kiso brick motif — ▌ half-block,
|
|
16
|
+
* dotted separators — deliberately NOT the CC rounded frame nor the pi
|
|
15
17
|
* editor (ADR-0039 Amendment 2).
|
|
16
18
|
*
|
|
17
19
|
* Pipes / NO_COLOR: the dock never activates; the v2a line mode stays
|
|
@@ -42,6 +44,13 @@ export declare class Dock {
|
|
|
42
44
|
exit(): void;
|
|
43
45
|
/** SIGWINCH: recompute the size, redraw the chrome. */
|
|
44
46
|
onResize(): void;
|
|
47
|
+
/** v3 §04: bind the editor's slash-command menu state — the menu rows
|
|
48
|
+
* render ABOVE the chrome (over the body's bottom rows; the menu
|
|
49
|
+
* opens while the buffer is a "/" prefix, when no tail is live). */
|
|
50
|
+
bindMenu(state: () => {
|
|
51
|
+
items: readonly import("./editor.js").MenuItem[];
|
|
52
|
+
selected: number;
|
|
53
|
+
} | null): void;
|
|
45
54
|
/** The input line's edit column — prompt width + cursor + 1. The
|
|
46
55
|
* dock's redraw and the body's cursor return both end here, so the
|
|
47
56
|
* ACTUAL cursor always equals what the editor tracks. The width is
|
|
@@ -57,11 +66,10 @@ export declare class Dock {
|
|
|
57
66
|
* input line by the caller's readline); clearQuestion() restores. */
|
|
58
67
|
showQuestion(question: string): void;
|
|
59
68
|
clearQuestion(): void;
|
|
60
|
-
/** The bottom
|
|
69
|
+
/** The bottom four rows, wrapped in CSI 2026 (synchronized output —
|
|
61
70
|
* the pi trick against flicker). The cursor ends at the input line's
|
|
62
|
-
* edit position.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* brick ▌you> + the editor's visible slice. */
|
|
71
|
+
* edit position. v3 §03: the upper ╌ row, the input row, the lower
|
|
72
|
+
* ╌ row, the status row — the status is dim (blue accents inside
|
|
73
|
+
* come from the CLI's composition). */
|
|
66
74
|
redraw(): void;
|
|
67
75
|
}
|
package/dist/dock.js
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* implementation: zero dependencies, line-level ANSI, no differential
|
|
5
5
|
* renderer.
|
|
6
6
|
*
|
|
7
|
-
* Layout (H = terminal height): rows 1..H-
|
|
8
|
-
* streams and scrolls here, never touching the bottom), row H-
|
|
9
|
-
* dotted separator (╌), row H-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* Layout (H = terminal height): rows 1..H-4 = the scroll region (the body
|
|
8
|
+
* streams and scrolls here, never touching the bottom), row H-3 = the
|
|
9
|
+
* upper dim dotted separator (╌), row H-2 = the input line (the blue
|
|
10
|
+
* brick ▌you> + the v2c editor's row — readline is gone from the TTY
|
|
11
|
+
* path), row H-1 = the lower dotted separator, row H = the live status
|
|
12
|
+
* bar (v3 §03: idle "▸ <mode> · /mode to switch · …", running
|
|
13
|
+
* "▖ working Ns · …"; a takeover question replaces it). Bottom redraws
|
|
14
|
+
* are wrapped in CSI 2026 (synchronized output) to avoid flicker — the
|
|
15
|
+
* pi trick. The visual identity is the kiso brick motif — ▌ half-block,
|
|
16
|
+
* dotted separators — deliberately NOT the CC rounded frame nor the pi
|
|
15
17
|
* editor (ADR-0039 Amendment 2).
|
|
16
18
|
*
|
|
17
19
|
* Pipes / NO_COLOR: the dock never activates; the v2a line mode stays
|
|
@@ -75,8 +77,8 @@ export class Dock {
|
|
|
75
77
|
}
|
|
76
78
|
const H = this.#height;
|
|
77
79
|
process.stdout.write("\x1b[r"); // reset the scroll region
|
|
78
|
-
for (let row = H -
|
|
79
|
-
process.stdout.write(`\x1b[${row};1H\x1b[0K`); // clear the
|
|
80
|
+
for (let row = H - 3; row <= H; row += 1) {
|
|
81
|
+
process.stdout.write(`\x1b[${row};1H\x1b[0K`); // clear the four rows
|
|
80
82
|
}
|
|
81
83
|
process.stdout.write(`\x1b[${H};1H`);
|
|
82
84
|
}
|
|
@@ -88,6 +90,13 @@ export class Dock {
|
|
|
88
90
|
this.#width = process.stdout.columns ?? this.#width;
|
|
89
91
|
this.redraw();
|
|
90
92
|
}
|
|
93
|
+
#menuState = null;
|
|
94
|
+
/** v3 §04: bind the editor's slash-command menu state — the menu rows
|
|
95
|
+
* render ABOVE the chrome (over the body's bottom rows; the menu
|
|
96
|
+
* opens while the buffer is a "/" prefix, when no tail is live). */
|
|
97
|
+
bindMenu(state) {
|
|
98
|
+
this.#menuState = state;
|
|
99
|
+
}
|
|
91
100
|
/** The input line's edit column — prompt width + cursor + 1. The
|
|
92
101
|
* dock's redraw and the body's cursor return both end here, so the
|
|
93
102
|
* ACTUAL cursor always equals what the editor tracks. The width is
|
|
@@ -122,12 +131,11 @@ export class Dock {
|
|
|
122
131
|
this.#question = null;
|
|
123
132
|
this.redraw();
|
|
124
133
|
}
|
|
125
|
-
/** The bottom
|
|
134
|
+
/** The bottom four rows, wrapped in CSI 2026 (synchronized output —
|
|
126
135
|
* the pi trick against flicker). The cursor ends at the input line's
|
|
127
|
-
* edit position.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* brick ▌you> + the editor's visible slice. */
|
|
136
|
+
* edit position. v3 §03: the upper ╌ row, the input row, the lower
|
|
137
|
+
* ╌ row, the status row — the status is dim (blue accents inside
|
|
138
|
+
* come from the CLI's composition). */
|
|
131
139
|
redraw() {
|
|
132
140
|
if (!this.#active)
|
|
133
141
|
return;
|
|
@@ -143,10 +151,25 @@ export class Dock {
|
|
|
143
151
|
// \x1b[?2026h/l, the pi source's exact form. Without it terminals
|
|
144
152
|
// silently ignore the mode and the anti-flicker never engages.
|
|
145
153
|
out.push("\x1b[?2026h"); // synchronized output ON (DEC 2026)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
// v3 §04: the slash-command menu — above the chrome, one row per
|
|
155
|
+
// filtered command, the selection highlighted. Drawn first so the
|
|
156
|
+
// chrome rows repaint on top of any overlap.
|
|
157
|
+
const menu = this.#menuState?.();
|
|
158
|
+
if (menu !== null && menu !== undefined) {
|
|
159
|
+
for (let i = 0; i < menu.items.length; i += 1) {
|
|
160
|
+
const item = menu.items[i];
|
|
161
|
+
const row = H - 4 - (menu.items.length - 1 - i);
|
|
162
|
+
const text = i === menu.selected
|
|
163
|
+
? `${p.blue}▸ ${item.name}${p.reset} ${item.desc}`
|
|
164
|
+
: `${p.dim} ${item.name} ${item.desc}${p.reset}`;
|
|
165
|
+
out.push(`\x1b[${row};1H\x1b[0K${text}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
out.push(`\x1b[${H - 3};1H\x1b[0K${sep}`);
|
|
169
|
+
out.push(`\x1b[${H - 2};1H\x1b[0K${this.#inputPrompt}${inp.line}`);
|
|
170
|
+
out.push(`\x1b[${H - 1};1H\x1b[0K${sep}`);
|
|
171
|
+
out.push(`\x1b[${H};1H\x1b[0K${statusLine}`);
|
|
172
|
+
out.push(`\x1b[${H - 2};${this.#inputCol()}H`); // back to the edit position
|
|
150
173
|
out.push("\x1b[?2026l"); // synchronized output OFF
|
|
151
174
|
process.stdout.write(out.join(""));
|
|
152
175
|
}
|
package/dist/editor.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export declare function widthOf(chars: readonly number[]): number;
|
|
|
24
24
|
export declare function displayWidth(text: string): number;
|
|
25
25
|
export declare const PROMPT = "\u258Cyou> ";
|
|
26
26
|
export declare const PROMPT_WIDTH: number;
|
|
27
|
+
/** v3 §04 — the slash-command menu's command table (English one-liners). */
|
|
28
|
+
export interface MenuItem {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly desc: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const MENU_ITEMS: readonly MenuItem[];
|
|
27
33
|
/**
|
|
28
34
|
* The editor. Raw mode + bracketed paste (?2004h) on enter, restored on
|
|
29
35
|
* exit. The input row is rendered by `onRender` (the CLI wires it to the
|
|
@@ -48,6 +54,11 @@ export declare class Editor {
|
|
|
48
54
|
line: string;
|
|
49
55
|
cursor: number;
|
|
50
56
|
};
|
|
57
|
+
/** v3 §04: the menu's visible state for the dock — null when closed. */
|
|
58
|
+
menuState(): {
|
|
59
|
+
items: readonly MenuItem[];
|
|
60
|
+
selected: number;
|
|
61
|
+
} | null;
|
|
51
62
|
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
52
63
|
question(_query: string, cb: (answer: string) => void): void;
|
|
53
64
|
/** Cancel a pending question — the buffer stays (its text becomes the
|
package/dist/editor.js
CHANGED
|
@@ -69,6 +69,13 @@ export function displayWidth(text) {
|
|
|
69
69
|
import { palette } from "./render.js";
|
|
70
70
|
export const PROMPT = "▌you> "; // the kiso brick motif: one blue half-block, then you>
|
|
71
71
|
export const PROMPT_WIDTH = displayWidth(PROMPT);
|
|
72
|
+
export const MENU_ITEMS = [
|
|
73
|
+
{ name: "/mode", desc: "switch the approval tier (manual/default/accept-edits/plan/bypass)" },
|
|
74
|
+
{ name: "/think", desc: "show the last full thinking block" },
|
|
75
|
+
{ name: "/last", desc: "show the most recent tool call's input and output" },
|
|
76
|
+
{ name: "/status", desc: "show session id, event count, and context estimate" },
|
|
77
|
+
{ name: "/help", desc: "print this list of commands" },
|
|
78
|
+
];
|
|
72
79
|
/**
|
|
73
80
|
* The editor. Raw mode + bracketed paste (?2004h) on enter, restored on
|
|
74
81
|
* exit. The input row is rendered by `onRender` (the CLI wires it to the
|
|
@@ -88,6 +95,8 @@ export class Editor {
|
|
|
88
95
|
#eotCb = null;
|
|
89
96
|
#escapeCb = null;
|
|
90
97
|
#onRender;
|
|
98
|
+
#menuOpen = false; // v3 §04: the slash-command menu
|
|
99
|
+
#menuSel = 0;
|
|
91
100
|
#pending = ""; // an incomplete ESC/CSI prefix across chunks
|
|
92
101
|
#decoder = new TextDecoder();
|
|
93
102
|
#entered = false;
|
|
@@ -136,6 +145,27 @@ export class Editor {
|
|
|
136
145
|
const col = (this.#scroll > 0 ? 1 : 0) + widthOf(this.#chars.slice(this.#scroll, this.#cursor));
|
|
137
146
|
return { line: `${prefix}${visible}`, cursor: col };
|
|
138
147
|
}
|
|
148
|
+
/** v3 §04: the menu's visible state for the dock — null when closed. */
|
|
149
|
+
menuState() {
|
|
150
|
+
if (!this.#menuOpen)
|
|
151
|
+
return null;
|
|
152
|
+
return { items: this.#menuFiltered(), selected: this.#menuSel };
|
|
153
|
+
}
|
|
154
|
+
/** v3 §04: the filtered command list for the current buffer — open
|
|
155
|
+
* only while the line is "/" + something (a bare "/" waits). */
|
|
156
|
+
#menuFiltered() {
|
|
157
|
+
const line = this.line();
|
|
158
|
+
if (!line.startsWith("/") || line === "/")
|
|
159
|
+
return [];
|
|
160
|
+
return MENU_ITEMS.filter((m) => m.name.startsWith(line));
|
|
161
|
+
}
|
|
162
|
+
#refreshMenu() {
|
|
163
|
+
const f = this.#menuFiltered();
|
|
164
|
+
this.#menuOpen = f.length > 0;
|
|
165
|
+
if (this.#menuSel >= f.length)
|
|
166
|
+
this.#menuSel = 0;
|
|
167
|
+
this.#onRender();
|
|
168
|
+
}
|
|
139
169
|
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
140
170
|
question(_query, cb) {
|
|
141
171
|
this.#questionCb = cb;
|
|
@@ -195,6 +225,13 @@ export class Editor {
|
|
|
195
225
|
else if (rest.startsWith("O")) {
|
|
196
226
|
i += 3; // SS3 (function keys) — ignored
|
|
197
227
|
}
|
|
228
|
+
else if (this.#menuOpen) {
|
|
229
|
+
// v3 §04: Esc closes the menu and clears the buffer.
|
|
230
|
+
this.#chars = [];
|
|
231
|
+
this.#cursor = 0;
|
|
232
|
+
this.#scroll = 0;
|
|
233
|
+
this.#refreshMenu();
|
|
234
|
+
}
|
|
198
235
|
else {
|
|
199
236
|
this.#escapeCb?.();
|
|
200
237
|
i += 1;
|
|
@@ -245,6 +282,18 @@ export class Editor {
|
|
|
245
282
|
this.#onRender();
|
|
246
283
|
i += 1;
|
|
247
284
|
}
|
|
285
|
+
else if (c === "\t" && this.#menuOpen) {
|
|
286
|
+
// v3 §04: Tab completes the buffer to the selected command.
|
|
287
|
+
const f = this.#menuFiltered();
|
|
288
|
+
const m = f[this.#menuSel];
|
|
289
|
+
if (m !== undefined) {
|
|
290
|
+
this.#chars = [...m.name].map((ch) => ch.codePointAt(0));
|
|
291
|
+
this.#cursor = this.#chars.length;
|
|
292
|
+
this.#reflow();
|
|
293
|
+
this.#refreshMenu();
|
|
294
|
+
}
|
|
295
|
+
i += 1;
|
|
296
|
+
}
|
|
248
297
|
else if (c !== undefined && c < " ") {
|
|
249
298
|
i += 1; // other control — ignored
|
|
250
299
|
}
|
|
@@ -266,6 +315,15 @@ export class Editor {
|
|
|
266
315
|
this.#onRender();
|
|
267
316
|
}
|
|
268
317
|
}
|
|
318
|
+
else if (final === "A" && this.#menuOpen) {
|
|
319
|
+
// v3 §04: ↑↓ move the menu selection, never the cursor.
|
|
320
|
+
this.#menuSel = Math.max(0, this.#menuSel - 1);
|
|
321
|
+
this.#onRender();
|
|
322
|
+
}
|
|
323
|
+
else if (final === "B" && this.#menuOpen) {
|
|
324
|
+
this.#menuSel = Math.min(this.#menuFiltered().length - 1, this.#menuSel + 1);
|
|
325
|
+
this.#onRender();
|
|
326
|
+
}
|
|
269
327
|
else if (final === "D") {
|
|
270
328
|
this.#move(-1);
|
|
271
329
|
}
|
|
@@ -287,7 +345,7 @@ export class Editor {
|
|
|
287
345
|
this.#cursor += 1;
|
|
288
346
|
this.#reflow();
|
|
289
347
|
if (!this.#pasting)
|
|
290
|
-
this.#
|
|
348
|
+
this.#refreshMenu();
|
|
291
349
|
}
|
|
292
350
|
#backspace() {
|
|
293
351
|
if (this.#cursor === 0)
|
|
@@ -296,7 +354,7 @@ export class Editor {
|
|
|
296
354
|
this.#cursor -= 1;
|
|
297
355
|
this.#reflow();
|
|
298
356
|
if (!this.#pasting)
|
|
299
|
-
this.#
|
|
357
|
+
this.#refreshMenu();
|
|
300
358
|
}
|
|
301
359
|
#delete() {
|
|
302
360
|
if (this.#cursor >= this.#chars.length)
|
|
@@ -304,7 +362,7 @@ export class Editor {
|
|
|
304
362
|
this.#chars.splice(this.#cursor, 1);
|
|
305
363
|
this.#reflow();
|
|
306
364
|
if (!this.#pasting)
|
|
307
|
-
this.#
|
|
365
|
+
this.#refreshMenu();
|
|
308
366
|
}
|
|
309
367
|
#move(delta) {
|
|
310
368
|
this.#cursor = Math.max(0, Math.min(this.#chars.length, this.#cursor + delta));
|
|
@@ -336,10 +394,18 @@ export class Editor {
|
|
|
336
394
|
this.#reflow();
|
|
337
395
|
}
|
|
338
396
|
#submit() {
|
|
339
|
-
|
|
397
|
+
let line = String.fromCodePoint(...this.#chars);
|
|
398
|
+
if (this.#menuOpen) {
|
|
399
|
+
// v3 §04: Enter submits the SELECTED command.
|
|
400
|
+
const m = this.#menuFiltered()[this.#menuSel];
|
|
401
|
+
if (m !== undefined)
|
|
402
|
+
line = m.name;
|
|
403
|
+
}
|
|
340
404
|
this.#chars = [];
|
|
341
405
|
this.#cursor = 0;
|
|
342
406
|
this.#scroll = 0;
|
|
407
|
+
this.#menuOpen = false;
|
|
408
|
+
this.#menuSel = 0;
|
|
343
409
|
const cb = this.#questionCb;
|
|
344
410
|
this.#questionCb = null;
|
|
345
411
|
if (cb !== null) {
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
import { createAgent, disposeExtensions, loadExtensions, loadProjectExtensions, projectArtifacts, recordTrust, SessionStore, trustFor, } from "@vincemakes/kiso-runtime";
|
|
28
28
|
import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
29
29
|
import { createCodingTools } from "@vincemakes/kiso-tools-node";
|
|
30
|
-
import { escapeTerminal, foldResult, foldThinking, palette, renderEvent, renderSessionLine, renderStatusLine, renderTerminalGap, renderToolSummary, } from "./render.js";
|
|
30
|
+
import { escapeTerminal, foldResult, foldThinking, palette, renderEvent, renderSessionLine, renderStatusLine, renderTerminalGap, renderToolSummary, bannerLines, kUnit, renderRecap, truncateRow, } from "./render.js";
|
|
31
31
|
import { Dock } from "./dock.js";
|
|
32
32
|
/** 发现#11: KISO_HOME is the ONE root — every default path derives from
|
|
33
33
|
* it (sessions, trust, extensions, mcp config, skills). The dedicated
|
|
@@ -67,17 +67,18 @@ catch {
|
|
|
67
67
|
* merges into the third row on TTY and stays a standalone line off-TTY.
|
|
68
68
|
* v2a: the logo rows stay dim; the TAGLINE (row 2) is the blue identity
|
|
69
69
|
* accent. */
|
|
70
|
-
const LOGO_TOP = "█ █ ▀█▀ █▀▀ █▀█\n█▀▄ █ ▀▀█ █ █ ";
|
|
71
|
-
const TAGLINE = "the coding agent that survives kill -9";
|
|
72
|
-
const LOGO_BOTTOM = "\n▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀";
|
|
73
70
|
function startupBanner() {
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
71
|
+
// v3 §01: the banner is block-split — three independent logo rows
|
|
72
|
+
// (TOP / tagline / BOTTOM), then TWO info rows (version,
|
|
73
|
+
// extensions), each truncated at the window width; < 40 columns
|
|
74
|
+
// skips the logo. The historical `[N extensions: names]` text rides
|
|
75
|
+
// the extensions row verbatim (the e2e assertions keep matching).
|
|
78
76
|
const p = palette();
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// A pty without a winsize reports columns = 0 (not undefined) — treat
|
|
78
|
+
// it as the default width, never as a 0-column truncation.
|
|
79
|
+
const W = process.stdout.columns ?? 0;
|
|
80
|
+
const rows = bannerLines(W > 0 ? W : 80, VERSION, bannerExtensionText().replace(/^ · /, ""));
|
|
81
|
+
return `${rows.map((r) => `${p.dim}${r}${p.reset}`).join("\n")}\n`;
|
|
81
82
|
}
|
|
82
83
|
/** v2a: the interactive prompt — blue, the identity accent. readline owns
|
|
83
84
|
* the echo of what the user types; we own the prompt's color. (v2c: the
|
|
@@ -194,6 +195,7 @@ function makeLineInput() {
|
|
|
194
195
|
editor.enter();
|
|
195
196
|
const p = palette();
|
|
196
197
|
dock.bindInput(() => editor.dockState(), `${p.blue}${EDITOR_PROMPT}${p.reset}`);
|
|
198
|
+
dock.bindMenu(() => editor.menuState()); // v3 §04: the slash-command menu
|
|
197
199
|
return editorInput(editor);
|
|
198
200
|
}
|
|
199
201
|
return readlineInput(createInterface({ input: process.stdin, output: process.stdout }));
|
|
@@ -214,27 +216,19 @@ function bodyLog(text) {
|
|
|
214
216
|
/** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
|
|
215
217
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
216
218
|
* first event. */
|
|
217
|
-
function startStatusSpinner() {
|
|
219
|
+
function startStatusSpinner(onTick) {
|
|
218
220
|
if (!dock.active)
|
|
219
221
|
return () => { };
|
|
220
|
-
|
|
222
|
+
// v3 §03/§05: the working glyph family ▖▘▝▗, 200ms rotation — the
|
|
223
|
+
// callback repaints the running status line with the new glyph.
|
|
224
|
+
const GLYPHS = ["▖", "▘", "▝", "▗"];
|
|
221
225
|
let i = 0;
|
|
222
|
-
const timer = setInterval(() =>
|
|
226
|
+
const timer = setInterval(() => onTick(GLYPHS[i++ % GLYPHS.length]), 200);
|
|
223
227
|
timer.unref();
|
|
224
|
-
return () =>
|
|
225
|
-
}
|
|
226
|
-
/** v2b: "running <tool> Ns" in the status bar while a tool executes. */
|
|
227
|
-
function startRunningTimer(name) {
|
|
228
|
-
if (!dock.active)
|
|
229
|
-
return () => { };
|
|
230
|
-
const started = Date.now();
|
|
231
|
-
const timer = setInterval(() => dock.setTail(`running ${name} ${Math.round((Date.now() - started) / 1000)}s`), 1000);
|
|
232
|
-
timer.unref();
|
|
233
|
-
return () => {
|
|
234
|
-
clearInterval(timer);
|
|
235
|
-
dock.setTail("");
|
|
236
|
-
};
|
|
228
|
+
return () => clearInterval(timer);
|
|
237
229
|
}
|
|
230
|
+
/** v3 §03: "running <tool> Ns" is gone — the running status line owns
|
|
231
|
+
* the wall clock; the per-tool timer was the old tail mechanism. */
|
|
238
232
|
/** The model name for the status bar — set by makeAgent. */
|
|
239
233
|
let agentModel = "faux";
|
|
240
234
|
/** E3: the `[N extensions: ...]` text — user-level names, then project-level
|
|
@@ -251,21 +245,6 @@ function bannerExtensionText() {
|
|
|
251
245
|
parts.push(`project: ${projectExtensions.map((e) => e.name).join(", ")}`);
|
|
252
246
|
return ` · [${total} extension${total === 1 ? "" : "s"}: ${parts.join(" · ")}]`;
|
|
253
247
|
}
|
|
254
|
-
/** Modes: the status-bar indicator — the default tier shows nothing; the
|
|
255
|
-
* others show their blue name, the dangerous ones (plan/bypass) with the
|
|
256
|
-
* ⚠ prefix. */
|
|
257
|
-
function modeStatusText() {
|
|
258
|
-
if (getMode() === "default")
|
|
259
|
-
return "";
|
|
260
|
-
const p = palette();
|
|
261
|
-
const danger = getMode() === "plan" || getMode() === "bypass" ? "⚠ " : "";
|
|
262
|
-
return `${p.blue}${danger}${getMode()}${p.reset}`;
|
|
263
|
-
}
|
|
264
|
-
/** Modes: append the mode indicator to a composed status base. */
|
|
265
|
-
function statusWithMode(base) {
|
|
266
|
-
const mode = modeStatusText();
|
|
267
|
-
return mode === "" ? base : `${base} · ${mode}`;
|
|
268
|
-
}
|
|
269
248
|
/** E1: the startup banner line(s) — TTY: logo + merged extensions; off-TTY:
|
|
270
249
|
* the historical `[N extensions: ...]` standalone line (zero change). */
|
|
271
250
|
function extensionsBanner() {
|
|
@@ -751,6 +730,11 @@ function approvalDiff(name, input) {
|
|
|
751
730
|
async function consumeRun(session, run, input, turnNo, faux, liveInput, statusCb) {
|
|
752
731
|
let last;
|
|
753
732
|
let usage = { in: null, out: null, cache: null, known: false };
|
|
733
|
+
// v3 §02: the recap line derives ENTIRELY from the local event stream
|
|
734
|
+
// (zero tokens) — wall seconds, tool/edit counts, usage, ctx left.
|
|
735
|
+
const turnStart = Date.now();
|
|
736
|
+
let toolCount = 0;
|
|
737
|
+
let editCount = 0;
|
|
754
738
|
try {
|
|
755
739
|
for await (const ev of run) {
|
|
756
740
|
last = ev;
|
|
@@ -776,6 +760,9 @@ async function consumeRun(session, run, input, turnNo, faux, liveInput, statusCb
|
|
|
776
760
|
body.thinkingAppend(ev.text);
|
|
777
761
|
break;
|
|
778
762
|
case "tool_call_end":
|
|
763
|
+
toolCount += 1;
|
|
764
|
+
if (ev.name === "edit_file")
|
|
765
|
+
editCount += 1;
|
|
779
766
|
body.toolStart(ev.name, ev.callId, ev.input ?? {});
|
|
780
767
|
break;
|
|
781
768
|
case "tool_execution_started":
|
|
@@ -828,14 +815,21 @@ async function consumeRun(session, run, input, turnNo, faux, liveInput, statusCb
|
|
|
828
815
|
await session.approve(decisionId, answer.trim().toLowerCase().startsWith("y"));
|
|
829
816
|
break;
|
|
830
817
|
}
|
|
831
|
-
case "terminal":
|
|
832
|
-
|
|
818
|
+
case "terminal": {
|
|
819
|
+
// v3 §02: the run's recap line REPLACES the old "done" label
|
|
820
|
+
// + status line — one local line, derived from this run's
|
|
821
|
+
// events (zero tokens). The dock's status bar still paints.
|
|
833
822
|
statusCb?.(usage, estimateCtxRatio(session));
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
823
|
+
const ratio = estimateCtxRatio(session);
|
|
824
|
+
bodyLog(renderRecap({
|
|
825
|
+
seconds: Math.round((Date.now() - turnStart) / 1000),
|
|
826
|
+
tools: toolCount,
|
|
827
|
+
edits: editCount,
|
|
828
|
+
usage,
|
|
829
|
+
ctxLeftPct: Number.isFinite(ratio) ? (1 - ratio) * 100 : null,
|
|
830
|
+
}));
|
|
838
831
|
break;
|
|
832
|
+
}
|
|
839
833
|
default: {
|
|
840
834
|
// Events without a cell (stop, …) — the generic render, byte-
|
|
841
835
|
// preserved for the pipe path.
|
|
@@ -892,10 +886,20 @@ async function chat(session, faux, input) {
|
|
|
892
886
|
currentRun = run;
|
|
893
887
|
turnNo += 1;
|
|
894
888
|
const myTurn = turnNo;
|
|
889
|
+
// v3 §03: the running state owns the status bar — the glyph
|
|
890
|
+
// rotates every 200ms; the idle state returns after the run.
|
|
891
|
+
runStart = Date.now();
|
|
892
|
+
runUsage = { in: null, out: null, cache: null, known: false };
|
|
893
|
+
const stopSpinner = startStatusSpinner((g) => {
|
|
894
|
+
runGlyph = g;
|
|
895
|
+
paintRunning();
|
|
896
|
+
});
|
|
895
897
|
(async () => {
|
|
896
898
|
let last;
|
|
897
899
|
try {
|
|
898
900
|
last = await consumeRun(session, run, input, myTurn, faux, liveInput, statusCb);
|
|
901
|
+
stopSpinner();
|
|
902
|
+
paintIdle();
|
|
899
903
|
currentRun = null;
|
|
900
904
|
// 八: a faux script that ran out of declared turns exits
|
|
901
905
|
// loudly with a non-zero status — never a silent status 0.
|
|
@@ -980,18 +984,31 @@ async function chat(session, faux, input) {
|
|
|
980
984
|
let queued = 0;
|
|
981
985
|
// v2b: the live status bar (docked only). Modes: /mode switches repaint
|
|
982
986
|
// it immediately through paintStatus (the last turn stats are kept).
|
|
983
|
-
|
|
984
|
-
|
|
987
|
+
// v3 §03: the status bar has TWO states. Idle: the mode is ALWAYS
|
|
988
|
+
// shown (default included) with the /mode hint. Running: the working
|
|
989
|
+
// glyph (▖▘▝▗ — the spinner drives it) + wall seconds + ↓ out tokens
|
|
990
|
+
// + the interrupt hint. ctx left is the live estimate everywhere.
|
|
991
|
+
let runUsage = { in: null, out: null, cache: null, known: false };
|
|
992
|
+
let runGlyph = "▖";
|
|
993
|
+
let runStart = Date.now();
|
|
994
|
+
const paintRunning = () => {
|
|
985
995
|
if (!dock.active)
|
|
986
996
|
return;
|
|
987
|
-
|
|
988
|
-
|
|
997
|
+
const ratio = estimateCtxRatio(session);
|
|
998
|
+
const pct = Number.isFinite(ratio) ? Math.round((1 - ratio) * 100) : null;
|
|
999
|
+
const out = runUsage.out !== null ? ` ↓ ${kUnit(runUsage.out)} tokens` : "";
|
|
1000
|
+
dock.setStatus(`${runGlyph} working ${Math.max(1, Math.round((Date.now() - runStart) / 1000))}s${out} · esc to interrupt · ctx left ~${pct}%`);
|
|
989
1001
|
};
|
|
990
|
-
const
|
|
1002
|
+
const paintIdle = () => {
|
|
991
1003
|
if (!dock.active)
|
|
992
1004
|
return;
|
|
993
|
-
const
|
|
994
|
-
|
|
1005
|
+
const ratio = estimateCtxRatio(session);
|
|
1006
|
+
const pct = Number.isFinite(ratio) ? Math.round((1 - ratio) * 100) : null;
|
|
1007
|
+
dock.setStatus(`▸ ${getMode()} · /mode to switch · ${agentModel} · ctx left ~${pct}%`);
|
|
1008
|
+
};
|
|
1009
|
+
const statusCb = (u, ctx) => {
|
|
1010
|
+
runUsage = u;
|
|
1011
|
+
paintRunning();
|
|
995
1012
|
};
|
|
996
1013
|
// The ONE dispatcher: slash commands, exit, and turns. The recovery
|
|
997
1014
|
// replay routes through it too — a queued "/last" must never become a
|
|
@@ -1080,7 +1097,7 @@ async function chat(session, faux, input) {
|
|
|
1080
1097
|
else {
|
|
1081
1098
|
setMode(m);
|
|
1082
1099
|
body.notice(`mode → ${m}`);
|
|
1083
|
-
|
|
1100
|
+
paintIdle();
|
|
1084
1101
|
}
|
|
1085
1102
|
input.prompt();
|
|
1086
1103
|
});
|
|
@@ -1147,22 +1164,41 @@ async function resume(session, prompt, faux, input) {
|
|
|
1147
1164
|
let currentRun = null;
|
|
1148
1165
|
let cancelled = false;
|
|
1149
1166
|
let turnNo = 0;
|
|
1150
|
-
//
|
|
1167
|
+
// v3 §03: the two-state status bar (see chat — same shapes).
|
|
1168
|
+
let runUsage = { in: null, out: null, cache: null, known: false };
|
|
1169
|
+
let runGlyph = "▖";
|
|
1170
|
+
let runStart = Date.now();
|
|
1151
1171
|
const statusCb = (u, ctx) => {
|
|
1172
|
+
runUsage = u;
|
|
1152
1173
|
if (!dock.active)
|
|
1153
1174
|
return;
|
|
1154
|
-
const
|
|
1155
|
-
const
|
|
1156
|
-
dock.setStatus(
|
|
1175
|
+
const pct = Number.isFinite(ctx) ? Math.round((1 - ctx) * 100) : null;
|
|
1176
|
+
const out = runUsage.out !== null ? ` ↓ ${kUnit(runUsage.out)} tokens` : "";
|
|
1177
|
+
dock.setStatus(`${runGlyph} working ${Math.max(1, Math.round((Date.now() - runStart) / 1000))}s${out} · esc to interrupt · ctx left ~${pct}%`);
|
|
1178
|
+
};
|
|
1179
|
+
const paintIdle = () => {
|
|
1180
|
+
if (!dock.active)
|
|
1181
|
+
return;
|
|
1182
|
+
const ratio = estimateCtxRatio(session);
|
|
1183
|
+
const pct = Number.isFinite(ratio) ? Math.round((1 - ratio) * 100) : null;
|
|
1184
|
+
dock.setStatus(`▸ ${getMode()} · /mode to switch · ${agentModel} · ctx left ~${pct}%`);
|
|
1157
1185
|
};
|
|
1158
1186
|
const withRun = async (run) => {
|
|
1159
1187
|
currentRun = run;
|
|
1188
|
+
runStart = Date.now();
|
|
1189
|
+
runUsage = { in: null, out: null, cache: null, known: false };
|
|
1190
|
+
const stopSpinner = startStatusSpinner((g) => {
|
|
1191
|
+
runGlyph = g;
|
|
1192
|
+
statusCb(runUsage, estimateCtxRatio(session));
|
|
1193
|
+
});
|
|
1160
1194
|
try {
|
|
1161
1195
|
turnNo += 1;
|
|
1162
1196
|
const last = await consumeRun(session, run, input, turnNo, faux, null, statusCb);
|
|
1163
1197
|
failOnFauxExhaustion(last, faux, input);
|
|
1164
1198
|
}
|
|
1165
1199
|
finally {
|
|
1200
|
+
stopSpinner();
|
|
1201
|
+
paintIdle();
|
|
1166
1202
|
currentRun = null;
|
|
1167
1203
|
}
|
|
1168
1204
|
};
|
|
@@ -1287,7 +1323,7 @@ async function main() {
|
|
|
1287
1323
|
}
|
|
1288
1324
|
case "help": {
|
|
1289
1325
|
const p = palette();
|
|
1290
|
-
console.log(`${p.dim}${
|
|
1326
|
+
console.log(`${p.dim}${bannerLines(80, VERSION, "").join("\n")}${p.reset}\n\n` +
|
|
1291
1327
|
"kiso — the coding agent that survives kill -9\n\n" +
|
|
1292
1328
|
" kiso [sessionId] interactive session (default command)\n" +
|
|
1293
1329
|
" kiso chat [sessionId] same as above\n" +
|
package/dist/render.d.ts
CHANGED
|
@@ -11,12 +11,14 @@ import { canonicalTargetPath } from "@vincemakes/kiso-tools-node";
|
|
|
11
11
|
* errors; dim for metadata. NO_COLOR set, or a non-TTY output → every
|
|
12
12
|
* code is empty, so pipes and CI carry ZERO ANSI (the existing byte-level
|
|
13
13
|
* e2e assertions guard it). Everything not listed here is plain.
|
|
14
|
+
* v3: `bg` — the user-message block background (SGR 48, dark gray 237).
|
|
14
15
|
*/
|
|
15
16
|
export interface Palette {
|
|
16
17
|
readonly blue: string;
|
|
17
18
|
readonly dim: string;
|
|
18
19
|
readonly red: string;
|
|
19
20
|
readonly green: string;
|
|
21
|
+
readonly bg: string;
|
|
20
22
|
readonly reset: string;
|
|
21
23
|
}
|
|
22
24
|
export declare const COLOR_ON: Palette;
|
|
@@ -73,6 +75,8 @@ export declare function renderToolSummary(name: string, input: Record<string, un
|
|
|
73
75
|
content: string;
|
|
74
76
|
isError: boolean;
|
|
75
77
|
}): string;
|
|
78
|
+
/** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
|
|
79
|
+
export declare function kUnit(value: number | null): string;
|
|
76
80
|
/** B 区: usage data gathered from the run's usage events. */
|
|
77
81
|
export interface RunUsage {
|
|
78
82
|
readonly in: number | null;
|
|
@@ -96,6 +100,31 @@ export declare function renderStatusLine(turn: number, usage: RunUsage, ctxRatio
|
|
|
96
100
|
* this verbatim; the render tests pin the sequence.
|
|
97
101
|
*/
|
|
98
102
|
export declare function renderTerminalGap(statusLine: string | null): string;
|
|
103
|
+
/**
|
|
104
|
+
* v3 §01 — the banner, block-split. The logo is three INDEPENDENT rows
|
|
105
|
+
* (TOP / the tagline / BOTTOM), then TWO info rows (version,
|
|
106
|
+
* extensions). Every row truncates at the terminal width with a " (+N)"
|
|
107
|
+
* marker (N = the hidden display width); a window narrower than 40
|
|
108
|
+
* columns skips the logo entirely — only the info rows. Pure.
|
|
109
|
+
*/
|
|
110
|
+
export declare const TAGLINE = "the coding agent that survives kill -9";
|
|
111
|
+
/** v3 §01: truncate a row at `width`, marking the hidden span " (+N)". */
|
|
112
|
+
export declare function truncateRow(row: string, width: number): string;
|
|
113
|
+
/** v3 §01: the banner lines for a width W — logo (skipped under 40
|
|
114
|
+
* columns) + version + extensions. */
|
|
115
|
+
export declare function bannerLines(W: number, version: string, extensionsText: string): string[];
|
|
116
|
+
/** v3 §02 — the recap line that ends a run, replacing the "done" label +
|
|
117
|
+
* the old status line. All fields derive LOCALLY from the event stream
|
|
118
|
+
* (zero tokens): wall seconds, tool counts, usage, cache hit %, ctx left.
|
|
119
|
+
*/
|
|
120
|
+
export interface RecapStats {
|
|
121
|
+
readonly seconds: number;
|
|
122
|
+
readonly tools: number;
|
|
123
|
+
readonly edits: number;
|
|
124
|
+
readonly usage: RunUsage;
|
|
125
|
+
readonly ctxLeftPct: number | null;
|
|
126
|
+
}
|
|
127
|
+
export declare function renderRecap(s: RecapStats): string;
|
|
99
128
|
/** One-line summary of a session, for `kiso sessions`. */
|
|
100
129
|
export declare function renderSessionLine(meta: {
|
|
101
130
|
id: string;
|
package/dist/render.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* the lines a human sees. Colors are raw ANSI — no dependencies.
|
|
4
4
|
*/
|
|
5
5
|
import { canonicalTargetPath } from "@vincemakes/kiso-tools-node";
|
|
6
|
-
export const COLOR_ON = { blue: "\x1b[38;5;75m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", reset: "\x1b[0m" };
|
|
7
|
-
export const COLOR_OFF = { blue: "", dim: "", red: "", green: "", reset: "" };
|
|
6
|
+
export const COLOR_ON = { blue: "\x1b[38;5;75m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", bg: "\x1b[48;5;237m", reset: "\x1b[0m" };
|
|
7
|
+
export const COLOR_OFF = { blue: "", dim: "", red: "", green: "", bg: "", reset: "" };
|
|
8
8
|
export function palette() {
|
|
9
9
|
return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
|
|
10
10
|
}
|
|
@@ -50,7 +50,7 @@ export function foldThinking(block) {
|
|
|
50
50
|
const p = palette();
|
|
51
51
|
const trimmed = escapeTerminal(block.trim());
|
|
52
52
|
const truncated = trimmed.length > 100;
|
|
53
|
-
return `${p.dim}…${trimmed.slice(0, 100)}${truncated ?
|
|
53
|
+
return `${p.dim}…${trimmed.slice(0, 100)}${truncated ? ` (${block.length} chars · /think)` : ""}${p.reset}\n`;
|
|
54
54
|
}
|
|
55
55
|
/** v2b — the [result] echo truncates at 160 chars + a /last hint. */
|
|
56
56
|
export function foldResult(content) {
|
|
@@ -215,7 +215,7 @@ function exitCodeOf(result) {
|
|
|
215
215
|
return m !== null ? Number(m[1]) : 1;
|
|
216
216
|
}
|
|
217
217
|
/** k-units for the status line: 12345 → 12.3k, 800 → 800, null → ?. */
|
|
218
|
-
function kUnit(value) {
|
|
218
|
+
export function kUnit(value) {
|
|
219
219
|
if (value === null)
|
|
220
220
|
return "?";
|
|
221
221
|
if (value >= 1000)
|
|
@@ -257,6 +257,66 @@ export function renderStatusLine(turn, usage, ctxRatio, faux = false) {
|
|
|
257
257
|
export function renderTerminalGap(statusLine) {
|
|
258
258
|
return `${statusLine === null ? "" : `${statusLine}\n`}\n`;
|
|
259
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* v3 §01 — the banner, block-split. The logo is three INDEPENDENT rows
|
|
262
|
+
* (TOP / the tagline / BOTTOM), then TWO info rows (version,
|
|
263
|
+
* extensions). Every row truncates at the terminal width with a " (+N)"
|
|
264
|
+
* marker (N = the hidden display width); a window narrower than 40
|
|
265
|
+
* columns skips the logo entirely — only the info rows. Pure.
|
|
266
|
+
*/
|
|
267
|
+
export const TAGLINE = "the coding agent that survives kill -9";
|
|
268
|
+
const LOGO_ROWS = ["█ █ ▀█▀ █▀▀ █▀█", TAGLINE, "▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀"];
|
|
269
|
+
/** Display width of a row (1-cell ASCII; 2-cell CJK/wide). */
|
|
270
|
+
function displayW(row) {
|
|
271
|
+
let w = 0;
|
|
272
|
+
for (let i = 0; i < row.length; i += 1) {
|
|
273
|
+
const cp = row.codePointAt(i);
|
|
274
|
+
w += cp > 0xff ? 2 : 1;
|
|
275
|
+
}
|
|
276
|
+
return w;
|
|
277
|
+
}
|
|
278
|
+
/** v3 §01: truncate a row at `width`, marking the hidden span " (+N)". */
|
|
279
|
+
export function truncateRow(row, width) {
|
|
280
|
+
if (displayW(row) <= width)
|
|
281
|
+
return row;
|
|
282
|
+
const cut = Math.max(0, width - 4);
|
|
283
|
+
let w = 0;
|
|
284
|
+
let i = 0;
|
|
285
|
+
for (; i < row.length; i += 1) {
|
|
286
|
+
const cw = row.codePointAt(i) > 0xff ? 2 : 1;
|
|
287
|
+
if (w + cw > cut)
|
|
288
|
+
break;
|
|
289
|
+
w += cw;
|
|
290
|
+
}
|
|
291
|
+
return `${row.slice(0, i)} (+${displayW(row) - w})`;
|
|
292
|
+
}
|
|
293
|
+
/** v3 §01: the banner lines for a width W — logo (skipped under 40
|
|
294
|
+
* columns) + version + extensions. */
|
|
295
|
+
export function bannerLines(W, version, extensionsText) {
|
|
296
|
+
const rows = [];
|
|
297
|
+
if (W >= 40)
|
|
298
|
+
for (const r of LOGO_ROWS)
|
|
299
|
+
rows.push(truncateRow(r, W));
|
|
300
|
+
rows.push(truncateRow(`v${version}`, W));
|
|
301
|
+
if (extensionsText !== "")
|
|
302
|
+
rows.push(truncateRow(extensionsText, W));
|
|
303
|
+
return rows;
|
|
304
|
+
}
|
|
305
|
+
export function renderRecap(s) {
|
|
306
|
+
const p = palette();
|
|
307
|
+
const parts = [`${s.seconds}s`, `${s.tools} tool${s.tools === 1 ? "" : "s"}${s.edits > 0 ? ` (${s.edits} edit${s.edits === 1 ? "" : "s"})` : ""}`];
|
|
308
|
+
if (s.usage.known) {
|
|
309
|
+
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)}` : ""}`;
|
|
310
|
+
if (seg !== "")
|
|
311
|
+
parts.push(seg);
|
|
312
|
+
if (s.usage.cache !== null && s.usage.in !== null && s.usage.in > 0) {
|
|
313
|
+
parts.push(`cache ${Math.round((s.usage.cache / s.usage.in) * 100)}%`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (s.ctxLeftPct !== null)
|
|
317
|
+
parts.push(`ctx left ~${Math.round(s.ctxLeftPct)}%`);
|
|
318
|
+
return `${p.blue}▞${p.reset} ${parts.join(" · ")}\n`;
|
|
319
|
+
}
|
|
260
320
|
/** One-line summary of a session, for `kiso sessions`. */
|
|
261
321
|
export function renderSessionLine(meta) {
|
|
262
322
|
const when = meta.updatedAt ? new Date(meta.updatedAt).toISOString().slice(0, 16) : "—";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "kiso CLI — the coding-agent reference product: kiso chat / kiso resume / kiso sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vincemakes/kiso-core": "0.1.
|
|
22
|
-
"@vincemakes/kiso-evals": "0.1.
|
|
23
|
-
"@vincemakes/kiso-provider-anthropic": "0.1.
|
|
24
|
-
"@vincemakes/kiso-provider-openai": "0.1.
|
|
25
|
-
"@vincemakes/kiso-runtime": "0.1.
|
|
26
|
-
"@vincemakes/kiso-tools-node": "0.1.
|
|
21
|
+
"@vincemakes/kiso-core": "0.1.18",
|
|
22
|
+
"@vincemakes/kiso-evals": "0.1.18",
|
|
23
|
+
"@vincemakes/kiso-provider-anthropic": "0.1.18",
|
|
24
|
+
"@vincemakes/kiso-provider-openai": "0.1.18",
|
|
25
|
+
"@vincemakes/kiso-runtime": "0.1.18",
|
|
26
|
+
"@vincemakes/kiso-tools-node": "0.1.18"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^26.1.2",
|