@vincemakes/kiso-tui 0.1.20 → 0.1.23

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 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: "/model", desc: "list model profiles; switch with /model <name|provider/model>" },
74
75
  { name: "/compact", desc: "summarize the older conversation to free context" },
75
76
  { name: "/think", desc: "show the last full thinking block" },
76
77
  { name: "/last", desc: "show the most recent tool call's input and output" },
@@ -98,6 +99,12 @@ export class Editor {
98
99
  #onRender;
99
100
  #menuOpen = false; // v3 §04: the slash-command menu
100
101
  #menuSel = 0;
102
+ // A2 (手感): the session-scoped input history — every submitted TURN
103
+ // line (never a question answer), capped at 100, never persisted. ↑↓
104
+ // navigate it ONLY from an empty input or while already browsing.
105
+ #history = [];
106
+ #historyIdx = null;
107
+ #preBrowse = [];
101
108
  #pending = ""; // an incomplete ESC/CSI prefix across chunks
102
109
  #decoder = new TextDecoder();
103
110
  #entered = false;
@@ -233,6 +240,15 @@ export class Editor {
233
240
  this.#scroll = 0;
234
241
  this.#refreshMenu();
235
242
  }
243
+ else if (this.#historyIdx !== null) {
244
+ // A2: Esc exits the history browse — the pre-browse
245
+ // (empty) input returns.
246
+ this.#historyIdx = null;
247
+ this.#chars = [...this.#preBrowse];
248
+ this.#cursor = this.#chars.length;
249
+ this.#reflow();
250
+ this.#onRender();
251
+ }
236
252
  else {
237
253
  this.#escapeCb?.();
238
254
  i += 1;
@@ -316,13 +332,20 @@ export class Editor {
316
332
  this.#onRender();
317
333
  }
318
334
  }
319
- else if (final === "A" && this.#menuOpen) {
320
- // v3 §04: ↑↓ move the menu selection, never the cursor.
321
- this.#menuSel = Math.max(0, this.#menuSel - 1);
322
- this.#onRender();
323
- }
324
- else if (final === "B" && this.#menuOpen) {
325
- this.#menuSel = Math.min(this.#menuFiltered().length - 1, this.#menuSel + 1);
335
+ else if (final === "A" || final === "B") {
336
+ // v3 §04: the menu owns ↑↓ while open (the selection, never the
337
+ // cursor). A2 (手感): otherwise ↑↓ navigate the session history
338
+ // — ONLY from an empty input or while already browsing; mid-edit
339
+ // the cursor semantics are unchanged (↑↓ do nothing).
340
+ if (this.#menuOpen) {
341
+ if (final === "A")
342
+ this.#menuSel = Math.max(0, this.#menuSel - 1);
343
+ else
344
+ this.#menuSel = Math.min(this.#menuFiltered().length - 1, this.#menuSel + 1);
345
+ }
346
+ else if (this.#historyIdx !== null || this.line() === "") {
347
+ this.#historyMove(final === "A" ? -1 : 1);
348
+ }
326
349
  this.#onRender();
327
350
  }
328
351
  else if (final === "D") {
@@ -342,6 +365,8 @@ export class Editor {
342
365
  }
343
366
  // ---- editing ----
344
367
  #insert(cp) {
368
+ if (this.#historyIdx !== null)
369
+ this.#historyIdx = null; // editing leaves the browse
345
370
  this.#chars.splice(this.#cursor, 0, cp);
346
371
  this.#cursor += 1;
347
372
  this.#reflow();
@@ -351,6 +376,8 @@ export class Editor {
351
376
  #backspace() {
352
377
  if (this.#cursor === 0)
353
378
  return;
379
+ if (this.#historyIdx !== null)
380
+ this.#historyIdx = null; // editing leaves the browse
354
381
  this.#chars.splice(this.#cursor - 1, 1);
355
382
  this.#cursor -= 1;
356
383
  this.#reflow();
@@ -397,10 +424,20 @@ export class Editor {
397
424
  #submit() {
398
425
  let line = String.fromCodePoint(...this.#chars);
399
426
  if (this.#menuOpen) {
400
- // v3 §04: Enter submits the SELECTED command.
427
+ // A1 (手感): Enter submits the EXACT selection directly; a
428
+ // PARTIAL selection COMPLETES the buffer (the Tab semantics)
429
+ // without submitting — the user reviews and presses Enter
430
+ // again. The old behavior executed the completed command on
431
+ // the first Enter, before the user had seen the completion.
401
432
  const m = this.#menuFiltered()[this.#menuSel];
402
- if (m !== undefined)
403
- line = m.name;
433
+ if (m !== undefined && m.name !== line) {
434
+ this.#chars = [...m.name].map((ch) => ch.codePointAt(0));
435
+ this.#cursor = this.#chars.length;
436
+ this.#reflow();
437
+ this.#refreshMenu();
438
+ this.#onRender();
439
+ return; // completed, not executed
440
+ }
404
441
  }
405
442
  this.#chars = [];
406
443
  this.#cursor = 0;
@@ -418,8 +455,42 @@ export class Editor {
418
455
  else {
419
456
  this.#pendingLines.push(line); // nobody wired yet — hold it
420
457
  }
458
+ // A2: the history remembers submitted TURN lines — never question
459
+ // answers, never empties; adjacent duplicates collapse.
460
+ if (cb === null && line !== "") {
461
+ if (this.#history[this.#history.length - 1] !== line)
462
+ this.#history.push(line);
463
+ if (this.#history.length > 100)
464
+ this.#history.shift();
465
+ }
421
466
  this.#onRender();
422
467
  }
468
+ /** A2: step the history browse; a delta past the newest exits back to
469
+ * the pre-browse input. */
470
+ #historyMove(delta) {
471
+ if (this.#history.length === 0)
472
+ return;
473
+ if (this.#historyIdx === null) {
474
+ this.#preBrowse = this.#chars; // entering from an empty input
475
+ this.#historyIdx = this.#history.length - 1;
476
+ }
477
+ else {
478
+ const next = this.#historyIdx + delta;
479
+ if (next < 0)
480
+ return; // the oldest entry — stay
481
+ this.#historyIdx = next;
482
+ if (next >= this.#history.length) {
483
+ this.#historyIdx = null; // past the newest — exit the browse
484
+ this.#chars = [...this.#preBrowse];
485
+ this.#cursor = this.#chars.length;
486
+ this.#reflow();
487
+ return;
488
+ }
489
+ }
490
+ this.#chars = [...this.#history[this.#historyIdx]].map((ch) => ch.codePointAt(0));
491
+ this.#cursor = this.#chars.length;
492
+ this.#reflow();
493
+ }
423
494
  // ---- width-based horizontal scroll ----
424
495
  #reflow() {
425
496
  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 events, produce
3
- * the lines a human sees. Colors are raw ANSI — no dependencies.
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: Event, prevThinking?: boolean, resolvePath?: PathResolver): RenderResult;
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 events, produce
3
- * the lines a human sees. Colors are raw ANSI — no dependencies.
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: "" };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.1.20",
4
- "description": "kiso tui the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
3
+ "version": "0.1.23",
4
+ "description": "kiso tui \u2014 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",
7
7
  "exports": {