@cplieger/web-terminal-engine 2.3.4 → 2.5.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/jsr.json +1 -1
- package/package.json +4 -1
- package/src/connection.ts +30 -1
- package/src/render.ts +7 -3
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/web-terminal-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc -p tsconfig.json",
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@eslint/js": "10.0.1",
|
|
13
13
|
"@playwright/test": "1.61.1",
|
|
14
|
+
"@stryker-mutator/core": "9.6.1",
|
|
15
|
+
"@stryker-mutator/typescript-checker": "9.6.1",
|
|
16
|
+
"@stryker-mutator/vitest-runner": "9.6.1",
|
|
14
17
|
"@types/pngjs": "6.0.5",
|
|
15
18
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
16
19
|
"@vitest/coverage-v8": "4.1.10",
|
package/src/connection.ts
CHANGED
|
@@ -34,6 +34,13 @@ type ConnState =
|
|
|
34
34
|
| { status: "connected"; sock: WebSocket; abort: AbortController }
|
|
35
35
|
| { status: "reconnecting"; timer: ReturnType<typeof setTimeout>; delayMs: number };
|
|
36
36
|
|
|
37
|
+
// The server's application close code for "the session's child process has
|
|
38
|
+
// exited" (terminal/terminal.go statusProcessExited). It marks a close as
|
|
39
|
+
// definitive — the session cannot produce output again — as opposed to a
|
|
40
|
+
// transient network drop that the backoff reconnect should heal. Private
|
|
41
|
+
// application range (4000-4999) per RFC 6455.
|
|
42
|
+
const PROCESS_EXITED_CLOSE_CODE = 4001;
|
|
43
|
+
|
|
37
44
|
let connState: ConnState = { status: "disconnected" };
|
|
38
45
|
let reconnectDelay = INITIAL_DELAY_MS;
|
|
39
46
|
let lastSentCols = 0;
|
|
@@ -179,6 +186,18 @@ export interface Callbacks {
|
|
|
179
186
|
onClose(): void;
|
|
180
187
|
onConnecting?(): void;
|
|
181
188
|
onOutboxFull?(): void;
|
|
189
|
+
/** Fired instead of onClose when the server closes with the
|
|
190
|
+
* process-exited application close code (4001): the session's child
|
|
191
|
+
* process has ended, so the close is DEFINITIVE, not a transient drop.
|
|
192
|
+
* When this callback is wired, the module does NOT auto-reconnect that
|
|
193
|
+
* socket — reconnecting a dead session just replays its final screen and
|
|
194
|
+
* earns another 4001 (the endless "Reconnecting…" flash) — and does not
|
|
195
|
+
* call onClose for it. The consumer decides what "ended" looks like
|
|
196
|
+
* (banner, tab state) and may still reconnect explicitly (setSession /
|
|
197
|
+
* reconnectNow) to re-view the final screen. When absent, every close
|
|
198
|
+
* keeps the legacy transient treatment (onClose + backoff reconnect), so
|
|
199
|
+
* existing consumers are unaffected until they opt in. */
|
|
200
|
+
onProcessExit?(): void;
|
|
182
201
|
/** Fired when the server's boot-epoch in resumeAck differs from the
|
|
183
202
|
* one observed on a previous connection — i.e. the server has
|
|
184
203
|
* restarted. By the time this fires, the connection module has
|
|
@@ -696,7 +715,7 @@ export function connect(): void {
|
|
|
696
715
|
|
|
697
716
|
sock.addEventListener(
|
|
698
717
|
"close",
|
|
699
|
-
() => {
|
|
718
|
+
(ev: CloseEvent) => {
|
|
700
719
|
// Only the active sock's close should drive reconnect logic; an
|
|
701
720
|
// already-superseded sock has been aborted and this listener
|
|
702
721
|
// wouldn't fire (signal removes it). The check stays as a belt-
|
|
@@ -709,6 +728,16 @@ export function connect(): void {
|
|
|
709
728
|
}
|
|
710
729
|
stopHeartbeat();
|
|
711
730
|
connState = { status: "disconnected" };
|
|
731
|
+
// A process-exited close (4001) is definitive: the child is gone, so a
|
|
732
|
+
// backoff reconnect can only replay the final screen and collect another
|
|
733
|
+
// 4001 — an endless, pointless churn that reads as a flapping
|
|
734
|
+
// "Reconnecting…" banner. Route it to onProcessExit (no reconnect, no
|
|
735
|
+
// onClose) when the consumer wired it; without the callback, keep the
|
|
736
|
+
// legacy transient treatment so existing consumers see no change.
|
|
737
|
+
if (ev.code === PROCESS_EXITED_CLOSE_CODE && cb?.onProcessExit) {
|
|
738
|
+
cb.onProcessExit();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
712
741
|
cb?.onClose();
|
|
713
742
|
scheduleReconnect();
|
|
714
743
|
},
|
package/src/render.ts
CHANGED
|
@@ -590,9 +590,13 @@ function buildRowSpans(runs: WireRun[], cursorAt: number): (HTMLSpanElement | HT
|
|
|
590
590
|
a.href = href;
|
|
591
591
|
a.target = "_blank";
|
|
592
592
|
a.rel = "noopener noreferrer";
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
|
|
593
|
+
// Server-stamped autolink (attr bit 1024, vt.AttrAutolink): a bare URL
|
|
594
|
+
// the server detected — joined across soft-wrap continuations, so every
|
|
595
|
+
// row segment carries the FULL href. Styled like the client's own
|
|
596
|
+
// autolinks (persistent underline; the anchor hugs exactly the URL
|
|
597
|
+
// text). An app-provided OSC 8 hyperlink keeps the hover-only base
|
|
598
|
+
// `.term-link` (it may span decorative cells).
|
|
599
|
+
a.className = (attrs & 1024) !== 0 ? "term-link term-autolink" : "term-link";
|
|
596
600
|
for (const s of runSpans) {
|
|
597
601
|
a.appendChild(s);
|
|
598
602
|
}
|