@cplieger/web-terminal-engine 3.10.2 → 4.0.1
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/README.md +1 -1
- package/jsr.json +3 -2
- package/package.json +3 -3
- package/src/render.ts +62 -0
- package/src/scroll.ts +10 -8
- package/wire-compatibility.json +1 -1
- package/src/wire-manifest.ts +0 -77
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ curl -fsSL https://jsr.io/@cplieger/web-terminal-engine/<version>/wire-compatibi
|
|
|
106
106
|
```json
|
|
107
107
|
{
|
|
108
108
|
"schemaVersion": 1,
|
|
109
|
-
"generatedBy": "web/src/wire-manifest.ts",
|
|
109
|
+
"generatedBy": "web/src/test-helpers/wire-manifest.ts",
|
|
110
110
|
"wireCompatibility": {
|
|
111
111
|
"protocolVersion": 4,
|
|
112
112
|
"minimumServerProtocolVersion": 3,
|
package/jsr.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
],
|
|
15
15
|
"exclude": [
|
|
16
16
|
"src/**/*.test.ts",
|
|
17
|
-
"src
|
|
17
|
+
"src/**/*-setup.ts",
|
|
18
|
+
"src/**/test-helpers/**"
|
|
18
19
|
]
|
|
19
20
|
}
|
|
20
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc -p tsconfig.json",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"files": [
|
|
68
68
|
"src/",
|
|
69
69
|
"!src/**/*.test.ts",
|
|
70
|
-
"!src
|
|
71
|
-
"!src/**/
|
|
70
|
+
"!src/**/*-setup.ts",
|
|
71
|
+
"!src/**/test-helpers/**",
|
|
72
72
|
"jsr.json",
|
|
73
73
|
"LICENSE",
|
|
74
74
|
"wire-compatibility.json"
|
package/src/render.ts
CHANGED
|
@@ -494,6 +494,11 @@ function queueRowsViewportFirst(): void {
|
|
|
494
494
|
* history; the existing per-frame budget spreads a large backlog across frames
|
|
495
495
|
* so the switch never janks. Used by bind(); also safe to call directly to
|
|
496
496
|
* force a full repaint of the current store.
|
|
497
|
+
*
|
|
498
|
+
* The wipe covers content space in full: the rows AND the four overlays anchored
|
|
499
|
+
* into it (see collapseContentSpaceOverlays). Leaving the overlays behind left
|
|
500
|
+
* the container a measured 81081px of scroll range over zero rows of content,
|
|
501
|
+
* which is a black pane the bottom pin refuses to correct.
|
|
497
502
|
*/
|
|
498
503
|
export function rebuild(): void {
|
|
499
504
|
// The wipe is the largest content shrink this module performs, and it happens
|
|
@@ -515,6 +520,9 @@ export function rebuild(): void {
|
|
|
515
520
|
// Fresh surface: a stale give-up streak from a prior store must not deny the
|
|
516
521
|
// rebuilt surface its full transient-retry budget.
|
|
517
522
|
renderNoProgressStreak = 0;
|
|
523
|
+
// The rows are not the only thing in content space. Collapse the overlays
|
|
524
|
+
// with them, BEFORE the offset below is read.
|
|
525
|
+
collapseContentSpaceOverlays();
|
|
518
526
|
// Alt screen paints from the ephemeral grid in the flush, so it needs no
|
|
519
527
|
// absolute-index queueing here.
|
|
520
528
|
if (!store.isAlt()) {
|
|
@@ -524,6 +532,60 @@ export function rebuild(): void {
|
|
|
524
532
|
scheduleFlush();
|
|
525
533
|
}
|
|
526
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Drop every CONTENT-SPACE overlay's hold on the scroll container, as part of a
|
|
537
|
+
* wipe. Four elements sit INSIDE the container carrying a `top` in content
|
|
538
|
+
* coordinates — the caret, the predicted cursor, the IME view and the consumer's
|
|
539
|
+
* hidden textarea — so each one holds the container's scrollable overflow at the
|
|
540
|
+
* offset it was last placed at. `output.replaceChildren()` removes the rows and
|
|
541
|
+
* leaves all four.
|
|
542
|
+
*
|
|
543
|
+
* Measured on a 4769-row tab in Chromium: after the wipe the scroller had ZERO
|
|
544
|
+
* rows of content and an 81081px scroll range, held by the caret and the
|
|
545
|
+
* textarea still sitting at 81064px, with the viewport parked at 80281px over
|
|
546
|
+
* nothing. Two consequences, and the second is why this is not cosmetic:
|
|
547
|
+
*
|
|
548
|
+
* - The reader sees that empty region, which paints the terminal's background:
|
|
549
|
+
* a black pane, and then a partial row clipped to the top edge once the
|
|
550
|
+
* drain's growing block reaches the parked offset.
|
|
551
|
+
* - `stickToBottom` is DISARMED. It measures `distanceFromBottom()` against
|
|
552
|
+
* the phantom height, reads 0 — already at the bottom — and pins nothing, so
|
|
553
|
+
* the invariant that would otherwise rescue the view refuses to act for
|
|
554
|
+
* exactly as long as the phantom height survives.
|
|
555
|
+
*
|
|
556
|
+
* The renderer heals all four at the END of the first flush
|
|
557
|
+
* (`positionCursorOverlay` plus `onCursorMove`), so the window is normally one
|
|
558
|
+
* frame and invisible. It stops being one frame whenever that tail does not
|
|
559
|
+
* run on time: a long frame (the wipe tears down thousands of rows), a throw
|
|
560
|
+
* mid-drain (the catch skips the tail and the bounded give-up can stop
|
|
561
|
+
* rescheduling), or a full reset arriving with the switch's reconnect. The
|
|
562
|
+
* reported symptom is a scroll tick clearing it, which fits: a scroll re-enters
|
|
563
|
+
* the built block, and the block sits at the TOP of the phantom range.
|
|
564
|
+
*
|
|
565
|
+
* Collapsing here also makes the wipe's clamp SYNCHRONOUS, which two callers
|
|
566
|
+
* need. `noteContentShrink` arms only when it observes the position actually
|
|
567
|
+
* move, so a deferred clamp left the wipe — the largest shrink this module
|
|
568
|
+
* performs — with nothing to announce. And `bind` records
|
|
569
|
+
* `pendingRestore.lastWrote` from the same offset, so a deferred clamp
|
|
570
|
+
* (measured: 80281 -> 4346) then read as a 76000px foreign write and threw away
|
|
571
|
+
* the reading position the switch was restoring.
|
|
572
|
+
*/
|
|
573
|
+
function collapseContentSpaceOverlays(): void {
|
|
574
|
+
// `cursorAbs` is -1 by now, which is the state positionCursorOverlay already
|
|
575
|
+
// hides for; go through it rather than restating the class name here.
|
|
576
|
+
positionCursorOverlay(undefined);
|
|
577
|
+
// The outgoing session's prediction cannot describe the incoming screen
|
|
578
|
+
// either. The consumer re-pushes it from the `onCursorMove` below, which is
|
|
579
|
+
// its call to make and cannot re-inflate anything: `rowEls` is empty, so
|
|
580
|
+
// setPredictedCursor's own fallback places it at the content origin.
|
|
581
|
+
predCursorEl?.classList.remove("visible");
|
|
582
|
+
// The IME view and the hidden textarea are the CONSUMER's, reachable only
|
|
583
|
+
// through the cursor seam — and after a wipe the cursor genuinely has no row,
|
|
584
|
+
// so the seam is accurate rather than borrowed. `getCursorPx` reports the
|
|
585
|
+
// content origin while `rowEls` is empty, so the handler moves them there.
|
|
586
|
+
onCursorMove?.();
|
|
587
|
+
}
|
|
588
|
+
|
|
527
589
|
/**
|
|
528
590
|
* Highest absolute line index the client holds, or -1 if empty. This is the
|
|
529
591
|
* resume `haveThrough` value (it replaces the old DOM-row count). Exposed so
|
package/src/scroll.ts
CHANGED
|
@@ -265,14 +265,16 @@ function writePreservingFollow(next: number): void {
|
|
|
265
265
|
* absolutely-positioned overlay) — and a lingering arm swallows the next real
|
|
266
266
|
* gesture.
|
|
267
267
|
*
|
|
268
|
-
* That last case
|
|
269
|
-
*
|
|
270
|
-
* hidden textarea all sit inside the scroll container carrying a
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
268
|
+
* That last case used to be live here rather than illustrative, and it is why
|
|
269
|
+
* the caller's ORDER matters: the caret, the predicted cursor, the IME view and
|
|
270
|
+
* the consumer's hidden textarea all sit inside the scroll container carrying a
|
|
271
|
+
* `top` in CONTENT coordinates, so any one of them left at the old offset holds
|
|
272
|
+
* scrollHeight above the built content and the clamp never happens. `rebuild`
|
|
273
|
+
* now collapses all four as part of its wipe, before it calls this
|
|
274
|
+
* (`render.ts` collapseContentSpaceOverlays), so the clamp it announces is real.
|
|
275
|
+
* A caller that removes content without collapsing whatever else is anchored in
|
|
276
|
+
* that space arms nothing and gets no announcement. See
|
|
277
|
+
* `docs/tab-switch-repaint.md` §6.2.
|
|
276
278
|
*
|
|
277
279
|
* A native-scroll-anchoring adjustment (Chrome/Firefox lowering scrollTop as
|
|
278
280
|
* rows are removed above the viewport) also moves the position and also arms
|
package/wire-compatibility.json
CHANGED
package/src/wire-manifest.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// Generator for the published, language-neutral wire-compatibility manifest
|
|
2
|
-
// (`web/wire-compatibility.json`).
|
|
3
|
-
//
|
|
4
|
-
// Why it exists: the wire revision and directional floor are published as
|
|
5
|
-
// TypeScript constants (wire-compatibility.ts) and as Go constants
|
|
6
|
-
// (terminal.WireProtocolVersion / MinSupportedClientWireVersion). A consumer
|
|
7
|
-
// that is neither — a Dockerfile, a shell release gate, a Python CI script —
|
|
8
|
-
// had no machine-readable surface to read them from and resorted to scraping
|
|
9
|
-
// the TypeScript source with `sed`, which breaks silently on any reformat.
|
|
10
|
-
//
|
|
11
|
-
// This module renders that surface FROM the existing WIRE_COMPATIBILITY export,
|
|
12
|
-
// so the numbers keep exactly one source of truth in the TypeScript half. The
|
|
13
|
-
// rendered text is checked into the repo (see the drift guard in
|
|
14
|
-
// wire-manifest.test.ts) because the publish pipeline runs no build step: npm
|
|
15
|
-
// and JSR publish the working tree as-is, so a manifest generated at publish
|
|
16
|
-
// time would never exist in the tarball.
|
|
17
|
-
//
|
|
18
|
-
// The manifest deliberately does NOT carry the package version. `npm pkg set
|
|
19
|
-
// version` injects that at publish time, after checkout, with no regeneration
|
|
20
|
-
// step — an embedded version would be frozen at the repo's placeholder (0.1.0)
|
|
21
|
-
// in every published artifact. Consumers read the package version from
|
|
22
|
-
// package.json / jsr.json, which the pipeline does rewrite.
|
|
23
|
-
//
|
|
24
|
-
// Public-artifact contract: see the "Wire compatibility manifest" section of
|
|
25
|
-
// web/README.md for what a consumer may rely on and what counts as a breaking
|
|
26
|
-
// change.
|
|
27
|
-
|
|
28
|
-
import { WIRE_COMPATIBILITY, type WireCompatibility } from "./wire-compatibility.js";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Schema revision of the manifest FILE, independent of the wire protocol
|
|
32
|
-
* revision it reports. A consumer must reject a `schemaVersion` it does not
|
|
33
|
-
* understand rather than guess at the shape. Bumped only for a breaking
|
|
34
|
-
* change to the manifest's own layout (a removed or repurposed field).
|
|
35
|
-
*/
|
|
36
|
-
export const WIRE_MANIFEST_SCHEMA_VERSION = 1;
|
|
37
|
-
|
|
38
|
-
/** Path of the checked-in artifact this module renders, relative to `web/`. */
|
|
39
|
-
export const WIRE_MANIFEST_PATH = "wire-compatibility.json";
|
|
40
|
-
|
|
41
|
-
/** Shape of the generated wire-compatibility manifest. */
|
|
42
|
-
export interface WireManifest {
|
|
43
|
-
/** Schema revision of this file's layout (see WIRE_MANIFEST_SCHEMA_VERSION). */
|
|
44
|
-
readonly schemaVersion: number;
|
|
45
|
-
/** Informational provenance note; not part of the parsed contract. */
|
|
46
|
-
readonly generatedBy: string;
|
|
47
|
-
/** The same values the TypeScript `WIRE_COMPATIBILITY` export carries. */
|
|
48
|
-
readonly wireCompatibility: WireCompatibility;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Builds the manifest object from the live TypeScript constants. Nesting the
|
|
53
|
-
* protocol values under `wireCompatibility` keeps file-level metadata
|
|
54
|
-
* (`schemaVersion`) in a separate namespace from the payload, so a future
|
|
55
|
-
* metadata field can never collide with a protocol field name.
|
|
56
|
-
*/
|
|
57
|
-
export function buildWireManifest(): WireManifest {
|
|
58
|
-
return {
|
|
59
|
-
schemaVersion: WIRE_MANIFEST_SCHEMA_VERSION,
|
|
60
|
-
generatedBy: "web/src/wire-manifest.ts",
|
|
61
|
-
wireCompatibility: {
|
|
62
|
-
protocolVersion: WIRE_COMPATIBILITY.protocolVersion,
|
|
63
|
-
minimumServerProtocolVersion: WIRE_COMPATIBILITY.minimumServerProtocolVersion,
|
|
64
|
-
incompatibleCloseCode: WIRE_COMPATIBILITY.incompatibleCloseCode,
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Renders the exact bytes of the checked-in manifest: two-space indentation and
|
|
71
|
-
* a trailing newline, which is what `prettier --check` requires of a JSON file
|
|
72
|
-
* in this repo. Comparing rendered text (not parsed objects) is what makes the
|
|
73
|
-
* drift guard able to fail on a hand edit that happens to parse the same.
|
|
74
|
-
*/
|
|
75
|
-
export function renderWireManifest(): string {
|
|
76
|
-
return `${JSON.stringify(buildWireManifest(), null, 2)}\n`;
|
|
77
|
-
}
|