@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 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" && 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);
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
- // v3 §04: Enter submits the SELECTED command.
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
- line = m.name;
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 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: "" };
@@ -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.19",
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",