@vincemakes/kiso-tui 0.5.0 → 0.6.0

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.d.ts CHANGED
@@ -48,6 +48,11 @@ export declare class Editor {
48
48
  onEot(cb: () => void): void;
49
49
  onEscape(cb: () => void): void;
50
50
  onExpand(cb: () => void): void;
51
+ /** KC2 §2: the redirect chain — the gesture hands the buffer's text
52
+ * over while the run is told to stop. Mirrors onEscape (a list, so
53
+ * listeners can coexist); the line arrives already gone from the
54
+ * composer, exactly as a submit's does. */
55
+ onRedirect(cb: (line: string) => void): void;
51
56
  /** W22: bind the pending-turn queue — the CLI's live slots. The ↑
52
57
  * pop walks them (each pop leaves the queue, cancelling the turn);
53
58
  * esc ends the walk after one more pop. */
package/dist/editor.js CHANGED
@@ -87,6 +87,12 @@ export class Editor {
87
87
  // (dispatch) coexist; a listener removes itself via an unarmed guard
88
88
  // (the compact's handler no-ops after its abort has fired).
89
89
  #escapeCbs = [];
90
+ // KC2 §2: the redirect LIST — mirrors #escapeCbs. The editor FORWARDS
91
+ // the gesture with the buffer's text; it never interprets it. What a
92
+ // redirect MEANS (abort the run, then run THIS ahead of the queue) is
93
+ // the CLI's — here it is only "these two keys, pressed together, hand
94
+ // the line over by a different door than Enter's".
95
+ #redirectCbs = [];
90
96
  // W15: the expand-key list (ctrl+r) — the CLI's dispatch decides the
91
97
  // target (a live cell toggles in place; a committed cell appends the
92
98
  // expanded block). Mirrors the escape list: multiple listeners can
@@ -144,6 +150,13 @@ export class Editor {
144
150
  onExpand(cb) {
145
151
  this.#expandCbs.push(cb);
146
152
  }
153
+ /** KC2 §2: the redirect chain — the gesture hands the buffer's text
154
+ * over while the run is told to stop. Mirrors onEscape (a list, so
155
+ * listeners can coexist); the line arrives already gone from the
156
+ * composer, exactly as a submit's does. */
157
+ onRedirect(cb) {
158
+ this.#redirectCbs.push(cb);
159
+ }
147
160
  /** W22: bind the pending-turn queue — the CLI's live slots. The ↑
148
161
  * pop walks them (each pop leaves the queue, cancelling the turn);
149
162
  * esc ends the walk after one more pop. */
@@ -408,6 +421,17 @@ export class Editor {
408
421
  else if (rest.startsWith("O")) {
409
422
  i += 3; // SS3 (function keys) — ignored
410
423
  }
424
+ else if (rest.startsWith("\x0d") && this.#composerIdle()) {
425
+ // KC2 §2 — Alt+Enter. A terminal sends Alt+X as ESC and X in
426
+ // ONE write, so SAME-CHUNK is the whole test: no timer, no
427
+ // hold, nothing parked. The identical two bytes arriving in
428
+ // SEPARATE chunks are NOT combined — they fall to the branch
429
+ // below, where the bare Esc fires at once (its immediacy is
430
+ // exactly what a hold would spend) and the next chunk's CR
431
+ // submits: today's two gestures, untouched.
432
+ this.#redirect();
433
+ i += 2; // both bytes belong to the one gesture
434
+ }
411
435
  else if (this.#menuOpen) {
412
436
  // v3 §04: Esc closes the menu and clears the buffer.
413
437
  // CA-4: the closing esc consumes its burst (the `i += 1`
@@ -541,6 +565,18 @@ export class Editor {
541
565
  this.#insert(NEWLINE);
542
566
  return;
543
567
  }
568
+ // KC2 §2 — Ctrl+Enter, the SAME two encodings with modifier 5
569
+ // (1 + ctrl): kitty's CSI-u and xterm's modifyOtherKeys. Never
570
+ // claimed universal — a terminal that encodes neither sends a plain
571
+ // CR, which is an ordinary submit/queue (the safe degrade). The
572
+ // chunk-split safety is the existing #pending CSI resume, shared
573
+ // with Shift+Enter above. Outside the normal composer state the
574
+ // sequence is simply unknown, exactly like any other stray CSI.
575
+ if ((final === "u" && params === "13;5") || (final === "~" && params === "27;5;13")) {
576
+ if (this.#composerIdle())
577
+ this.#redirect();
578
+ return;
579
+ }
544
580
  if (final === "~") {
545
581
  const n = Number(params);
546
582
  if (n === 3)
@@ -783,8 +819,76 @@ export class Editor {
783
819
  this.#cursor = i;
784
820
  this.#reflow();
785
821
  }
822
+ /** KC1/KC2 — the buffer LEAVES: the flat chars, the cursor, the
823
+ * horizontal scroll, the ↑/↓ goal, the menu and the pop-walk all
824
+ * reset together (W22: a departing line ends the pop-walk, so the
825
+ * next esc at rest interrupts again). Shared by the submit and the
826
+ * redirect — the two doors a line can leave by. */
827
+ #takeLine() {
828
+ const line = String.fromCodePoint(...this.#chars);
829
+ this.#chars = [];
830
+ this.#cursor = 0;
831
+ this.#scroll = 0;
832
+ this.#verticalGoalCol = null;
833
+ this.#menuOpen = false;
834
+ this.#menuSel = 0;
835
+ this.#queuePopMode = false;
836
+ return line;
837
+ }
838
+ /** A2: the history remembers submitted TURN lines — never question
839
+ * answers, never empties; adjacent duplicates collapse, the tail
840
+ * caps at 100. A redirect is a turn, so it is remembered too. */
841
+ #remember(line) {
842
+ if (this.#history[this.#history.length - 1] !== line)
843
+ this.#history.push(line);
844
+ if (this.#history.length > 100)
845
+ this.#history.shift();
846
+ }
847
+ /** KC2 §2 — the NORMAL composer state: the redirect gesture is live
848
+ * ONLY here. The approval panel, the slash menu, the history browse
849
+ * and the queue-pop walk each OWN their keys first (the W21 "the
850
+ * panel owns the keys" design, restated as a gate); a pending
851
+ * question is the panel's dock-less twin (askPanel routes to
852
+ * question() when the dock cannot render, so the ask owns the keys
853
+ * there too); and a bracketed paste is literal TEXT, where an ESC CR
854
+ * is the pasted content's own bytes and never a keypress. In every
855
+ * one of those states the two bytes fall through to today's
856
+ * handling — two gestures, unchanged. */
857
+ #composerIdle() {
858
+ return (this.#panel === null &&
859
+ !this.#menuOpen &&
860
+ this.#historyIdx === null &&
861
+ !this.#queuePopMode &&
862
+ !this.#pasting &&
863
+ this.#questionCb === null);
864
+ }
865
+ /**
866
+ * KC2 §2 — the gesture's meaning, kept as small as it can honestly be.
867
+ *
868
+ * An EMPTY buffer carries no correction, so the gesture degenerates to
869
+ * the bare Esc: the abort alone, nothing submitted. With text, the
870
+ * line leaves exactly as a submit's does and the listeners decide (the
871
+ * CLI aborts a live run and front-jumps the correction; idle, it is
872
+ * simply an Enter). UNWIRED — the recovery flow never binds it — the
873
+ * gesture IS a submit: a line is never lost to a missing binding.
874
+ */
875
+ #redirect() {
876
+ if (this.#chars.length === 0) {
877
+ for (const cb of [...this.#escapeCbs])
878
+ cb();
879
+ return;
880
+ }
881
+ if (this.#redirectCbs.length === 0) {
882
+ this.#submit();
883
+ return;
884
+ }
885
+ const line = this.#takeLine();
886
+ this.#remember(line);
887
+ for (const cb of [...this.#redirectCbs])
888
+ cb(line);
889
+ this.#onRender();
890
+ }
786
891
  #submit() {
787
- let line = String.fromCodePoint(...this.#chars);
788
892
  if (this.#menuOpen) {
789
893
  // A1 (the feel): Enter submits the EXACT selection directly; a
790
894
  // PARTIAL selection COMPLETES the buffer (the Tab semantics)
@@ -792,7 +896,7 @@ export class Editor {
792
896
  // again. The old behavior executed the completed command on
793
897
  // the first Enter, before the user had seen the completion.
794
898
  const m = this.#menuFiltered()[this.#menuSel];
795
- if (m !== undefined && m.name !== line) {
899
+ if (m !== undefined && m.name !== this.line()) {
796
900
  this.#chars = [...m.name].map((ch) => ch.codePointAt(0));
797
901
  this.#cursor = this.#chars.length;
798
902
  this.#reflow();
@@ -801,13 +905,7 @@ export class Editor {
801
905
  return; // completed, not executed
802
906
  }
803
907
  }
804
- this.#chars = [];
805
- this.#cursor = 0;
806
- this.#scroll = 0;
807
- this.#verticalGoalCol = null;
808
- this.#menuOpen = false;
809
- this.#menuSel = 0;
810
- this.#queuePopMode = false; // W22: a submit ends the pop-walk — the next esc at rest interrupts again
908
+ const line = this.#takeLine();
811
909
  const cb = this.#questionCb;
812
910
  this.#questionCb = null;
813
911
  if (cb !== null) {
@@ -819,14 +917,8 @@ export class Editor {
819
917
  else {
820
918
  this.#pendingLines.push(line); // nobody wired yet — hold it
821
919
  }
822
- // A2: the history remembers submitted TURN lines — never question
823
- // answers, never empties; adjacent duplicates collapse.
824
- if (cb === null && line !== "") {
825
- if (this.#history[this.#history.length - 1] !== line)
826
- this.#history.push(line);
827
- if (this.#history.length > 100)
828
- this.#history.shift();
829
- }
920
+ if (cb === null && line !== "")
921
+ this.#remember(line);
830
922
  this.#onRender();
831
923
  }
832
924
  /** A2: step the history browse; a delta past the newest exits back to
package/dist/index.d.ts CHANGED
@@ -11,3 +11,4 @@ export { Container, foldLine, visibleWidth, SPINNER, type Component, type FrameC
11
11
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
12
12
  export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type PathResolver, type RecapStats, type ResumeMeta, type RenderInput, type RenderResult, type RunUsage, } from "./render.js";
13
13
  export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
14
+ export { STATUS_GLYPHS, idleStatus, runningStatus } from "./status.js";
package/dist/index.js CHANGED
@@ -14,3 +14,6 @@ export { Container, foldLine, visibleWidth, SPINNER } from "./components.js";
14
14
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
15
15
  export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
16
16
  export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
17
+ // KC2 §5: the status rows' formatters — the CLI keeps the state and the
18
+ // repaint, the terminal layer owns what the row says.
19
+ export { STATUS_GLYPHS, idleStatus, runningStatus } from "./status.js";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * KC2 §5 — the status line's FORMATTERS, extracted from the CLI (the
3
+ * ADR-0041 escape hatch: extraction, never a fifth raise). The split is
4
+ * the one the ADR names: the CLI keeps the STATE (the rotating glyph,
5
+ * the run's start instant, the live usage, whether the dock is up) and
6
+ * the REPAINT; what a status row SAYS is presentation, and presentation
7
+ * belongs to the terminal layer.
8
+ *
9
+ * Two callers built these rows independently before the move — chat's
10
+ * REPL and the recovery flow — with the running row duplicated verbatim
11
+ * in both. One definition now serves both, and the tier stays a
12
+ * PARAMETER precisely because the two callers disagree on it (chat
13
+ * spells plan's read-only posture out per W19, the recovery flow prints
14
+ * the bare mode): the extraction must not silently unify a difference it
15
+ * was not asked to settle.
16
+ *
17
+ * The rows are byte-for-byte what the CLI built before the move — the
18
+ * v2b/v3 §03 shapes the e2e transcripts pin by substring — with ONE
19
+ * deliberate exception: the running row's interrupt hint, which KC2 §2
20
+ * widens to name the new gesture.
21
+ */
22
+ /** v3 §03/§05 — the working glyph family; the CLI's 200ms spinner walks
23
+ * it and hands each glyph back to `runningStatus`. */
24
+ export declare const STATUS_GLYPHS: readonly ["▖", "▘", "▝", "▗"];
25
+ /**
26
+ * The RUNNING row: the rotating glyph, the wall seconds since `since`
27
+ * (never below 1 — a run that just started still reads "1s", so the row
28
+ * never claims a turn took no time), the streamed output tokens once the
29
+ * count is known, the interrupt hints, and the live ctx estimate.
30
+ *
31
+ * KC2 §2: the hint names BOTH gestures. Esc still stops; alt+⏎ redirects
32
+ * — stop, and do THIS instead. The row is where the gesture is taught,
33
+ * because it is on screen exactly when the gesture is useful.
34
+ */
35
+ export declare function runningStatus(glyph: string, since: number, outTokens: number | null, ctxRatio: number): string;
36
+ /** The IDLE row: the approval tier as the CALLER names it, the /mode
37
+ * hint, the model driving the session, and the ctx estimate. */
38
+ export declare function idleStatus(tier: string, model: string, ctxRatio: number): string;
package/dist/status.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * KC2 §5 — the status line's FORMATTERS, extracted from the CLI (the
3
+ * ADR-0041 escape hatch: extraction, never a fifth raise). The split is
4
+ * the one the ADR names: the CLI keeps the STATE (the rotating glyph,
5
+ * the run's start instant, the live usage, whether the dock is up) and
6
+ * the REPAINT; what a status row SAYS is presentation, and presentation
7
+ * belongs to the terminal layer.
8
+ *
9
+ * Two callers built these rows independently before the move — chat's
10
+ * REPL and the recovery flow — with the running row duplicated verbatim
11
+ * in both. One definition now serves both, and the tier stays a
12
+ * PARAMETER precisely because the two callers disagree on it (chat
13
+ * spells plan's read-only posture out per W19, the recovery flow prints
14
+ * the bare mode): the extraction must not silently unify a difference it
15
+ * was not asked to settle.
16
+ *
17
+ * The rows are byte-for-byte what the CLI built before the move — the
18
+ * v2b/v3 §03 shapes the e2e transcripts pin by substring — with ONE
19
+ * deliberate exception: the running row's interrupt hint, which KC2 §2
20
+ * widens to name the new gesture.
21
+ */
22
+ import { kUnit } from "./render.js";
23
+ /** v3 §03/§05 — the working glyph family; the CLI's 200ms spinner walks
24
+ * it and hands each glyph back to `runningStatus`. */
25
+ export const STATUS_GLYPHS = ["▖", "▘", "▝", "▗"];
26
+ /** The ~ctx estimate as the whole-percent LEFT. A non-finite ratio (no
27
+ * window, no estimate) yields null and the row prints "~null%" — the
28
+ * long-standing shape, kept on purpose: an honest null beats an
29
+ * invented percentage. */
30
+ function ctxLeft(ratio) {
31
+ return Number.isFinite(ratio) ? Math.round((1 - ratio) * 100) : null;
32
+ }
33
+ /**
34
+ * The RUNNING row: the rotating glyph, the wall seconds since `since`
35
+ * (never below 1 — a run that just started still reads "1s", so the row
36
+ * never claims a turn took no time), the streamed output tokens once the
37
+ * count is known, the interrupt hints, and the live ctx estimate.
38
+ *
39
+ * KC2 §2: the hint names BOTH gestures. Esc still stops; alt+⏎ redirects
40
+ * — stop, and do THIS instead. The row is where the gesture is taught,
41
+ * because it is on screen exactly when the gesture is useful.
42
+ */
43
+ export function runningStatus(glyph, since, outTokens, ctxRatio) {
44
+ const out = outTokens !== null ? ` ↓ ${kUnit(outTokens)} tokens` : "";
45
+ const seconds = Math.max(1, Math.round((Date.now() - since) / 1000));
46
+ return `${glyph} working ${seconds}s${out} · esc stop · alt+⏎ redirect · ctx left ~${ctxLeft(ctxRatio)}%`;
47
+ }
48
+ /** The IDLE row: the approval tier as the CALLER names it, the /mode
49
+ * hint, the model driving the session, and the ctx estimate. */
50
+ export function idleStatus(tier, model, ctxRatio) {
51
+ return `▸ ${tier} · /mode to switch · ${model} · ctx left ~${ctxLeft(ctxRatio)}%`;
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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",
@@ -35,6 +35,6 @@
35
35
  },
36
36
  "homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
37
37
  "dependencies": {
38
- "@vincemakes/kiso-tui-cells": "0.5.0"
38
+ "@vincemakes/kiso-tui-cells": "0.6.0"
39
39
  }
40
40
  }