@vincemakes/kiso-tui 0.16.0 → 0.16.2

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.
@@ -1473,7 +1473,11 @@ export class Body {
1473
1473
  // frame, and the PTY gates that wait on the boot stream feel
1474
1474
  // it; inside the frame it is four sequences and no new event.
1475
1475
  this.#needsReset = false;
1476
- out.push("\x1b[r\x1b[?69l\x1b[?7h\x1b[?25h");
1476
+ // REL-0161: the reset ESTABLISHES the hidden cursor (?25l)
1477
+ // the steady state for the session's whole life. It repairs a
1478
+ // killed predecessor straight into the same state. The visible
1479
+ // cursor comes back exactly once, in editor.exit().
1480
+ out.push("\x1b[r\x1b[?69l\x1b[?7h\x1b[?25l");
1477
1481
  // REL-0152-D20 — the mechanism is understood and the obvious fix
1478
1482
  // is NOT taken here. See the finding.
1479
1483
  //
@@ -1493,7 +1497,12 @@ export class Body {
1493
1497
  // for stdin).
1494
1498
  }
1495
1499
  out.push("\x1b[?7l");
1496
- out.push(this.#conservative ? "\x1b[?25l" : "\x1b[?2026h"); // D1: sync ON, or cursor-hide where 2026 is dead bytes
1500
+ // REL-0161: ?25l is the STEADY state now, not a frame bracket
1501
+ // re-asserted at every frame open as self-healing (a subprocess
1502
+ // or stray output that showed the cursor is corrected here), and
1503
+ // never paired with a ?25h. Sync (2026) still brackets the frame
1504
+ // where the terminal understands it.
1505
+ out.push(this.#conservative ? "\x1b[?25l" : "\x1b[?2026h\x1b[?25l");
1497
1506
  // A8: the bottom-anchored window (the model's last H rows) shifts
1498
1507
  // DOWN when the live region SHRINKS — the done-fold, the fold-hold
1499
1508
  // release at the terminal event. The steady path's scroll syncs
@@ -1517,7 +1526,12 @@ export class Body {
1517
1526
  // with it.
1518
1527
  this.#drawFull(out, W, H, liveTop, liveLines, queueRows, menuRows, editor);
1519
1528
  this.#fullRedraw = false;
1520
- out.push(this.#conservative ? "\x1b[?25h" : "\x1b[?2026l");
1529
+ // REL-0161: the frame close no longer shows the cursor — hidden IS
1530
+ // the steady state (Terminal.app infers "a prompt line" from
1531
+ // bracketed-paste + a visible cursor and decorates it with a Mark;
1532
+ // the composer draws its own cursor cell instead — #inputRowBytes).
1533
+ if (!this.#conservative)
1534
+ out.push("\x1b[?2026l");
1521
1535
  out.push("\x1b[?7h"); // REL-0152-D14: autowrap back on for everything outside the frame
1522
1536
  this.#inFrame = true;
1523
1537
  try {
@@ -1902,16 +1916,47 @@ export class Body {
1902
1916
  markerLine += CURSOR_MARKER;
1903
1917
  markerCell = w;
1904
1918
  }
1905
- const stripped0 = markerLine.replace(CURSOR_MARKER, "");
1906
1919
  if (W < 4) {
1907
1920
  // the degenerate screen: the box cannot hold its walls — the
1908
1921
  // bare row (the pre-W6 bytes; the fold probe's pass-through
1909
1922
  // line still crashes invariant ① downstream, as before)
1910
- return { stripped: stripped0, markerCell };
1923
+ return { stripped: markerLine.replace(CURSOR_MARKER, ""), markerCell };
1924
+ }
1925
+ // REL-0161 — the DRAWN cursor. The hardware cursor is hidden for
1926
+ // the session's life (Terminal.app infers "a prompt line" from
1927
+ // bracketed-paste + a visible cursor and Marks it), so the cell
1928
+ // at the marker renders inverse instead. The hardware cursor
1929
+ // still PARKS at the marker — the IME anchor is its position.
1930
+ const mi = markerLine.indexOf(CURSOR_MARKER);
1931
+ let stripped0;
1932
+ let cursorPad = 0; // 1 when the drawn cursor consumed a pad cell
1933
+ if (mi < 0) {
1934
+ stripped0 = markerLine; // a row that does not own the cursor
1935
+ }
1936
+ else {
1937
+ const before = markerLine.slice(0, mi);
1938
+ const after = markerLine.slice(mi + CURSOR_MARKER.length);
1939
+ if (after.length === 0) {
1940
+ // the cursor rests past the content — an inverse space,
1941
+ // taken OUT of the pad (the walk capped content at W−4,
1942
+ // so the pad is ≥ 1 and the row still totals W)
1943
+ stripped0 = `${before}\x1b[7m \x1b[27m`;
1944
+ cursorPad = 1;
1945
+ }
1946
+ else if (after[0] === "\x1b" || (after.codePointAt(0) >= 0xd800 && after.codePointAt(0) <= 0xdfff)) {
1947
+ // never wrap a sequence, never split a surrogate pair —
1948
+ // the cell keeps its bytes, the parked cursor still marks
1949
+ // the position for the terminal's own affordances
1950
+ stripped0 = before + after;
1951
+ }
1952
+ else {
1953
+ const glyph = String.fromCodePoint(after.codePointAt(0));
1954
+ stripped0 = `${before}\x1b[7m${glyph}\x1b[27m${after.slice(glyph.length)}`;
1955
+ }
1911
1956
  }
1912
1957
  // the pad completes the row to W — the content stopped at W−4,
1913
- // so the pad is ≥ 1
1914
- const padW = W - 3 - w;
1958
+ // so the pad is ≥ 1 (≥ 0 after the drawn cursor consumed one)
1959
+ const padW = W - 3 - w - cursorPad;
1915
1960
  return { stripped: `\x1b[2m│ \x1b[0m${stripped0}\x1b[2m${" ".repeat(padW)}│\x1b[0m`, markerCell };
1916
1961
  }
1917
1962
  /** KC1 §6 — the focus component's input ROWS (N = 1 today's single
package/dist/editor.js CHANGED
@@ -270,6 +270,19 @@ export class Editor {
270
270
  #history = [];
271
271
  #historyIdx = null;
272
272
  #preBrowse = [];
273
+ // UD-1: minimal draft undo. Two stacks of FROZEN snapshots beside
274
+ // the one mutable buffer (the KC1 flat-buffer discipline holds —
275
+ // the stacks are history, never a second projection). A checkpoint
276
+ // is pushed only by a gesture about to discard ≥1 code point (the
277
+ // kills, the menu-esc clear, each queue-pop replacement, the
278
+ // @-apply splice); typing and single backspace push nothing — v1
279
+ // is loss-recovery, not char-granular edit history. ctrl+z
280
+ // restores text+cursor exactly; ctrl+y mirrors; undo never
281
+ // discards (what it replaces always lands on the redo stack).
282
+ // Both stacks clear on submit/clearLine — a sent turn is in the
283
+ // durable log and the ↑ history, not a loss.
284
+ #undoStack = [];
285
+ #redoStack = [];
273
286
  // W22: the pending-turn queue's bound state — the CLI's live slots
274
287
  // (chat.ts). ↑ pops the LAST queued message into the buffer and
275
288
  // enters the pop-mode (the walk: repeated ↑ pop older ones, each
@@ -355,6 +368,8 @@ export class Editor {
355
368
  return this.#sheetOpen;
356
369
  }
357
370
  clearLine() {
371
+ this.#undoStack.length = 0; // UD-1
372
+ this.#redoStack.length = 0;
358
373
  this.#chars = [];
359
374
  this.#cursor = 0;
360
375
  this.#scroll = 0;
@@ -577,6 +592,8 @@ export class Editor {
577
592
  if (view === null)
578
593
  return;
579
594
  const insert = [...`@${view.matches[view.selected].path} `].map((ch) => ch.codePointAt(0));
595
+ if (this.#cursor - view.start >= 1)
596
+ this.#checkpoint(); // UD-1
580
597
  this.#chars.splice(view.start, this.#cursor - view.start, ...insert);
581
598
  this.#cursor = view.start + insert.length;
582
599
  this.#atClose();
@@ -748,6 +765,12 @@ export class Editor {
748
765
  process.stdout.write(MOUSE_OFF);
749
766
  this.#mouseOn = false;
750
767
  process.stdout.write("\x1b[?2004l"); // bracketed paste OFF
768
+ // REL-0161: the hardware cursor was hidden for the session's whole
769
+ // life (the compositor's entry reset); this is the one place kiso
770
+ // hands the terminal back. kill -9 skips it — the same exposure
771
+ // the reference implementation accepts; the entry repair covers
772
+ // the next kiso, and `reset` covers the shell.
773
+ process.stdout.write("\x1b[?25h");
751
774
  process.stdin.setRawMode(false);
752
775
  this.#closedResolve();
753
776
  }
@@ -1096,6 +1119,8 @@ export class Editor {
1096
1119
  // v3 §04: Esc closes the menu and clears the buffer.
1097
1120
  // CA-4: the closing esc consumes its burst (the `i += 1`
1098
1121
  // convention) — a double-esc can never abort the turn.
1122
+ if (this.#chars.length > 0)
1123
+ this.#checkpoint(); // UD-1
1099
1124
  this.#chars = [];
1100
1125
  this.#cursor = 0;
1101
1126
  this.#scroll = 0;
@@ -1195,6 +1220,14 @@ export class Editor {
1195
1220
  this.#killWord();
1196
1221
  i += 1;
1197
1222
  }
1223
+ else if (c === "\x1a" || c === "\x1f") {
1224
+ this.#undoOp(); // UD-1: ctrl+z (and the readline ctrl+_)
1225
+ i += 1;
1226
+ }
1227
+ else if (c === "\x19") {
1228
+ this.#redoOp(); // UD-1: ctrl+y
1229
+ i += 1;
1230
+ }
1198
1231
  else if (c === "\x01") {
1199
1232
  this.#cursor = this.#cursorBounds().start; // A3: line-local (a single line starts at 0 — unchanged)
1200
1233
  this.#reflow();
@@ -1885,8 +1918,66 @@ export class Editor {
1885
1918
  if (!this.#pasting)
1886
1919
  this.#onRender();
1887
1920
  }
1921
+ // ---- UD-1: the undo machinery ----
1922
+ /** The caps: entries and total code points (a D13-class 260KB paste
1923
+ * fits with room). Evict oldest; both stacks share the shape. */
1924
+ static #UNDO_CAP = 64;
1925
+ static #UNDO_CP_CAP = 2 * 1024 * 1024;
1926
+ #snap() {
1927
+ return { chars: [...this.#chars], cursor: this.#cursor };
1928
+ }
1929
+ #sameSnap(a, b) {
1930
+ return a.cursor === b.cursor && a.chars.length === b.chars.length && a.chars.every((c, i) => c === b.chars[i]);
1931
+ }
1932
+ #capStack(stack) {
1933
+ while (stack.length > _a.#UNDO_CAP)
1934
+ stack.shift();
1935
+ let total = stack.reduce((n, s) => n + s.chars.length, 0);
1936
+ while (stack.length > 1 && total > _a.#UNDO_CP_CAP)
1937
+ total -= stack.shift().chars.length;
1938
+ }
1939
+ /** Push the CURRENT state before a destructive gesture. Always
1940
+ * clears the redo stack (a new destruction forks history); the
1941
+ * push itself is deduped against the top. */
1942
+ #checkpoint() {
1943
+ this.#redoStack.length = 0;
1944
+ const cur = this.#snap();
1945
+ const top = this.#undoStack.at(-1);
1946
+ if (top !== undefined && this.#sameSnap(top, cur))
1947
+ return;
1948
+ this.#undoStack.push(cur);
1949
+ this.#capStack(this.#undoStack);
1950
+ }
1951
+ #restoreSnap(s) {
1952
+ this.#chars = [...s.chars];
1953
+ this.#cursor = s.cursor;
1954
+ this.#scroll = 0;
1955
+ this.#verticalGoalCol = null;
1956
+ this.#reflow();
1957
+ this.#refreshMenu();
1958
+ if (!this.#pasting)
1959
+ this.#onRender();
1960
+ }
1961
+ #undoOp() {
1962
+ const prev = this.#undoStack.pop();
1963
+ if (prev === undefined)
1964
+ return;
1965
+ this.#redoStack.push(this.#snap());
1966
+ this.#capStack(this.#redoStack);
1967
+ this.#restoreSnap(prev);
1968
+ }
1969
+ #redoOp() {
1970
+ const next = this.#redoStack.pop();
1971
+ if (next === undefined)
1972
+ return;
1973
+ this.#undoStack.push(this.#snap());
1974
+ this.#capStack(this.#undoStack);
1975
+ this.#restoreSnap(next);
1976
+ }
1888
1977
  #killToStart() {
1889
1978
  const { start } = this.#cursorBounds(); // A3: line-local (0 on a single line — unchanged)
1979
+ if (this.#cursor > start)
1980
+ this.#checkpoint(); // UD-1
1890
1981
  this.#chars.splice(start, this.#cursor - start);
1891
1982
  this.#cursor = start;
1892
1983
  this.#reflow();
@@ -1895,6 +1986,8 @@ export class Editor {
1895
1986
  }
1896
1987
  #killToEnd() {
1897
1988
  const { end } = this.#cursorBounds(); // A3: line-local (the buffer's end on a single line — unchanged)
1989
+ if (end > this.#cursor)
1990
+ this.#checkpoint(); // UD-1
1898
1991
  this.#chars.splice(this.#cursor, end - this.#cursor);
1899
1992
  this.#reflow();
1900
1993
  if (!this.#pasting)
@@ -1909,6 +2002,8 @@ export class Editor {
1909
2002
  i -= 1; // trailing spaces
1910
2003
  while (i > 0 && this.#chars[i - 1] !== 0x20)
1911
2004
  i -= 1; // the word
2005
+ if (i < this.#cursor)
2006
+ this.#checkpoint(); // UD-1
1912
2007
  this.#chars.splice(i, this.#cursor - i);
1913
2008
  this.#cursor = i;
1914
2009
  this.#reflow();
@@ -2023,6 +2118,8 @@ export class Editor {
2023
2118
  }
2024
2119
  #takeLine() {
2025
2120
  const line = String.fromCodePoint(...this.#chars);
2121
+ this.#undoStack.length = 0; // UD-1: a sent turn is not a loss
2122
+ this.#redoStack.length = 0;
2026
2123
  this.#chars = [];
2027
2124
  this.#cursor = 0;
2028
2125
  this.#scroll = 0;
@@ -2190,6 +2287,8 @@ export class Editor {
2190
2287
  const line = this.#queuePop();
2191
2288
  if (line === null)
2192
2289
  return;
2290
+ if (this.#chars.length > 0)
2291
+ this.#checkpoint(); // UD-1: a mid-walk edit is recoverable
2193
2292
  this.#chars = [...line].map((ch) => ch.codePointAt(0));
2194
2293
  this.#cursor = this.#chars.length;
2195
2294
  this.#scroll = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
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",
@@ -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.16.0"
38
+ "@vincemakes/kiso-tui-cells": "0.16.2"
39
39
  }
40
40
  }