@vincemakes/kiso-tui 0.1.19 → 0.1.21
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/editor.js +81 -10
- package/dist/index.d.ts +1 -1
- package/dist/render.d.ts +91 -4
- package/dist/render.js +11 -2
- package/package.json +1 -1
package/dist/editor.js
CHANGED
|
@@ -71,6 +71,7 @@ export const PROMPT = "▌you> "; // the kiso brick motif: one blue half-block,
|
|
|
71
71
|
export const PROMPT_WIDTH = displayWidth(PROMPT);
|
|
72
72
|
export const MENU_ITEMS = [
|
|
73
73
|
{ name: "/mode", desc: "switch the approval tier (manual/default/accept-edits/plan/bypass)" },
|
|
74
|
+
{ name: "/compact", desc: "summarize the older conversation to free context" },
|
|
74
75
|
{ name: "/think", desc: "show the last full thinking block" },
|
|
75
76
|
{ name: "/last", desc: "show the most recent tool call's input and output" },
|
|
76
77
|
{ name: "/status", desc: "show session id, event count, and context estimate" },
|
|
@@ -97,6 +98,12 @@ export class Editor {
|
|
|
97
98
|
#onRender;
|
|
98
99
|
#menuOpen = false; // v3 §04: the slash-command menu
|
|
99
100
|
#menuSel = 0;
|
|
101
|
+
// A2 (手感): the session-scoped input history — every submitted TURN
|
|
102
|
+
// line (never a question answer), capped at 100, never persisted. ↑↓
|
|
103
|
+
// navigate it ONLY from an empty input or while already browsing.
|
|
104
|
+
#history = [];
|
|
105
|
+
#historyIdx = null;
|
|
106
|
+
#preBrowse = [];
|
|
100
107
|
#pending = ""; // an incomplete ESC/CSI prefix across chunks
|
|
101
108
|
#decoder = new TextDecoder();
|
|
102
109
|
#entered = false;
|
|
@@ -232,6 +239,15 @@ export class Editor {
|
|
|
232
239
|
this.#scroll = 0;
|
|
233
240
|
this.#refreshMenu();
|
|
234
241
|
}
|
|
242
|
+
else if (this.#historyIdx !== null) {
|
|
243
|
+
// A2: Esc exits the history browse — the pre-browse
|
|
244
|
+
// (empty) input returns.
|
|
245
|
+
this.#historyIdx = null;
|
|
246
|
+
this.#chars = [...this.#preBrowse];
|
|
247
|
+
this.#cursor = this.#chars.length;
|
|
248
|
+
this.#reflow();
|
|
249
|
+
this.#onRender();
|
|
250
|
+
}
|
|
235
251
|
else {
|
|
236
252
|
this.#escapeCb?.();
|
|
237
253
|
i += 1;
|
|
@@ -315,13 +331,20 @@ export class Editor {
|
|
|
315
331
|
this.#onRender();
|
|
316
332
|
}
|
|
317
333
|
}
|
|
318
|
-
else if (final === "A"
|
|
319
|
-
// v3 §04: ↑↓
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
334
|
+
else if (final === "A" || final === "B") {
|
|
335
|
+
// v3 §04: the menu owns ↑↓ while open (the selection, never the
|
|
336
|
+
// cursor). A2 (手感): otherwise ↑↓ navigate the session history
|
|
337
|
+
// — ONLY from an empty input or while already browsing; mid-edit
|
|
338
|
+
// the cursor semantics are unchanged (↑↓ do nothing).
|
|
339
|
+
if (this.#menuOpen) {
|
|
340
|
+
if (final === "A")
|
|
341
|
+
this.#menuSel = Math.max(0, this.#menuSel - 1);
|
|
342
|
+
else
|
|
343
|
+
this.#menuSel = Math.min(this.#menuFiltered().length - 1, this.#menuSel + 1);
|
|
344
|
+
}
|
|
345
|
+
else if (this.#historyIdx !== null || this.line() === "") {
|
|
346
|
+
this.#historyMove(final === "A" ? -1 : 1);
|
|
347
|
+
}
|
|
325
348
|
this.#onRender();
|
|
326
349
|
}
|
|
327
350
|
else if (final === "D") {
|
|
@@ -341,6 +364,8 @@ export class Editor {
|
|
|
341
364
|
}
|
|
342
365
|
// ---- editing ----
|
|
343
366
|
#insert(cp) {
|
|
367
|
+
if (this.#historyIdx !== null)
|
|
368
|
+
this.#historyIdx = null; // editing leaves the browse
|
|
344
369
|
this.#chars.splice(this.#cursor, 0, cp);
|
|
345
370
|
this.#cursor += 1;
|
|
346
371
|
this.#reflow();
|
|
@@ -350,6 +375,8 @@ export class Editor {
|
|
|
350
375
|
#backspace() {
|
|
351
376
|
if (this.#cursor === 0)
|
|
352
377
|
return;
|
|
378
|
+
if (this.#historyIdx !== null)
|
|
379
|
+
this.#historyIdx = null; // editing leaves the browse
|
|
353
380
|
this.#chars.splice(this.#cursor - 1, 1);
|
|
354
381
|
this.#cursor -= 1;
|
|
355
382
|
this.#reflow();
|
|
@@ -396,10 +423,20 @@ export class Editor {
|
|
|
396
423
|
#submit() {
|
|
397
424
|
let line = String.fromCodePoint(...this.#chars);
|
|
398
425
|
if (this.#menuOpen) {
|
|
399
|
-
//
|
|
426
|
+
// A1 (手感): Enter submits the EXACT selection directly; a
|
|
427
|
+
// PARTIAL selection COMPLETES the buffer (the Tab semantics)
|
|
428
|
+
// without submitting — the user reviews and presses Enter
|
|
429
|
+
// again. The old behavior executed the completed command on
|
|
430
|
+
// the first Enter, before the user had seen the completion.
|
|
400
431
|
const m = this.#menuFiltered()[this.#menuSel];
|
|
401
|
-
if (m !== undefined)
|
|
402
|
-
|
|
432
|
+
if (m !== undefined && m.name !== line) {
|
|
433
|
+
this.#chars = [...m.name].map((ch) => ch.codePointAt(0));
|
|
434
|
+
this.#cursor = this.#chars.length;
|
|
435
|
+
this.#reflow();
|
|
436
|
+
this.#refreshMenu();
|
|
437
|
+
this.#onRender();
|
|
438
|
+
return; // completed, not executed
|
|
439
|
+
}
|
|
403
440
|
}
|
|
404
441
|
this.#chars = [];
|
|
405
442
|
this.#cursor = 0;
|
|
@@ -417,8 +454,42 @@ export class Editor {
|
|
|
417
454
|
else {
|
|
418
455
|
this.#pendingLines.push(line); // nobody wired yet — hold it
|
|
419
456
|
}
|
|
457
|
+
// A2: the history remembers submitted TURN lines — never question
|
|
458
|
+
// answers, never empties; adjacent duplicates collapse.
|
|
459
|
+
if (cb === null && line !== "") {
|
|
460
|
+
if (this.#history[this.#history.length - 1] !== line)
|
|
461
|
+
this.#history.push(line);
|
|
462
|
+
if (this.#history.length > 100)
|
|
463
|
+
this.#history.shift();
|
|
464
|
+
}
|
|
420
465
|
this.#onRender();
|
|
421
466
|
}
|
|
467
|
+
/** A2: step the history browse; a delta past the newest exits back to
|
|
468
|
+
* the pre-browse input. */
|
|
469
|
+
#historyMove(delta) {
|
|
470
|
+
if (this.#history.length === 0)
|
|
471
|
+
return;
|
|
472
|
+
if (this.#historyIdx === null) {
|
|
473
|
+
this.#preBrowse = this.#chars; // entering from an empty input
|
|
474
|
+
this.#historyIdx = this.#history.length - 1;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
const next = this.#historyIdx + delta;
|
|
478
|
+
if (next < 0)
|
|
479
|
+
return; // the oldest entry — stay
|
|
480
|
+
this.#historyIdx = next;
|
|
481
|
+
if (next >= this.#history.length) {
|
|
482
|
+
this.#historyIdx = null; // past the newest — exit the browse
|
|
483
|
+
this.#chars = [...this.#preBrowse];
|
|
484
|
+
this.#cursor = this.#chars.length;
|
|
485
|
+
this.#reflow();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
this.#chars = [...this.#history[this.#historyIdx]].map((ch) => ch.codePointAt(0));
|
|
490
|
+
this.#cursor = this.#chars.length;
|
|
491
|
+
this.#reflow();
|
|
492
|
+
}
|
|
422
493
|
// ---- width-based horizontal scroll ----
|
|
423
494
|
#reflow() {
|
|
424
495
|
const W = (process.stdout.columns ?? 0) || 80; // degenerate 0 falls back to 80
|
package/dist/index.d.ts
CHANGED
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
export { Body, type BodyOptions } from "./body.js";
|
|
8
8
|
export { Dock } from "./dock.js";
|
|
9
9
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
|
|
10
|
-
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 RenderResult, type RunUsage, } from "./render.js";
|
|
10
|
+
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
11
|
export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
|
package/dist/render.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Event rendering for the terminal. Pure (testable): given
|
|
3
|
-
* the lines a human sees. Colors are raw ANSI — no
|
|
2
|
+
* Event rendering for the terminal. Pure (testable): given the render
|
|
3
|
+
* input, produce the lines a human sees. Colors are raw ANSI — no
|
|
4
|
+
* dependencies.
|
|
5
|
+
*
|
|
6
|
+
* 手感批 C5: the input is the tui's OWN data shape (RenderInput), never
|
|
7
|
+
* kiso-core's Event — the CLI translates Event → RenderInput. The tui
|
|
8
|
+
* package has ZERO kiso-core imports: input is data, output is bytes.
|
|
4
9
|
*/
|
|
5
|
-
import type { Event } from "@vincemakes/kiso-core";
|
|
6
10
|
/**
|
|
7
11
|
* v2a — the palette, centralized (no hard-coded codes elsewhere): ONE
|
|
8
12
|
* accent — blue, ANSI 256 color 75 — for the identity accents (the you>
|
|
@@ -39,6 +43,89 @@ export interface RenderResult {
|
|
|
39
43
|
readonly newline: boolean;
|
|
40
44
|
readonly prompt: boolean;
|
|
41
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* 手感批 C5 — the render input, the tui's OWN data shape: the subset of
|
|
48
|
+
* an event stream the renderer reads, keyed by type. The CLI translates
|
|
49
|
+
* its Event stream into this (Event → RenderInput) before rendering —
|
|
50
|
+
* the tui package never imports kiso-core. Field names mirror the
|
|
51
|
+
* rendered Event members so the render body stays byte-identical.
|
|
52
|
+
*/
|
|
53
|
+
export type RenderInput = {
|
|
54
|
+
readonly type: "user_input";
|
|
55
|
+
readonly content: string | readonly {
|
|
56
|
+
readonly type?: string;
|
|
57
|
+
readonly text?: string;
|
|
58
|
+
}[];
|
|
59
|
+
} | {
|
|
60
|
+
readonly type: "text_delta";
|
|
61
|
+
readonly text: string;
|
|
62
|
+
} | {
|
|
63
|
+
readonly type: "text_end";
|
|
64
|
+
} | {
|
|
65
|
+
readonly type: "thinking";
|
|
66
|
+
readonly text: string;
|
|
67
|
+
} | {
|
|
68
|
+
readonly type: "tool_call_end";
|
|
69
|
+
readonly name: string;
|
|
70
|
+
readonly input: Readonly<Record<string, unknown>> | null;
|
|
71
|
+
} | {
|
|
72
|
+
readonly type: "tool_execution_started";
|
|
73
|
+
} | {
|
|
74
|
+
readonly type: "tool_execution_succeeded";
|
|
75
|
+
} | {
|
|
76
|
+
readonly type: "tool_execution_failed";
|
|
77
|
+
readonly error: string;
|
|
78
|
+
} | {
|
|
79
|
+
readonly type: "tool_result";
|
|
80
|
+
readonly content: string | readonly {
|
|
81
|
+
readonly type?: string;
|
|
82
|
+
readonly text?: string;
|
|
83
|
+
}[];
|
|
84
|
+
readonly isError: boolean;
|
|
85
|
+
} | {
|
|
86
|
+
readonly type: "permission_requested";
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
89
|
+
} | {
|
|
90
|
+
readonly type: "permission_decided";
|
|
91
|
+
readonly decision: "approved" | "denied";
|
|
92
|
+
readonly reason?: string;
|
|
93
|
+
} | {
|
|
94
|
+
readonly type: "terminal";
|
|
95
|
+
readonly outcome: {
|
|
96
|
+
readonly kind: "completed";
|
|
97
|
+
} | {
|
|
98
|
+
readonly kind: "max_tokens";
|
|
99
|
+
} | {
|
|
100
|
+
readonly kind: "max_turns";
|
|
101
|
+
readonly turns: number;
|
|
102
|
+
} | {
|
|
103
|
+
readonly kind: "error";
|
|
104
|
+
readonly error: {
|
|
105
|
+
readonly message: string;
|
|
106
|
+
};
|
|
107
|
+
} | {
|
|
108
|
+
readonly kind: "aborted";
|
|
109
|
+
readonly by: string;
|
|
110
|
+
} | {
|
|
111
|
+
readonly kind: "hook_stopped";
|
|
112
|
+
readonly hook: string;
|
|
113
|
+
};
|
|
114
|
+
} | {
|
|
115
|
+
readonly type: "compacted";
|
|
116
|
+
readonly cleared: readonly {
|
|
117
|
+
readonly eventSeq?: number;
|
|
118
|
+
readonly callId: string;
|
|
119
|
+
}[];
|
|
120
|
+
} | {
|
|
121
|
+
readonly type: "summarized";
|
|
122
|
+
readonly coversToSeq: number;
|
|
123
|
+
} | {
|
|
124
|
+
readonly type: "uncertain_pending";
|
|
125
|
+
readonly name: string;
|
|
126
|
+
readonly executionId: string;
|
|
127
|
+
readonly error: string;
|
|
128
|
+
};
|
|
42
129
|
/**
|
|
43
130
|
* Render one event. `text` may be a continuation (text_delta appends to the
|
|
44
131
|
* current line); `newline` says whether the line is complete.
|
|
@@ -58,7 +145,7 @@ export interface RenderResult {
|
|
|
58
145
|
export declare function foldThinking(block: string): string;
|
|
59
146
|
/** v2b — the [result] echo truncates at 160 chars + a /last hint. */
|
|
60
147
|
export declare function foldResult(content: string): string;
|
|
61
|
-
export declare function renderEvent(ev:
|
|
148
|
+
export declare function renderEvent(ev: RenderInput, prevThinking?: boolean, resolvePath?: PathResolver): RenderResult;
|
|
62
149
|
/**
|
|
63
150
|
* B 区: one-line summary of a completed tool call, e.g.
|
|
64
151
|
* ✓ edit src/foo.ts (+12 -3) ✓ read src/bar.ts (140 lines)
|
package/dist/render.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Event rendering for the terminal. Pure (testable): given
|
|
3
|
-
* the lines a human sees. Colors are raw ANSI — no
|
|
2
|
+
* Event rendering for the terminal. Pure (testable): given the render
|
|
3
|
+
* input, produce the lines a human sees. Colors are raw ANSI — no
|
|
4
|
+
* dependencies.
|
|
5
|
+
*
|
|
6
|
+
* 手感批 C5: the input is the tui's OWN data shape (RenderInput), never
|
|
7
|
+
* kiso-core's Event — the CLI translates Event → RenderInput. The tui
|
|
8
|
+
* package has ZERO kiso-core imports: input is data, output is bytes.
|
|
4
9
|
*/
|
|
5
10
|
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" };
|
|
6
11
|
export const COLOR_OFF = { blue: "", dim: "", red: "", green: "", bg: "", reset: "" };
|
|
@@ -117,6 +122,10 @@ export function renderEvent(ev, prevThinking = false, resolvePath = (p) => p) {
|
|
|
117
122
|
}
|
|
118
123
|
case "compacted":
|
|
119
124
|
return { text: `${p.dim} [compacted ${ev.cleared.length} results]${p.reset}\n`, newline: true, prompt: false };
|
|
125
|
+
case "summarized":
|
|
126
|
+
// ADR-0044: the /compact event is OFF-LOOP — it never appears in
|
|
127
|
+
// a run stream; rendered for the switch's completeness only.
|
|
128
|
+
return { text: `${p.dim} [summarized up to seq ${ev.coversToSeq}]${p.reset}\n`, newline: true, prompt: false };
|
|
120
129
|
case "uncertain_pending":
|
|
121
130
|
return {
|
|
122
131
|
text: `${p.red}⚠ ${escapeTerminal(ev.name)} failed (${ev.executionId}): ${escapeTerminal(ev.error.slice(0, 160))}${p.reset}\n`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
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",
|