@vincemakes/kiso-tui 0.15.9 → 0.15.11

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.
@@ -212,6 +212,32 @@ export declare class Body {
212
212
  * live region's first row) — NEVER at the formula's committed-count
213
213
  * top: external writes (the CLI's console.error CRLF) can shift the
214
214
  * committed content down, and a formula-top ED0 would clear it. */
215
+ /**
216
+ * REL-0152-D18 — a window DRAG is one resize, not forty.
217
+ *
218
+ * SIGWINCH fires continuously while a drag is in progress — every few
219
+ * pixels is another signal — and this used to answer each one at once
220
+ * with an erase and a full repaint. A real terminal REFLOWS on a width
221
+ * change and pushes the rows it displaces into its scrollback, so
222
+ * every one of those repaints deposited another copy of the screen
223
+ * into the history. The owner's two-second drag left six identical
224
+ * copies of the same tool block in the transcript.
225
+ *
226
+ * Nothing clever can undo that afterwards: the scrollback is not ours
227
+ * to rewrite. The only fix is to not paint into the middle of a drag.
228
+ *
229
+ * The geometry is adopted IMMEDIATELY — every cap and bound is read
230
+ * from `#opts` at frame time, so the model is already at the new size
231
+ * and nothing is computed against a stale width. What waits is the
232
+ * PAINT. When the signals stop, one erase and one repaint.
233
+ *
234
+ * The competitor does not have this problem, for a structural reason
235
+ * worth naming rather than envying: it runs on the alternate screen,
236
+ * which has no scrollback to accumulate into. kiso is on the primary
237
+ * screen deliberately — the transcript IS the terminal's own
238
+ * scrollback, which is the product's whole claim — so it pays for
239
+ * that choice here, and has to pay carefully.
240
+ */
215
241
  onResize(): void;
216
242
  /** W18: the status row's right-aligned hint is part of the status
217
243
  * state — the compacting row passes "esc to cancel" (the affordance
@@ -64,6 +64,10 @@ const SPINNER_MS = 200; // the spinner cadence — a ONE-SHOT re-armed on demand
64
64
  /** REL-0152-R1: a held row that has never been painted, so the first
65
65
  * frame writes every row rather than trusting an empty string. */
66
66
  const NOT_PAINTED = "\u0000never";
67
+ /** REL-0152-D18 — how long a drag has to be quiet before it is over.
68
+ * Long enough that a continuous drag never crosses it, short enough
69
+ * that a single resize still feels immediate. */
70
+ const RESIZE_SETTLE_MS = 80;
67
71
  const CHROME_ROWS = 4; // box top + input + box bottom + status — the design §03 chrome (V6-3; the box is W6)
68
72
  /** W20 — the whole-table-replace comparison: the live task block only
69
73
  * redraws when the items actually changed (the task extension's
@@ -141,6 +145,11 @@ export class Body {
141
145
  #scrolledOff = 0;
142
146
  /** REL-0152-R1: this frame is the repaint after a SIGWINCH. */
143
147
  #resizeFrame = false;
148
+ /** REL-0152-D18: a drag's signals are still arriving. */
149
+ #resizePending = false;
150
+ /** REL-0152-D19: the inherited terminal has not been released yet. */
151
+ #needsReset = false;
152
+ #resizeTimer = null;
144
153
  #lastH = 0;
145
154
  // KC1 §6: the composer's recorded extent — the row count the last
146
155
  // frame drew (exit's clear walks it) and the row its CHA parked the
@@ -868,6 +877,42 @@ export class Body {
868
877
  return;
869
878
  this.#docked = true;
870
879
  this.#guardOutput();
880
+ // REL-0152-D19: RELEASE the terminal we were handed, before the
881
+ // first frame.
882
+ //
883
+ // The dock resets on the way OUT and reset nothing on the way IN,
884
+ // so every session began in whatever state the previous occupant
885
+ // left. For THIS product that is not an edge case: kiso's claim is
886
+ // that it survives kill -9, which makes "the last instance died
887
+ // without running its teardown" a supported and advertised way to
888
+ // arrive here.
889
+ //
890
+ // Both of these confine writes to a SUB-REGION of the screen,
891
+ // which is the only way content can survive at a column the dock
892
+ // paints — every chrome row is exactly W wide and written from
893
+ // column 1, so an erase plus a W-wide write covers the row end to
894
+ // end unless the write cannot reach the ends.
895
+ //
896
+ // ESC[r the scroll region back to the whole screen
897
+ // ESC[?69l left/right margin mode OFF (DECLRMM) — ESC[r does
898
+ // NOT release margins, which is why resetting only the
899
+ // region on exit was never enough
900
+ //
901
+ // And two that a KILLED kiso leaves set, which REL-0152-D14
902
+ // introduced and this is the first thing to defend against it: a
903
+ // frame turns autowrap off and the cursor invisible and restores
904
+ // both at its end, so a process that dies BETWEEN those two points
905
+ // hands the shell — and the next kiso — a terminal with wrapping
906
+ // off and no cursor. `kill -9` cannot be caught, so the entry is
907
+ // the only place this can be repaired, and a product whose claim
908
+ // is that it survives kill -9 has to repair it there.
909
+ //
910
+ // ESC[?7h autowrap back to its default
911
+ // ESC[?25h the cursor visible again
912
+ //
913
+ // Idempotent, sixteen bytes, once per session, and correct whether
914
+ // or not anything was actually left set.
915
+ this.#needsReset = true;
871
916
  this.#attachResize();
872
917
  this.#fullRedraw = true;
873
918
  this.#dirty = true;
@@ -877,6 +922,13 @@ export class Body {
877
922
  * chrome rows cleared, the cursor home at the input line. */
878
923
  exit() {
879
924
  this.#unguard?.();
925
+ // REL-0152-D18: a drag in flight must not repaint into a torn-down
926
+ // dock — the timer outlives the compositor otherwise.
927
+ if (this.#resizeTimer !== null) {
928
+ clearTimeout(this.#resizeTimer);
929
+ this.#resizeTimer = null;
930
+ }
931
+ this.#resizePending = false;
880
932
  // TUI2-R3v2 ②: the mouse disable rides the SAME teardown as CSI r,
881
933
  // and rides it BEFORE the docked guard — an un-docked compositor is
882
934
  // exactly the state a superseded or half-torn-down one is in, and
@@ -1001,9 +1053,50 @@ export class Body {
1001
1053
  * live region's first row) — NEVER at the formula's committed-count
1002
1054
  * top: external writes (the CLI's console.error CRLF) can shift the
1003
1055
  * committed content down, and a formula-top ED0 would clear it. */
1056
+ /**
1057
+ * REL-0152-D18 — a window DRAG is one resize, not forty.
1058
+ *
1059
+ * SIGWINCH fires continuously while a drag is in progress — every few
1060
+ * pixels is another signal — and this used to answer each one at once
1061
+ * with an erase and a full repaint. A real terminal REFLOWS on a width
1062
+ * change and pushes the rows it displaces into its scrollback, so
1063
+ * every one of those repaints deposited another copy of the screen
1064
+ * into the history. The owner's two-second drag left six identical
1065
+ * copies of the same tool block in the transcript.
1066
+ *
1067
+ * Nothing clever can undo that afterwards: the scrollback is not ours
1068
+ * to rewrite. The only fix is to not paint into the middle of a drag.
1069
+ *
1070
+ * The geometry is adopted IMMEDIATELY — every cap and bound is read
1071
+ * from `#opts` at frame time, so the model is already at the new size
1072
+ * and nothing is computed against a stale width. What waits is the
1073
+ * PAINT. When the signals stop, one erase and one repaint.
1074
+ *
1075
+ * The competitor does not have this problem, for a structural reason
1076
+ * worth naming rather than envying: it runs on the alternate screen,
1077
+ * which has no scrollback to accumulate into. kiso is on the primary
1078
+ * screen deliberately — the transcript IS the terminal's own
1079
+ * scrollback, which is the product's whole claim — so it pays for
1080
+ * that choice here, and has to pay carefully.
1081
+ */
1004
1082
  onResize() {
1005
1083
  if (!this.#isActive())
1006
1084
  return;
1085
+ this.#resizePending = true;
1086
+ if (this.#resizeTimer !== null)
1087
+ clearTimeout(this.#resizeTimer);
1088
+ this.#resizeTimer = setTimeout(() => {
1089
+ this.#resizeTimer = null;
1090
+ this.#settleResize();
1091
+ }, RESIZE_SETTLE_MS);
1092
+ if (this.#resizeTimer.unref !== undefined)
1093
+ this.#resizeTimer.unref();
1094
+ }
1095
+ /** The one repaint a drag earns, once its signals have stopped. */
1096
+ #settleResize() {
1097
+ if (!this.#resizePending || !this.#isActive())
1098
+ return;
1099
+ this.#resizePending = false;
1007
1100
  const H = this.#opts.height();
1008
1101
  const liveRows = this.#lastLiveRows > 0 ? this.#lastLiveRows : 3;
1009
1102
  const from = Math.max(1, (this.#lastH > 0 ? this.#lastH : H) - liveRows + 1);
@@ -1011,7 +1104,7 @@ export class Body {
1011
1104
  this.#fullRedraw = true;
1012
1105
  this.#resizeFrame = true;
1013
1106
  this.#dirty = true;
1014
- this.render(); // the immediate redraw at the NEW geometry
1107
+ this.render();
1015
1108
  }
1016
1109
  /** W18: the status row's right-aligned hint is part of the status
1017
1110
  * state — the compacting row passes "esc to cancel" (the affordance
@@ -1373,6 +1466,32 @@ export class Body {
1373
1466
  // Restored at the end of every frame: prose written OUTSIDE a
1374
1467
  // frame (bodyLog, an error) is ordinary output and must still
1375
1468
  // wrap, and the shell inherits the terminal when kiso exits.
1469
+ if (this.#needsReset) {
1470
+ // REL-0152-D19: released as the FIRST bytes of the FIRST frame
1471
+ // rather than as a write of its own. A separate write would be
1472
+ // one more thing between the dock coming up and its first
1473
+ // frame, and the PTY gates that wait on the boot stream feel
1474
+ // it; inside the frame it is four sequences and no new event.
1475
+ this.#needsReset = false;
1476
+ out.push("\x1b[r\x1b[?69l\x1b[?7h\x1b[?25h");
1477
+ // REL-0152-D20 — the mechanism is understood and the obvious fix
1478
+ // is NOT taken here. See the finding.
1479
+ //
1480
+ // The first frame addresses rows 1..H absolutely and draws over
1481
+ // whatever the terminal was showing. Scrolling a screenful away
1482
+ // first would fix that, and it was built and measured: it pushes
1483
+ // up to H BLANK rows into the scrollback, and TUI2-R2pre's
1484
+ // blank-share gate went from 14/43 to 29/43 — past the "a
1485
+ // healthy session's scrollback is mostly content" invariant that
1486
+ // gate exists to hold. Trading a symptom with a five-second
1487
+ // user-side setting for a broken invariant is a worse deal.
1488
+ //
1489
+ // The correct version scrolls only as far as the terminal's
1490
+ // content actually reaches, which needs a cursor-position query
1491
+ // at boot — its own round, with its own risk (this file already
1492
+ // declined a boot-time round-trip once, for racing the editor
1493
+ // for stdin).
1494
+ }
1376
1495
  out.push("\x1b[?7l");
1377
1496
  out.push(this.#conservative ? "\x1b[?25l" : "\x1b[?2026h"); // D1: sync ON, or cursor-hide where 2026 is dead bytes
1378
1497
  // A8: the bottom-anchored window (the model's last H rows) shifts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.15.9",
3
+ "version": "0.15.11",
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.15.9"
38
+ "@vincemakes/kiso-tui-cells": "0.15.11"
39
39
  }
40
40
  }