@hex1b/web-terminal 0.172.0 → 0.173.0-alpha.1824.1.98d8766

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 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
- The API does not retain the producer after exit or promise a final rendered frame.
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
@@ -557,6 +557,11 @@ function validateMetadata(metadata2) {
557
557
  if (commandMark !== null) validateCommandMark(commandMark);
558
558
  const columns = integer(metadata2.columns, "columns", 1, 1024);
559
559
  const rows = integer(metadata2.rows, "rows", 1, 512);
560
+ if (metadata2.lineRenditions !== void 0) {
561
+ array(metadata2.lineRenditions, "line renditions", rows);
562
+ if (metadata2.lineRenditions.length !== rows) throw new Error("Invalid line rendition count");
563
+ for (const rendition of metadata2.lineRenditions) integer(rendition, "line rendition", 0, 3);
564
+ }
560
565
  if (typeof metadata2.mouseTracking !== "number" || ![0, 9, 1e3, 1002, 1003].includes(metadata2.mouseTracking)) {
561
566
  throw new Error("Unsupported mouse tracking mode");
562
567
  }
@@ -762,9 +767,14 @@ var init_transport_session = __esm({
762
767
  #ready = Promise.withResolvers();
763
768
  #connection;
764
769
  #frame = false;
770
+ #receiving;
771
+ #closing = false;
765
772
  #sending = false;
766
773
  #queue = [];
767
774
  #bytes = 0;
775
+ get closing() {
776
+ return this.#closing;
777
+ }
768
778
  start(transport2) {
769
779
  void this.#connect(transport2);
770
780
  }
@@ -772,16 +782,20 @@ var init_transport_session = __esm({
772
782
  try {
773
783
  const connection = await transport2.connect({
774
784
  signal: this.#abort.signal,
775
- onFrame: (frame) => this.#receive(frame),
785
+ onFrame: (frame) => {
786
+ const receiving = this.#receive(frame);
787
+ this.#receiving = receiving;
788
+ return receiving;
789
+ },
776
790
  onClose: (details) => {
777
- if (this.#abort.signal.aborted) return;
791
+ if (this.#abort.signal.aborted || this.#closing) return;
778
792
  if (!details || typeof details.reason !== "string" || details.code !== void 0 && (!Number.isInteger(details.code) || typeof details.wasClean !== "boolean")) {
779
793
  this.#fail(new TypeError("Invalid terminal transport close details"));
780
794
  return;
781
795
  }
782
796
  const snapshot = details.code === void 0 ? { reason: details.reason } : { code: details.code, reason: details.reason, wasClean: details.wasClean };
783
- this.dispose();
784
- this.callbacks.onClose(snapshot);
797
+ this.#closing = true;
798
+ void this.#close(snapshot);
785
799
  },
786
800
  onError: (error) => this.#fail(error)
787
801
  });
@@ -801,6 +815,7 @@ var init_transport_session = __esm({
801
815
  async #receive(frame) {
802
816
  try {
803
817
  this.#abort.signal.throwIfAborted();
818
+ if (this.#closing) throw new Error("Terminal transport is closing");
804
819
  if (this.#frame) throw new Error("Concurrent terminal transport frames are not allowed");
805
820
  if (!(frame instanceof ArrayBuffer) && !(frame instanceof Uint8Array && frame.buffer instanceof ArrayBuffer))
806
821
  throw new TypeError("Transport frames must be ArrayBuffer or Uint8Array backed by ArrayBuffer");
@@ -818,8 +833,19 @@ var init_transport_session = __esm({
818
833
  throw error;
819
834
  }
820
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
+ }
821
846
  send(control) {
822
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"));
823
849
  const bytes = new TextEncoder().encode(control).byteLength;
824
850
  if (this.#queue.length >= 256 || this.#bytes + bytes > 1024 * 1024) {
825
851
  const error = new Error("Terminal transport control queue exceeded its bounded capacity");
@@ -837,7 +863,7 @@ var init_transport_session = __esm({
837
863
  this.#sending = true;
838
864
  try {
839
865
  await this.#ready.promise;
840
- while (!this.#abort.signal.aborted && this.#queue.length) {
866
+ while (!this.#abort.signal.aborted && !this.#closing && this.#queue.length) {
841
867
  const item = this.#queue[0];
842
868
  await this.#connection.send(item.control);
843
869
  if (this.#abort.signal.aborted) return;
@@ -846,7 +872,7 @@ var init_transport_session = __esm({
846
872
  item.resolve();
847
873
  }
848
874
  } catch (error) {
849
- this.#fail(error);
875
+ if (!this.#closing) this.#fail(error);
850
876
  } finally {
851
877
  this.#sending = false;
852
878
  }
@@ -2154,32 +2180,45 @@ var init_renderer = __esm({
2154
2180
  [left, top, Math.max(0, right - left), Math.max(0, bottom - top)]
2155
2181
  );
2156
2182
  }
2157
- decorations(cell, x, y, width, foreground, underline = cell.underlineColor, clip) {
2158
- if (cell.attributes & 128) this.solid(x, y + 10, width, 1, foreground, clip);
2159
- if (cell.attributes & 256) this.solid(x, y + 1, width, 1, foreground, clip);
2183
+ decorations(cell, x, y, width, foreground, underline = cell.underlineColor, clip, scaleY = 1) {
2184
+ if (cell.attributes & 128) this.solid(x, y + 10 * scaleY, width, scaleY, foreground, clip);
2185
+ if (cell.attributes & 256) this.solid(x, y + scaleY, width, scaleY, foreground, clip);
2160
2186
  const style = cell.underlineStyle || (cell.attributes & 8 ? 1 : 0);
2161
- this.underline(style, x, y, width, rgba(underline), clip);
2187
+ this.underline(style, x, y, width, rgba(underline), clip, scaleY);
2162
2188
  }
2163
- underline(style, x, y, width, color2, clip) {
2164
- if (style === 1) this.solid(x, y + 18, width, 1, color2, clip);
2189
+ underline(style, x, y, width, color2, clip, scaleY = 1) {
2190
+ if (style === 1) this.solid(x, y + 18 * scaleY, width, scaleY, color2, clip);
2165
2191
  else if (style === 2) {
2166
- this.solid(x, y + 16, width, 1, color2, clip);
2167
- this.solid(x, y + 18, width, 1, color2, clip);
2192
+ this.solid(x, y + 16 * scaleY, width, scaleY, color2, clip);
2193
+ this.solid(x, y + 18 * scaleY, width, scaleY, color2, clip);
2168
2194
  } else if (style === 3) {
2169
2195
  for (let dx = 0; dx < width; dx++) {
2170
- this.solid(x + dx, y + 17 + Math.round(Math.sin((x + dx) * Math.PI / 4)), 1, 1, color2, clip);
2196
+ this.solid(x + dx, y + (17 + Math.round(Math.sin((x + dx) * Math.PI / 4))) * scaleY, 1, scaleY, color2, clip);
2171
2197
  }
2172
2198
  } else if (style === 4 || style === 5) {
2173
2199
  const step = style === 4 ? 2 : 5;
2174
2200
  const segment = style === 4 ? 1 : 3;
2175
- for (let dx = 0; dx < width; dx += step) this.solid(x + dx, y + 18, Math.min(segment, width - dx), 1, color2, clip);
2201
+ for (let dx = 0; dx < width; dx += step) this.solid(x + dx, y + 18 * scaleY, Math.min(segment, width - dx), scaleY, color2, clip);
2176
2202
  }
2177
2203
  }
2178
2204
  renderCell(cell, index, metadata2, blinkOn, palette2, linkDecoration, selectionClip) {
2179
2205
  if (!cell.width || cell.attributes & 64 || cell.attributes & 16 && !blinkOn) return;
2180
- const x = index % this.columns * CELL_WIDTH;
2181
- const y = Math.floor(index / this.columns) * CELL_HEIGHT;
2182
- const width = Math.min(cell.width * CELL_WIDTH, this.width - x);
2206
+ const row = Math.floor(index / this.columns);
2207
+ const rendition = metadata2.lineRenditions?.[row] ?? 0;
2208
+ const scaleX = rendition ? 2 : 1;
2209
+ const scaleY = rendition >= 2 ? 2 : 1;
2210
+ const column = index % this.columns;
2211
+ if (column >= Math.max(1, Math.floor(this.columns / scaleX))) return;
2212
+ const x = column * CELL_WIDTH * scaleX;
2213
+ const rowY = row * CELL_HEIGHT;
2214
+ const y = rowY - (rendition === 3 ? CELL_HEIGHT : 0);
2215
+ const width = Math.min(cell.width * CELL_WIDTH * scaleX, this.width - x);
2216
+ const clip = !rendition ? selectionClip : selectionClip ? [
2217
+ selectionClip[0],
2218
+ Math.max(rowY, selectionClip[1]),
2219
+ selectionClip[2],
2220
+ Math.min(rowY + CELL_HEIGHT, selectionClip[1] + selectionClip[3]) - Math.max(rowY, selectionClip[1])
2221
+ ] : [0, rowY, this.width, CELL_HEIGHT];
2183
2222
  const foreground = rgba(selectionClip ? palette2.selectionForeground : foregroundColor(cell, metadata2, palette2));
2184
2223
  const glyph = isKgpPlaceholder(cell) ? void 0 : this.glyphs.get(glyphKey(cell));
2185
2224
  if (glyph) {
@@ -2188,12 +2227,12 @@ var init_renderer = __esm({
2188
2227
  this.atlas,
2189
2228
  x,
2190
2229
  y,
2191
- cell.width * CELL_WIDTH,
2192
- CELL_HEIGHT,
2230
+ cell.width * CELL_WIDTH * scaleX,
2231
+ CELL_HEIGHT * scaleY,
2193
2232
  tint,
2194
2233
  glyph.colored ? 2 : 1,
2195
2234
  [glyph.u0, glyph.v0, glyph.u1, glyph.v1],
2196
- selectionClip
2235
+ clip
2197
2236
  );
2198
2237
  }
2199
2238
  this.decorations(
@@ -2203,10 +2242,11 @@ var init_renderer = __esm({
2203
2242
  width,
2204
2243
  foreground,
2205
2244
  selectionClip ? palette2.selectionForeground : underlineColor(cell, metadata2, palette2),
2206
- selectionClip
2245
+ clip,
2246
+ scaleY
2207
2247
  );
2208
2248
  if (linkDecoration && !cell.underlineStyle && !(cell.attributes & 8) && !isKgpPlaceholder(cell)) {
2209
- this.underline(linkDecoration, x, y, width, foreground, selectionClip);
2249
+ this.underline(linkDecoration, x, y, width, foreground, clip, scaleY);
2210
2250
  }
2211
2251
  }
2212
2252
  render(cells2, metadata2, blinkOn, linkDecorations, palette2 = DEFAULT_PALETTE) {
@@ -2222,10 +2262,13 @@ var init_renderer = __esm({
2222
2262
  for (let i = 0; i < cells2.length; i++) {
2223
2263
  const cell = cells2[i];
2224
2264
  if (!cell) continue;
2265
+ const row = Math.floor(i / this.columns);
2266
+ const scaleX = metadata2.lineRenditions?.[row] ? 2 : 1;
2267
+ if (i % this.columns >= Math.max(1, Math.floor(this.columns / scaleX))) continue;
2225
2268
  this.solid(
2226
- i % this.columns * CELL_WIDTH,
2227
- Math.floor(i / this.columns) * CELL_HEIGHT,
2228
- CELL_WIDTH,
2269
+ i % this.columns * CELL_WIDTH * scaleX,
2270
+ row * CELL_HEIGHT,
2271
+ CELL_WIDTH * scaleX,
2229
2272
  CELL_HEIGHT,
2230
2273
  rgba(backgroundColor(cell, metadata2, palette2))
2231
2274
  );
@@ -2239,10 +2282,11 @@ var init_renderer = __esm({
2239
2282
  const selection = metadata2.history?.selection;
2240
2283
  if (selection?.status === "valid") {
2241
2284
  for (const range of selection.ranges) {
2285
+ const scaleX = metadata2.lineRenditions?.[range.row] ? 2 : 1;
2242
2286
  const clip = [
2243
- range.startColumn * CELL_WIDTH,
2287
+ range.startColumn * CELL_WIDTH * scaleX,
2244
2288
  range.row * CELL_HEIGHT,
2245
- (range.endColumn - range.startColumn) * CELL_WIDTH,
2289
+ (range.endColumn - range.startColumn) * CELL_WIDTH * scaleX,
2246
2290
  CELL_HEIGHT
2247
2291
  ];
2248
2292
  this.solid(...clip, rgba(palette2.selectionBackground));
@@ -2260,13 +2304,14 @@ var init_renderer = __esm({
2260
2304
  if (cursor.visible && (!cursorBlink || blinkOn) && cursor.x >= 0 && cursor.x < this.columns && cursor.y >= 0 && cursor.y < this.rows) {
2261
2305
  const cell = cells2[cursor.y * this.columns + cursor.x];
2262
2306
  const color2 = rgba(palette2.cursor ?? (cell ? foregroundColor(cell, metadata2, palette2) : palette2.foreground));
2263
- const x = cursor.x * CELL_WIDTH;
2307
+ const scaleX = metadata2.lineRenditions?.[cursor.y] ? 2 : 1;
2308
+ const x = cursor.x * CELL_WIDTH * scaleX;
2264
2309
  const y = cursor.y * CELL_HEIGHT;
2265
- if (cursor.shape === 3 || cursor.shape === 4) this.solid(x, y + 18, CELL_WIDTH, 2, color2);
2310
+ if (cursor.shape === 3 || cursor.shape === 4) this.solid(x, y + 18, CELL_WIDTH * scaleX, 2, color2);
2266
2311
  else if (cursor.shape === 5 || cursor.shape === 6) this.solid(x, y, 2, CELL_HEIGHT, color2);
2267
2312
  else {
2268
2313
  color2[3] *= 0.55;
2269
- this.solid(x, y, CELL_WIDTH, CELL_HEIGHT, color2);
2314
+ this.solid(x, y, CELL_WIDTH * scaleX, CELL_HEIGHT, color2);
2270
2315
  }
2271
2316
  }
2272
2317
  const base = rgba(metadata2.colorEncoding === "indexed-v1" ? palette2.background : metadata2.defaultBackground ?? cells2[0]?.background ?? 4278190080);
@@ -2479,10 +2524,21 @@ function postStatus(message, level = "info") {
2479
2524
  self.postMessage({ type: "status", message, level });
2480
2525
  }
2481
2526
  function send(message) {
2482
- if (stats.connected && !failed && !stopped) void transport?.send(JSON.stringify(message)).catch(fail);
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
+ }
2483
2532
  }
2484
2533
  function emitStats(text) {
2485
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
+ });
2486
2542
  self.postMessage({ type: "stats", stats: { ...stats }, ...text === void 0 ? {} : { text } });
2487
2543
  }
2488
2544
  function fail(error) {
@@ -2500,11 +2556,11 @@ function fail(error) {
2500
2556
  }
2501
2557
  function scheduleRender() {
2502
2558
  needsRender = true;
2503
- 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;
2504
2560
  scheduled = true;
2505
2561
  self.requestAnimationFrame(() => {
2506
2562
  scheduled = false;
2507
- if (processing || drawing || failed || stopped || markerPages.pending || awaitingFull) return;
2563
+ if (processing || drawing || failed || stopped || disconnected && stats.connected || markerPages.pending || awaitingFull) return;
2508
2564
  renderPromise = drawFrame();
2509
2565
  });
2510
2566
  }
@@ -2517,7 +2573,7 @@ async function drawFrame() {
2517
2573
  if (frame) links.prepare(cells, metadata);
2518
2574
  const linkSubmission = links.submission();
2519
2575
  renderer.resize(metadata.columns, metadata.rows, viewport);
2520
- const blink = Math.floor(performance.now() / 600) % 2 === 0;
2576
+ const blink = disconnected || Math.floor(performance.now() / 600) % 2 === 0;
2521
2577
  const result = renderer.render(cells, metadata, blink, linkSubmission.mask, palette);
2522
2578
  await renderer.idle();
2523
2579
  if (failed || stopped) return;
@@ -2562,6 +2618,7 @@ async function drawFrame() {
2562
2618
  commandMark: metadata.commandMark,
2563
2619
  text,
2564
2620
  hyperlinks: metadata.hyperlinks,
2621
+ lineRenditions: metadata.lineRenditions,
2565
2622
  ...links.present(cells, metadata)
2566
2623
  });
2567
2624
  send({ type: "ack", revision: frame.revision });
@@ -2580,7 +2637,7 @@ async function drawFrame() {
2580
2637
  }
2581
2638
  }
2582
2639
  async function receiveFrame(buffer) {
2583
- if (failed || stopped || !renderer) return;
2640
+ if (failed || stopped || disconnected || !renderer) return;
2584
2641
  if (frameInFlight) throw new Error("Server sent a second state frame before acknowledgement");
2585
2642
  frameInFlight = true;
2586
2643
  processing = true;
@@ -2641,6 +2698,7 @@ async function receiveFrame(buffer) {
2641
2698
  }
2642
2699
  async function initialize(message) {
2643
2700
  if (renderer || transport) throw new Error("Worker is already initialized");
2701
+ preserveOnDisconnect = message.preserveOnDisconnect ?? false;
2644
2702
  palette = compilePalette(normalizePalette(message.palette === void 0 ? defaultDarkPalette : message.palette));
2645
2703
  if (typeof self.requestAnimationFrame !== "function") {
2646
2704
  throw new Error("This browser does not support requestAnimationFrame in a dedicated OffscreenCanvas worker");
@@ -2668,17 +2726,31 @@ async function initialize(message) {
2668
2726
  onFrame: receiveFrame,
2669
2727
  onError: fail,
2670
2728
  onClose(details) {
2671
- if (failed || stopped) return;
2672
- stats.connected = false;
2673
- stats.gpu = "stopped";
2674
- stats.fps = 0;
2675
- stopped = true;
2729
+ if (failed || stopped || disconnected) return;
2730
+ disconnected = true;
2676
2731
  clearInterval(metricsTimer);
2677
2732
  clearInterval(blinkTimer);
2678
- renderer?.dispose();
2679
- self.postMessage({ type: "closed", details });
2680
- postStatus(`View disconnected (${details.code === void 0 ? details.reason : `${details.code}${details.reason ? `: ${details.reason}` : ""}`}). Attach another view to reconnect.`, "error");
2681
- emitStats();
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
+ }
2682
2754
  }
2683
2755
  });
2684
2756
  metricsTimer = setInterval(() => {
@@ -2707,7 +2779,7 @@ async function initialize(message) {
2707
2779
  }
2708
2780
  });
2709
2781
  }
2710
- 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;
2711
2783
  var init_terminal_worker = __esm({
2712
2784
  "src/terminal-worker.ts"() {
2713
2785
  "use strict";
@@ -2721,6 +2793,8 @@ var init_terminal_worker = __esm({
2721
2793
  init_transport_session();
2722
2794
  failed = false;
2723
2795
  stopped = false;
2796
+ disconnected = false;
2797
+ preserveOnDisconnect = false;
2724
2798
  processing = false;
2725
2799
  drawing = false;
2726
2800
  frameInFlight = false;
@@ -2822,7 +2896,7 @@ var init_terminal_worker = __esm({
2822
2896
  } catch (error) {
2823
2897
  fail(error);
2824
2898
  }
2825
- } else if (message.type === "linkDetection" && !failed && !stopped) {
2899
+ } else if (message.type === "linkDetection" && !failed && !stopped && !disconnected) {
2826
2900
  try {
2827
2901
  if (!links.configure(message.enabled, message.generation)) return;
2828
2902
  if (!processing && !drawing && !pendingFrame) {
@@ -2833,7 +2907,7 @@ var init_terminal_worker = __esm({
2833
2907
  } catch (error) {
2834
2908
  fail(error);
2835
2909
  }
2836
- } else if (message.type === "linkDecorations" && !failed && !stopped) {
2910
+ } else if (message.type === "linkDecorations" && !failed && !stopped && !disconnected) {
2837
2911
  try {
2838
2912
  if (links.accept(
2839
2913
  message.revision,
@@ -3199,7 +3273,10 @@ function captureMouse(canvas, send2, focus, inspection = {}) {
3199
3273
  const originalCursor = canvas.style.cursor;
3200
3274
  const state = () => ({ tracking, ...inspection.state?.() });
3201
3275
  function point(event, clamp2 = false) {
3202
- return cellPoint(event, canvas.getBoundingClientRect(), columns, rows, clamp2);
3276
+ const position = cellPoint(event, canvas.getBoundingClientRect(), columns, rows, clamp2);
3277
+ if (position && inspection.lineRenditions?.()[position.y])
3278
+ position.x = Math.min(Math.max(0, Math.floor(columns / 2) - 1), Math.floor(position.x / 2));
3279
+ return position;
3203
3280
  }
3204
3281
  function refreshHover(modifiers = hoverModifiers) {
3205
3282
  if (!inspection.hyperlink) return;
@@ -5468,6 +5545,7 @@ var WebTerminal = class _WebTerminal {
5468
5545
  #connected = false;
5469
5546
  #closed = false;
5470
5547
  #readOnly;
5548
+ #preserveOnDisconnect;
5471
5549
  #disposed = false;
5472
5550
  #hasGeometry = false;
5473
5551
  #resizeTimer;
@@ -5508,6 +5586,7 @@ var WebTerminal = class _WebTerminal {
5508
5586
  #selectionUIError = "";
5509
5587
  #canvasSize = { width: 0, height: 0 };
5510
5588
  #hyperlinks = new Hyperlinks();
5589
+ #lineRenditions = [];
5511
5590
  #links;
5512
5591
  #linkDetector;
5513
5592
  #linkGeneration = 1;
@@ -5546,6 +5625,9 @@ var WebTerminal = class _WebTerminal {
5546
5625
  if (options.readOnly !== void 0 && typeof options.readOnly !== "boolean")
5547
5626
  throw new TypeError("readOnly must be a boolean");
5548
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;
5549
5631
  this.#renderer = normalizeRenderer(options.renderer);
5550
5632
  this.#colorMode = normalizeColorMode(options.colorMode);
5551
5633
  this.#palettes = {
@@ -5796,6 +5878,7 @@ var WebTerminal = class _WebTerminal {
5796
5878
  }
5797
5879
  };
5798
5880
  this.#mouse = captureMouse(this.#canvas, (command) => this.#inputCommand(command), () => this.focus(), {
5881
+ lineRenditions: () => this.#lineRenditions,
5799
5882
  state: () => ({
5800
5883
  historical: !this.viewport.following || this.viewport.pending,
5801
5884
  readOnly: this.#readOnly,
@@ -5859,7 +5942,8 @@ var WebTerminal = class _WebTerminal {
5859
5942
  scale,
5860
5943
  font,
5861
5944
  renderer: this.#renderer,
5862
- palette: this.#palettes[this.resolvedColorMode]
5945
+ palette: this.#palettes[this.resolvedColorMode],
5946
+ preserveOnDisconnect: this.#preserveOnDisconnect
5863
5947
  }, [canvas]);
5864
5948
  this.#applyPalette();
5865
5949
  this.#postLinkConfiguration();
@@ -5884,7 +5968,8 @@ var WebTerminal = class _WebTerminal {
5884
5968
  this.#transportSession.start(this.#transport);
5885
5969
  } else if (message.type === "transportSend") {
5886
5970
  void Promise.resolve(this.#transportFrame?.promise).then(() => this.#transportSession?.send(message.control)).then(() => this.#post({ type: "transportSent" })).catch((error) => {
5887
- if (!this.#disposed) this.#post({ type: "transportError", message: errorMessage(error) });
5971
+ if (!this.#disposed && !this.#transportSession?.closing)
5972
+ this.#post({ type: "transportError", message: errorMessage(error) });
5888
5973
  });
5889
5974
  } else if (message.type === "transportReceived") {
5890
5975
  this.#transportFrame?.resolve();
@@ -5914,6 +5999,7 @@ var WebTerminal = class _WebTerminal {
5914
5999
  }
5915
6000
  this.#options.onStatus?.(message.message, message.level);
5916
6001
  } else if (message.type === "geometry") {
6002
+ this.#lineRenditions = message.lineRenditions ?? [];
5917
6003
  this.#linkRevision = message.revision;
5918
6004
  this.#osc8Rows.clear();
5919
6005
  for (const range of message.hyperlinks) {
@@ -6075,7 +6161,8 @@ var WebTerminal = class _WebTerminal {
6075
6161
  const element = document.createElement("span");
6076
6162
  element.className = "highlight";
6077
6163
  element.setAttribute("part", "selection-highlight");
6078
- element.style.cssText = `left:${range.startColumn / this.#geometry.columns * 100}%;top:${range.row / this.#geometry.rows * 100}%;width:${(range.endColumn - range.startColumn) / this.#geometry.columns * 100}%;height:${100 / this.#geometry.rows}%`;
6164
+ const scaleX = this.#lineRenditions[range.row] ? 2 : 1;
6165
+ element.style.cssText = `left:${range.startColumn * scaleX / this.#geometry.columns * 100}%;top:${range.row / this.#geometry.rows * 100}%;width:${(range.endColumn - range.startColumn) * scaleX / this.#geometry.columns * 100}%;height:${100 / this.#geometry.rows}%`;
6079
6166
  return element;
6080
6167
  }));
6081
6168
  const live = requiredElement(this.#inspection, ".return-live", HTMLButtonElement);