@hex1b/web-terminal 0.173.0-alpha.1801.1.3dc2d9b → 0.173.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/README.md +37 -1
- package/dist/index.js +80 -25
- package/dist/index.js.map +2 -2
- package/dist/transport-session.d.ts +1 -0
- package/dist/transport-session.d.ts.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/web-terminal.d.ts.map +1 -1
- package/dist/wire-types.d.ts +1 -0
- package/dist/wire-types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,6 +169,7 @@ const container = document.getElementById("terminal");
|
|
|
169
169
|
if (!container) throw new Error("Missing terminal container");
|
|
170
170
|
const terminal = await WebTerminal.mount(container, {
|
|
171
171
|
url: "/ws/terminal",
|
|
172
|
+
preserveOnDisconnect: true,
|
|
172
173
|
colorMode: "system", // "light", "dark", or the browser's prefers-color-scheme.
|
|
173
174
|
lightModePalette: { ...defaultLightPalette, background: "#fafafa" },
|
|
174
175
|
darkModePalette: { ...defaultDarkPalette, background: "#202020" },
|
|
@@ -297,7 +298,41 @@ view after transport loss or leave an ended tab/dialog visible. Abort, explicit
|
|
|
297
298
|
disposal, mount timeout, and local initialization/renderer failures do not
|
|
298
299
|
synthesize `onClose`; no callback runs after disposal. Callback exceptions are
|
|
299
300
|
reported to the host, not swallowed or retried, and do not leave mounting pending.
|
|
300
|
-
|
|
301
|
+
Set `preserveOnDisconnect: true` to finish presenting the last accepted complete
|
|
302
|
+
frame before `onClose` and keep it visible until `dispose()`. The retained view
|
|
303
|
+
can repaint when its container or palette changes, but input and producer-backed
|
|
304
|
+
history, selection, and copying are disconnected. Cursor/text blinking stops.
|
|
305
|
+
The default is `false`, preserving the existing renderer-release behavior.
|
|
306
|
+
Explicit disposal or abort still removes the view and releases its GPU resources.
|
|
307
|
+
Local rendering failures cannot guarantee preservation.
|
|
308
|
+
|
|
309
|
+
Preservation retains the received KGP and Sixel textures and placements, including
|
|
310
|
+
PNG/compressed KGP, cropped and relative placements, Unicode placeholders, and
|
|
311
|
+
the current server-driven animation frame. Animation freezes rather than replaying
|
|
312
|
+
locally. Styled/wide/combining text, DEC enlarged rows, the current alternate
|
|
313
|
+
screen, link decorations already presented, selection highlights, viewport text,
|
|
314
|
+
markers, and activity metadata remain available for display and local redraw.
|
|
315
|
+
Link activation and producer-backed navigation/copy operations stop. Only the
|
|
316
|
+
current received history viewport is retained, not a browsable offline scrollback
|
|
317
|
+
archive; historical HWT1 viewports do not currently include graphics.
|
|
318
|
+
|
|
319
|
+
Preservation alone cannot recover bytes the server never sent. On the server,
|
|
320
|
+
await `Hex1bTerminal.RunAsync()` to finish consuming process output, then await
|
|
321
|
+
`Hwt1PresentationAdapter.DrainAsync(cancellationToken)` **while the frame sender
|
|
322
|
+
and client-message receiver remain running**. Only after the drain completes
|
|
323
|
+
should the host cancel those loops, close the transport, and dispose the
|
|
324
|
+
producer. The drain requests a fresh snapshot and waits for the final
|
|
325
|
+
presentation acknowledgement, including all marker inventory pages; its total
|
|
326
|
+
wait is bounded by `AcknowledgementTimeout`, and cancellation or timeout reports
|
|
327
|
+
failure rather than claiming delivery. An HMP1 replica must also finish consuming
|
|
328
|
+
its ordered output before its HWT1 drain. The demo does this for direct and relay
|
|
329
|
+
views on normal workload exit.
|
|
330
|
+
|
|
331
|
+
HWT1 transfers coalesced terminal state, not every intermediate output event.
|
|
332
|
+
The guarantee is the final retained terminal view, not an unlimited output log.
|
|
333
|
+
Abrupt network loss can retain only the last complete frame actually received;
|
|
334
|
+
it cannot promise the producer's final state. The API does not retain the producer
|
|
335
|
+
after exit.
|
|
301
336
|
|
|
302
337
|
### Browser and deployment requirements
|
|
303
338
|
|
|
@@ -425,6 +460,7 @@ does not claim universal or individually verified bundler support.
|
|
|
425
460
|
| `label` | Accessible label for the terminal's hidden keyboard input. |
|
|
426
461
|
| `onTitleChange` | Initial authoritative workload title, then distinct presented changes; see below. |
|
|
427
462
|
| `onClose` | Native WebSocket close details, including pre-mount transport failure; not workload completion. |
|
|
463
|
+
| `preserveOnDisconnect` | Opt-in final-frame presentation and retained display after transport close, until disposal; defaults to `false`. |
|
|
428
464
|
| `onProgressChange`, `onShellIntegrationChange` | Initial authoritative activity, then distinct presented changes for host-owned chrome. |
|
|
429
465
|
| `onWorkingDirectoryChange`, `onCommandMarkChange` | Initial authoritative OSC 7 directory and latest OSC 133 marker, then distinct presented changes; see below. |
|
|
430
466
|
| `inputBindings`, `onInput`, `actions` | Per-view input policy and custom actions. |
|
package/dist/index.js
CHANGED
|
@@ -767,9 +767,14 @@ var init_transport_session = __esm({
|
|
|
767
767
|
#ready = Promise.withResolvers();
|
|
768
768
|
#connection;
|
|
769
769
|
#frame = false;
|
|
770
|
+
#receiving;
|
|
771
|
+
#closing = false;
|
|
770
772
|
#sending = false;
|
|
771
773
|
#queue = [];
|
|
772
774
|
#bytes = 0;
|
|
775
|
+
get closing() {
|
|
776
|
+
return this.#closing;
|
|
777
|
+
}
|
|
773
778
|
start(transport2) {
|
|
774
779
|
void this.#connect(transport2);
|
|
775
780
|
}
|
|
@@ -777,16 +782,20 @@ var init_transport_session = __esm({
|
|
|
777
782
|
try {
|
|
778
783
|
const connection = await transport2.connect({
|
|
779
784
|
signal: this.#abort.signal,
|
|
780
|
-
onFrame: (frame) =>
|
|
785
|
+
onFrame: (frame) => {
|
|
786
|
+
const receiving = this.#receive(frame);
|
|
787
|
+
this.#receiving = receiving;
|
|
788
|
+
return receiving;
|
|
789
|
+
},
|
|
781
790
|
onClose: (details) => {
|
|
782
|
-
if (this.#abort.signal.aborted) return;
|
|
791
|
+
if (this.#abort.signal.aborted || this.#closing) return;
|
|
783
792
|
if (!details || typeof details.reason !== "string" || details.code !== void 0 && (!Number.isInteger(details.code) || typeof details.wasClean !== "boolean")) {
|
|
784
793
|
this.#fail(new TypeError("Invalid terminal transport close details"));
|
|
785
794
|
return;
|
|
786
795
|
}
|
|
787
796
|
const snapshot = details.code === void 0 ? { reason: details.reason } : { code: details.code, reason: details.reason, wasClean: details.wasClean };
|
|
788
|
-
this
|
|
789
|
-
this
|
|
797
|
+
this.#closing = true;
|
|
798
|
+
void this.#close(snapshot);
|
|
790
799
|
},
|
|
791
800
|
onError: (error) => this.#fail(error)
|
|
792
801
|
});
|
|
@@ -806,6 +815,7 @@ var init_transport_session = __esm({
|
|
|
806
815
|
async #receive(frame) {
|
|
807
816
|
try {
|
|
808
817
|
this.#abort.signal.throwIfAborted();
|
|
818
|
+
if (this.#closing) throw new Error("Terminal transport is closing");
|
|
809
819
|
if (this.#frame) throw new Error("Concurrent terminal transport frames are not allowed");
|
|
810
820
|
if (!(frame instanceof ArrayBuffer) && !(frame instanceof Uint8Array && frame.buffer instanceof ArrayBuffer))
|
|
811
821
|
throw new TypeError("Transport frames must be ArrayBuffer or Uint8Array backed by ArrayBuffer");
|
|
@@ -823,8 +833,19 @@ var init_transport_session = __esm({
|
|
|
823
833
|
throw error;
|
|
824
834
|
}
|
|
825
835
|
}
|
|
836
|
+
async #close(details) {
|
|
837
|
+
try {
|
|
838
|
+
await this.#receiving;
|
|
839
|
+
} catch {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (this.#abort.signal.aborted) return;
|
|
843
|
+
this.dispose();
|
|
844
|
+
this.callbacks.onClose(details);
|
|
845
|
+
}
|
|
826
846
|
send(control) {
|
|
827
847
|
if (this.#abort.signal.aborted) return Promise.reject(this.#abort.signal.reason);
|
|
848
|
+
if (this.#closing) return Promise.reject(new DOMException("Terminal transport is closing", "AbortError"));
|
|
828
849
|
const bytes = new TextEncoder().encode(control).byteLength;
|
|
829
850
|
if (this.#queue.length >= 256 || this.#bytes + bytes > 1024 * 1024) {
|
|
830
851
|
const error = new Error("Terminal transport control queue exceeded its bounded capacity");
|
|
@@ -842,7 +863,7 @@ var init_transport_session = __esm({
|
|
|
842
863
|
this.#sending = true;
|
|
843
864
|
try {
|
|
844
865
|
await this.#ready.promise;
|
|
845
|
-
while (!this.#abort.signal.aborted && this.#queue.length) {
|
|
866
|
+
while (!this.#abort.signal.aborted && !this.#closing && this.#queue.length) {
|
|
846
867
|
const item = this.#queue[0];
|
|
847
868
|
await this.#connection.send(item.control);
|
|
848
869
|
if (this.#abort.signal.aborted) return;
|
|
@@ -851,7 +872,7 @@ var init_transport_session = __esm({
|
|
|
851
872
|
item.resolve();
|
|
852
873
|
}
|
|
853
874
|
} catch (error) {
|
|
854
|
-
this.#fail(error);
|
|
875
|
+
if (!this.#closing) this.#fail(error);
|
|
855
876
|
} finally {
|
|
856
877
|
this.#sending = false;
|
|
857
878
|
}
|
|
@@ -2503,10 +2524,21 @@ function postStatus(message, level = "info") {
|
|
|
2503
2524
|
self.postMessage({ type: "status", message, level });
|
|
2504
2525
|
}
|
|
2505
2526
|
function send(message) {
|
|
2506
|
-
if (stats.connected && !failed && !stopped)
|
|
2527
|
+
if (stats.connected && !disconnected && !transport?.closing && !failed && !stopped) {
|
|
2528
|
+
void transport?.send(JSON.stringify(message)).catch((error) => {
|
|
2529
|
+
if (!transport?.closing) fail(error);
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2507
2532
|
}
|
|
2508
2533
|
function emitStats(text) {
|
|
2509
2534
|
if (renderer && !renderer.disposed) Object.assign(stats, renderer.metrics());
|
|
2535
|
+
else if (renderer?.disposed) Object.assign(stats, {
|
|
2536
|
+
imageCount: 0,
|
|
2537
|
+
textureBytes: 0,
|
|
2538
|
+
atlasGlyphs: 0,
|
|
2539
|
+
atlasBytes: 0,
|
|
2540
|
+
instanceBufferBytes: 0
|
|
2541
|
+
});
|
|
2510
2542
|
self.postMessage({ type: "stats", stats: { ...stats }, ...text === void 0 ? {} : { text } });
|
|
2511
2543
|
}
|
|
2512
2544
|
function fail(error) {
|
|
@@ -2524,11 +2556,11 @@ function fail(error) {
|
|
|
2524
2556
|
}
|
|
2525
2557
|
function scheduleRender() {
|
|
2526
2558
|
needsRender = true;
|
|
2527
|
-
if (scheduled || drawing || processing || failed || stopped || !metadata || markerPages.pending || awaitingFull) return;
|
|
2559
|
+
if (scheduled || drawing || processing || failed || stopped || disconnected && stats.connected || !metadata || markerPages.pending || awaitingFull) return;
|
|
2528
2560
|
scheduled = true;
|
|
2529
2561
|
self.requestAnimationFrame(() => {
|
|
2530
2562
|
scheduled = false;
|
|
2531
|
-
if (processing || drawing || failed || stopped || markerPages.pending || awaitingFull) return;
|
|
2563
|
+
if (processing || drawing || failed || stopped || disconnected && stats.connected || markerPages.pending || awaitingFull) return;
|
|
2532
2564
|
renderPromise = drawFrame();
|
|
2533
2565
|
});
|
|
2534
2566
|
}
|
|
@@ -2541,7 +2573,7 @@ async function drawFrame() {
|
|
|
2541
2573
|
if (frame) links.prepare(cells, metadata);
|
|
2542
2574
|
const linkSubmission = links.submission();
|
|
2543
2575
|
renderer.resize(metadata.columns, metadata.rows, viewport);
|
|
2544
|
-
const blink = Math.floor(performance.now() / 600) % 2 === 0;
|
|
2576
|
+
const blink = disconnected || Math.floor(performance.now() / 600) % 2 === 0;
|
|
2545
2577
|
const result = renderer.render(cells, metadata, blink, linkSubmission.mask, palette);
|
|
2546
2578
|
await renderer.idle();
|
|
2547
2579
|
if (failed || stopped) return;
|
|
@@ -2605,7 +2637,7 @@ async function drawFrame() {
|
|
|
2605
2637
|
}
|
|
2606
2638
|
}
|
|
2607
2639
|
async function receiveFrame(buffer) {
|
|
2608
|
-
if (failed || stopped || !renderer) return;
|
|
2640
|
+
if (failed || stopped || disconnected || !renderer) return;
|
|
2609
2641
|
if (frameInFlight) throw new Error("Server sent a second state frame before acknowledgement");
|
|
2610
2642
|
frameInFlight = true;
|
|
2611
2643
|
processing = true;
|
|
@@ -2666,6 +2698,7 @@ async function receiveFrame(buffer) {
|
|
|
2666
2698
|
}
|
|
2667
2699
|
async function initialize(message) {
|
|
2668
2700
|
if (renderer || transport) throw new Error("Worker is already initialized");
|
|
2701
|
+
preserveOnDisconnect = message.preserveOnDisconnect ?? false;
|
|
2669
2702
|
palette = compilePalette(normalizePalette(message.palette === void 0 ? defaultDarkPalette : message.palette));
|
|
2670
2703
|
if (typeof self.requestAnimationFrame !== "function") {
|
|
2671
2704
|
throw new Error("This browser does not support requestAnimationFrame in a dedicated OffscreenCanvas worker");
|
|
@@ -2693,17 +2726,31 @@ async function initialize(message) {
|
|
|
2693
2726
|
onFrame: receiveFrame,
|
|
2694
2727
|
onError: fail,
|
|
2695
2728
|
onClose(details) {
|
|
2696
|
-
if (failed || stopped) return;
|
|
2697
|
-
|
|
2698
|
-
stats.gpu = "stopped";
|
|
2699
|
-
stats.fps = 0;
|
|
2700
|
-
stopped = true;
|
|
2729
|
+
if (failed || stopped || disconnected) return;
|
|
2730
|
+
disconnected = true;
|
|
2701
2731
|
clearInterval(metricsTimer);
|
|
2702
2732
|
clearInterval(blinkTimer);
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2733
|
+
void finishClose().catch(fail);
|
|
2734
|
+
async function finishClose() {
|
|
2735
|
+
if (preserveOnDisconnect) {
|
|
2736
|
+
await renderPromise;
|
|
2737
|
+
if (failed || stopped) return;
|
|
2738
|
+
if ((needsRender || hasBlink && !lastBlink) && !markerPages.pending && !awaitingFull) {
|
|
2739
|
+
needsRender = true;
|
|
2740
|
+
await drawFrame();
|
|
2741
|
+
}
|
|
2742
|
+
if (failed || stopped) return;
|
|
2743
|
+
} else {
|
|
2744
|
+
stopped = true;
|
|
2745
|
+
renderer?.dispose();
|
|
2746
|
+
}
|
|
2747
|
+
stats.connected = false;
|
|
2748
|
+
stats.gpu = "stopped";
|
|
2749
|
+
stats.fps = 0;
|
|
2750
|
+
self.postMessage({ type: "closed", details });
|
|
2751
|
+
postStatus(`View disconnected (${details.code === void 0 ? details.reason : `${details.code}${details.reason ? `: ${details.reason}` : ""}`}). Attach another view to reconnect.`, "error");
|
|
2752
|
+
emitStats();
|
|
2753
|
+
}
|
|
2707
2754
|
}
|
|
2708
2755
|
});
|
|
2709
2756
|
metricsTimer = setInterval(() => {
|
|
@@ -2732,7 +2779,7 @@ async function initialize(message) {
|
|
|
2732
2779
|
}
|
|
2733
2780
|
});
|
|
2734
2781
|
}
|
|
2735
|
-
var renderer, transport, bridge, bridgeReady, bridgeSent, failed, stopped, processing, drawing, frameInFlight, scheduled, needsRender, hasBlink, lastBlink, metadata, cells, localRevision, pendingFrame, renderPromise, metricsTimer, blinkTimer, viewport, links, markerPages, awaitingFull, requestedColorEncoding, palette, stats, sample;
|
|
2782
|
+
var renderer, transport, bridge, bridgeReady, bridgeSent, failed, stopped, disconnected, preserveOnDisconnect, processing, drawing, frameInFlight, scheduled, needsRender, hasBlink, lastBlink, metadata, cells, localRevision, pendingFrame, renderPromise, metricsTimer, blinkTimer, viewport, links, markerPages, awaitingFull, requestedColorEncoding, palette, stats, sample;
|
|
2736
2783
|
var init_terminal_worker = __esm({
|
|
2737
2784
|
"src/terminal-worker.ts"() {
|
|
2738
2785
|
"use strict";
|
|
@@ -2746,6 +2793,8 @@ var init_terminal_worker = __esm({
|
|
|
2746
2793
|
init_transport_session();
|
|
2747
2794
|
failed = false;
|
|
2748
2795
|
stopped = false;
|
|
2796
|
+
disconnected = false;
|
|
2797
|
+
preserveOnDisconnect = false;
|
|
2749
2798
|
processing = false;
|
|
2750
2799
|
drawing = false;
|
|
2751
2800
|
frameInFlight = false;
|
|
@@ -2847,7 +2896,7 @@ var init_terminal_worker = __esm({
|
|
|
2847
2896
|
} catch (error) {
|
|
2848
2897
|
fail(error);
|
|
2849
2898
|
}
|
|
2850
|
-
} else if (message.type === "linkDetection" && !failed && !stopped) {
|
|
2899
|
+
} else if (message.type === "linkDetection" && !failed && !stopped && !disconnected) {
|
|
2851
2900
|
try {
|
|
2852
2901
|
if (!links.configure(message.enabled, message.generation)) return;
|
|
2853
2902
|
if (!processing && !drawing && !pendingFrame) {
|
|
@@ -2858,7 +2907,7 @@ var init_terminal_worker = __esm({
|
|
|
2858
2907
|
} catch (error) {
|
|
2859
2908
|
fail(error);
|
|
2860
2909
|
}
|
|
2861
|
-
} else if (message.type === "linkDecorations" && !failed && !stopped) {
|
|
2910
|
+
} else if (message.type === "linkDecorations" && !failed && !stopped && !disconnected) {
|
|
2862
2911
|
try {
|
|
2863
2912
|
if (links.accept(
|
|
2864
2913
|
message.revision,
|
|
@@ -5496,6 +5545,7 @@ var WebTerminal = class _WebTerminal {
|
|
|
5496
5545
|
#connected = false;
|
|
5497
5546
|
#closed = false;
|
|
5498
5547
|
#readOnly;
|
|
5548
|
+
#preserveOnDisconnect;
|
|
5499
5549
|
#disposed = false;
|
|
5500
5550
|
#hasGeometry = false;
|
|
5501
5551
|
#resizeTimer;
|
|
@@ -5575,6 +5625,9 @@ var WebTerminal = class _WebTerminal {
|
|
|
5575
5625
|
if (options.readOnly !== void 0 && typeof options.readOnly !== "boolean")
|
|
5576
5626
|
throw new TypeError("readOnly must be a boolean");
|
|
5577
5627
|
this.#readOnly = options.readOnly ?? false;
|
|
5628
|
+
if (options.preserveOnDisconnect !== void 0 && typeof options.preserveOnDisconnect !== "boolean")
|
|
5629
|
+
throw new TypeError("preserveOnDisconnect must be a boolean");
|
|
5630
|
+
this.#preserveOnDisconnect = options.preserveOnDisconnect ?? false;
|
|
5578
5631
|
this.#renderer = normalizeRenderer(options.renderer);
|
|
5579
5632
|
this.#colorMode = normalizeColorMode(options.colorMode);
|
|
5580
5633
|
this.#palettes = {
|
|
@@ -5889,7 +5942,8 @@ var WebTerminal = class _WebTerminal {
|
|
|
5889
5942
|
scale,
|
|
5890
5943
|
font,
|
|
5891
5944
|
renderer: this.#renderer,
|
|
5892
|
-
palette: this.#palettes[this.resolvedColorMode]
|
|
5945
|
+
palette: this.#palettes[this.resolvedColorMode],
|
|
5946
|
+
preserveOnDisconnect: this.#preserveOnDisconnect
|
|
5893
5947
|
}, [canvas]);
|
|
5894
5948
|
this.#applyPalette();
|
|
5895
5949
|
this.#postLinkConfiguration();
|
|
@@ -5914,7 +5968,8 @@ var WebTerminal = class _WebTerminal {
|
|
|
5914
5968
|
this.#transportSession.start(this.#transport);
|
|
5915
5969
|
} else if (message.type === "transportSend") {
|
|
5916
5970
|
void Promise.resolve(this.#transportFrame?.promise).then(() => this.#transportSession?.send(message.control)).then(() => this.#post({ type: "transportSent" })).catch((error) => {
|
|
5917
|
-
if (!this.#disposed
|
|
5971
|
+
if (!this.#disposed && !this.#transportSession?.closing)
|
|
5972
|
+
this.#post({ type: "transportError", message: errorMessage(error) });
|
|
5918
5973
|
});
|
|
5919
5974
|
} else if (message.type === "transportReceived") {
|
|
5920
5975
|
this.#transportFrame?.resolve();
|