@cplieger/web-terminal-engine 6.4.0-dev.4 → 6.4.0-dev.6

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/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "6.4.0-dev.4",
3
+ "version": "6.4.0-dev.6",
4
4
  "license": "MPL-2.0",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/web-terminal-engine",
3
- "version": "6.4.0-dev.4",
3
+ "version": "6.4.0-dev.6",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "typecheck": "tsc -p tsconfig.json",
@@ -16,7 +16,7 @@
16
16
  "devDependencies": {
17
17
  "@cplieger/web-terminal-engine-v3": "npm:@cplieger/web-terminal-engine@2.8.0",
18
18
  "@eslint/js": "10.0.1",
19
- "@playwright/test": "1.64.0",
19
+ "@playwright/test": "1.64.1",
20
20
  "@stryker-mutator/core": "10.0.0",
21
21
  "@stryker-mutator/typescript-checker": "10.0.0",
22
22
  "@stryker-mutator/vitest-runner": "10.0.0",
@@ -28,11 +28,11 @@
28
28
  "eslint": "10.12.0",
29
29
  "fast-check": "4.10.2",
30
30
  "knip": "6.41.0",
31
- "playwright": "1.64.0",
31
+ "playwright": "1.64.1",
32
32
  "pngjs": "7.0.0",
33
33
  "prettier": "3.9.10",
34
34
  "typescript": "npm:@typescript/typescript6@6.0.2",
35
- "typescript-eslint": "8.71.1",
35
+ "typescript-eslint": "8.71.2",
36
36
  "vitest": "5.0.4"
37
37
  },
38
38
  "overrides": {
package/src/keyboard.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Keyboard event → terminal byte sequence mapping, mirroring xterm.js's
2
- // evaluateKeyboardEvent (src/common/input/Keyboard.ts) so vim, readline, Ink
3
- // and the host application get the keys they expect. Modifier digit per the
2
+ // evaluateKeyboardEvent (src/common/input/Keyboard.ts) so vim, readline and
3
+ // full-screen programs get the keys they expect. Modifier digit per the
4
4
  // xterm/VT520 convention: 1=Shift, 2=Alt, 4=Ctrl, 8=Meta, summed then +1 for
5
5
  // CSI 1;{n}{letter}. DECCKM and DECKPAM come from the injected mode state,
6
6
  // which the server's modes frame keeps current.
@@ -162,9 +162,9 @@ const KEYPAD_SS3: Record<string, string | undefined> = {
162
162
 
163
163
  // -- Kitty keyboard protocol (progressive enhancement) ----------------------
164
164
  // Under an active kitty flag, keys that do NOT produce text are encoded per the
165
- // kitty protocol so an app that enabled it (Codex via crossterm) gets
166
- // unambiguous key events. Only the disambiguate flag (0x1) is honored; the
167
- // server masks off 0x2/0x4/0x8/0x10, so the CSI ?u query reports only 0x1.
165
+ // kitty protocol so an app that enabled it gets unambiguous key events. Only
166
+ // the disambiguate flag (0x1) is honored; the server masks off
167
+ // 0x2/0x4/0x8/0x10, so the CSI ?u query reports only 0x1.
168
168
  // Text-producing keys still flow through the hidden textarea as text (this
169
169
  // encoder returns "ignore" for them), per the spec and the IME model.
170
170
 
@@ -485,9 +485,9 @@ export function mapKeyboardEvent(ev: KeyboardEvent, modes: KeyboardModes): Keybo
485
485
  // handle them. Heuristic: only take Ctrl+letter when Shift is NOT
486
486
  // held AND the letter is one of the ones xterm consumes (a-z),
487
487
  // skipping the few that browsers commonly reserve.
488
- // (We deliberately pass Ctrl+R / Ctrl+T / Ctrl+W through to the PTY
489
- // — the host application's Ctrl+T transcript view depends on it. Users on macOS
490
- // who want browser-reserved combos can use Cmd instead.)
488
+ // (Ctrl+R, Ctrl+T and Ctrl+W still go to the PTY: readline binds them to
489
+ // reverse search, transpose and word rubout, and full-screen programs bind
490
+ // them too. On macOS, Cmd reaches the browser's own combos.)
491
491
 
492
492
  // Shift+PageUp / Shift+PageDown — local scrollback navigation
493
493
  // (matches xterm.js KeyboardResultType.PAGE_UP/PAGE_DOWN).
package/src/render.ts CHANGED
@@ -330,8 +330,8 @@ export function createRenderer(opts: RendererOptions): Renderer {
330
330
  let historyBudgetFn: (() => number) | null = opts.historyBudget ?? null;
331
331
 
332
332
  // Rows awaiting a DOM (re)build, drained at most MAX_ROWS_PER_FRAME per frame:
333
- // a session restore or `cat bigfile` dumps thousands of lines in one frame,
334
- // and building them in one rAF hangs a constrained device. The drain is
333
+ // a session's transcript replay or `cat bigfile` dumps thousands of lines in
334
+ // one frame, and building them in one rAF hangs a constrained device. The drain is
335
335
  // VIEWPORT-FIRST (live-window rows, then scrollback newest->oldest so the
336
336
  // backlog fills upward, offscreen above the pinned viewport), and the cursor
337
337
  // row is always built so the caret never lags.
@@ -35,7 +35,7 @@ export interface SessionInfo {
35
35
  /** the session's short public name, safe to put in a URL: 1 to 64 characters
36
36
  * of `[A-Za-z0-9_-]`, unique among live sessions, and NOT a capability
37
37
  * (nothing attaches with it; only `id` does). The server mints a random one
38
- * and a host may replace it (web-terminal-kiro sets the kiro-cli thread id).
38
+ * and a host may replace it (for example with its program's own session id).
39
39
  * Absent from a server before 6.2 and from a status event carrying
40
40
  * `removed`. Untrusted wire text: validate it before use. */
41
41
  readonly alias?: string;
@@ -73,7 +73,7 @@ export interface SessionInfo {
73
73
  * server accepts it and it becomes the arrangement every device sees. */
74
74
  readonly order?: number;
75
75
  /** true once the session has emitted a genuine activity signal — OSC 9;4
76
- * progress (kiro-cli, Claude Code, …) or a classified OSC 9 notification.
76
+ * progress or a classified OSC 9 notification.
77
77
  * Sticky for the session's life. Consumers reveal the per-tab activity dot
78
78
  * only when this is set; a program that emits no OSC 9 signal (a plain shell)
79
79
  * keeps its tab dot hidden. */
package/src/store.ts CHANGED
@@ -433,25 +433,12 @@ export class LineStore {
433
433
  private paging = false;
434
434
 
435
435
  /**
436
- * The highest index the APPLICATION erased (ED3), or -1. A second watermark
437
- * beside `everEvictedThrough` because the two answer different questions and
438
- * only one of them is a trim.
439
- *
440
- * `everEvictedThrough` means "we dropped this to stay inside a memory budget",
441
- * and it drives the "earlier output trimmed" affordance. ED3 is not that: the
442
- * app discarded its own scrollback, so a marker would be permanent noise on
443
- * kiro-cli (which clears on every resize redraw), which is why ED3 deliberately
444
- * leaves it alone. That left NOTHING refusing an erased index, and a page reply
445
- * whose data timeout had already released single-flight comes back
446
- * UNCORRELATED — through the ordinary scroll path, classified TAIL, so outside
447
- * the cache budget and the TTL both. It never leaves.
448
- *
449
- * Refusing an erased index is unconditionally safe within an epoch: the server's
450
- * ring `Clear()` preserves `committed`, so an erased absolute index is never
451
- * reused, and no legitimate later frame can carry one. The companion reprint an
452
- * app emits in the same tick lands at NEW indices above everything the client
453
- * held, which is why this watermark tracks what was actually DROPPED rather than
454
- * the cleared bound — the bound would also refuse that reprint.
436
+ * The highest index the application erased (ED3), or -1. Not
437
+ * `everEvictedThrough`, which marks budget trims and drives the "earlier output
438
+ * trimmed" marker that ED3 must not show. Without it a late, uncorrelated page
439
+ * reply lands as TAIL, outside the cache budget and TTL, and never leaves. Safe
440
+ * within an epoch: the ring's `Clear()` keeps `committed`, so no erased index is
441
+ * reused. It tracks what was dropped; the cleared bound would refuse the reprint.
455
442
  */
456
443
  private erasedThrough = -1;
457
444
 
@@ -536,23 +523,12 @@ export class LineStore {
536
523
  private unconfirmedFrom = Number.POSITIVE_INFINITY;
537
524
 
538
525
  /**
539
- * The resume REPLAY-EXCLUSION BOUNDARY: the highest index this client will not
540
- * ask the server to re-send. Send this as `haveThrough`, never `highestIndex()`.
541
- *
542
- * The two differ by whatever the store holds provisionally, and that
543
- * difference is a correctness bug once it reaches the wire. While a store has
544
- * no socket (a background tab) it observes no base advance, so its window rows
545
- * keep the content the app drew there, and claiming down to the window's
546
- * bottom row tells the server "no need to re-send those" about rows whose
547
- * committed content this client has never seen. The server replays strictly
548
- * above `haveThrough`, so the provisional copies are never corrected and
549
- * surface as scrollback under the new window: measured as a frozen kiro-cli
550
- * composer box parked above the live region after almost every tab switch.
551
- *
552
- * NOT a residency fact and NOT "the highest confirmed row": the index this
553
- * returns may be one the store never held or has since evicted. The only
554
- * guarantee is `replayBoundary() <= highestIndex()`, with equality whenever
555
- * nothing held is provisional.
526
+ * The resume replay-exclusion boundary: the highest index this client will not
527
+ * ask the server to re-send. Send it as `haveThrough`, never `highestIndex()`: a
528
+ * background tab's window rows hold what the app drew there, and claiming them
529
+ * means their committed content is never re-sent, so a stale input box surfaces
530
+ * as scrollback. It may name an index never held or since evicted; the only
531
+ * guarantee is `replayBoundary() <= highestIndex()`, equal when none is provisional.
556
532
  */
557
533
  replayBoundary(): number {
558
534
  if (this.highest < 0) {
@@ -690,30 +666,21 @@ export class LineStore {
690
666
  // Re-check the cap AFTER the window settled: a screen frame can LOWER the
691
667
  // retention bound without applying a single line — a height shrink whose
692
668
  // rows are byte-identical short-circuits applyLine on the idempotency
693
- // guard, and applyLine is otherwise the only enforceCap caller. The R3
694
- // adversarial rounds' interleaved property test failed on exactly this
695
- // shape (~60% of seeds) until the bound was re-checked per FRAME, not
696
- // only per content change.
669
+ // guard, and applyLine is otherwise the only enforceCap caller.
697
670
  this.enforceCap();
698
671
  }
699
672
 
700
673
  /** Apply a decoded scroll/history frame: each line at firstIndex + i. */
701
674
  applyScroll(msg: ScrollMessage): void {
702
675
  if (this.alt) {
703
- // History strictly below the frozen MAIN window is safe to store while
704
- // the alternate screen is active: the alt display renders from altRows,
705
- // never from the abs store, and lines below win.base can only be
706
- // main-screen scrollback — a resume batch's replay, or a post-resume
707
- // durable chunk that raced an alt flip (the 2026-08 write-ordering
708
- // race's last residual, closed here). They surface at alt exit's store
709
- // rebuild. Anything AT or ABOVE the frozen base still violates the
710
- // protocol invariant (the server never emits live scroll during alt)
711
- // and is dropped rather than corrupting the frozen window region. With
712
- // no main window ever seen (a fresh tab attaching straight into an
713
- // in-alt session) there is no region to protect, so everything is
714
- // accepted exactly as on the main-screen path — the alt-exit full
715
- // frame overwrites the window region either way. applyLine's guard
716
- // set applies to whatever passes.
676
+ // In alt, rows below the frozen MAIN window base are main-screen history
677
+ // (a resume replay, or a durable chunk that raced an alt flip) and are
678
+ // stored: the alt display renders from altRows, and they surface at alt
679
+ // exit's rebuild. Rows AT or ABOVE the base break the protocol (no live
680
+ // scroll during alt) and are dropped to keep the frozen window intact.
681
+ // With no main window ever seen (attaching straight into alt) there is
682
+ // nothing to protect, so all rows are accepted; the alt-exit full frame
683
+ // overwrites the window region either way.
717
684
  const base = this.win.height > 0 ? this.win.base : Number.POSITIVE_INFINITY;
718
685
  for (let i = 0; i < msg.lines.length; i++) {
719
686
  const abs = msg.firstIndex + i;
@@ -1133,8 +1100,7 @@ export class LineStore {
1133
1100
  * The store used to derive it from `viewportAbs >= win.base`, and every
1134
1101
  * failure of that derivation was a real defect. It read a window descriptor
1135
1102
  * that step 4 of this very transition RETIRES, so the answer was wrong for
1136
- * every predicted replay jump (r2's confirmed high, patched by capturing the
1137
- * value earlier — a fix that left the hazard in place). With no descriptor at
1103
+ * every predicted replay jump. With no descriptor at
1138
1104
  * all, which is the DOMINANT shape here (this ack is the first frame of its
1139
1105
  * batch, so it precedes the window frame, and a hydrated store persists no
1140
1106
  * window), the fallback was wrong in both directions: "not following" strands
@@ -1669,8 +1635,8 @@ export class LineStore {
1669
1635
  return s !== null && abs >= s.lo && abs < s.hi;
1670
1636
  }
1671
1637
 
1672
- // applyScrollbackCleared handles ED3 (which an inline TUI such as kiro-cli
1673
- // emits on every resize): drop lines below `base`, leave everEvictedThrough
1638
+ // applyScrollbackCleared handles ED3 (which an inline TUI can emit on every
1639
+ // resize): drop lines below `base`, leave everEvictedThrough
1674
1640
  // alone (a deliberate erase is not a trim), CANCEL the in-flight request
1675
1641
  // (its reply would re-admit the erased rows through the guard-2 exemption),
1676
1642
  // and snap the paging floor to `base` (or the trigger re-fetches them). The
@@ -1754,33 +1720,21 @@ export class LineStore {
1754
1720
  }
1755
1721
 
1756
1722
  private enforceCap(): void {
1757
- // The gate and the victim
1758
- // target read `tailCount`, not `lines.size`. Browse cache lives under its
1759
- // OWN budget, enforced at page-apply time, so live output must not be able
1760
- // to evict a page the reader is browsing — at the default batch that would
1761
- // be up to 93 cache lines per applied line.
1723
+ // The gate and the victim target read `tailCount`, not `lines.size`: browse
1724
+ // cache has its OWN budget, enforced at page-apply time, so live output must
1725
+ // not evict a page the reader is browsing (up to 93 cache lines per applied
1726
+ // line at the default batch).
1762
1727
  if (this.tailCount <= this.tailBound) {
1763
1728
  return;
1764
1729
  }
1765
1730
  const target = this.tailBound - evictionBatch(this.tailBound) + 1;
1766
- // ONE oldest-first eviction walk that SKIPS the live window. The window
1767
- // is never evictable — the screen is documented as a fixed,
1768
- // always-present block, so the cap is a HISTORY budget floored at the
1769
- // live screen (a cap at or below the terminal height keeps the full
1770
- // screen with zero scrollback). Content ABOVE the window (a resume
1771
- // replay delivered before its window frame; a malformed stream that
1772
- // never advances the base) is ordinary evictable history too: the
1773
- // cursor hops over the window and keeps walking, so the bound
1774
- // size <= max(maxLines, window height) holds whatever shape the frames
1775
- // arrive in. Evicting that band OLDEST-first keeps the NEWEST lines —
1776
- // the ones adjacent to the incoming window — so after the window
1777
- // advances the retained set converges back to one contiguous block
1778
- // (R3 adversarial finding: the earlier newest-first tail trim parked a
1779
- // permanent interior hole under the live screen that resume, which
1780
- // replays only above haveThrough, could never backfill).
1781
- // everEvictedThrough advances over every TAIL eviction, including the
1782
- // hopped band — safe for the window frame that follows a replay because
1783
- // apply-line guard 2 exempts the live window (see applyLine).
1731
+ // ONE oldest-first eviction walk that SKIPS the live window: the cap is a
1732
+ // history budget floored at the screen height. Content above the window (a
1733
+ // replay delivered before its window frame, a malformed stream) is evictable,
1734
+ // so size <= max(maxLines, window height) holds for any frame order. Oldest-
1735
+ // first keeps the lines next to the window; a newest-first trim leaves a hole
1736
+ // under the live screen that resume cannot backfill. everEvictedThrough also
1737
+ // covers the hopped band, safe because applyLine's guard 2 exempts the window.
1784
1738
  const winBottom = this.win.height > 0 ? this.win.base + this.win.height - 1 : -1;
1785
1739
  const hasWindow = this.win.height > 0;
1786
1740
  let cursor = this.oldest;
@@ -1867,7 +1821,7 @@ export class LineStore {
1867
1821
  if (hopped) {
1868
1822
  // The cursor hopped the window or a browse interval, so the minimum
1869
1823
  // retained key is not the cursor: recompute both bounds once per pass
1870
- // (bounded scan; once per PASS, never per victim — R3 perf finding).
1824
+ // (bounded scan; once per PASS, never per victim).
1871
1825
  let min = -1;
1872
1826
  let max = -1;
1873
1827
  for (const k of this.lines.keys()) {
@@ -1945,8 +1899,8 @@ export class LineStore {
1945
1899
  // the alt frame. This keeps the retention bound (max(maxLines, window
1946
1900
  // height)) and the guard-2 window exemption defined over the MAIN screen,
1947
1901
  // the region alt exit must restore, rather than over a phantom range
1948
- // mixing the main base with the alt height (R4 review, all three models:
1949
- // a smaller alt frame otherwise appeared to lower the bound below the
1902
+ // mixing the main base with the alt height (a smaller alt frame otherwise
1903
+ // appeared to lower the bound below the
1950
1904
  // protected main rows, and the exemption described rows that were not the
1951
1905
  // writable screen).
1952
1906
  const next: WindowState = {
package/src/types.ts CHANGED
@@ -61,7 +61,7 @@ export interface ScreenMessage {
61
61
  /**
62
62
  * True when the app issued ED3 (`CSI 3 J`, "erase saved lines") this frame:
63
63
  * the client drops its scrollback history (all lines below `base`), matching
64
- * a real terminal. Inline TUIs (kiro-cli) emit ED3 on every resize redraw to
64
+ * a real terminal. Inline TUIs can emit ED3 on every resize redraw to
65
65
  * discard the previous frame; honoring it stops stale frames accumulating.
66
66
  */
67
67
  scrollbackCleared?: boolean;