From 37b85f6ef11be12f6e25907f5e1017b6e39ae656 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 5 Oct 2026 10:05:35 +1100 Subject: [PATCH 01/21] Preserve disconnected terminal output and add recovery actions Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- .mcp.json | 2 +- Directory.Packages.props | 6 +- NuGet.config | 7 + docs/specs/with-terminal.md | 38 +++- .../Controls/TerminalDisconnectedBanner.razor | 49 +++++ .../TerminalDisconnectedBanner.razor.cs | 205 ++++++++++++++++++ .../TerminalDisconnectedBanner.razor.css | 36 +++ .../Components/Controls/TerminalView.razor | 26 ++- .../Components/Controls/TerminalView.razor.cs | 12 + .../Controls/TerminalView.razor.css | 37 +++- .../Components/Controls/TerminalView.razor.js | 60 ++++- .../Components/Layout/TerminalDock.razor | 1 + .../Resources/TerminalStrings.Designer.cs | 30 +++ .../Resources/TerminalStrings.resx | 15 ++ .../Resources/xlf/TerminalStrings.cs.xlf | 25 +++ .../Resources/xlf/TerminalStrings.de.xlf | 25 +++ .../Resources/xlf/TerminalStrings.es.xlf | 25 +++ .../Resources/xlf/TerminalStrings.fr.xlf | 25 +++ .../Resources/xlf/TerminalStrings.it.xlf | 25 +++ .../Resources/xlf/TerminalStrings.ja.xlf | 25 +++ .../Resources/xlf/TerminalStrings.ko.xlf | 25 +++ .../Resources/xlf/TerminalStrings.pl.xlf | 25 +++ .../Resources/xlf/TerminalStrings.pt-BR.xlf | 25 +++ .../Resources/xlf/TerminalStrings.ru.xlf | 25 +++ .../Resources/xlf/TerminalStrings.tr.xlf | 25 +++ .../Resources/xlf/TerminalStrings.zh-Hans.xlf | 25 +++ .../Resources/xlf/TerminalStrings.zh-Hant.xlf | 25 +++ .../Terminal/TerminalWebSocketProxy.cs | 37 +++- src/Aspire.Dashboard/package-lock.json | 8 +- src/Aspire.Dashboard/package.json | 2 +- src/Aspire.Dashboard/wwwroot/js/README.md | 20 +- .../js/hex1b-web-terminal/dist/index.min.js | 4 +- src/Aspire.TerminalHost/DcpUpstreamAdapter.cs | 5 + .../Hmp1UdsServerListenerFilter.cs | 10 +- src/Aspire.TerminalHost/TerminalReplica.cs | 3 +- .../TerminalDisconnectedBannerTests.cs | 127 +++++++++++ .../Controls/TerminalViewTests.cs | 75 +++++++ .../JavaScript/TerminalView.test.mjs | 163 ++++++++++++-- .../Layout/TerminalDockTests.cs | 33 +++ .../Shared/TerminalSetupHelpers.cs | 1 + .../Terminal/TerminalWebSocketTests.cs | 72 ++++-- .../TerminalHostAppTests.cs | 73 ++++++- 43 files changed, 1397 insertions(+), 87 deletions(-) create mode 100644 src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor create mode 100644 src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs create mode 100644 src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css create mode 100644 tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c98f9be0d0a..83959c32817 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "hex1b.tool": { - "version": "0.171.0", + "version": "0.173.0-alpha.1824.1.98d8766", "commands": [ "hex1b" ] diff --git a/.mcp.json b/.mcp.json index 4ed4d3cb865..b460f4571b8 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,7 +4,7 @@ "type": "stdio", "command": "dnx", "args": [ - "Hex1b.McpServer@0.171.0", + "Hex1b.McpServer@0.173.0-alpha.1824.1.98d8766", "--yes" ] }, diff --git a/Directory.Packages.props b/Directory.Packages.props index 47f8a266883..e04e37ebe6c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -118,9 +118,9 @@ - - - + + + diff --git a/NuGet.config b/NuGet.config index b6a9c24227a..f4507eb8d07 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,6 +2,8 @@ + + @@ -29,6 +31,11 @@ + + + + + diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index b2887489e34..0443ef4f144 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -295,9 +295,11 @@ matches `ResourceViewModel.Name`, including the generated instance suffix. binary WebSocket message, without dropping or reordering frames. 4. Hex1b owns input encoding, primary-role negotiation, selection, history, graphics projection, acknowledgements and state resynchronization. The - dashboard bounds handshake and send times and cancels both pumps when - either transport ends. Disposing a view disconnects only that peer, not - the AppHost-owned producer. + dashboard bounds handshake and send times. On producer disconnect it waits + for the mirror to finish processing output and gives the final HWT frame a + bounded acknowledgement period before cancelling the pumps and closing the + WebSocket. Disposing a view disconnects only that peer, not the AppHost-owned + producer. The browser never sees `consumerUdsPath` and cannot induce the dashboard to connect to an arbitrary local socket — it can only ask for @@ -326,6 +328,34 @@ gRPC attachment failures such as `Unavailable` retain HTTP 503 responses; a resource replica that is not yet available retains HTTP 404. Neither marks the producer as ended. +The browser enables Hex1b's `preserveOnDisconnect` option to retain the last +received screen, including Sixel and Kitty graphics. Disconnected views dim +their output and show a localized banner over the retained screen. Resource +terminals offer **Restart resource** using the resource's advertised Restart +or Start command, with the usual command availability, confirmation and error +reporting. Docked terminals offer **Close tab** through the existing dock close +action. Other surfaces show the disconnected message without a close-tab action. +All banners offer **Show terminal**, which hides the banner and accompanying +retry error behind a floating disconnected icon at the top right. The icon +reopens the recovery actions without reconnecting, restarting, or replacing the +retained renderer. Collapse persists through transport retries and resets after +a successful reconnect. The floating icon is inset from the terminal's focus +border, and the overlay intercepts input only on its visible controls. Hex1b's +redundant "Terminal view is not connected" status is suppressed while disconnected; +unrelated errors and selection feedback remain visible. +The current Hex1b alpha does not support selecting or copying output while +disconnected; that requires upstream offline-selection support, not merely +collapsing the banner. +Retryable transport loss keeps the retained view above replacement mounts until +a connected frame arrives; successful reconnect removes the treatment and +releases the old view. Producer completion keeps the screen until the view is +closed. Rebinding to another endpoint discards the previous screen. Retention +does not imply producer completion or guarantee output that was never received. +The terminal host defers viewer handshakes until its producer has connected, +including after a recycle. Its listening socket alone is not proof of a live +terminal: reconnecting to an empty relay while a shell is stopped must not +replace the retained display. + Each component registers a separate input policy with the dashboard and passes its opaque `viewId` with the WebSocket URL. Changes to a view's read-only state update that policy before updating browser input behavior. The bridge @@ -341,7 +371,7 @@ it does not lock the terminal, its creator's automation, or other viewers. ### Browser requirements and package pairing The dashboard uses `@hex1b/web-terminal` and the `Hex1b` NuGet package at -exactly `0.171.0`. HWT1 is experimental state transfer +exactly `0.173.0-alpha.1824.1.98d8766`. HWT1 is experimental state transfer between these paired packages, not a stable wire contract implemented by Aspire. Upgrade both together. The vendored runtime consists of one bundled JavaScript file for the client and both workers, a font, and two licenses. diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor new file mode 100644 index 00000000000..40e9c3e4580 --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor @@ -0,0 +1,49 @@ +@namespace Aspire.Dashboard.Components.Controls + +@if (_collapsed) +{ +
+ +
+} +else +{ +
+ + @Loc[nameof(Resources.TerminalStrings.TerminalDisconnectedMessage)] + @if (RestartCommand is not null) + { + + @Loc[nameof(Resources.TerminalStrings.TerminalRestartResource)] + + } + @if (OnClose.HasDelegate) + { + + @Loc[nameof(Resources.TerminalStrings.TerminalCloseTab)] + + } + + @Loc[nameof(Resources.TerminalStrings.TerminalShowOutput)] + +
+} diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs new file mode 100644 index 00000000000..bc5aec89abc --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs @@ -0,0 +1,205 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Dashboard.Model; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using Microsoft.FluentUI.AspNetCore.Components; + +namespace Aspire.Dashboard.Components.Controls; + +/// +/// Shows recovery actions without discarding the disconnected terminal's output. +/// +public sealed partial class TerminalDisconnectedBanner : ComponentBase, IAsyncDisposable +{ + private readonly CancellationTokenSource _cts = new(); + private readonly Dictionary _resources = new(StringComparers.ResourceName); + private Task? _watchTask; + private bool _executing; + private bool _disposed; + private bool _collapsed; + private bool _focusToggle; + private FluentButton? _toggleButton; + + /// Gets or sets the exact resource instance that owns this terminal. + [Parameter] + public string? ResourceName { get; set; } + + /// Gets or sets whether resource commands are unavailable in this view. + [Parameter] + public bool ReadOnly { get; set; } + + /// Raised when the user closes the owning dock tab. + [Parameter] + public EventCallback OnClose { get; set; } + + /// Raised when the banner is collapsed or expanded over the retained output. + [Parameter] + public EventCallback OnCollapsedChanged { get; set; } + + [Inject] + public required IDashboardClient DashboardClient { get; init; } + + [Inject] + public required DashboardDataSource DataSource { get; init; } + + [Inject] + public required DashboardCommandExecutor CommandExecutor { get; init; } + + [Inject] + public required IStringLocalizer Loc { get; init; } + + [Inject] + public required ILogger Logger { get; init; } + + private ResourceViewModel? Resource => ResourceName is not null ? _resources.GetValueOrDefault(ResourceName) : null; + + private CommandViewModel? RestartCommand + { + get + { + if (ReadOnly || DashboardClient.IsReadOnly || Resource is not { } resource) + { + return null; + } + + // Finished resources commonly expose only Start. Use the advertised + // command rather than synthesizing Restart, which the server may reject. + var commands = resource.Commands.Where(c => c.State != CommandViewModelState.Hidden && + c.Name is CommandViewModel.RestartCommand or CommandViewModel.StartCommand); + return commands.OrderBy(c => c.State != CommandViewModelState.Enabled) + .ThenBy(c => c.Name != CommandViewModel.RestartCommand).FirstOrDefault(); + } + } + + private bool RestartDisabled => _executing || RestartCommand is not { State: CommandViewModelState.Enabled } command || + Resource is not { } resource || CommandExecutor.IsExecuting(resource.Name, command.Name); + + protected override void OnInitialized() + { + if (ResourceName is not null && DashboardClient.IsEnabled) + { + _watchTask = WatchResourcesAsync(_cts.Token); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (_focusToggle && _toggleButton is not null) + { + _focusToggle = false; + await _toggleButton.Element.FocusAsync(preventScroll: true); + } + } + + private async Task SetCollapsedAsync(bool collapsed) + { + _collapsed = collapsed; + _focusToggle = true; + await OnCollapsedChanged.InvokeAsync(collapsed); + } + + private async Task WatchResourcesAsync(CancellationToken cancellationToken) + { + try + { + await DashboardClient.WhenResourcesReady.WaitAsync(cancellationToken); + var (snapshot, updates) = await DataSource.ResourceRepository.SubscribeResourcesAsync(cancellationToken); + await InvokeAsync(() => + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + foreach (var resource in snapshot) + { + _resources[resource.Name] = resource; + } + StateHasChanged(); + }); + await foreach (var changes in updates.WithCancellation(cancellationToken)) + { + await InvokeAsync(() => + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + foreach (var (changeType, resource) in changes) + { + if (changeType == ResourceViewModelChangeType.Delete) + { + _resources.Remove(resource.Name); + } + else + { + _resources[resource.Name] = resource; + } + } + StateHasChanged(); + }); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to watch the disconnected terminal resource."); + await DispatchExceptionAsync(ex); + } + } + + private async Task RestartResourceAsync() + { + if (RestartDisabled || Resource is not { } resource || RestartCommand is not { } command) + { + Logger.LogDebug("The disconnected terminal's restart command is no longer available."); + return; + } + + _executing = true; + try + { + await CommandExecutor.ExecuteAsync(resource, command, r => ResourceViewModel.GetResourceName(r, _resources)); + } + finally + { + _executing = false; + } + } + + private async Task CloseAsync() + { + if (_executing) + { + return; + } + _executing = true; + try + { + await OnClose.InvokeAsync(); + } + finally + { + _executing = false; + } + } + + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + _disposed = true; + await _cts.CancelAsync(); + if (_watchTask is not null) + { + await _watchTask; + } + _cts.Dispose(); + } +} diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css new file mode 100644 index 00000000000..7247116139e --- /dev/null +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css @@ -0,0 +1,36 @@ +.terminal-disconnected-banner { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 12px; + border: 1px solid var(--colorNeutralStroke1); + border-radius: var(--borderRadiusMedium); + background: var(--colorNeutralBackground1); + color: var(--colorNeutralForeground1); + box-shadow: var(--shadow4); +} + +.terminal-disconnected-banner span { + flex: 1 1 200px; + min-width: 0; +} + +@media (forced-colors: active) { + .terminal-disconnected-banner { + border-color: CanvasText; + background: Canvas; + color: CanvasText; + } +} + +.terminal-disconnected-indicator { + align-self: flex-end; +} + +.terminal-disconnected-indicator ::deep .terminal-show-banner { + width: 36px; + height: 36px; + border-radius: var(--borderRadiusMedium); + box-shadow: var(--shadow4); +} diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index 63feaa78243..40fd7dddbe0 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -27,19 +27,29 @@ } } -
+
- @if (_terminalError is not null) + @if (_state.Disconnected || _terminalError is not null) { -
- @GetErrorMessage() - @if (_terminalError is "input-failed" or "sizing-failed" or "palette-failed") +
+ @if (_state.Disconnected) { - @Loc[nameof(Resources.TerminalStrings.TerminalDismissError)] + } - else + @if (_terminalError is not null && !(_state.Disconnected && _bannerCollapsed)) { - @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] +
+ @GetErrorMessage() + @if (_terminalError is "input-failed" or "sizing-failed" or "palette-failed") + { + @Loc[nameof(Resources.TerminalStrings.TerminalDismissError)] + } + else + { + @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] + } +
}
} diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs index d29e934c94f..90c1907617f 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs @@ -32,6 +32,7 @@ public sealed partial class TerminalView : ComponentBase, IAsyncDisposable private bool _reconciling; private Task? _initializationTask; private string? _terminalError; + private bool _bannerCollapsed; private TerminalToolbarState _state = new(); private IReadOnlyList _sizePresets = []; private TerminalViewSession? _viewSession; @@ -104,6 +105,10 @@ public sealed partial class TerminalView : ComponentBase, IAsyncDisposable [Parameter] public EventCallback OnToolbarStateChanged { get; set; } + /// Raised when the disconnected banner offers to close the owning dock tab. + [Parameter] + public EventCallback OnClose { get; set; } + [Inject] public required IJSRuntime JS { get; init; } @@ -266,6 +271,7 @@ public async Task ReconnectAsync(string? newEndpoint) await _jsModule.InvokeVoidAsync("disposeTerminal", id); } _state = new(); + _bannerCollapsed = false; _terminalError = null; if (!_disposed) { @@ -326,6 +332,10 @@ public async Task OnTerminalStateChanged(TerminalToolbarState state) _connectedGeneration = state.Generation; if (_state != state) { + if (_state.Disconnected != state.Disconnected) + { + _bannerCollapsed = false; + } _state = state; if (!_initializationFailed) { @@ -546,6 +556,8 @@ public sealed record TerminalToolbarState public string Status { get; init; } = "connecting"; /// Whether a connected frame has been presented. public bool Connected { get; init; } + /// Whether the transport closed, including while reconnecting or after producer completion. + public bool Disconnected { get; init; } /// The workload-reported title, or empty when unset. public string Title { get; init; } = string.Empty; /// The decoded working directory reported by the shell, or null when unset. diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css index 9dccba6c5ea..d5ce2cfb16b 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css @@ -188,6 +188,36 @@ pointer-events: none; } +.terminal-disconnected .terminal-container { + opacity: 0.55; +} + +.terminal-overlays { + position: absolute; + inset-inline-start: 12px; + inset-inline-end: 12px; + top: 12px; + z-index: 2; + display: flex; + flex-direction: column; + gap: 8px; + max-height: calc(100% - 24px); + overflow: auto; + /* Only the visible banner/icon intercepts input; retained output stays accessible around it. */ + pointer-events: none; +} + +.terminal-overlays > ::deep * { + pointer-events: auto; +} + +.terminal-overlays-collapsed { + inset-inline-start: 20px; + inset-inline-end: 20px; + top: 20px; + max-height: calc(100% - 40px); +} + .terminal-container:focus-within::after { outline: var(--strokeWidthThick) solid var(--terminal-focus-color); outline-offset: calc(-1 * var(--strokeWidthThick)); @@ -237,12 +267,13 @@ .terminal-container { --terminal-focus-color: Highlight; } + + .terminal-disconnected .terminal-container { + opacity: 1; + } } .terminal-error { - position: absolute; - inset-inline: 12px; - top: 12px; display: flex; align-items: center; justify-content: space-between; diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index d9ca8db383e..a94ed6988ac 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -149,9 +149,11 @@ function updateAppearance(state) { state.viewElement.style.setProperty("--terminal-background", palette.background); state.viewElement.style.setProperty("--terminal-foreground", palette.foreground); state.client?.setColorMode(state.colorMode); + state.retainedClient?.client.setColorMode(state.colorMode); state.scrollbar = scrollbarConfiguration(state); // setScrollbar replaces, rather than merges, the configuration. state.client?.setScrollbar(state.scrollbar); + state.retainedClient?.client.setScrollbar(state.scrollbar); notifyToolbar(state); } @@ -245,8 +247,16 @@ function cancelReconnect(state) { } } -function releaseClient(state) { - state.inspectionObserver?.disconnect(); +function releaseRetainedClient(state) { + const retained = state.retainedClient; + state.retainedClient = null; + retained?.inspectionObserver?.disconnect(); + retained?.controller?.abort(); + retained?.client.dispose(); +} + +function releaseClient(state, preserve = false) { + const inspectionObserver = state.inspectionObserver; state.inspectionObserver = null; if (state.client?.element.contains(document.activeElement)) { requestFocus(state); @@ -255,6 +265,17 @@ function releaseClient(state) { const client = state.client; state.controller = null; state.client = null; + if (preserve && client && !client.connected) { + releaseRetainedClient(state); + // Keep the real GPU-backed view above replacement mounts until their first frame. + // Aborting its mount signal would dispose it and discard retained graphics. + client.element.style.position = "absolute"; + client.element.style.inset = "0"; + client.element.style.zIndex = "1"; + state.retainedClient = { client, controller, inspectionObserver }; + return; + } + inspectionObserver?.disconnect(); controller?.abort(); client?.dispose(); } @@ -274,6 +295,7 @@ function configureTerminalChrome(client) { style.textContent = ` .return-live { display: none !important; } .inspection-message[data-aspire-history-position] { display: none !important; } + .inspection-message[data-aspire-disconnected-status] { display: none !important; } :host([data-aspire-pointer-input="true"]) .scrollbar-accessibility:focus-visible { outline: none; } .viewport { background-color: color-mix(in srgb, var(--terminal-background) 92%, black); @@ -304,6 +326,11 @@ function configureTerminalChrome(client) { // feedback and navigation errors and must not be suppressed. status.toggleAttribute("data-aspire-history-position", status.dataset.level !== "error" && /^\d+ rows above live$/.test(status.textContent)); + // Hex1b can report "Terminal view is not connected" directly or with an + // action prefix, e.g. "Scrollbar: Terminal view is not connected". + // Aspire's recovery banner owns this state; keep unrelated errors visible. + status.toggleAttribute("data-aspire-disconnected-status", !client.connected && + /^(?:(?:Input action failed|Copy failed|Scrollbar(?: tooltip)?): )?Terminal view is not connected$/.test(status.textContent)); }; const observer = new MutationObserver(update); observer.observe(status, { childList: true, characterData: true, subtree: true, attributes: true, attributeFilter: ["data-level"] }); @@ -329,7 +356,7 @@ function scheduleReconnect(state, generation) { }, delay); } -function connectionFailed(state, generation, error) { +function connectionFailed(state, generation, error, preserve) { if (!isCurrent(state, generation) || state.ended || state.failurePending) { return; } @@ -338,8 +365,8 @@ function connectionFailed(state, generation, error) { state.peer = { id: null, primaryId: null, isPrimary: false }; state.pendingSizing = null; console.warn("Dashboard terminal connection failed.", error); - state.error = "mount-failed"; - releaseClient(state); + releaseClient(state, preserve); + state.error = state.retainedClient ? null : "mount-failed"; scheduleReconnect(state, generation); notifyToolbar(state); } @@ -348,10 +375,11 @@ function connectionClosed(state, generation, details) { if (!isCurrent(state, generation) || state.ended) { return; } + state.disconnected = true; if (details.code !== TERMINAL_ENDED_CLOSE_CODE) { // Normal closure (1000), missing close frames (1006), reasons and wasClean // describe transport state, never whether the producer has completed. - connectionFailed(state, generation, new Error(`Terminal WebSocket closed (${details.code}).`)); + connectionFailed(state, generation, new Error(`Terminal WebSocket closed (${details.code}).`), true); return; } state.ended = true; @@ -598,6 +626,7 @@ async function mountClient(state, generation, controller) { label: state.options.label, sizing: state.sizing, readOnly: state.readOnly, + preserveOnDisconnect: true, colorMode: state.colorMode, lightModePalette: lightPalette, darkModePalette: darkPalette, @@ -669,7 +698,7 @@ async function mountClient(state, generation, controller) { state.error = "input-failed"; notifyToolbar(state); } else { - connectionFailed(state, generation, message); + connectionFailed(state, generation, message, false); } }, onGeometry(geometry) { @@ -716,7 +745,9 @@ async function mountClient(state, generation, controller) { notifyToolbar(state); return; } + releaseRetainedClient(state); state.connected = client.connected; + state.disconnected = !client.connected; state.peer = client.peer; state.geometry = client.geometry; state.sizing = client.sizing; @@ -728,7 +759,7 @@ async function mountClient(state, generation, controller) { notifyToolbar(state); } catch (error) { if (current()) { - connectionFailed(state, generation, error); + connectionFailed(state, generation, error, false); } } } @@ -789,11 +820,13 @@ export function initTerminal(element, wsUrl, dotNetRef, options, selectionTempla readOnly: !!options.readOnly, autoFit: !!options.autoFit, client: null, + retainedClient: null, inspectionObserver: null, controller: null, disposed: false, ended: false, connected: false, + disconnected: false, title: "", workingDirectory: null, workingDirectoryUri: null, @@ -883,6 +916,8 @@ export function reconnectTerminal(id, wsUrl) { return state?.generation ?? 0; } if (state.wsUrl !== wsUrl) { + releaseRetainedClient(state); + state.disconnected = false; state.title = ""; state.workingDirectory = null; state.workingDirectoryUri = null; @@ -913,6 +948,7 @@ export function disposeTerminal(id) { state.footerObserver.disconnect(); state.listeners.abort(); releaseClient(state); + releaseRetainedClient(state); state.dotNetRef = null; terminals.delete(id); } @@ -1024,7 +1060,7 @@ export function getToolbarState(id) { terminalId: id, generation: state.generation, status: !connected ? "connecting" : isPrimary ? "primary" : state.peer.primaryId === null ? "no-primary" : "viewer", - connected, isPrimary, canTakeControl, + connected, disconnected: state.disconnected, isPrimary, canTakeControl, title: state.title, workingDirectory: state.workingDirectory, workingDirectoryUri: state.workingDirectoryUri, @@ -1056,9 +1092,9 @@ export function getTerminalSnapshot(element) { ...getToolbarState(state.id), readOnly: state.readOnly || state.ended, ended: state.ended, - screenText: state.client?.screenText ?? "", - selection: state.client?.selection ?? null, - viewport: state.client?.viewport ?? null, + screenText: (state.client ?? state.retainedClient?.client)?.screenText ?? "", + selection: (state.client ?? state.retainedClient?.client)?.selection ?? null, + viewport: (state.client ?? state.retainedClient?.client)?.viewport ?? null, }; } } diff --git a/src/Aspire.Dashboard/Components/Layout/TerminalDock.razor b/src/Aspire.Dashboard/Components/Layout/TerminalDock.razor index ab4947b725a..25ce9b3ddee 100644 --- a/src/Aspire.Dashboard/Components/Layout/TerminalDock.razor +++ b/src/Aspire.Dashboard/Components/Layout/TerminalDock.razor @@ -163,6 +163,7 @@ ShowDimensionsPicker="false" AutoFit="@(_isVisible && IsPaneActive(terminal.TerminalId))" OnToolbarStateChanged="@OnTerminalToolbarStateChanged" + OnClose="@(() => CloseTerminalAsync(terminal.TerminalId, terminal.Title))" SizeMemoryKey="@($"dock:{terminal.TerminalId}")" /> }
diff --git a/src/Aspire.Dashboard/Resources/TerminalStrings.Designer.cs b/src/Aspire.Dashboard/Resources/TerminalStrings.Designer.cs index 7c6bc33e0d4..33dfea42f2a 100644 --- a/src/Aspire.Dashboard/Resources/TerminalStrings.Designer.cs +++ b/src/Aspire.Dashboard/Resources/TerminalStrings.Designer.cs @@ -181,6 +181,36 @@ public static string TerminalDisconnected { } } + public static string TerminalDisconnectedMessage { + get { + return ResourceManager.GetString("TerminalDisconnectedMessage", resourceCulture); + } + } + + public static string TerminalRestartResource { + get { + return ResourceManager.GetString("TerminalRestartResource", resourceCulture); + } + } + + public static string TerminalCloseTab { + get { + return ResourceManager.GetString("TerminalCloseTab", resourceCulture); + } + } + + public static string TerminalShowOutput { + get { + return ResourceManager.GetString("TerminalShowOutput", resourceCulture); + } + } + + public static string TerminalShowDisconnectedBanner { + get { + return ResourceManager.GetString("TerminalShowDisconnectedBanner", resourceCulture); + } + } + public static string TerminalInputFailed { get { return ResourceManager.GetString("TerminalInputFailed", resourceCulture); diff --git a/src/Aspire.Dashboard/Resources/TerminalStrings.resx b/src/Aspire.Dashboard/Resources/TerminalStrings.resx index 4e2199764c5..77f622f8db8 100644 --- a/src/Aspire.Dashboard/Resources/TerminalStrings.resx +++ b/src/Aspire.Dashboard/Resources/TerminalStrings.resx @@ -186,6 +186,21 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + + + Restart resource + + + Close tab + + + Show terminal + + + Terminal disconnected. Show recovery actions + The terminal could not complete the input or clipboard action. Check browser clipboard permissions and try again. diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.cs.xlf index 72d34e6f846..5257f30da86 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.cs.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.de.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.de.xlf index bfce3378326..a2d1f8d2283 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.de.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.es.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.es.xlf index 9f5e5688417..0721f123080 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.es.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.fr.xlf index c0bec32e9fe..60e837e3121 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.fr.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.it.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.it.xlf index 3da56871b3f..70861ba09da 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.it.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ja.xlf index d0b7296c8f4..8d45a53c8d1 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ja.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ko.xlf index 1eeb037eb56..a4656118052 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ko.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pl.xlf index 1ef10bc1cec..9631254e477 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pl.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pt-BR.xlf index ae91b181444..b5f92f899b9 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pt-BR.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ru.xlf index 154f4f3f41d..56edb88f31f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ru.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.tr.xlf index 557668aa285..1d9ceef0506 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.tr.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hans.xlf index cb81956cc68..762e3e8e650 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hans.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hant.xlf index c880066a50b..41f1cfa35ec 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hant.xlf @@ -7,6 +7,11 @@ Toggle terminal (`) + + Close tab + Close tab + + Could not copy to clipboard. Could not copy to clipboard. @@ -27,6 +32,11 @@ The terminal disconnected and automatic retries have stopped. Check that the resource is running, then retry. + + Terminal disconnected. The last output has been preserved. + Terminal disconnected. The last output has been preserved. + + Dismiss Dismiss @@ -187,11 +197,26 @@ Warning + + Restart resource + Restart resource + + Reconnect terminal Reconnect terminal + + Terminal disconnected. Show recovery actions + Terminal disconnected. Show recovery actions + + + + Show terminal + Show terminal + + Size Size diff --git a/src/Aspire.Dashboard/Terminal/TerminalWebSocketProxy.cs b/src/Aspire.Dashboard/Terminal/TerminalWebSocketProxy.cs index 154a47371af..37ad6cdf871 100644 --- a/src/Aspire.Dashboard/Terminal/TerminalWebSocketProxy.cs +++ b/src/Aspire.Dashboard/Terminal/TerminalWebSocketProxy.cs @@ -309,11 +309,21 @@ private static async Task BridgeAsync(WebSocket socket, Hmp1WorkloadAdapter work .WithScrollback(10000) .Build(); await using var terminalLifetime = terminal.ConfigureAwait(false); - await PumpViewAsync(socket, presentation, workload, upstream, session, logger, cancellationToken).ConfigureAwait(false); + using var running = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var completion = terminal.RunAsync(running.Token); + try + { + await PumpViewAsync(socket, presentation, workload, upstream, completion, session, logger, cancellationToken).ConfigureAwait(false); + } + finally + { + await running.CancelAsync().ConfigureAwait(false); + await ObserveTeardownAsync(completion, logger).ConfigureAwait(false); + } } private static async Task PumpViewAsync(WebSocket socket, Hwt1PresentationAdapter presentation, Hmp1WorkloadAdapter workload, - Stream upstream, TerminalViewSession? session, ILogger logger, CancellationToken cancellationToken) + Stream upstream, Task completion, TerminalViewSession? session, ILogger logger, CancellationToken cancellationToken) { using var stopping = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); using var sending = CancellationTokenSource.CreateLinkedTokenSource(stopping.Token); @@ -325,12 +335,31 @@ private static async Task PumpViewAsync(WebSocket socket, Hwt1PresentationAdapte var closeReason = "Terminal closed"; try { - var completed = await Task.WhenAny(tasks).ConfigureAwait(false); + var completed = await Task.WhenAny(send, receive, disconnected, completion).ConfigureAwait(false); await completed.ConfigureAwait(false); - if (completed == disconnected) + if (completed == disconnected || completed == completion) { closeStatus = WebSocketCloseStatus.EndpointUnavailable; closeReason = "Terminal transport disconnected"; + // HMP disconnect completes its output channel before the mirror has + // applied the last bytes. Then HWT needs a final acknowledged frame, + // with both WebSocket pumps alive, before we close the browser transport. + // https://github.com/mitchdenny/hex1b/blob/98d8766/samples/WebTerminalDemo/Hmp1BrowserView.cs + using var drain = CancellationTokenSource.CreateLinkedTokenSource(stopping.Token); + drain.CancelAfter(s_closeTimeout); + try + { + await completion.WaitAsync(drain.Token).ConfigureAwait(false); + await presentation.DrainAsync(drain.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!stopping.IsCancellationRequested) + { + logger.LogWarning("Timed out draining the final terminal frame."); + } + catch (TimeoutException ex) + { + logger.LogWarning(ex, "The browser did not acknowledge the final terminal frame."); + } } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) diff --git a/src/Aspire.Dashboard/package-lock.json b/src/Aspire.Dashboard/package-lock.json index e53023847c9..6af634fdfee 100644 --- a/src/Aspire.Dashboard/package-lock.json +++ b/src/Aspire.Dashboard/package-lock.json @@ -6,16 +6,16 @@ "": { "name": "aspire-dashboard-assets", "dependencies": { - "@hex1b/web-terminal": "0.171.0" + "@hex1b/web-terminal": "0.173.0-alpha.1824.1.98d8766" }, "devDependencies": { "terser": "5.51.2" } }, "node_modules/@hex1b/web-terminal": { - "version": "0.171.0", - "resolved": "https://registry.npmjs.org/@hex1b/web-terminal/-/web-terminal-0.171.0.tgz", - "integrity": "sha512-MIqGy/JPvgPoECWuYvIUJqKEFR8h+qDRl6XU158CSMDhpR3V1WjEXCdkWOfco42teFh9UawwpsKNu4Y8Kxuoag==", + "version": "0.173.0-alpha.1824.1.98d8766", + "resolved": "https://registry.npmjs.org/@hex1b/web-terminal/-/web-terminal-0.173.0-alpha.1824.1.98d8766.tgz", + "integrity": "sha512-C6eqmH/CbbcUABwBZ/N0Of5w9ue32bPJ+2rkxSScW2TOOqGFDbN7IBjJ8VlfHLa8gH0yzserxEfUkNzetVEENw==", "license": "MIT", "engines": { "node": ">=22" diff --git a/src/Aspire.Dashboard/package.json b/src/Aspire.Dashboard/package.json index 2e1178813f0..79e8049c453 100644 --- a/src/Aspire.Dashboard/package.json +++ b/src/Aspire.Dashboard/package.json @@ -8,7 +8,7 @@ "test": "node --test ../../tests/Aspire.Dashboard.Components.Tests/JavaScript/*.test.mjs" }, "dependencies": { - "@hex1b/web-terminal": "0.171.0" + "@hex1b/web-terminal": "0.173.0-alpha.1824.1.98d8766" }, "devDependencies": { "terser": "5.51.2" diff --git a/src/Aspire.Dashboard/wwwroot/js/README.md b/src/Aspire.Dashboard/wwwroot/js/README.md index aa9444e0a02..d5d906b6158 100644 --- a/src/Aspire.Dashboard/wwwroot/js/README.md +++ b/src/Aspire.Dashboard/wwwroot/js/README.md @@ -18,7 +18,7 @@ If we ever want to show more chart types than those, we'll need to change the bu ## Hex1b web terminal -`hex1b-web-terminal/` vendors the published `@hex1b/web-terminal` **0.171.0** release, +`hex1b-web-terminal/` vendors the published `@hex1b/web-terminal` **0.173.0-alpha.1824.1.98d8766** alpha, paired with the Hex1b, Hex1b.McpServer, and Hex1b.Tool NuGet packages and the repository-local `hex1b` tool at the same version. The client and server use the evolving HWT1 presentation transport and must be updated together. Do not substitute a @@ -60,8 +60,9 @@ path `woff2/CascadiaMonoNF.woff2`. Its relative path under `dist/fonts/cascadia-mono-nf/` is preserved. Do not hand-edit generated assets. `TerminalView.razor.js` imports only the minified public entry point. -The matching NuGet packages are available through the approved `dotnet-public` feed; -no additional package source is required. +For this alpha trial, `NuGet.config` temporarily maps only the three Hex1b packages +to nuget.org. Remove that temporary source and mapping once these versions are +available through an approved internal feed. Desktop and Android Firefox terminals use `renderer: "webgl2"` until the [WebGPU performance issue](https://bugzilla.mozilla.org/show_bug.cgi?id=1870699) @@ -194,7 +195,7 @@ The terminal uses Hex1b's default Canvas2D **overlay** scrollbar, not a native HTML scrollbar or a reserved gutter. The mount requests 3 CSS pixels of internal padding on every side. `createDefaultScrollbarRenderer` keeps the upstream capsule thumb, marker drawing, gestures, hit testing and auto-hide. -The pinned renderer draws rectangular markers on a flat translucent track, +The pinned renderer draws rectangular markers on a translucent track, with upstream marker navigation and a thumb focus outline. A scoped shadow-DOM override hides the track's DOM focus outline after pointer input, restoring the upstream `:focus-visible` outline on keyboard @@ -230,7 +231,16 @@ zero. Browser-owned bookmarks remain per-view and do not survive reconnect. ### View lifecycle -Each reconnect aborts the previous mount and creates a new client. Mounting is +Each reconnect creates a new client. A disconnected client with a received frame +stays mounted above its replacements until a connected frame arrives, retaining +text, Sixel and Kitty graphics through failed retries. `preserveOnDisconnect` +keeps the received screen locally; it does not guarantee unreceived final output. +Disconnected output is dimmed with a localized **Disconnected** badge. Successful +reconnect removes the treatment and disposes the retained client. Completed +views retain their screen until closed; switching endpoints releases the previous +screen immediately. Renderer failures still surface an error and dispose the +unusable client rather than treating it as a retained transport disconnect. +Mounting is deferred while initially hidden; once connected, changing the Console/Terminal view retains the client, selection, and producer-backed history. Disposal closes only this view, never the server-side producer. Sizing changes explicitly request diff --git a/src/Aspire.Dashboard/wwwroot/js/hex1b-web-terminal/dist/index.min.js b/src/Aspire.Dashboard/wwwroot/js/hex1b-web-terminal/dist/index.min.js index b3598f395c1..5134676547b 100644 --- a/src/Aspire.Dashboard/wwwroot/js/hex1b-web-terminal/dist/index.min.js +++ b/src/Aspire.Dashboard/wwwroot/js/hex1b-web-terminal/dist/index.min.js @@ -1,2 +1,2 @@ -// @hex1b/web-terminal 0.171.0; minified with Terser. See ../LICENSE. -var e=Object.defineProperty,t=Object.getOwnPropertyNames,i=(e,i,r)=>function(){if(r)throw r[0];try{return e&&(i=(0,e[t(e)[0]])(e=0)),i}catch(e){throw r=[e],e}};function r(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)}function n(e){return r(e)&&"string"==typeof e.message?e.message:String(e)}var s=i({"src/validation.ts"(){}});function o(){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();const e=crypto.getRandomValues(new Uint8Array(16));e[6]=15&e[6]|64,e[8]=63&e[8]|128;const t=Array.from(e,e=>e.toString(16).padStart(2,"0")).join("");return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var a,l,c=i({"src/random-id.ts"(){}});function h(e,t=import.meta.url){if(e??={family:"Cascadia Mono NF",faces:[{url:a,weight:"200 700"}]},"object"!=typeof e||Array.isArray(e)||"string"!=typeof e.family||!e.family.trim()||e.family.length>256||/[\u0000-\u001f\u007f]/u.test(e.family))throw new TypeError("font.family must be a single, nonempty font family name");const i=e.faces??[];if(!Array.isArray(i)||i.length>16)throw new TypeError("font.faces must contain at most 16 font sources");return{family:e.family.trim(),faces:i.map(e=>{if(!e||"string"!=typeof e.url||!e.url.trim())throw new TypeError("Each font face requires a URL");for(const t of["weight","style"])if(void 0!==e[t]&&("string"!=typeof e[t]||e[t].length>64))throw new TypeError(`Font face ${t} must be a CSS descriptor string`);return{url:new URL(e.url,t).href,weight:e.weight??"400",style:e.style??"normal"}})}}async function d(e){const{family:t,faces:i}=h(e),n=globalThis,s="fonts"in n&&(r(o=n.fonts)&&"function"==typeof o.add&&"function"==typeof o.delete)?n.fonts:globalThis.document?.fonts;var o;if(!s||"function"!=typeof FontFace)throw new Error("Terminal font loading requires the CSS Font Loading API in the rendering context");const a=l.has(t.toLowerCase()),c=a?t.toLowerCase():JSON.stringify(t);if(a&&i.length)throw new TypeError("A generic font family cannot have downloadable faces");const d=i.length?i:a?[]:[{local:t}],u=[];for(const e of d)try{const i=new FontFace(t,"local"in e?`local(${JSON.stringify(e.local)})`:`url(${JSON.stringify(e.url)})`,{weight:e.weight??"400",style:e.style??"normal"});u.push(await i.load())}catch(i){throw new Error(`Could not load terminal font "${t}" from ${"url"in e?e.url:"local fonts"}`,{cause:i})}for(const e of u)s.add(e);return{family:t,cssFamily:c,dispose(){for(const e of u)s.delete(e)}}}var u=i({"src/terminal-font.ts"(){s(),a=new URL("./fonts/cascadia-mono-nf/CascadiaMonoNF.woff2",import.meta.url).href,l=new Set(["monospace","serif","sans-serif","system-ui"])}});function p(e="auto"){if("auto"===e||"webgpu"===e||"webgl2"===e)return e;throw new TypeError('renderer must be "auto", "webgpu", or "webgl2"')}var f,m,g=i({"src/renderer-options.ts"(){}});function w(e="dark"){if("light"===e||"dark"===e||"system"===e)return e;throw new TypeError('colorMode must be "light", "dark", or "system"')}function y(e,t){if("string"!=typeof e||!/^#[0-9a-f]{6}$/iu.test(e))throw new TypeError(`${t} must be a #RRGGBB color`);return e.toLowerCase()}function v(e){if(!r(e))throw new TypeError("A terminal palette object is required");if(!Array.isArray(e.ansi)||16!==e.ansi.length)throw new TypeError("palette.ansi must contain exactly 16 colors");const t=Array.from(e.ansi,(e,t)=>y(e,`palette.ansi[${t}]`)),i={};if(void 0!==e.extended){if(!r(e.extended))throw new TypeError("palette.extended must be an object");for(const[t,r]of Object.entries(e.extended)){const e=Number(t);if(!Number.isInteger(e)||e<16||e>255||String(e)!==t)throw new TypeError("palette.extended indices must be integers from 16 to 255");i[e]=y(r,`palette.extended[${t}]`)}}return Object.freeze({foreground:y(e.foreground,"palette.foreground"),background:y(e.background,"palette.background"),ansi:Object.freeze(t),...void 0===e.cursor?{}:{cursor:y(e.cursor,"palette.cursor")},...void 0===e.selectionForeground?{}:{selectionForeground:y(e.selectionForeground,"palette.selectionForeground")},...void 0===e.selectionBackground?{}:{selectionBackground:y(e.selectionBackground,"palette.selectionBackground")},...void 0===e.extended?{}:{extended:Object.freeze(i)}})}function b(e,t,i){return(e|t<<8|i<<16|4278190080)>>>0}function k(e){const t=Number.parseInt(e.slice(1),16);return b(t>>16,t>>8&255,255&t)}function x(e){const t=new Uint32Array(256);for(let i=0;i<16;i++)t[i]=k(e.ansi[i]);const i=[0,95,135,175,215,255];for(let e=16;e<232;e++){const r=e-16;t[e]=b(i[Math.floor(r/36)],i[Math.floor(r/6)%6],i[r%6])}for(let e=232;e<256;e++){const i=8+10*(e-232);t[e]=b(i,i,i)}for(const[i,r]of Object.entries(e.extended??{}))t[Number(i)]=k(r);return{foreground:k(e.foreground),background:k(e.background),selectionForeground:k(e.selectionForeground??e.background),selectionBackground:k(e.selectionBackground??e.foreground),...void 0===e.cursor?{}:{cursor:k(e.cursor)},indexed:t}}function E(e,t=!1){if(!(e>>>24==255||e>=16777216&&e<=16777471||33554432===e||50331648===e||t&&67108864===e))throw new Error("Invalid indexed-v1 color reference")}function T(e,t){switch(e>>>24){case 1:return t.indexed[255&e];case 2:return t.foreground;case 3:return t.background;default:return e}}function C(e,t,i){if("indexed-v1"!==t.colorEncoding)return e.foreground;const r=T(32&e.attributes?e.background:e.foreground,i);return 2&e.attributes?((255&r)>>1|(r>>>8&255)>>1<<8|(r>>>16&255)>>1<<16|4278190080)>>>0:r}function S(e,t,i){return"indexed-v1"!==t.colorEncoding?e.background:32&e.attributes||50331648!==e.background?T(32&e.attributes?e.foreground:e.background,i):16777215&i.background}var I,M=i({"src/terminal-palette.ts"(){s(),f=Object.freeze({foreground:"#d4d0c8",background:"#323232",selectionForeground:"#323232",selectionBackground:"#d4d0c8",ansi:Object.freeze(["#242424","#ed888f","#8dae82","#c3a150","#7baad1","#bc99c5","#64b1b5","#b7b4ae","#9a9a9a","#ff99a0","#9bc08e","#d7b156","#87bbe7","#d0a8da","#6cc4c8","#dedad3"])}),m=Object.freeze({foreground:"#323232",background:"#d4d0c8",selectionForeground:"#d4d0c8",selectionBackground:"#323232",ansi:Object.freeze(["#24262b","#9e3141","#3e6131","#705402","#255c86","#704b7a","#006367","#b7b4ae","#595959","#982338","#355a27","#664d01","#175480","#694273","#015a5e","#e3dfd7"])})}});function L(e){return"string"==typeof e&&/^[1-9][0-9]{0,18}$/u.test(e)&&BigInt(e)<=9223372036854775807n}function A(e){if("string"!=typeof e||!/^(?:custom|command):[A-Za-z0-9-]{1,100}$/u.test(e))throw new TypeError("Invalid terminal marker ID")}var R,P,O,z=i({"src/marker-state.ts"(){c(),I=class{#e;#t;#i=Object.freeze([]);#r=new Map;#n=[];#s=0;#o=0;#a=!1;constructor(e,t){this.#e=e,this.#t=t}get markers(){return this.#i}accept(e,t){if(t<=this.#o)return;this.#o=t;const i=(e?.markers??[]).map(e=>Object.freeze({...e,...this.#r.get(e.id)})),r=!this.#a||JSON.stringify(i)!==JSON.stringify(this.#i);this.#a=!0,this.#i=Object.freeze(i);const n=e?.markerResult,s=this.#n[0];n&&s&&n.requestId===s.command.requestId&&(this.#n.shift(),clearTimeout(s.timer),n.success?s.resolve(n):s.reject(new Error(n.error||"Marker operation failed")),this.#l());const o=new Set(i.map(e=>e.id));for(const e of this.#n)"add"===e.command.action&&o.add(e.command.id);for(const e of this.#r.keys())o.has(e)||this.#r.delete(e);r&&this.#t()}#c(e){const t=Promise.withResolvers();return this.#n.push({command:{type:"marker",...e,requestId:++this.#s},resolve:t.resolve,reject:t.reject}),1===this.#n.length&&this.#l(),t.promise}#l(){const e=this.#n[0];if(e)try{this.#e(e.command),e.timer=setTimeout(()=>{this.#h(new Error("Timed out waiting for the marker operation; resync before retrying."))},1e4)}catch(t){this.#n.shift(),e.reject(t instanceof Error?t:new Error(String(t))),this.#l()}}async add(e){const t=e?.position;if(!t||!L(t.generation)||!L(t.rowId)||!Number.isInteger(t.column)||t.column<0||t.column>1024)throw new TypeError("A marker requires a valid presented generation, row ID, and column");if(void 0!==e.label&&("string"!=typeof e.label||e.label.length>4096))throw new TypeError("Marker label must be at most 4096 characters");if(void 0!==e.color&&("string"!=typeof e.color||e.color.length>256||!CSS.supports("color",e.color)))throw new TypeError("Marker color must be a CSS color");const i=`custom:${o()}`;this.#r.set(i,{...void 0===e.label?{}:{label:e.label},...void 0===e.color?{}:{color:e.color}});try{await this.#c({action:"add",id:i,...t});const e=this.#i.find(e=>e.id===i);if(!e)throw new Error("Marker registration succeeded without a retained marker");return e}catch(e){throw this.#r.delete(i),e}}async remove(e){A(e),await this.#c({action:"remove",id:e}),this.#r.delete(e)}async jump(e){A(e),await this.#c({action:"jump",id:e})}async details(e){A(e);const t=await this.#c({action:"details",id:e});if(!t.details)throw new Error("Command marker details were not returned");return Object.freeze({...t.details})}disconnect(e=new Error("Terminal disconnected before the marker operation completed.")){this.#h(e),this.#r.clear()}#h(e){for(const t of this.#n.splice(0))clearTimeout(t.timer),t.reject(e)}}}});function U(e,t,i=0,r=Number.MAX_SAFE_INTEGER){if("number"!=typeof e||!Number.isSafeInteger(e)||er)throw new Error(`Invalid ${t}: ${String(e)}`);return e}function D(e,t,i){if(!Array.isArray(e)||e.length>i)throw new Error(`Invalid ${t}`)}function B(e){if("string"!=typeof e||!e.length||e.length>1024)throw new Error("Invalid image key")}function q(e,t){if("string"!=typeof e||!/^[1-9][0-9]{0,18}$/u.test(e)||BigInt(e)>9223372036854775807n)throw new Error(`Invalid ${t}`)}function j(e){if(!r(e)||"string"!=typeof e.phase||!["unknown","prompt","commandLine","executing","finished"].includes(e.phase))throw new Error("Invalid terminal command mark");if(null!==e.exitCode&&(U(e.exitCode,"terminal command mark exit code",-2147483648,2147483647),"finished"!==e.phase))throw new Error("Non-finished terminal command mark has an exit code");if(null!==e.rawParameters&&("string"!=typeof e.rawParameters||e.rawParameters.length>R.commandMarkParameterUnits))throw new Error("Invalid terminal command mark parameters")}function N(e){if("string"!=typeof e.status||!["none","valid","invalidated"].includes(e.status)||("valid"===e.status?"string"!=typeof e.text||e.text.length>R.metadataBytes:null!==e.text))throw new Error("Invalid selection text")}function $(e){if(!r(e)||1!==e.version||"boolean"!=typeof e.full)throw new Error("Unsupported frame metadata version");if(U(e.revision,"revision",1),U(e.baseRevision,"base revision"),void 0!==e.colorEncoding&&null!==e.colorEncoding&&"indexed-v1"!==e.colorEncoding)throw new Error("Unsupported color encoding");if(void 0!==e.colorEncodings&&(D(e.colorEncodings,"color encodings",16),e.colorEncodings.some(e=>"string"!=typeof e||e.length>64)))throw new Error("Invalid color encodings");if("string"!=typeof e.title||e.title.length>R.titleUnits||/[\u0000-\u001f\u007f-\u009f\ud800-\udfff]/u.test(e.title))throw new Error("Invalid terminal title");const t=e.progress;if(!r(t)||"string"!=typeof t.state||!["none","normal","error","indeterminate","warning"].includes(t.state))throw new Error("Invalid terminal progress");if("none"===t.state||"indeterminate"===t.state){if(null!==t.percentage)throw new Error("Unexpected terminal progress percentage")}else U(t.percentage,"terminal progress percentage",0,100);const i=e.shellIntegration;if(!r(i)||"string"!=typeof i.phase||!["unknown","prompt","commandLine","executing","finished"].includes(i.phase))throw new Error("Invalid terminal shell integration");if(null!==i.lastExitCode&&U(i.lastExitCode,"terminal shell exit code",-2147483648,2147483647),"unknown"===i.phase&&null!==i.lastExitCode)throw new Error("Unknown terminal shell phase has an exit code");const n=e.workingDirectory;if(!r(n))throw new Error("Invalid terminal working directory");const s=[n.uri,n.host,n.path];if(s.every(e=>null===e));else if(s.some(e=>"string"!=typeof e||e.length>R.metadataBytes))throw new Error("Invalid terminal working directory");const o=e.commandMark;null!==o&&j(o);const a=U(e.columns,"columns",1,1024),l=U(e.rows,"rows",1,512);if("number"!=typeof e.mouseTracking||![0,9,1e3,1002,1003].includes(e.mouseTracking))throw new Error("Unsupported mouse tracking mode");if(!r(e.peer)||"boolean"!=typeof e.peer.isPrimary)throw new Error("Invalid peer state");for(const t of["id","primaryId"]){const i=e.peer[t];if(null!==i&&("string"!=typeof i||!i.length||i.length>256))throw new Error(`Invalid peer ${t}`)}if(null!==e.peer.id&&e.peer.isPrimary!==(e.peer.id===e.peer.primaryId))throw new Error("Inconsistent primary peer state");U(a*l,"cell count",1,R.cells),function(e,t,i){if(null===e)return;if(!r(e))throw new Error("Missing history metadata");if(q(e.generation,"history generation"),"main"!==e.buffer&&"alternate"!==e.buffer)throw new Error("Invalid history buffer");const n=U(e.totalRows,"history total rows",i,2147483647),s=U(e.liveTop,"history live top",0,n-i);if(s!==n-i)throw new Error("Inconsistent history extent");if(U(e.top,"history top",0,s),"boolean"!=typeof e.following||e.following&&e.top!==e.liveTop)throw new Error("Invalid history following state");if(U(e.requestId,"viewport request id"),void 0!==e.viewportError&&null!==e.viewportError&&("string"!=typeof e.viewportError||e.viewportError.length>4096))throw new Error("Invalid viewport error");if(D(e.rowIds,"viewport row ids",i),e.rowIds.length!==i||new Set(e.rowIds).size!==i)throw new Error("Invalid viewport row ids");for(const t of e.rowIds)q(t,"viewport row id");const o=e.selection;if(!r(o)||"string"!=typeof o.status||!["none","valid","invalidated"].includes(o.status))throw new Error("Invalid selection status");if(U(o.requestId,"selection request id"),"string"!=typeof o.mode||!["character","word","line","rectangle"].includes(o.mode))throw new Error("Invalid selection mode");D(o.ranges,"selection ranges",i);let a=-1;for(const e of o.ranges){if(!r(e))throw new Error("Invalid selection range");const n=U(e.row,"selection range row",a+1,i-1),s=U(e.startColumn,"selection start column",0,t-1);U(e.endColumn,"selection end column",s+1,t),a=n}if(N(o),"valid"!==o.status&&o.ranges.length)throw new Error("Inactive selection has highlight ranges");if(null!==e.copy){if(!r(e.copy))throw new Error("Missing copy metadata");U(e.copy.requestId,"copy request id",1),N(e.copy)}if(void 0!==e.markers){D(e.markers,"history markers",R.metadataBytes/48);const t=new Set;for(const i of e.markers){if(!r(i)||"string"!=typeof i.id)throw new Error("Invalid history marker");if(A(i.id),t.has(i.id))throw new Error("Duplicate history marker ID");if(t.add(i.id),"command"!==i.source&&"custom"!==i.source)throw new Error("Invalid marker source");if(!i.id.startsWith(`${i.source}:`))throw new Error("Marker source disagrees with its ID");if("main"!==i.buffer&&"alternate"!==i.buffer)throw new Error("Invalid marker buffer");if(U(i.column,"marker column",0,1024),null!==i.row&&(U(i.row,"marker row",0,n-1),i.buffer!==e.buffer))throw new Error("Inactive-buffer marker has a resolved row");if("command"===i.source)j({...i,rawParameters:null});else if(void 0!==i.phase&&null!==i.phase)throw new Error("Custom marker has a shell phase")}}if(void 0!==e.markerPage&&null!==e.markerPage){const t=e.markerPage;if(!r(t)||"string"!=typeof t.revision||!t.revision.length||t.revision.length>64)throw new Error("Invalid marker page revision");const i=U(t.total,"marker inventory count",1,Math.floor(R.metadataBytes/48)),n=U(t.offset,"marker page offset",0,i-1);if(!Array.isArray(e.markers)||!e.markers.length||n+e.markers.length>i)throw new Error("Invalid marker page length")}if(void 0!==e.markerResult&&null!==e.markerResult){const t=e.markerResult;if(!r(t)||"boolean"!=typeof t.success)throw new Error("Invalid marker result");if(U(t.requestId,"marker request ID",1),void 0!==t.error&&null!==t.error&&("string"!=typeof t.error||t.error.length>4096))throw new Error("Invalid marker error");if(void 0!==t.markerId&&null!==t.markerId){if("string"!=typeof t.markerId)throw new Error("Invalid result marker ID");A(t.markerId)}void 0!==t.details&&null!==t.details&&j(t.details)}}(e.history,a,l),D(e.hyperlinks,"hyperlinks",a*l);let c=0;for(const t of e.hyperlinks){if(!r(t))throw new Error("Invalid hyperlink");const e=U(t.row,"hyperlink row",0,l-1),i=U(t.startColumn,"hyperlink start column",0,a-1),n=U(t.endColumn,"hyperlink end column",i+1,a);if(e*a+iR.metadataBytes)throw new Error("Invalid hyperlink URI")}if(10!==e.cellWidth||20!==e.cellHeight)throw new Error("This spike requires server geometry of 10 × 20 logical pixels");for(const t of["defaultBackground","defaultForeground"])void 0!==e[t]&&U(e[t],t,0,4294967295);if(!r(e.cursor)||"boolean"!=typeof e.cursor.visible)throw new Error("Invalid cursor");U(e.cursor.x,"cursor x",-1,1024),U(e.cursor.y,"cursor y",-1,512);if("string"==typeof e.cursor.shape&&(e.cursor.shape=["Default","BlinkingBlock","SteadyBlock","BlinkingUnderline","SteadyUnderline","BlinkingBar","SteadyBar"].indexOf(e.cursor.shape)),U(e.cursor.shape,"cursor shape",0,6),D(e.images,"images",R.images),D(e.retainedImages,"retained image keys",R.images),D(e.placements,"placements",R.placements),D(e.warnings,"warnings",256),e.warnings.some(e=>"string"!=typeof e))throw new Error("Invalid warning");if(!r(e.stats))throw new Error("Invalid server metrics");for(const t of["workloadBytes","outputBatches","captureMs","elapsedMs"]){const i=e.stats[t];if("number"!=typeof i||!Number.isFinite(i)||i<0)throw new Error(`Invalid server metric ${t}`)}const h=new Set;for(const t of e.retainedImages){if(B(t),h.has(t))throw new Error("Duplicate retained image key");h.add(t)}const d=new Set;let u=0;for(const t of e.images){if(!r(t))throw new Error("Invalid image");if(B(t.key),d.has(t.key)||!h.has(t.key))throw new Error("Inconsistent new image keys");d.add(t.key);const e=U(t.width,"image width",1,16384),i=U(t.height,"image height",1,16384),n=U(t.byteLength,"image byte length",1,R.frameBytes);if("rgba"!==t.format&&"png"!==t.format)throw new Error("Unsupported image format");if("rgba"===t.format&&n!==e*i*4)throw new Error("RGBA image size mismatch");if(u+=e*i*4,u>R.textureBytes)throw new Error("New images exceed decoded texture budget")}for(const t of e.placements){if(!r(t))throw new Error("Invalid placement");if(B(t.key),!h.has(t.key))throw new Error("Placement references an unretained image");if("kgp"!==t.kind&&"sixel"!==t.kind)throw new Error("Invalid placement kind");for(const e of["x","y","width","height","sourceX","sourceY","sourceWidth","sourceHeight","clipX","clipY","clipWidth","clipHeight","z"]){const i=t[e];if("number"!=typeof i||!Number.isFinite(i)||Math.abs(i)>2147483648)throw new Error(`Invalid placement ${e}`)}for(const e of["width","height","sourceWidth","sourceHeight","clipWidth","clipHeight"]){const i=t[e];if("number"!=typeof i||i<0)throw new Error(`Negative placement ${e}`)}}}var F,G=i({"src/protocol.ts"(){s(),z(),M(),R=Object.freeze({commandBytes:65536,frameBytes:100663296,metadataBytes:8388608,titleUnits:4096,commandMarkParameterUnits:8192,cells:262144,images:4096,placements:16384,textureBytes:268435456}),P=new TextDecoder("utf-8",{fatal:!0}),O=new TextEncoder}});function W(e,t){if("function"!=typeof e&&("string"!=typeof e||!t.has(e)))throw new TypeError("Link action must be a registered custom action or callback")}function _(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)}function H(e,t){if(!1===e)return!1;if(void 0===e)return{};if(!_(e))throw new TypeError("Invalid links configuration");const i={};if(!1===e.osc8)i.osc8=!1;else if(void 0!==e.osc8){if(!_(e.osc8))throw new TypeError("Invalid OSC 8 configuration");W(e.osc8.action,t),i.osc8={action:e.osc8.action}}const r=e.detection;if(!1===r)i.detection=!1;else if(void 0!==r){if(!_(r)||!Array.isArray(r.rules))throw new TypeError("Invalid detection configuration");if(r.rules.length>F.rules)throw new TypeError("Too many link rules");if(void 0!==r.activation&&!["modifierClick","click"].includes(r.activation))throw new TypeError("Invalid link activation");if(void 0!==r.decoration&&!["always","hover","none"].includes(r.decoration))throw new TypeError("Invalid link decoration");if(void 0!==r.underlineStyle&&!["solid","dashed"].includes(r.underlineStyle))throw new TypeError("Invalid link underline style");const e=new Set,n=r.rules.map(i=>{if(!_(i)||"string"!=typeof i.id||!i.id.length||i.id.length>256||e.has(i.id))throw new TypeError("Link rule IDs must be nonempty and unique");if(e.add(i.id),void 0!==i.enabled&&"boolean"!=typeof i.enabled)throw new TypeError("Invalid enabled flag");if(void 0!==i.text&&!["physicalRow","logicalLine","viewport"].includes(i.text))throw new TypeError("Invalid link text mode");if(void 0!==i.resolve&&"function"!=typeof i.resolve)throw new TypeError("Invalid resolver");W(i.action,t);const r={id:i.id,action:i.action,enabled:i.enabled??!0,text:i.text??"logicalLine",resolve:i.resolve};if(void 0!==i.builtin){if(!["url","uri","absolutePath","homePath"].includes(i.builtin)||void 0!==i.pattern||void 0!==i.kind)throw new TypeError("Invalid builtin rule");return{...r,builtin:i.builtin}}if(!(i.pattern instanceof RegExp&&["uri","path","custom"].includes(i.kind)))throw new TypeError("Custom rules require a RegExp and kind");if(i.pattern.source.length>F.pattern||/[^dgimsuv]/u.test(i.pattern.flags))throw new TypeError("Unsupported regex flags or oversized pattern (sticky is not supported)");return{...r,kind:i.kind,pattern:new RegExp(i.pattern.source,i.pattern.flags)}});i.detection={rules:n,activation:r.activation??"modifierClick",decoration:r.decoration??"always",underlineStyle:r.underlineStyle??"solid"}}return i}function K(e){if("function"!=typeof e)throw new TypeError("Expected a link action callback");return(t,i,r)=>{if(!function(e){if(!_(e))return!1;const t=e;return!(!(["detected","osc8"].includes(t.source)&&["uri","path","custom"].includes(t.kind)&&"string"==typeof t.text&&"string"==typeof t.target&&Number.isSafeInteger(t.revision))||t.revision<0||!Array.isArray(t.ranges)||0===t.ranges.length)&&("osc8"===t.source?null===t.ruleId&&"uri"===t.kind:"string"==typeof t.ruleId&&0!==t.ruleId.length)&&t.ranges.every(e=>_(e)&&Number.isSafeInteger(e.row)&&e.row>=0&&Number.isSafeInteger(e.startColumn)&&e.startColumn>=0&&Number.isSafeInteger(e.endColumn)&&e.endColumn>e.startColumn)}(i))throw new TypeError("Expected a terminal link activation");return e(t,i,r)}}var V=i({"src/link-options.ts"(){F=Object.freeze({rules:32,pattern:8192,chunk:65536,totalText:262144,cells:262144,matches:2048,resultText:262144,cacheEntries:2048,cacheText:1048576,timeoutMs:250})}});function X(e){let t=e.text.length+16;for(const i of e.captures)t+=8+(i?.length??0);for(const[i,r]of Object.entries(e.groups))t+=8+i.length+(r?.length??0);return t}var Y,J,Z,Q,ee,te,ie=i({"src/link-worker-protocol.ts"(){}}),re=i({"src/render-backend.ts"(){Y=16,J=class extends Error{}}}),ne=i({"src/webgpu-backend.ts"(){re(),Z=class{constructor(e,t,i,r,n){this.device=e,this.texture=t,this.bindGroup=i,this.width=r,this.height=n}device;texture;bindGroup;width;height;writePixels(e,t,i,r=0,n=0){this.device.queue.writeTexture({texture:this.texture,origin:[r,n]},e,{bytesPerRow:4*t,rowsPerImage:i},[t,i])}writeBitmap(e){this.device.queue.copyExternalImageToTexture({source:e},{texture:this.texture,premultipliedAlpha:!1},[this.width,this.height])}destroy(){this.texture.destroy()}},Q=class e{constructor(e){this.device=e,this.format=navigator.gpu.getPreferredCanvasFormat(),this.uniform=e.createBuffer({size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.sampler=e.createSampler({minFilter:"linear",magFilter:"linear"})}device;kind="webgpu";instanceBufferBytes=0;instanceBuffer;disposed=!1;context;format;uniform;sampler;pipeline;static async create(t,i){if(!globalThis.isSecureContext)throw new J("WebGPU requires HTTPS or localhost");if(!navigator.gpu)throw new J("WebGPU is unavailable in this browser worker");const r=await navigator.gpu.requestAdapter();if(!r)throw new J("No WebGPU adapter is available");let n,s;try{n=await r.requestDevice()}catch(e){if(e instanceof DOMException&&"OperationError"===e.name)throw new J(`Could not acquire a WebGPU device: ${e.message}`,{cause:e});throw e}try{s=new e(n);const r=s;return n.lost.then(e=>{r.disposed||i(new Error(`WebGPU device lost: ${e.message||e.reason}`))}),n.addEventListener("uncapturederror",e=>{r.disposed||i(new Error(`WebGPU error: ${e.error.message}`))}),await s.initialize(t),s}catch(e){throw s?s.dispose():n.destroy(),e}}get maxTextureDimension2D(){return this.device.limits.maxTextureDimension2D}get maxCanvasDimension2D(){return this.maxTextureDimension2D}async initialize(e){const t=this.device.createShaderModule({code:"\nstruct Viewport { size: vec2f, padding: vec2f }\n@group(0) @binding(0) var viewport: Viewport;\n@group(0) @binding(1) var image: texture_2d;\n@group(0) @binding(2) var imageSampler: sampler;\n\nstruct VertexOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n @location(1) color: vec4f,\n @location(2) @interpolate(flat) mode: f32,\n}\n\n@vertex fn vertex(\n @builtin(vertex_index) index: u32,\n @location(0) rect: vec4f,\n @location(1) uvRect: vec4f,\n @location(2) color: vec4f,\n @location(3) mode: f32,\n) -> VertexOut {\n let corners = array(\n vec2f(0, 0), vec2f(1, 0), vec2f(0, 1),\n vec2f(0, 1), vec2f(1, 0), vec2f(1, 1)\n );\n let corner = corners[index];\n let position = rect.xy + corner * rect.zw;\n var out: VertexOut;\n out.position = vec4f(position / viewport.size * vec2f(2, -2) + vec2f(-1, 1), 0, 1);\n out.uv = mix(uvRect.xy, uvRect.zw, corner);\n out.color = color;\n out.mode = mode;\n return out;\n}\n\n@fragment fn fragment(in: VertexOut) -> @location(0) vec4f {\n // Explicit LOD avoids derivative-uniformity requirements across solid/mask/image batches.\n let texel = textureSampleLevel(image, imageSampler, in.uv, 0);\n if (in.mode < 0.5) { return in.color; }\n if (in.mode < 1.5) { return vec4f(in.color.rgb, in.color.a * texel.a); }\n return texel * in.color;\n}"});this.pipeline=await this.device.createRenderPipelineAsync({layout:"auto",vertex:{module:t,entryPoint:"vertex",buffers:[{arrayStride:4*Y,stepMode:"instance",attributes:[{shaderLocation:0,offset:0,format:"float32x4"},{shaderLocation:1,offset:16,format:"float32x4"},{shaderLocation:2,offset:32,format:"float32x4"},{shaderLocation:3,offset:48,format:"float32"}]}]},fragment:{module:t,entryPoint:"fragment",targets:[{format:this.format,blend:{color:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha"},alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha"}}}]},primitive:{topology:"triangle-list"}});const i=e.getContext("webgpu");if(!i)throw new J("Could not create an OffscreenCanvas WebGPU context");this.context=i,i.configure({device:this.device,format:this.format,alphaMode:"opaque"})}createTexture(e,t,i){if(e>this.maxTextureDimension2D||t>this.maxTextureDimension2D)throw new Error(`${i} exceeds the GPU texture dimension limit`);if(!this.pipeline)throw new Error("WebGPU backend is not initialized");const r=this.device.createTexture({label:i,size:[e,t],format:"rgba8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT});try{const i=this.device.createBindGroup({layout:this.pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:this.uniform}},{binding:1,resource:r.createView()},{binding:2,resource:this.sampler}]});return new Z(this.device,r,i,e,t)}catch(e){throw r.destroy(),e}}resize(e,t){this.device.queue.writeBuffer(this.uniform,0,new Float32Array([e,t,0,0]))}submit(e,t,i,r){if(this.disposed||!this.context||!this.pipeline)throw new Error("WebGPU backend is not initialized");const n=t*Y*4;(!this.instanceBuffer||n>this.instanceBufferBytes)&&(this.instanceBuffer?.destroy(),this.instanceBufferBytes=Math.max(256,e.byteLength),this.instanceBuffer=this.device.createBuffer({size:this.instanceBufferBytes,usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST})),n&&this.device.queue.writeBuffer(this.instanceBuffer,0,e,0,t*Y);const s=this.device.createCommandEncoder(),o=s.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:r[0],g:r[1],b:r[2],a:1},loadOp:"clear",storeOp:"store"}]});o.setPipeline(this.pipeline),o.setVertexBuffer(0,this.instanceBuffer);for(const e of i){if(!(e.resource instanceof Z))throw new Error("Texture belongs to a different rendering backend");o.setBindGroup(0,e.resource.bindGroup),o.draw(6,e.count,0,e.start)}o.end(),this.device.queue.submit([s.finish()])}async idle(){await this.device.queue.onSubmittedWorkDone()}dispose(){this.disposed||(this.disposed=!0,this.instanceBuffer?.destroy(),this.uniform.destroy(),this.context?.unconfigure(),this.device.destroy())}}}});function se(e){return e instanceof Error?e:new Error(String(e))}function oe(e){return Number.isSafeInteger(e)&&e>0}function ae(e){e.pixelStorei(e.UNPACK_ALIGNMENT,1),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!1),e.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1),e.pixelStorei(e.UNPACK_COLORSPACE_CONVERSION_WEBGL,e.NONE),e.pixelStorei(e.UNPACK_ROW_LENGTH,0),e.pixelStorei(e.UNPACK_SKIP_PIXELS,0),e.pixelStorei(e.UNPACK_SKIP_ROWS,0)}var le=i({"src/webgl2-backend.ts"(){re(),ee=class{constructor(e,t,i,r,n,s,o){this.owner=e,this.gl=t,this.texture=i,this.width=r,this.height=n,this.assertActive=s,this.release=o}owner;gl;texture;width;height;assertActive;release;destroyed=!1;bind(){if(this.assertActive(),this.destroyed)throw new Error("WebGL2 texture is destroyed");this.gl.bindTexture(this.gl.TEXTURE_2D,this.texture),ae(this.gl)}writePixels(e,t,i,r=0,n=0){if(!oe(t)||!oe(i)||!Number.isSafeInteger(r)||!Number.isSafeInteger(n)||r<0||n<0||r+t>this.width||n+i>this.height||e.byteLength{const t=e.statusMessage;this.contextLost(new Error("WebGL2 context lost"+(t?`: ${t}`:"")))};static async create(t,i){const r=t.getContext("webgl2",{alpha:!1,antialias:!1,depth:!1,stencil:!1,premultipliedAlpha:!1,preserveDrawingBuffer:!1});if(!r)throw new J("Could not create an OffscreenCanvas WebGL2 context");const n=new e(t,r,i);try{return n.initialize(),n}catch(e){throw n.dispose(),e}}get maxTextureDimension2D(){return this.textureLimit}get maxCanvasDimension2D(){return this.canvasLimit}get instanceBufferBytes(){return this.bufferBytes}initialize(){const e=this.gl;this.canvas.addEventListener("webglcontextlost",this.onContextLost),this.checkErrors("initialization"),this.textureLimit=e.getParameter(e.MAX_TEXTURE_SIZE);const t=e.getParameter(e.MAX_VIEWPORT_DIMS);if(this.canvasLimit=Math.min(this.textureLimit,e.getParameter(e.MAX_RENDERBUFFER_SIZE),t[0],t[1]),!oe(this.textureLimit)||!oe(this.canvasLimit))throw new Error("Invalid WebGL2 texture or viewport limits");const i=this.compile(e.VERTEX_SHADER,"#version 300 es\nprecision highp float;\nprecision highp int;\n\nlayout(location = 0) in vec4 rect;\nlayout(location = 1) in vec4 uvRect;\nlayout(location = 2) in vec4 color;\nlayout(location = 3) in float mode;\nuniform vec2 viewport;\nout vec2 fragmentUv;\nout vec4 fragmentTint;\nflat out float fragmentMode;\n\nvoid main() {\n const vec2 corners[6] = vec2[6](\n vec2(0, 0), vec2(1, 0), vec2(0, 1),\n vec2(0, 1), vec2(1, 0), vec2(1, 1)\n );\n vec2 corner = corners[gl_VertexID];\n vec2 position = rect.xy + corner * rect.zw;\n gl_Position = vec4(position / viewport * vec2(2, -2) + vec2(-1, 1), 0, 1);\n fragmentUv = mix(uvRect.xy, uvRect.zw, corner);\n fragmentTint = color;\n fragmentMode = mode;\n}"),r=this.compile(e.FRAGMENT_SHADER,"#version 300 es\nprecision highp float;\nprecision highp int;\n\nuniform highp sampler2D image;\nin vec2 fragmentUv;\nin vec4 fragmentTint;\nflat in float fragmentMode;\nout vec4 fragmentColor;\n\nvoid main() {\n vec4 texel = textureLod(image, fragmentUv, 0.0);\n if (fragmentMode < 0.5) {\n fragmentColor = fragmentTint;\n } else if (fragmentMode < 1.5) {\n fragmentColor = vec4(fragmentTint.rgb, fragmentTint.a * texel.a);\n } else {\n fragmentColor = texel * fragmentTint;\n }\n}"),n=e.createProgram();if(!n)throw new Error("Could not allocate a WebGL2 shader program");if(this.program=n,e.attachShader(n,i),e.attachShader(n,r),e.linkProgram(n),!e.getProgramParameter(n,e.LINK_STATUS))throw new Error(`WebGL2 shader link failed: ${e.getProgramInfoLog(n)||"No diagnostic available"}`);for(const t of this.shaders)e.detachShader(n,t),e.deleteShader(t);this.shaders.clear();const s=e.getUniformLocation(n,"viewport"),o=e.getUniformLocation(n,"image");if(null===s||null===o)throw new Error("WebGL2 shader uniforms are unavailable");this.viewport=s;const a=e.createBuffer();if(!a)throw new Error("Could not allocate a WebGL2 instance buffer");this.instanceBuffer=a;const l=e.createVertexArray();if(!l)throw new Error("Could not allocate a WebGL2 vertex array");this.vertexArray=l,e.useProgram(n),e.uniform1i(o,0),e.activeTexture(e.TEXTURE0),e.bindVertexArray(l),e.bindBuffer(e.ARRAY_BUFFER,a);for(let t=0;t<4;t++)e.enableVertexAttribArray(t),e.vertexAttribDivisor(t,1);e.enable(e.BLEND),e.blendEquationSeparate(e.FUNC_ADD,e.FUNC_ADD),e.blendFuncSeparate(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA,e.ONE,e.ONE_MINUS_SRC_ALPHA),e.disable(e.DEPTH_TEST),e.disable(e.STENCIL_TEST),e.disable(e.CULL_FACE),e.disable(e.SCISSOR_TEST),e.disable(e.DITHER),this.resize(this.canvas.width,this.canvas.height)}compile(e,t){const i=this.gl,r=i.createShader(e);if(!r)throw new Error("Could not allocate a WebGL2 shader");if(this.shaders.add(r),i.shaderSource(r,t),i.compileShader(r),!i.getShaderParameter(r,i.COMPILE_STATUS))throw new Error(`WebGL2 shader compilation failed: ${i.getShaderInfoLog(r)||"No diagnostic available"}`);return r}assertActive(){if(this.disposed)throw new Error("WebGL2 backend is disposed");if(this.lostError)throw this.lostError}contextLost(e){if(!this.disposed&&!this.lostError){this.lostError=e;for(const t of this.fences)this.settleFence(t,e);this.onFatal(e)}}checkErrors(e){this.assertActive();const t=this.gl,i=t.getError();if(i===t.CONTEXT_LOST_WEBGL||t.isContextLost()){const e=new Error("WebGL2 context lost");throw this.contextLost(e),e}if(i!==t.NO_ERROR){const r=i===t.OUT_OF_MEMORY?"OUT_OF_MEMORY":i===t.INVALID_ENUM?"INVALID_ENUM":i===t.INVALID_VALUE?"INVALID_VALUE":i===t.INVALID_OPERATION?"INVALID_OPERATION":i===t.INVALID_FRAMEBUFFER_OPERATION?"INVALID_FRAMEBUFFER_OPERATION":`0x${i.toString(16)}`;throw new Error(`WebGL2 ${e} failed: ${r}`)}}createTexture(e,t,i){if(this.assertActive(),!oe(e)||!oe(t)||e>this.textureLimit||t>this.textureLimit)throw new Error(`${i} has invalid dimensions or exceeds the WebGL2 texture dimension limit`);const r=this.gl,n=r.createTexture();if(!n)throw this.checkErrors("texture allocation"),new Error(`Could not allocate WebGL2 texture: ${i}`);try{r.bindTexture(r.TEXTURE_2D,n),ae(r),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),r.texImage2D(r.TEXTURE_2D,0,r.RGBA8,e,t,0,r.RGBA,r.UNSIGNED_BYTE,null),this.checkErrors(`texture allocation (${i})`);const s=new ee(this,r,n,e,t,()=>this.assertActive(),e=>this.textures.delete(e));return this.textures.add(s),s}catch(e){throw r.deleteTexture(n),e}}resize(e,t){if(this.assertActive(),!Number.isFinite(e)||!Number.isFinite(t)||e<=0||t<=0)throw new Error("Invalid WebGL2 logical viewport dimensions");if(!oe(this.canvas.width)||!oe(this.canvas.height)||this.canvas.width>this.canvasLimit||this.canvas.height>this.canvasLimit)throw new Error("Canvas exceeds the WebGL2 drawing buffer dimension limit");const i=this.gl;if(i.drawingBufferWidth!==this.canvas.width||i.drawingBufferHeight!==this.canvas.height)throw this.checkErrors("drawing buffer resize"),new Error("WebGL2 could not allocate the requested canvas drawing buffer");i.viewport(0,0,this.canvas.width,this.canvas.height),i.useProgram(this.program),i.uniform2f(this.viewport,e,t),this.checkErrors("resize")}submit(e,t,i,r){if(this.assertActive(),!Number.isSafeInteger(t)||t<0||t*Y>e.length)throw new Error("Invalid WebGL2 instance data");for(const e of i){if(!(e.resource instanceof ee)||e.resource.owner!==this)throw new Error("Texture belongs to a different rendering backend");if(e.resource.destroyed)throw new Error("WebGL2 texture is destroyed");if(!Number.isSafeInteger(e.start)||!Number.isSafeInteger(e.count)||e.start<0||e.count<0||e.start+e.count>t)throw new Error("Invalid WebGL2 batch instance range")}const n=this.gl,s=Y*Float32Array.BYTES_PER_ELEMENT,o=t*s;if(n.bindVertexArray(this.vertexArray),n.bindBuffer(n.ARRAY_BUFFER,this.instanceBuffer),!this.bufferBytes||o>this.bufferBytes){const t=Math.max(256,e.byteLength);n.bufferData(n.ARRAY_BUFFER,t,n.DYNAMIC_DRAW),this.checkErrors("instance buffer allocation"),this.bufferBytes=t}o&&n.bufferSubData(n.ARRAY_BUFFER,0,e,0,t*Y),n.useProgram(this.program),n.activeTexture(n.TEXTURE0),n.clearColor(r[0],r[1],r[2],1),n.clear(n.COLOR_BUFFER_BIT);for(const e of i){if(!e.count)continue;const t=e.resource;n.bindTexture(n.TEXTURE_2D,t.texture);const i=e.start*s;n.vertexAttribPointer(0,4,n.FLOAT,!1,s,i),n.vertexAttribPointer(1,4,n.FLOAT,!1,s,i+16),n.vertexAttribPointer(2,4,n.FLOAT,!1,s,i+32),n.vertexAttribPointer(3,1,n.FLOAT,!1,s,i+48),n.drawArraysInstanced(n.TRIANGLES,0,6,e.count)}this.checkErrors("frame submission")}async idle(){this.checkErrors("GPU completion");const e=this.gl,t=e.fenceSync(e.SYNC_GPU_COMMANDS_COMPLETE,0);if(!t)throw this.checkErrors("GPU fence allocation"),new Error("Could not allocate a WebGL2 GPU fence");return new Promise((i,r)=>{const n={sync:t,resolve:i,reject:r};this.fences.add(n);try{e.flush(),this.checkErrors("GPU fence submission"),this.pollFence(n)}catch(e){this.settleFence(n,se(e))}})}pollFence(e){if(this.fences.has(e))try{this.assertActive();const t=this.gl;if(t.isContextLost()){const e=new Error("WebGL2 context lost");throw this.contextLost(e),e}const i=t.clientWaitSync(e.sync,0,0);if(i===t.ALREADY_SIGNALED||i===t.CONDITION_SATISFIED)this.checkErrors("GPU completion"),this.settleFence(e);else{if(i!==t.TIMEOUT_EXPIRED)throw this.checkErrors("GPU fence wait"),new Error("WebGL2 GPU fence wait failed");e.timer=setTimeout(()=>{e.timer=void 0,this.pollFence(e)},0)}}catch(t){this.settleFence(e,se(t))}}settleFence(e,t){this.fences.delete(e)&&(void 0!==e.timer&&clearTimeout(e.timer),this.gl.deleteSync(e.sync),t?e.reject(t):e.resolve())}dispose(){if(this.disposed)return;this.disposed=!0,this.canvas.removeEventListener("webglcontextlost",this.onContextLost);for(const e of this.fences)this.settleFence(e,new Error("WebGL2 backend is disposed"));for(const e of this.textures)e.destroy();const e=this.gl;this.instanceBuffer&&e.deleteBuffer(this.instanceBuffer),this.vertexArray&&e.deleteVertexArray(this.vertexArray),this.program&&e.deleteProgram(this.program);for(const t of this.shaders)e.deleteShader(t);this.shaders.clear(),this.instanceBuffer=void 0,this.vertexArray=void 0,this.program=void 0,this.bufferBytes=0,e.isContextLost()||e.getExtension("WEBGL_lose_context")?.loseContext()}}}});var ce,he,de,ue,pe,fe,me,ge,we=i({"src/backend-selection.ts"(){re(),g(),ne(),le()}});function ye(e){return[(255&e)/255,(e>>>8&255)/255,(e>>>16&255)/255,(e>>>24&255)/255]}function ve(e){return`${5&e.attributes}/${e.width}/${e.text}`}function be(e){return 1109742===e.text.codePointAt(0)}function ke(e,t,i,r={x:0,y:0,rowHeight:0}){let{x:n,y:s,rowHeight:o}=r;const a=[];for(const[r,l]of e){const e=Math.ceil(l.width*ce*i)+4,c=Math.ceil(he*i)+4;if(e>t||c>t)return null;if(n+e>t&&(n=0,s+=o,o=0),s+c>t)return null;a.push({key:r,cell:l,x:n,y:s,width:e,height:c}),n+=e,o=Math.max(o,c)}return{placements:a,shelf:{x:n,y:s,rowHeight:o}}}var xe,Ee,Te=i({"src/renderer.ts"(){G(),u(),we(),re(),M(),ce=10,he=20,de=1048576,ue=1048576,pe=Y,fe=[1,1,1,1],me=x(f),ge=class e{canvas;scale;backingScale;backend;fallbackReason;fontConfiguration;fontMetrics;images;glyphs;imageUploadBytes;imagePayloadBytes;glyphUploadBytes;atlasRebuilds;textureBytes;instances;disposed;columns;rows;font;rasterCanvas;raster;atlas;glyphKeyUnits=0;shelf={x:0,y:0,rowHeight:0};canvasLimited=!1;width=0;height=0;quadCount=0;batches=[];static async create(t,i,r,n,s="auto"){const o=h(n);if(!Number.isFinite(i)||i<.5||i>3)throw new Error("Invalid backing scale");const{backend:a,fallbackReason:l}=await async function(e,t,i="auto"){const r=p(i);if("webgl2"===r)return{backend:await te.create(e,t)};try{return{backend:await Q.create(e,t)}}catch(i){if("auto"!==r||!(i instanceof J))throw i;try{return{backend:await te.create(e,t),fallbackReason:i.message}}catch(e){throw new AggregateError([i,e],`WebGPU unavailable (${i.message}); WebGL2 initialization failed: ${e instanceof Error?e.message:String(e)}`)}}}(t,r,s),c=new e(t,i,a,o);c.fallbackReason=l;try{return await c.initialize(),c}catch(e){throw c.dispose(),e}}constructor(e,t,i,r){if(!Number.isFinite(t)||t<.5||t>3)throw new Error("Invalid backing scale");this.canvas=e,this.scale=t,this.backingScale=t,this.backend=i,this.fontConfiguration=r,this.fontMetrics=new Map,this.images=new Map,this.glyphs=new Map,this.imageUploadBytes=0,this.imagePayloadBytes=0,this.glyphUploadBytes=0,this.atlasRebuilds=0,this.textureBytes=0,this.instances=new Float32Array(4096*pe),this.disposed=!1,this.columns=0,this.rows=0}async initialize(){this.font=await d(this.fontConfiguration),this.rasterCanvas=new OffscreenCanvas(1,1);const e=this.rasterCanvas.getContext("2d",{willReadFrequently:!0});if(!e)throw new Error("Worker glyph rasterization is unavailable");this.raster=e,this.resetAtlas(Math.min(2048,this.backend.maxTextureDimension2D))}createTexture(e,t,i){return this.backend.createTexture(e,t,i)}resetAtlas(e){this.atlas?.destroy(),this.atlas=this.createTexture(e,e,"Glyph atlas"),this.glyphs.clear(),this.glyphKeyUnits=0,this.shelf={x:0,y:0,rowHeight:0}}resize(e,t,i){const r=e*ce,n=t*he,s=this.backend.maxCanvasDimension2D,o=Math.min(this.scale,i?i.width/r:1/0,i?i.height/n:1/0);this.canvasLimited=r*o>s||n*o>s,this.backingScale=Math.min(o,s/r,s/n);const a=Math.max(1,Math.min(s,Math.ceil(r*this.backingScale))),l=Math.max(1,Math.min(s,Math.ceil(n*this.backingScale)));this.columns===e&&this.rows===t&&this.canvas.width===a&&this.canvas.height===l||(this.columns=e,this.rows=t,this.width=r,this.height=n,this.canvas.width=a,this.canvas.height=l,this.backend.resize(r,n))}async updateImages(e,t){const i=new Set(t),r=new Map(e.map(e=>[e.key,e]));let n=0;for(const e of i){const t=r.get(e)||this.images.get(e);if(!t)throw new Error(`Missing retained image resource: ${e}`);n+=t.width*t.height*4}if(n>R.textureBytes)throw new Error("Retained images exceed the 256 MiB texture budget");for(const[e,t]of this.images)i.has(e)&&!r.has(e)||(t.destroy(),this.textureBytes-=t.width*t.height*4,this.images.delete(e));for(const t of e){const e=this.createTexture(t.width,t.height,`Image ${t.key}`);try{if("rgba"===t.format)e.writePixels(t.bytes,t.width,t.height);else{const i=new DataView(t.bytes.buffer,t.bytes.byteOffset,t.bytes.byteLength);if(i.byteLength<33||2303741511!==i.getUint32(0)||218765834!==i.getUint32(4)||13!==i.getUint32(8)||1229472850!==i.getUint32(12)||i.getUint32(16)!==t.width||i.getUint32(20)!==t.height)throw new Error(`PNG header dimensions do not match resource ${t.key}`);const r=await createImageBitmap(new Blob([t.bytes],{type:"image/png"}),{premultiplyAlpha:"none",colorSpaceConversion:"none"});try{if(r.width!==t.width||r.height!==t.height)throw new Error("Decoded PNG dimension mismatch");e.writeBitmap(r)}finally{r.close()}}this.images.set(t.key,e),this.textureBytes+=t.width*t.height*4,this.imageUploadBytes+=t.width*t.height*4,this.imagePayloadBytes+=t.byteLength}catch(t){throw e.destroy(),t}}}prepareGlyphs(e){const t=new Map;for(const i of e)!(i&&i.width&&i.text.trim())||64&i.attributes||be(i)||t.set(ve(i),i);const i=e=>[...e.keys()].reduce((e,t)=>e+t.length,0);if(t.size>16384||i(t)>ue)throw new Error("Visible glyph metadata exceeds the bounded glyph cache");const r=new Map([...t].filter(([e])=>!this.glyphs.has(e)));if(!r.size)return;let n=this.glyphs.size+r.size<=16384&&this.glyphKeyUnits+i(r)<=ue?ke(r,this.atlas.width,this.scale,this.shelf):null;if(!n){let e=this.atlas.width;const i=Math.min(4096,this.backend.maxTextureDimension2D);for(;!(n=ke(t,e,this.scale))&&e=de)throw new Error("Frame exceeds bounded quad budget");const p=this.quadCount*pe;if(p+pe>this.instances.length){const e=new Float32Array(Math.min(2*this.instances.length,de*pe));e.set(this.instances),this.instances=e}const f=a[2]-a[0],m=a[3]-a[1];this.instances.set([c,h,d-c,u-h,a[0]+(c-t)/r*f,a[1]+(h-i)/n*m,a[0]+(d-t)/r*f,a[1]+(u-i)/n*m,...s,o,0,0,0],p);const g=this.batches[this.batches.length-1];g?.resource===e?g.count++:this.batches.push({resource:e,start:this.quadCount,count:1}),this.quadCount++}solid(e,t,i,r,n,s){this.quad(this.atlas,e,t,i,r,n,0,void 0,s)}placement(e){const t=this.images.get(e.key);if(!t)throw new Error(`Placement texture is missing: ${e.key}`);const{sourceX:i,sourceY:r,sourceWidth:n,sourceHeight:s}=e;if(!(n&&s&&e.width&&e.height))return;const o=e.x+Math.max(0,-i)/n*e.width,a=e.y+Math.max(0,-r)/s*e.height,l=e.x+Math.min(n,t.width-i)/n*e.width,c=e.y+Math.min(s,t.height-r)/s*e.height,h=Math.max(o,e.clipX),d=Math.max(a,e.clipY),u=Math.min(l,e.clipX+e.clipWidth),p=Math.min(c,e.clipY+e.clipHeight);this.quad(t,e.x,e.y,e.width,e.height,fe,2,[i/t.width,r/t.height,(i+n)/t.width,(r+s)/t.height],[h,d,Math.max(0,u-h),Math.max(0,p-d)])}decorations(e,t,i,r,n,s=e.underlineColor,o){128&e.attributes&&this.solid(t,i+10,r,1,n,o),256&e.attributes&&this.solid(t,i+1,r,1,n,o);const a=e.underlineStyle||(8&e.attributes?1:0);this.underline(a,t,i,r,ye(s),o)}underline(e,t,i,r,n,s){if(1===e)this.solid(t,i+18,r,1,n,s);else if(2===e)this.solid(t,i+16,r,1,n,s),this.solid(t,i+18,r,1,n,s);else if(3===e)for(let e=0;e({placement:e,order:t,z:"sixel"===e.kind?-1:e.z})).sort((e,t)=>e.z-t.z||e.order-t.order);for(const e of o)e.z<-1073741824&&this.placement(e.placement);for(let i=0;i=-1073741824&&e.z<0&&this.placement(e.placement);for(let s=0;s=0&&this.placement(e.placement);const a=t.history?.selection;if("valid"===a?.status)for(const s of a.ranges){const o=[s.startColumn*ce,s.row*he,(s.endColumn-s.startColumn)*ce,he];this.solid(...o,ye(n.selectionBackground));const a=s.row*this.columns;let l=a+s.startColumn;for(;l>a&&0===e[l]?.width;)l--;for(let c=l;c=0&&l.x=0&&l.ye!==s?.rowIds[t]))return!1;if(t.hyperlinks.length!==r.hyperlinks.length||t.hyperlinks.some((e,t)=>{const i=r.hyperlinks[t];return e.row!==i.row||e.startColumn!==i.startColumn||e.endColumn!==i.endColumn||e.uri!==i.uri}))return!1;if(e.length!==i.length)return!1;for(let t=0;tNumber.isSafeInteger(e)&&e>=0)||!Array.isArray(r)||!["solid","dashed"].includes(s))throw new Error("Invalid link decorations");const o=this.presented?.metadata;if(!this.enabled||n||!o||e!==o.revision||t!==this.generation||i<=this.lastSerial)return!1;if(r.length>o.columns*o.rows||r.some(e=>!e||!Number.isInteger(e.row)||e.row<0||e.row>=o.rows||!Number.isInteger(e.startColumn)||!Number.isInteger(e.endColumn)||e.startColumn<0||e.endColumn<=e.startColumn||e.endColumn>o.columns))throw new Error("Invalid link decoration ranges");const a=new Uint8Array(o.columns*o.rows),l="dashed"===s?5:1;for(const e of r)a.fill(l,e.row*o.columns+e.startColumn,e.row*o.columns+e.endColumn);return this.lastSerial=i,this.decorations=this.pending={revision:e,generation:t,serial:i,mask:a},!0}submission(){return{mask:this.decorations?.mask,acknowledgement:this.pending}}acknowledge(e,t){if(e&&e===this.pending&&!t&&this.enabled&&e.generation===this.generation&&e.revision===this.presented?.metadata.revision)return this.pending=void 0,{revision:e.revision,generation:e.generation,serial:e.serial}}}}}),et=i({"src/marker-pages.ts"(){G(),Se=class{#d;get pending(){return void 0!==this.#d}reset(){this.#d=void 0}accept(e){if(!e?.markerPage)return this.reset(),e;const{markers:t=[],markerPage:i,...r}=e,n=JSON.stringify(r);0===i.offset&&(this.#d={revision:i.revision,total:i.total,signature:n,markers:[],ids:new Set,bytes:0});const s=this.#d;if(!s||s.revision!==i.revision||s.total!==i.total||s.markers.length!==i.offset||s.signature!==n)throw new Error("Marker pages do not describe the same contiguous history snapshot");if(s.bytes+=(new TextEncoder).encode(JSON.stringify(t)).byteLength,s.bytes>R.metadataBytes)throw new Error("Retained marker inventory exceeds the 8 MiB client budget");for(const e of t){if(s.ids.has(e.id))throw new Error("Duplicate marker ID across inventory pages");s.ids.add(e.id),s.markers.push(e)}return s.markers.length{ze=!1,Re||Pe||Le||Ae||Ke.pending||Ve||(Fe=async function(){if(!Ue||!qe||!Ie)return;Ue=!1,Pe=!0;const e=$e;try{e&&He.prepare(je,qe);const t=He.submission();Ie.resize(qe.columns,qe.rows,_e);const i=Math.floor(performance.now()/600)%2==0,r=Ie.render(je,qe,i,t.mask,Ye);if(await Ie.idle(),Le||Ae)return;if(Be=i,Je.presentations++,Je.rendererCpuMs=r.cpuMs,Je.quads=r.quads,Je.drawCalls=r.drawCalls,Je.warnings=Ie.canvasLimited?[...qe.warnings,`Canvas resolution capped by the GPU; terminal grid remains ${qe.columns}x${qe.rows}`]:qe.warnings,e){$e=void 0,Oe=!1,Je.frames++,e.full&&Je.fullFrames++,Je.revision=e.revision,Je.changedCells+=e.changedCells,Je.lastChangedCells=e.changedCells,Je.captureMs=qe.stats.captureMs,Je.workloadBytes=qe.stats.workloadBytes,Je.outputBatches=qe.stats.outputBatches,Je.serverElapsedMs=qe.stats.elapsedMs,Je.columns=qe.columns,Je.rows=qe.rows,Je.mouseTracking=qe.mouseTracking,Je.peer=qe.peer,Je.history=qe.history;const t=function(e,t,i){const r=[];for(let n=0;nR.frameBytes)throw new Error("Invalid or oversized binary frame");const t=new DataView(e);let i=0;const r=e=>{if(e<0||e>t.byteLength-i)throw new Error("Truncated HWT1 frame")},n=()=>{r(4);const e=t.getUint32(i,!0);return i+=4,e};if(827610952!==n())throw new Error("Unsupported frame magic (expected HWT1)");const s=U(n(),"metadata length",2,R.metadataBytes);r(s);const o=JSON.parse(P.decode(new Uint8Array(e,i,s)));i+=s,$(o);const a=o.columns*o.rows,l=U(n(),"changed cell count",0,a);if(l>Math.floor((t.byteLength-i)/22))throw new Error("Truncated cell records");if(o.full&&l!==a)throw new Error("Incomplete full frame");const c=[],h=new Set;for(let s=0;s=a||h.has(s))throw new Error("Invalid or duplicate cell index");h.add(s);const l=n(),d=n(),u=n();"indexed-v1"===o.colorEncoding&&(E(l),E(d),E(u,!0));const p=t.getUint16(i,!0),f=t.getUint8(i+2),m=t.getUint8(i+3),g=t.getUint16(i+4,!0);if(i+=6,m>5)throw new Error("Unsupported underline style");r(g);const w=P.decode(new Uint8Array(e,i,g));i+=g,c.push({index:s,foreground:l,background:d,underlineColor:u,attributes:p,width:f,underlineStyle:m,text:w})}if(o.images.reduce((e,t)=>e+t.byteLength,0)!==t.byteLength-i)throw new Error("Image payload length mismatch");const d=o.images.map(t=>{const r=new Uint8Array(e,i,t.byteLength);return i+=t.byteLength,{...t,bytes:r}});return{metadata:o,cells:c,images:d}}(e),i=t.metadata;if(!Xe&&i.colorEncodings?.includes("indexed-v1")&&(Xe=!0,rt({type:"colorEncoding",value:"indexed-v1"})),!i.full&&(Ve||i.baseRevision!==Ne||i.revision<=Ne||!qe||i.columns!==qe.columns||i.rows!==qe.rows||(i.colorEncoding??null)!==(qe.colorEncoding??null)))return Je.discardedFrames++,Oe=!1,Ke.reset(),Ve=!0,Ue=!1,$e=void 0,rt({type:"ack",revision:i.revision}),rt({type:"resync"}),void it(`Revision mismatch (local ${Ne}, base ${i.baseRevision}); requesting a full frame`);if(await Fe,Le||Ae)return;if(await Ie.idle(),await Ie.updateImages(t.images,i.retainedImages),Le||Ae)return;const r=performance.now(),n=i.full?new Array(i.columns*i.rows):je.slice();for(const e of t.cells)n[e.index]=e;Ie.prepareGlyphs(n),je=n,qe=i,Ne=i.revision;const s=Ke.pending&&!i.full,o=Ke.accept(i.history);if(Ve=!1,$e={revision:i.revision,full:i.full||s&&!!$e?.full,changedCells:t.cells.length+(s?$e?.changedCells??0:0)},De=je.some(e=>e&&16&e.attributes&&!(64&e.attributes))||i.cursor.visible&&(0===i.cursor.shape||i.cursor.shape%2==1),Je.preparationCpuMs=performance.now()-r,void 0===o)return Ue=!1,Oe=!1,void rt({type:"ack",revision:i.revision});i.history=o,Ue=!0}finally{Re=!1,Ue&&ot()}}}var lt=i({"src/terminal-worker.ts"(){G(),Te(),Qe(),et(),s(),M(),Le=!1,Ae=!1,Re=!1,Pe=!1,Oe=!1,ze=!1,Ue=!1,De=!1,Be=!0,je=[],Ne=0,Fe=Promise.resolve(),He=new Ee,Ke=new Se,Ve=!1,Xe=!1,Ye=x(f),Je={revision:0,fullFrames:0,frames:0,presentations:0,changedCells:0,lastChangedCells:0,discardedFrames:0,imageCount:0,textureBytes:0,atlasGlyphs:0,atlasBytes:0,bytesReceived:0,imageUploadBytes:0,imagePayloadBytes:0,gpu:"initializing",connected:!1,warnings:[],fps:0,receivedKBps:0,workloadMBps:0,captureMs:0,rendererCpuMs:0,preparationCpuMs:0,workloadBytes:0,outputBatches:0,serverElapsedMs:0},Ze={time:performance.now(),presentations:0,bytes:0,workload:0},self.addEventListener("error",e=>{e.preventDefault(),st(e.error||new Error(e.message))}),self.addEventListener("unhandledrejection",e=>{e.preventDefault(),st(e.reason)}),self.addEventListener("message",e=>{const t=e.data;if("init"===t.type)(async function(e){if(Ie||Me)throw new Error("Worker is already initialized");if(Ye=x(v(void 0===e.palette?f:e.palette)),"function"!=typeof self.requestAnimationFrame)throw new Error("This browser does not support requestAnimationFrame in a dedicated OffscreenCanvas worker");if(it("Loading terminal font and initializing renderer..."),Ie=await ge.create(e.canvas,e.scale,st,e.font,e.renderer),Le||Ae)return void Ie?.dispose();Je.gpu="ready",Je.backingScale=e.scale,nt();const t="webgpu"===Ie.backend.kind?"WebGPU":"WebGL2";Ie.fallbackReason&&it(`Using WebGL2: ${Ie.fallbackReason}`),it(`${t} ready. Attaching terminal view...`);const i=new URL(e.url);if(!["ws:","wss:"].includes(i.protocol))throw new Error("The terminal WebSocket URL must use ws: or wss:");(Me=new WebSocket(i)).binaryType="arraybuffer",Me.addEventListener("open",()=>{Le||Ae||(Je.connected=!0,self.postMessage({type:"connected"}),it(`Connected · ${t} worker · server-authoritative cells and graphics`,"ready"),nt())}),Me.addEventListener("message",e=>{e.data instanceof ArrayBuffer?at(e.data).catch(st):st(new Error(`Expected binary HWT1 frame, received ${String(e.data).slice(0,200)}`))}),Me.addEventListener("close",e=>{Je.connected=!1,Le||Ae||(Je.gpu="stopped",Je.fps=0,Ae=!0,clearInterval(Ge),clearInterval(We),Ie?.dispose(),self.postMessage({type:"closed",details:{code:e.code,reason:e.reason,wasClean:e.wasClean}}),it(`View disconnected (${e.code}${e.reason?`: ${e.reason}`:""}). Attach another view to reconnect.`,"error"),nt())}),Ge=setInterval(()=>{const e=performance.now(),t=(e-Ze.time)/1e3;Je.fps=(Je.presentations-Ze.presentations)/t,Je.receivedKBps=(Je.bytesReceived-Ze.bytes)/t/1e3,Je.workloadMBps=Math.max(0,Je.workloadBytes-Ze.workload)/t/1e6,Ze={time:e,presentations:Je.presentations,bytes:Je.bytesReceived,workload:Je.workloadBytes},nt()},1e3),We=setInterval(()=>{const e=Math.floor(performance.now()/600)%2==0;De&&e!==Be&&ot()},100)})(t).catch(st);else if("stop"===t.type)Ae=!0,clearInterval(Ge),clearInterval(We),Me?.close(1e3,"View detached"),Ie?.dispose(),self.close();else if("viewport"!==t.type||Le||Ae)if("command"!==t.type||Le||Ae)if("palette"!==t.type||Le||Ae)if("linkDetection"!==t.type||Le||Ae){if("linkDecorations"===t.type&&!Le&&!Ae)try{He.accept(t.revision,t.generation,t.serial,t.ranges,Re||Oe||!!$e,t.underlineStyle)&&ot()}catch(e){st(e)}}else try{if(!He.configure(t.enabled,t.generation))return;if(!Re&&!Pe&&!$e){const e=He.snapshot();e&&self.postMessage({type:"linkSnapshot",generation:He.generation,snapshot:e})}ot()}catch(e){st(e)}else try{Ye=x(v(t.palette)),ot()}catch(e){st(e)}else rt(t.command);else{if(!Number.isFinite(t.width)||!Number.isFinite(t.height)||t.width<0||t.height<0)return void st(new Error("Invalid mounted viewport dimensions"));if(_e?.width===t.width&&_e?.height===t.height)return;_e={width:t.width,height:t.height},ot()}})}}),ct={};function ht(e){let t=e.length;const i={")":"(","]":"[","}":"{"};for(;t>0;){const r=e[t-1];if(!".,;:!?".includes(r)){if(i[r]){const n=e.slice(0,t);if(n.split(r).length>n.split(i[r]).length){t--;continue}}break}t--}return e.slice(0,t)}function dt(e){try{const t="builtin"in e.rule?e.rule.builtin:void 0;let i;if("source"in e.rule){const t=e.rule.flags.replaceAll("g","");i=new RegExp(e.rule.source,`${t}g`)}else{const t={url:String.raw`https?:\/\/[^\s\0<>"'\x60]+`,uri:String.raw`[A-Za-z][A-Za-z0-9+.-]*:[^\s\0<>"'\x60]+`,absolutePath:String.raw`(?:[A-Za-z]:[\\/]|/)[^\s\0<>"'\x60]*`,homePath:String.raw`~/[^\s\0<>"'\x60]+`};i=new RegExp(t[e.rule.builtin],"giu")}let r=0,n=0,s=0,o=0;const a=[];for(const l of e.chunks){if(s+=l.text.length,l.text.length>F.chunk||s>F.totalText)return{id:e.id,error:"limit",message:"Link scan text limit exceeded"};i.lastIndex=0;const c=[];let h;for(;null!==(h=i.exec(l.text));){if(++n>F.totalText||r>=F.matches)return{id:e.id,error:"limit",message:"Link match limit exceeded"};if(!h[0].length){const e=l.text.codePointAt(i.lastIndex);i.lastIndex+=(i.unicode||i.unicodeSets)&&void 0!==e&&e>65535?2:1;continue}let s=h[0];if(t){const e=l.text[h.index-1];if(void 0!==e&&!/[\s([{"'<>=]/u.test(e))continue;if("uri"===t&&/^[a-z]:[\\/]/iu.test(s))continue;if(s=ht(s),!s||"absolutePath"===t&&("/"===s||/^[a-z]:[\\/]$/iu.test(s)))continue;if("url"===t)try{if(!new URL(s).hostname)continue}catch{continue}}const a={index:h.index,text:s,captures:h.slice(1),groups:{...h.groups}};if(o+=X(a),o>F.resultText)return{id:e.id,error:"limit",message:"Link result text limit exceeded"};c.push(a),r++}a.push({key:l.key,matches:c})}return{id:e.id,results:a}}catch(t){return{id:e.id,error:"worker",message:t instanceof Error?t.message:String(t)}}}((t,i)=>{for(var r in i)e(t,r,{get:i[r],enumerable:!0})})(ct,{scanLinks:()=>dt});var ut=i({"src/link-detection-worker.ts"(){V(),ie(),"undefined"!=typeof self&&"function"==typeof self.postMessage&&self.addEventListener("message",e=>{self.postMessage(dt(e.data))})}});var pt=class{x=0;y=0;reset(){this.x=this.y=0}take(e,t,i){const r=t.height/i;if(!(r>0))return{x:0,y:0};const n=1===e.deltaMode?1:2===e.deltaMode?i:1/r;this.x+=e.deltaX*n,this.y+=e.deltaY*n;const s=Math.trunc(this.x),o=Math.trunc(this.y);return this.x-=s,this.y-=o,{x:Math.max(-32,Math.min(32,s)),y:Math.max(-32,Math.min(32,o))}}},ft=class{owner=null;mode="character";endpoint=null;begin(e,t,{tracking:i,historical:r,readOnly:n,selection:s}){if(this.owner)return null;const o=r||n||!i||e.shiftKey;if(o&&0!==e.button)return null;if(this.owner=o?"local":"app",!o)return{owner:"app"};const a=!!e.shiftKey&&!!s&&["valid","pending"].includes(s.status)&&!1!==s.canExtend;return this.mode=a&&s?s.mode:e.altKey?"rectangle":e.detail>=3?"line":2===e.detail?"word":"character",this.endpoint={x:t.x,y:t.y},{owner:this.owner,mode:this.mode,extend:a}}move(e){return"local"!==this.owner?null:(this.endpoint={x:e.x,y:e.y},{...this.endpoint})}scrollPoint(e){if("local"===this.owner)return{x:"rectangle"===this.mode&&this.endpoint?this.endpoint.x:e.x,y:e.y}}wheelOwner(e,{tracking:t,historical:i,readOnly:r}){return this.owner??(i||r||!t||e.shiftKey?"local":"app")}end(){this.owner=null}};s();var mt=Object.freeze({Continue:"continue",Consume:"consume",Application:"application",Browser:"browser"}),gt=Object.freeze({CopySelection:"copySelection",PasteClipboard:"pasteClipboard",CopyOrPaste:"copyOrPaste",ClearSelection:"clearSelection",ScrollToLive:"scrollToLive",ScrollLines:"scrollLines"}),wt=new Set(["l","t","v","w","+","-","=","0"]),yt=new Set(["F1","F3","F5","F6","F7","F10","F11","F12"]),vt=new Set(["Tab","Enter","Backspace","Delete","Escape","ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Insert",...Array.from({length:12},(e,t)=>`F${t+1}`)]),bt=new Set(Object.values(mt));function kt(){return[{id:"clipboard.copy-key",match:e=>"key"===e.type&&"c"===e.key.toLowerCase()&&!e.alt&&(e.meta&&!e.ctrl||e.ctrl&&e.shift&&!e.meta),action:gt.CopySelection},{id:"browser.shortcuts",match:e=>"key"===e.type&&(e.meta||yt.has(e.key)||e.ctrl&&("Tab"===e.key||"F4"===e.key||wt.has(e.key.toLowerCase())||e.shift&&1===e.key.length)||e.alt&&!e.ctrl),route:mt.Browser},{id:"terminal.keys",match:e=>"key"===e.type&&(vt.has(e.key)||e.ctrl&&1===e.key.length),route:mt.Application},{id:"clipboard.context-click",match:(e,t)=>"pointer"===e.type&&"right"===e.button&&!e.meta&&(!t.mouseCaptured||e.shift||t.historical||t.readOnly),action:gt.CopyOrPaste}]}function xt(e,t){if("function"!=typeof e||"AsyncFunction"===e.constructor.name)throw new TypeError(`${t} must be a synchronous function`)}var Et=class{#u;#p;#f;constructor({inputBindings:e=[],onInput:t,actions:i={}}={}){if(!Array.isArray(e))throw new TypeError("inputBindings must be an array");if(!i||"object"!=typeof i||Array.isArray(i))throw new TypeError("actions must be an object");this.#f=new Set(Object.values(gt));for(const[e,t]of Object.entries(i)){if(!e||this.#f.has(e)||"function"!=typeof t)throw new TypeError(`Invalid or reserved action: ${e}`);this.#f.add(e)}void 0!==t&&xt(t,"onInput"),this.#p=t;const r=kt(),n=new Set,s=e.map(e=>{if(!e||"string"!=typeof e.id||!e.id||n.has(e.id))throw new TypeError("Each input binding needs a unique, nonempty id");if(n.add(e.id),!0===e.remove){if(!r.some(t=>t.id===e.id))throw new TypeError(`Cannot remove unknown default binding: ${e.id}`);if(Object.keys(e).some(e=>!["id","remove"].includes(e)))throw new TypeError(`Removed binding ${e.id} must contain only id and remove`);return null}return xt(e.match,`Binding ${e.id}.match`),void 0!==e.when&&xt(e.when,`Binding ${e.id}.when`),this.#m(e),Object.freeze({...e})}).filter(e=>null!==e);this.#u=[...s,...r.filter(e=>!n.has(e.id))]}get bindings(){return this.#u.map(e=>({...e}))}#m(e){if(Tt(e))return{route:e};if(!r(e)||"function"==typeof e.then)throw new TypeError("Input routing must synchronously return a route or action");const t=Object.hasOwn(e,"action"),i=Object.hasOwn(e,"route");if(t===i)throw new TypeError("Specify exactly one action or route");if(i){if(!Tt(e.route))throw new TypeError(`Unknown input route: ${String(e.route)}`);return{route:e.route}}if(!(function(e){return"function"==typeof e}(e.action)||"string"==typeof e.action&&this.#f.has(e.action)))throw new TypeError(`Unknown terminal action: ${String(e.action)}`);return{action:e.action,args:e.args}}resolve(e,t){const i=this.#p?.(e,t);if(void 0!==i){const e=this.#m(i);if(e.route!==mt.Continue)return e}for(const i of this.#u){const r=i.match(e,t);if("boolean"!=typeof r)throw new TypeError(`Binding ${i.id}.match must return a boolean`);if(!r)continue;if(i.when){const r=i.when(t,e);if("boolean"!=typeof r)throw new TypeError(`Binding ${i.id}.when must return a boolean`);if(!r)continue}const n=this.#m(i);if(n.route!==mt.Continue)return n}return{route:mt.Continue}}};function Tt(e){return"string"==typeof e&&[...bt].some(t=>t===e)}function Ct(e){return{ctrl:!!e.ctrlKey,alt:!!e.altKey,shift:!!e.shiftKey,meta:!!e.metaKey}}var St=[["left",1],["middle",4],["right",2]],It=["left","middle","right"];function Mt(e,t,i,r={}){const n=new AbortController,s={signal:n.signal};let o=1,a=1,l=0,c=null;const h=new Set;let d,u,p,f;const m=new pt,g=new ft;let w,y,v,b,k,x,E,T,C,S,I,M={count:0,time:0,x:-1,y:-1};const L=e.title,A=e.style.cursor,R=()=>({tracking:l,...r.state?.()});function P(t,i=!1){return function(e,t,i,r,n=!1){if(!(t.width>0&&t.height>0))return null;const s=Math.floor((e.clientX-t.left)*i/t.width),o=Math.floor((e.clientY-t.top)*r/t.height);return!n&&(s<0||o<0||s>=i||o>=r)?null:{x:Math.max(0,Math.min(i-1,s)),y:Math.max(0,Math.min(r-1,o)),shift:!!e.shiftKey,alt:!!e.altKey,ctrl:!!e.ctrlKey}}(t,e.getBoundingClientRect(),o,a,i)}function O(t=S){if(!r.hyperlink)return;S=t;const i=C&&P(C),n=i?r.hyperlink(i):null;e.title=n?`${n.target}\n${"click"===n.activation?"Click":"Ctrl/Cmd+click"} to activate link`:L,e.style.cursor=n&&t&&("click"===n.activation||t.ctrlKey||t.metaKey)&&!t.altKey&&!t.shiftKey?"pointer":A,T&&T.link.id!==n?.id&&(T.dragged=!0),I!==n?.id&&(I=n?.id,r.hoverHyperlink?.(n))}function z(){if(clearTimeout(v),v=void 0,"local"!==g.owner||!y)return;const t=e.getBoundingClientRect(),i=y.clientY=t.bottom?y.clientY-t.bottom+1:0;i&&(v=setTimeout(()=>{if(v=void 0,!y)return;const e=P(y,!0);if("local"===g.owner&&e){b&&(b.dragged=!0),M.count=0;const n=Math.sign(i)*Math.min(8,Math.max(1,Math.ceil(Math.abs(i)/(t.height/a))));r.scroll?.(n,g.scrollPoint(e)),z()}},60))}function U(){void 0!==f&&cancelAnimationFrame(f),f=void 0,u&&t(u),u=void 0}function D(e,i){for(const[r,n]of St){const s=0!==(e.buttons&n);s!==h.has(r)&&(U(),s?h.add(r):h.delete(r),(s||9!==l)&&t({type:"mouse",action:s?"down":"up",button:r,...i}),p=void 0)}}function B(i=!0,n=!0){if(r.end?.(n),clearTimeout(v),v=void 0,y=void 0,b=void 0,k=void 0,T=void 0,g.end(),x=void 0,i?U():(void 0!==f&&cancelAnimationFrame(f),f=void 0,u=void 0),i&&9!==l&&d)for(const e of h)t({type:"mouse",action:"up",button:e,...d});h.clear();const s=c;c=null,null!==s&&e.hasPointerCapture(s)&&e.releasePointerCapture(s),p=void 0,m.reset(),w=void 0}return e.addEventListener("pointerdown",t=>{if(t.defaultPrevented&&null===c)return;if("mouse"!==t.pointerType||![0,1,2].includes(t.button))return;const n=P(t);if(!n)return;if(null!==c)return T&&(T.dragged=!0),x!==mt.Browser&&t.preventDefault(),void("app"!==g.owner&&x!==mt.Application||c!==t.pointerId||D(t,n));i();const s={type:"pointer",button:It[t.button],point:Object.freeze({...n}),...Ct(t)},o=r.resolve?.(s)??{route:mt.Continue};let a=void 0!==o.action?mt.Consume:o.route;if(a===mt.Continue&&0===t.button&&!t.altKey&&!t.shiftKey){const e=r.hyperlink?.(n);e&&("click"===e.activation||t.ctrlKey||t.metaKey)&&(T={link:e,input:s,x:t.clientX,y:t.clientY,dragged:!1},a=mt.Consume)}if(E=2===t.button?a:void 0,T&&(E=mt.Consume),C=t,O(t),a!==mt.Continue)return k=b=void 0,M.count=0,x=a,c=t.pointerId,d=n,e.setPointerCapture(c),a!==mt.Browser&&t.preventDefault(),a===mt.Application?U():(void 0!==f&&cancelAnimationFrame(f),f=void 0,u=void 0),a===mt.Application&&D(t,n),void(void 0!==o.action&&r.execute?.(o,s));if(t.preventDefault(),t.metaKey)return;const l=performance.now();M.count=l-M.time<500&&n.x===M.x&&n.y===M.y?M.count%3+1:1,M={count:M.count,time:l,x:n.x,y:n.y};const h=g.begin({button:t.button,shiftKey:t.shiftKey,altKey:t.altKey,detail:t.detail||M.count},n,R());h&&(c=t.pointerId,y=t,d=n,k=void 0,e.setPointerCapture(c),"local"===h.owner?(void 0!==f&&cancelAnimationFrame(f),f=void 0,u=void 0,p=void 0,b={point:n,mode:h.mode,extend:h.extend,dragged:!1},r.begin?.(n,h)):D(t,n))},s),e.addEventListener("pointermove",e=>{if("mouse"!==e.pointerType)return;if(null===c&&e.buttons)return;if(null!==c&&c!==e.pointerId)return;C=e,O(e),T&&Math.hypot(e.clientX-T.x,e.clientY-T.y)>=4&&(T.dragged=!0);const t=P(e,null!==c);if(!t)return;if(d=t,x&&x!==mt.Application)return;if("local"===g.owner){y=e;const i=g.endpoint,n=g.move(t);return n&&i&&(n.x!==i.x||n.y!==i.y)&&(M.count=0,b&&(b.dragged=!0),r.extend?.(n)),void z()}if(!l||!x&&null===g.owner&&(R().historical||R().readOnly||e.shiftKey))return;if(c===e.pointerId&&D(e,t),e.metaKey||1003!==l&&(1002!==l||!h.size))return;const i={type:"mouse",action:"move",button:St.find(([e])=>h.has(e))?.[0]??"none",...t},n=JSON.stringify(i);n!==p&&(p=n,u=i,void 0===f&&(f=requestAnimationFrame(U)))},s),e.addEventListener("pointerup",e=>{if(c!==e.pointerId)return;if(T){e.preventDefault();const t=T,i=P(e),n=0===e.button&&0===e.buttons&&!t.dragged&&Math.hypot(e.clientX-t.x,e.clientY-t.y)<4&&i&&r.hyperlink?.(i)?.id===t.link.id;return 0===e.buttons?B(!1,!1):t.dragged=!0,void(n&&r.openHyperlink?.(t.link,t.input))}const t=P(e,!0)??d;if(x)return x===mt.Application&&t&&D(e,t),void(0===e.buttons&&B(x===mt.Application));if("local"===g.owner){if(t&&g.endpoint&&(t.x!==g.endpoint.x||t.y!==g.endpoint.y)){b&&(b.dragged=!0);const e=g.move(t);e&&r.extend?.(e)}const e=b;return B(!1,!1),void(k=e)}t&&D(e,t),h.size||B()},s),e.addEventListener("click",e=>{const t=k;if(k=void 0,!t||t.dragged||!Number.isInteger(e.detail)||e.detail<1)return;M.count=e.detail;const i=e.detail>=3?"line":2===e.detail?"word":"character";t.extend||"rectangle"===t.mode||i===t.mode||r.begin?.(t.point,{mode:i,extend:!1})},s),e.addEventListener("pointercancel",()=>B(),s),e.addEventListener("pointerleave",()=>{C=void 0,O()},s),e.addEventListener("lostpointercapture",()=>{null!==c&&B()},s),window.addEventListener("blur",()=>{B(),C=void 0,O()},s),window.addEventListener("keydown",e=>O(e),{...s,capture:!0}),window.addEventListener("keyup",e=>O(e),{...s,capture:!0}),e.addEventListener("contextmenu",e=>{if(void 0!==E&&E!==mt.Continue){const t=E;return E=void 0,void(t!==mt.Browser&&e.preventDefault())}(l||"local"===g.owner)&&e.preventDefault()},s),e.addEventListener("wheel",i=>{T&&(T.dragged=!0);const n={type:"wheel",deltaX:i.deltaX,deltaY:i.deltaY,deltaMode:i.deltaMode,point:P(i),...Ct(i)},s=null===c?r.resolve?.(n)??{route:mt.Continue}:{route:x??mt.Continue};if(s.route===mt.Browser)return;if(void 0!==s.action||s.route===mt.Consume)return i.preventDefault(),void(void 0!==s.action&&r.execute?.(s,n));if(s.route!==mt.Application&&(i.ctrlKey||i.metaKey))return;const o=s.route===mt.Application?"app":g.wheelOwner(i,R());if("app"===o&&9===l)return;const h=P("local"===g.owner&&y?y:i,"local"===g.owner);if(!h)return;i.preventDefault(),U();const d=e.getBoundingClientRect();w!==o&&m.reset(),w=o;const{x:u,y:p}=m.take(i,d,a);if("local"===o)return void(p&&(b&&(b.dragged=!0),M.count=0,r.scroll?.(p,g.scrollPoint(h))));const f=[[p,"wheelUp","wheelDown"],[u,"wheelLeft","wheelRight"]];for(const[e,i,r]of f)e&&t({type:"mouse",action:"wheel",button:e<0?i:r,count:Math.min(32,Math.abs(e)),...h})},{...s,passive:!1}),{update(e,t,i){o===e&&a===t&&l===i||(d&&(d={...d,x:Math.min(d.x,e-1),y:Math.min(d.y,t-1)}),o===e&&a===t||B(0!==i),o=e,a=t,l=i),O()},refresh(){O()},cancel(){B()},dispose(){B(!1),n.abort(),e.title=L,e.style.cursor=A}}}function Lt(e){const t=new URL(import.meta.url);return t.hash=`hex1b-${e}-worker`,t}c(),u(),g(),M();var At=8,Rt=32;function Pt(e,t){if(!Number.isInteger(e)||e<1||e>300||!Number.isInteger(t)||t<1||t>100)throw new RangeError("Requested grid must be 1-300 columns by 1-100 rows");return{columns:e,rows:t}}function Ot(e={mode:"auto"},t=16){if(!e||!["auto","fixed"].includes(e.mode))throw new TypeError("Sizing mode must be 'auto' or 'fixed'");const i=e.fontSize??t;if(!Number.isInteger(i)||i<8||i>32)throw new RangeError("Font size must be an integer from 8 to 32");return"fixed"===e.mode?{mode:"fixed",fontSize:i,...Pt(e.columns,e.rows)}:{mode:"auto",fontSize:i}}var zt="Text history is not available for this terminal view",Ut="Selection expired: its text was changed, evicted, reset, or resized. Select the text again.",Dt="Selection changed before copy completed. Copy the new selection again.",Bt=class{#e;#t;#g=null;#o=0;#w=0;#y=0;#v=0;#b="character";#k="clear";#x;#E;constructor(e,t=()=>{}){this.#e=e,this.#t=t}get viewport(){if(!this.#g)return{available:!1,following:!0,pending:!1};const{selection:e,copy:t,markers:i,markerResult:r,markerPage:n,viewportError:s,...o}=this.#g;return{...o,rowIds:[...o.rowIds],available:!0,revision:this.#o,...s&&o.requestId>=this.#v?{navigationError:s}:{},pending:o.requestId({...e})),text:null,message:"Resolving selection…"}:{...e,canExtend:"valid"===e.status,ranges:e.ranges.map(e=>({...e})),revision:this.#o,message:"invalidated"===e.status?Ut:""}}accept(e,t){if(t<=this.#o)return!1;const i=this.#g?.generation;this.#g=e,this.#o=t,(!e||i&&i!==e.generation||"invalidated"===e.selection.status)&&(this.endGesture(!0),this.#T(new Error(Ut)));const r=this.#x;if(r&&e?.copy?.requestId===r.requestId&&("valid"!==e.copy.status||"valid"!==e.selection.status||e.selection.requestId!==r.selectionRequestId||e.generation!==r.generation||this.#y>r.selectionRequestId||e.copy.text!==e.selection.text?this.#T(new Error(Ut)):(clearTimeout(r.timer),this.#x=void 0,r.resolve(e.copy.text))),this.#E&&!this.viewport.pending){const e=this.#E;this.#E=void 0,this.extend(e)}return this.#t(),!0}#C(){if(!this.#g)throw new Error(zt);return this.#g}#S(e){const t=this.#C();if(!e||!Number.isInteger(e.x)||e.x<0||e.x>1023||!Number.isInteger(e.y)||e.y<0||"string"!=typeof t.rowIds[e.y]||!t.rowIds[e.y].length)throw new Error("Terminal viewport is not ready for that selection point. Wait for the next frame and try again.");return t.rowIds[e.y]}#I(e=this.selection.mode,t="extend"){return this.#T(new Error(Dt)),this.#b=e,this.#k=t,this.#y=++this.#w,this.#y}begin(e,{mode:t,extend:i=!1}){const r=this.#S(e);this.#E=void 0;const n=this.#I(t,i?"extend":"start");this.#e({type:"selection",action:i?"extend":"start",mode:t,requestId:n,generation:this.#C().generation,rowId:r,column:e.x}),this.#t()}extend(e){if(this.#S(e),this.viewport.pending)return this.#T(new Error(Dt)),this.#b=this.selection.mode,this.#k="extend",this.#E={...e},void this.#t();this.begin(e,{mode:this.selection.mode,extend:!0})}scroll(e,t){if(this.#C(),!Number.isSafeInteger(e)||e<-2147483648||e>2147483647)throw new RangeError("Scroll delta must be a signed 32-bit integer");if(t&&this.#S(t),!e&&!t)return;t?this.#E=void 0:this.#M();const i=t?this.#I():++this.#w;this.#v=i,this.#e({type:"viewport",requestId:i,delta:e,...t?{extend:{row:t.y,column:t.x}}:{}}),this.#t()}live(){this.#C(),this.#M(),this.#v=++this.#w,this.#e({type:"viewport",live:!0,requestId:this.#v}),this.#t()}scrollTo(e){const t=this.#C();if(!Number.isSafeInteger(e)||e<0||e>2147483647)throw new RangeError("Scroll top must be a nonnegative 32-bit integer");this.#M(),this.#v=++this.#w,this.#e({type:"viewport",requestId:this.#v,top:Math.min(e,t.liveTop),generation:t.generation,originRowId:t.rowIds[0],originTop:t.top}),this.#t()}clear(){this.#C(),this.#E=void 0;const e=this.#I(this.selection.mode,"clear");this.#e({type:"selection",action:"clear",requestId:e}),this.#t()}endGesture(e=!1){e&&this.#E&&(this.#E=void 0,this.#t())}#M(){if(!this.#E)return;const e=this.#E;this.#E=void 0,this.scroll(0,e)}cancelCopy(e){this.#T(e)}copy(){const e=this.#C(),t=this.selection;if("valid"!==t.status)throw new Error(t.message||"Select text before copying.");this.#T(new Error("A newer copy request replaced this request."));const i=++this.#w,{promise:r,resolve:n,reject:s}=Promise.withResolvers(),o=setTimeout(()=>this.#T(new Error("Timed out resolving selection. Copy again.")),1e4);this.#x={requestId:i,selectionRequestId:t.requestId,generation:e.generation,resolve:n,reject:s,timer:o};try{this.#e({type:"copy",requestId:i,selectionRequestId:t.requestId,generation:e.generation})}catch(e){this.#T(e)}return r}#T(e){this.#x&&(clearTimeout(this.#x.timer),this.#x.reject(e),this.#x=void 0)}disconnect(){this.#T(new Error("Terminal view disconnected before copy completed.")),this.#E=void 0}};function qt(e=0){if("number"!=typeof e&&!r(e))throw new TypeError("Padding must be a number or per-edge object");const t=t=>{const i="number"==typeof e?e:void 0===e[t]?0:e[t];if("number"!=typeof i||!Number.isFinite(i)||i<0)throw new RangeError(`Padding ${t} must be finite nonnegative CSS pixels`);return i};return Object.freeze({top:t("top"),right:t("right"),bottom:t("bottom"),left:t("left")})}function jt(e,t,i){return{width:Math.max(0,e.width-t.left-t.right-(i&&"beside"===i.placement?i.width:0)),height:Math.max(0,e.height-t.top-t.bottom)}}function Nt(e,t,i,r,n,s){const o=jt(e,n,s),a=function(e,t,i,r){const n=i&&"auto"===r.mode?r.fontSize/16:1/0;return Math.max(0,Math.min(n,e.width/(t.columns*t.cellWidth),e.height/(t.rows*t.cellHeight)))}(o,t,i,r),l=t.columns*t.cellWidth*a,c=t.rows*t.cellHeight*a,h=Math.min(e.width,n.left)+(o.width-l)/2,d=Math.min(e.height,n.top)+(o.height-c)/2,u=s?Math.min(s.width,"overlay"===s.placement?l:Math.max(0,e.width-n.right-h-l)):0;return Object.freeze({width:e.width,height:e.height,content:Object.freeze({left:h,top:d,width:l,height:c}),scrollbar:s?Object.freeze({left:h+l-("overlay"===s.placement?u:0),top:d,width:u,height:c}):null,padding:n,cellWidth:l/t.columns,cellHeight:c/t.rows})}z(),s();var $t=/\b(?:var|env|attr|light-dark|currentcolor|inherit|initial|unset|revert|revert-layer|default|context-fill|context-stroke|accentcolor|accentcolortext|activetext|buttonborder|buttonface|buttontext|canvas|canvastext|field|fieldtext|graytext|highlight|highlighttext|linktext|mark|marktext|selecteditem|selecteditemtext|visitedtext|activeborder|activecaption|appworkspace|background|buttonhighlight|buttonshadow|captiontext|inactiveborder|inactivecaption|inactivecaptiontext|infobackground|infotext|menu|menutext|scrollbar|threeddarkshadow|threedface|threedhighlight|threedlightshadow|threedshadow|window|windowframe|windowtext)\b/i;function Ft(e,t){if(null===e||"object"!=typeof e||"[object Object]"!==Object.prototype.toString.call(e))throw new TypeError(`${t} must be an options object`)}function Gt(e){const t=function(e={}){let t;Ft(e,"scrollbar.appearance");const i=(e,i)=>{if(void 0===e)return;if("string"!=typeof e||e.length>256||!e.trim()||/\\|\/\*|--/.test(e)||$t.test(e))throw new TypeError(`${i} must be a concrete CSS color of 1-256 characters`);const r=e.trim();if(void 0===t&&(t="undefined"!=typeof OffscreenCanvas?new OffscreenCanvas(1,1).getContext("2d"):"undefined"!=typeof document?document.createElement("canvas").getContext("2d"):null),!t)throw new TypeError(`${i} requires a browser canvas to validate CSS colors`);t.fillStyle="#010203",t.fillStyle=r;const n=t.fillStyle;if(t.fillStyle="#040506",t.fillStyle=r,n!==t.fillStyle)throw new TypeError(`${i} must be a valid concrete CSS color`);return r},r=(e,t,r)=>{void 0!==e&&Ft(e,r);const{color:n,opacity:s=t}=e??{};if("number"!=typeof s||!Number.isFinite(s)||s<0||s>1)throw new RangeError(`${r}.opacity must be a finite number between 0 and 1`);return Object.freeze({color:i(n,`${r}.color`),opacity:s})},{track:n,thumb:s,markers:o}=e,a=r(o,1,"scrollbar.appearance.markers");return Object.freeze({track:r(n,.35,"scrollbar.appearance.track"),thumb:r(s,1,"scrollbar.appearance.thumb"),markers:Object.freeze({...a,errorColor:i(o?.errorColor,"scrollbar.appearance.markers.errorColor")})})}(e);return e=>{if(e.opacity<=0)return;const{context:i,track:r,thumb:n,colors:s}=e,o=i.globalAlpha*e.opacity;i.save();try{if(i.globalAlpha=o*t.track.opacity,i.fillStyle=t.track.color??s.track,r.width>0&&r.height>0&&i.fillRect(r.left,r.top,r.width,r.height),n.width>0&&n.height>0){const e=Math.min(2,n.width/4),r=n.width-2*e;i.globalAlpha=o*t.thumb.opacity,i.fillStyle=t.thumb.color??s.thumb,i.beginPath(),i.roundRect(n.left+e,n.top,r,n.height,Math.min(r,n.height)/2),i.fill()}i.globalAlpha=o*t.markers.opacity;for(const{marker:r,bounds:n,color:o}of e.markers)i.fillStyle=null!=r.exitCode&&0!==r.exitCode?t.markers.errorColor??o??s.error:t.markers.color??o??s.marker,r.color&&(i.fillStyle=r.color),n.width>0&&n.height>0&&i.fillRect(n.left,n.top,n.width,n.height);if(e.interaction.focused&&!e.interaction.dragging&&n.width>0&&n.height>0){i.globalAlpha=o,i.strokeStyle=t.thumb.color??s.thumb;const e=Math.min(1,n.width/2,n.height/2),r=n.width-e,a=n.height-e;i.lineWidth=e,i.beginPath(),i.roundRect(n.left+e/2,n.top+e/2,r,a,Math.min(r,a)/2),i.stroke()}}finally{i.restore()}}}var Wt=Gt();function _t(e){Wt(e)}var Ht=Object.freeze({track:"#202020",thumb:"#999999",marker:"#aaaaaa",error:"#eeeeee",prompt:"#666666","command-line":"#888888",executing:"#bbbbbb",success:"#999999",custom:"#dddddd"});function Kt(e,t){if("custom"===e.source)return t("custom");if(null!=e.exitCode&&0!==e.exitCode)return t("error");switch(e.phase){case"prompt":return t("prompt");case"commandLine":return t("command-line");case"executing":return t("executing");case"finished":return t(0===e.exitCode?"success":"marker");default:return t("marker")}}function Vt(e){if(!1===e)return!1;if(void 0!==e&&(null===e||"object"!=typeof e||Array.isArray(e)))throw new TypeError("scrollbar must be false or an options object");const t=e??{},i=(e,t,i,r=0,n=1/0)=>{const s=void 0===e?t:e;if(!Number.isFinite(s)||sn)throw new RangeError(`scrollbar.${i} must be a finite number between ${r} and ${n}`);return s};if(void 0!==t.placement&&"overlay"!==t.placement&&"beside"!==t.placement)throw new TypeError("scrollbar.placement must be overlay or beside");if(void 0!==t.markers&&"boolean"!=typeof t.markers)throw new TypeError("scrollbar.markers must be boolean");if(void 0!==t.render&&("function"!=typeof t.render||"AsyncFunction"===t.render.constructor.name))throw new TypeError("scrollbar.render must be a synchronous function");if(void 0!==t.tooltip&&!1!==t.tooltip&&("function"!=typeof t.tooltip||"AsyncFunction"===t.tooltip.constructor.name))throw new TypeError("scrollbar.tooltip must be false or a synchronous function");return Object.freeze({placement:t.placement??"overlay",width:i(t.width,12,"width",4,64),proximity:i(t.proximity,24,"proximity"),hideDelay:i(t.hideDelay,900,"hideDelay"),fadeDuration:i(t.fadeDuration,300,"fadeDuration"),markers:t.markers??!0,...t.render?{render:t.render}:{},...void 0!==t.tooltip?{tooltip:t.tooltip}:{}})}var Xt=(e,t,i)=>Math.max(t,Math.min(i,e)),Yt=(e,t,i,r=0)=>t>=e.left-r&&t=e.top-r&&iYt(e.bounds,t,i,3)).sort((e,t)=>Math.abs(e.bounds.top+e.bounds.height/2-i)-Math.abs(t.bounds.top+t.bounds.height/2-i)||(e.marker.idt.marker.id?1:0))[0]?.marker}var Zt=class{constructor(e){this.options=e,this.motion="function"==typeof matchMedia?matchMedia("(prefers-reduced-motion: reduce)"):void 0;const t=(e,t,i,r=!1)=>{e.addEventListener(t,i,{capture:!0,passive:r}),this.listeners.push(()=>e.removeEventListener?.(t,i,{capture:!0}))};t(e.element,"pointerdown",e=>this.pointerDown(e)),t(e.element,"pointermove",e=>this.pointerMove(e)),t(e.element,"pointerup",e=>this.pointerEnd(e,!1)),t(e.element,"pointercancel",e=>this.pointerEnd(e,!0)),t(e.element,"lostpointercapture",e=>this.pointerEnd(e,!0)),t(e.element,"pointerleave",()=>{this.near=this.hovered=!1,this.clearHover(),this.activity()}),t(e.element,"wheel",e=>this.wheel(e));for(const i of["mousedown","mouseup","click","dblclick","contextmenu"])t(e.element,i,e=>{this.contentPointers.size||(null!==this.pointer||this.hit(e)||this.suppressClick&&("click"===i||"mouseup"===i))&&(this.consume(e),"click"===i&&(this.suppressClick=!1))});if(t(e.accessibility,"keydown",e=>this.keyDown(e)),t(e.accessibility,"focus",()=>{this.focused=!0,this.activity()}),t(e.accessibility,"blur",()=>{this.focused=!1,this.activity()}),this.motion){const e=()=>this.invalidate();this.motion.addEventListener("change",e),this.listeners.push(()=>this.motion?.removeEventListener("change",e))}e.accessibility.setAttribute("role","scrollbar"),e.accessibility.setAttribute("aria-label","Terminal scrollback"),e.accessibility.setAttribute("aria-orientation","vertical"),e.accessibility.style.position="absolute",e.accessibility.style.background="transparent",this.refresh()}options;context;listeners=[];motion;configuration;frameId;fadeTimer;disposed=!1;failed=!1;dormant=!1;near=!1;hovered=!1;focused=!1;lastActivityAt=-1/0;pointer=null;dragOffset=null;contentPointers=new Set;suppressClick=!1;desired=null;queued=null;requestBaseline=0;generation;wheelRemainder=0;hoverPoint=null;refresh(){if(this.disposed)return;const e=this.options.getState();e.configuration!==this.configuration&&(this.cancel(),this.configuration=e.configuration,this.failed=!1,this.lastActivityAt=this.now()),e.viewport.generation!==this.generation&&(this.endGesture(),this.desired=this.queued=null,this.generation=e.viewport.generation),null===this.pointer&&null===this.queued&&e.viewport.available&&!e.viewport.pending&&(e.viewport.top===this.desired||e.viewport.requestId>this.requestBaseline)&&(this.desired=null);const t=this.geometry(e);if(!t)return this.cancel(),this.options.accessibility.hidden=!0,this.options.accessibility.tabIndex=-1,void this.clear();const{track:i}=t,r=this.options.accessibility;r.hidden=!1,r.tabIndex=0,r.matches?.(":focus")&&(this.focused=!0),Object.assign(r.style,{left:`${i.left}px`,top:`${i.top}px`,width:`${i.width}px`,height:`${i.height}px`}),r.setAttribute("aria-valuemin","0"),r.setAttribute("aria-valuemax",String(e.viewport.liveTop)),r.setAttribute("aria-valuenow",String(this.desired??e.viewport.top)),r.setAttribute("aria-valuetext",e.viewport.following?"Live terminal output":`Scrollback row ${this.desired??e.viewport.top}`),r.setAttribute("aria-busy",String(e.viewport.pending)),this.options.onMarkerHover?.(this.hoveredMarker()),this.schedule()}activity(){this.disposed||(this.dormant=!1,this.lastActivityAt=this.now(),void 0!==this.fadeTimer&&clearTimeout(this.fadeTimer),this.fadeTimer=void 0,this.schedule())}invalidate(){this.dormant=!1,this.refresh()}cancel(){this.clearHover(),this.endGesture(),this.contentPointers.clear(),this.desired=this.queued=null,this.near=this.hovered=this.focused=this.suppressClick=!1,this.dormant=!1,this.wheelRemainder=0,void 0!==this.frameId&&cancelAnimationFrame(this.frameId),void 0!==this.fadeTimer&&clearTimeout(this.fadeTimer),this.frameId=this.fadeTimer=void 0,this.options.accessibility.hidden=!0,this.options.accessibility.tabIndex=-1,this.options.accessibility.title="",this.clear()}dispose(){if(!this.disposed){this.disposed=!0,this.cancel();for(const e of this.listeners)e();this.options.accessibility.hidden=!0,this.options.accessibility.tabIndex=-1}}now(){return performance.now()}geometry(e=this.options.getState()){return e.connected&&e.configuration?function(e,t,i,r=null){const n=e.scrollbar;if(!n||n.width<=0||n.height<=0||!t.available||"main"!==t.buffer||t.liveTop<=0)return null;const s=Math.max(1,t.totalRows-t.liveTop),o=Math.min(n.height,Math.max(24,n.height*s/t.totalRows)),a=Object.freeze({left:n.left,top:n.top+(n.height-o)*Xt(r??t.top,0,t.liveTop)/t.liveTop,width:n.width,height:o}),l=i.filter(e=>e.buffer===t.buffer&&null!==e.row&&Number.isFinite(e.row)&&e.row>=0&&e.row{const i=Math.min(3,n.height);return Object.freeze({marker:Object.freeze({...e}),bounds:Object.freeze({left:n.left,top:n.top+(n.height-i)*e.row/Math.max(1,t.totalRows-1),width:n.width,height:i})})});return Object.freeze({track:Object.freeze({...n}),thumb:a,markers:Object.freeze(l)})}(e.layout,e.viewport,e.configuration.markers?e.markers:[],this.desired):null}point(e){const t=this.options.element.getBoundingClientRect(),i=this.options.getState().layout;return{x:t.width>0?(e.clientX-t.left)*i.width/t.width:-1/0,y:t.height>0?(e.clientY-t.top)*i.height/t.height:-1/0}}hit(e){const t=this.geometry(),i=this.point(e);return!!t&&Yt(t.track,i.x,i.y)}consume(e){e.preventDefault(),e.stopImmediatePropagation()}pointerDown(e){if(this.clearHover(),this.suppressClick=!1,!this.hit(e))return void this.contentPointers.add(e.pointerId);if(this.contentPointers.size)return;if(this.consume(e),0!==e.button||null!==this.pointer)return;const t=this.geometry(),i=this.point(e);this.pointer=e.pointerId;try{this.options.element.setPointerCapture(e.pointerId)}catch(e){return this.endGesture(),void this.report(e)}if(this.options.accessibility.setAttribute("data-pointer-active","true"),this.options.accessibility.focus({preventScroll:!0}),this.focused=!0,this.activity(),Yt(t.thumb,i.x,i.y))return void(this.dragOffset=i.y-t.thumb.top);const r=Jt(t.markers,i.x,i.y);if(r)return void this.jump(r.id);const n=this.options.getState().viewport;n.available&&this.navigate((this.desired??n.top)+(i.y0&&this.navigate((i.y-this.dragOffset-t.track.top)/r*e.liveTop)}return void this.activity()}if(this.contentPointers.size||0!==e.buttons)return void this.clearHover();const r=this.hoveredMarker();this.hoverPoint={clientX:e.clientX,clientY:e.clientY};const n=this.hoveredMarker();this.options.onMarkerHover?.(n),r?.marker.id!==n?.marker.id&&(this.dormant=!1,this.schedule());const s=this.options.getState().configuration,o=Yt(t.track,i.x,i.y,s?s.proximity:0),a=Yt(t.track,i.x,i.y);o===this.near&&a===this.hovered||(this.near=o,this.hovered=a,this.activity()),a&&this.consume(e)}clearHover(){this.hoverPoint=null,this.options.onMarkerHover?.(null)}hoveredMarker(){if(!this.hoverPoint||null!==this.pointer||this.contentPointers.size)return null;const e=this.geometry();if(!e)return null;const t=this.point(this.hoverPoint);if(!Yt(e.track,t.x,t.y))return null;const i=Jt(e.markers,t.x,t.y);return e.markers.find(e=>e.marker.id===i?.id)??null}pointerEnd(e,t){this.contentPointers.delete(e.pointerId),this.pointer===e.pointerId&&(this.consume(e),this.suppressClick=!t,this.endGesture(),t&&(this.desired=this.queued=null),this.activity())}endGesture(){const e=this.pointer;if(this.pointer=this.dragOffset=null,this.options.accessibility.setAttribute("data-pointer-active","false"),null!==e)try{this.options.element.hasPointerCapture(e)&&this.options.element.releasePointerCapture(e)}catch(e){this.report(e)}}wheel(e){if(this.contentPointers.size||!this.hit(e))return;this.consume(e);const{viewport:t,layout:i}=this.options.getState();if(!t.available||!Number.isFinite(e.deltaY))return;const r=Math.max(1,t.totalRows-t.liveTop),n=this.options.element.getBoundingClientRect(),s=i.height>0?n.height/i.height:1;this.wheelRemainder+=e.deltaY*(2===e.deltaMode?r:1===e.deltaMode?1:1/Math.max(1,i.cellHeight*s));const o=Math.trunc(this.wheelRemainder);this.wheelRemainder-=o,0!==o&&this.navigate((this.desired??t.top)+o),this.activity()}keyDown(e){const{viewport:t}=this.options.getState();if(!this.geometry()||!t.available||e.ctrlKey||e.metaKey)return;const i=this.desired??t.top,r=Math.max(1,t.totalRows-t.liveTop);if(e.altKey&&("ArrowDown"===e.key||"ArrowUp"===e.key)){this.consume(e);const t="ArrowDown"===e.key,r=this.geometry().markers.map(e=>e.marker).filter(e=>t?e.row>i:e.row(t?1:-1)*(e.row-i.row)||(e.idi.id?1:0));return r[0]&&this.jump(r[0].id),void this.activity()}if(e.altKey)return;const n={ArrowUp:i-1,ArrowDown:i+1,PageUp:i-r,PageDown:i+r,Home:0,End:t.liveTop};Object.hasOwn(n,e.key)&&(this.consume(e),this.navigate(n[e.key]),this.activity())}jump(e){this.desired=this.queued=null;try{this.options.scrollToMarker(e).catch(e=>this.report(e))}catch(e){this.report(e)}}navigate(e){const t=this.options.getState().viewport;t.available&&Number.isFinite(e)&&(this.requestBaseline=t.requestId,this.desired=this.queued=Math.round(Xt(e,0,t.liveTop)),this.schedule())}schedule(){this.disposed||void 0!==this.frameId||!this.geometry()||this.dormant&&null===this.queued||"function"!=typeof requestAnimationFrame||(this.frameId=requestAnimationFrame(()=>{this.frameId=void 0,this.paint()}))}clear(){this.context&&(this.context.save(),this.context.setTransform(1,0,0,1,0,0),this.context.clearRect(0,0,this.options.canvas.width,this.options.canvas.height),this.context.restore())}paint(){if(this.disposed)return;const e=this.options.getState();if(!this.geometry(e)||!e.configuration)return;if(null!==this.queued){const t=this.queued;this.queued=null;try{e.viewport.available&&t>=e.viewport.liveTop?this.options.scrollToLive():this.options.scrollToRow(t)}catch(e){this.desired=null,this.report(e)}}const t=this.geometry(e);if(this.options.onMarkerHover?.(this.hoveredMarker()),this.disposed||e.configuration!==this.options.getState().configuration)return;const{canvas:i,accessibility:r}=this.options;if(r.setAttribute("aria-valuenow",String(this.desired??e.viewport.top)),!this.failed&&void 0===this.context)try{if(this.context=i.getContext("2d"),!this.context)throw new Error("The scrollbar Canvas2D context is unavailable")}catch(e){this.failed=!0,this.endGesture(),this.report(e)}if(!this.context||this.failed)return;const n=this.context,s=Xt("number"==typeof devicePixelRatio?devicePixelRatio:1,1,3),o=Math.max(1,Math.round(e.layout.width*s)),a=Math.max(1,Math.round(e.layout.height*s));i.width!==o&&(i.width=o),i.height!==a&&(i.height=a),this.clear();const l=this.now(),c=this.near||this.focused||null!==this.pointer,h="overlay"===e.configuration.placement,d=this.motion?.matches??!1,u=function(e,t,i,r,n){if("beside"===r.placement||i)return 1;const s=e-t-r.hideDelay;return s<0?1:n||0===r.fadeDuration?0:Xt(1-s/r.fadeDuration,0,1)}(l,this.lastActivityAt,c,e.configuration,d),p="function"==typeof getComputedStyle?getComputedStyle(this.options.element):null,f=e=>((e,t)=>p?.getPropertyValue?.(e).trim()||t)(`--cp-view-scrollbar-${e}`,Ht[e]),m=Object.freeze({...e.layout,content:Object.freeze({...e.layout.content}),padding:Object.freeze({...e.layout.padding}),scrollbar:t.track}),g=Object.freeze({...e.viewport,...e.viewport.rowIds?{rowIds:Object.freeze([...e.viewport.rowIds])}:{}}),w=Object.freeze({canvas:i,context:n,layout:m,viewport:g,...t,now:l,opacity:u,pendingTarget:this.desired,markers:Object.freeze(t.markers.map(e=>Object.freeze({...e,color:Kt(e.marker,f)}))),hoveredMarker:this.hoveredMarker(),interaction:Object.freeze({near:this.near,hovered:this.hovered,dragging:null!==this.dragOffset,focused:this.focused,lastActivityAt:this.lastActivityAt,reducedMotion:d}),colors:Object.freeze({track:f("track"),thumb:f("thumb"),marker:f("marker"),error:f("error")})});let y=!1;n.save();try{n.setTransform(s,0,0,s,0,0);const t=(e.configuration.render??_t)(w);if(void 0!==t&&"boolean"!=typeof t)throw t instanceof Promise&&t.catch(e=>{this.disposed||this.report(e)}),new TypeError("scrollbar.render must synchronously return boolean or undefined");y=t}catch(e){this.failed=!0,this.endGesture(),this.desired=this.queued=null,void 0!==this.frameId&&cancelAnimationFrame(this.frameId),void 0!==this.fadeTimer&&clearTimeout(this.fadeTimer),this.frameId=this.fadeTimer=void 0,this.report(e)}finally{n.restore()}this.failed?this.clear():this.disposed||e.configuration!==this.options.getState().configuration||(this.dormant=!c&&0===u&&!0!==y,!0===y||h&&!c&&u>0&&l>=this.lastActivityAt+e.configuration.hideDelay?this.schedule():h&&!c&&u>0&&void 0===this.fadeTimer&&(this.fadeTimer=setTimeout(()=>{this.fadeTimer=void 0,this.schedule()},Math.min(2147483647,Math.max(0,this.lastActivityAt+e.configuration.hideDelay-l)))))}report(e){this.disposed||this.options.reportError(e)}};function Qt(e){const t=new Map;if(!e)return t;for(const i of e.split(";")){const e=i.indexOf("=");e<0||t.set(i.slice(0,e),i.slice(e+1))}return t}function ei(e){return e?Qt(e.rawParameters).get("cmdline_url")??null:null}function ti(e){const t=document.createElement("div");t.className="hex1b-scrollbar-tooltip",t.setAttribute("role","tooltip"),t.style.cssText="padding:8px 10px;border:1px solid var(--cp-view-text-muted,#888);border-radius:6px;background:var(--cp-view-surface,#202020);color:var(--cp-view-text,#eee);font:12px/1.4 var(--cp-view-font-family,monospace);white-space:pre-wrap;overflow-wrap:anywhere";const{marker:i,details:r}=e,n=[i.label||("custom"===i.source?"Bookmark":"Terminal command")];if("command"===i.source&&(n.push(`Phase: ${i.phase??"unknown"}`),null!=i.exitCode&&n.push(`Exit code: ${i.exitCode}`),e.loading&&n.push("Loading command details..."),e.error&&n.push(`Command details unavailable: ${e.error}`),r)){const e=ei(r);if(e)try{n.push(decodeURIComponent(e))}catch(t){if(!(t instanceof URIError))throw t;n.push(`Command (invalid URL encoding): ${e}`)}else r.rawParameters?n.push(r.rawParameters):n.push("This shell did not provide command text.")}return t.textContent=n.join("\n"),t}s();var ii=class{constructor(e){this.options=e}options;target=null;result=null;fetching=!1;lifetime;observer;element=null;failed=!1;disposed=!1;update(e,t,i=ti){if(this.disposed)return;if(!e||!1===i)return this.target=this.result=null,this.failed=!1,void this.clearView();const r=JSON.stringify({tick:e,layout:t});if(this.target?.signature===r&&this.target.renderer===i)return;const n=JSON.stringify([e.marker.id,e.marker.phase,e.marker.exitCode]);this.target?.key===n&&this.target.renderer===i||(this.result=null,this.failed=!1),this.target={tick:e,layout:t,renderer:i,key:n,signature:r},this.render(),this.loadDetails()}dispose(){this.disposed=!0,this.target=this.result=null,this.clearView()}clearView(){this.observer?.disconnect(),this.observer=void 0;const e=this.lifetime;this.lifetime=void 0,this.element=null,this.options.overlay.replaceChildren(),e?.abort()}render(){const e=this.target;if(this.clearView(),!e||this.target!==e||this.failed||this.disposed)return;const t=this.lifetime=new AbortController,i=this.result?.key===e.key?this.result:null,n=Object.freeze({marker:e.tick.marker,anchor:e.tick.bounds,layout:e.layout,details:i?.details??null,error:i?.error??null,loading:"command"===e.tick.marker.source&&!i,signal:t.signal});try{const i=e.renderer(n);if(r(i)&&"function"==typeof i.then&&Promise.resolve(i).catch(e=>{t.signal.aborted||this.options.reportError(e)}),t.signal.aborted)return;if(null!==i&&!(i instanceof HTMLElement))throw new TypeError("scrollbar.tooltip must synchronously return an HTMLElement or null");if(!i)return;this.element=i,Object.assign(i.style,{position:"absolute",boxSizing:"border-box",margin:"0",pointerEvents:"none",maxWidth:`${Math.max(0,Math.min(360,e.layout.width-16))}px`,maxHeight:`${Math.max(0,e.layout.height-16)}px`,overflow:"hidden"}),this.options.overlay.append(i),this.position(),this.observer=new ResizeObserver(()=>this.position()),this.observer.observe(i)}catch(e){this.failed=!0,this.clearView(),this.options.reportError(e)}}position(){if(!this.target||!this.element)return;const{bounds:e}=this.target.tick,{width:t,height:i}=this.target.layout,r=e.left-this.element.offsetWidth-8,n=e.top+e.height/2-this.element.offsetHeight/2;this.element.style.left=`${Math.max(0,Math.min(Math.max(8,r),t-this.element.offsetWidth-8))}px`,this.element.style.top=`${Math.max(0,Math.min(Math.max(8,n),i-this.element.offsetHeight-8))}px`}loadDetails(){const e=this.target;!e||this.fetching||this.failed||this.disposed||"command"!==e.tick.marker.source||this.result?.key===e.key||(this.fetching=!0,Promise.resolve().then(()=>this.disposed||this.target?.key!==e.key?null:this.options.getDetails(e.tick.marker.id)).then(t=>{t&&this.target?.key===e.key&&!this.disposed&&(this.result={key:e.key,details:Object.freeze({...t}),error:null},this.render())},t=>{this.target?.key!==e.key||this.disposed||(this.result={key:e.key,details:null,error:t instanceof Error?t.message:String(t)},this.render())}).finally(()=>{this.fetching=!1,this.loadDetails()}))}},ri="\n --cp-terminal-surface: #ffffff;\n --cp-terminal-text: #242424;\n --cp-terminal-text-muted: #5c5c5c;\n --cp-terminal-border-strong: #919191;\n --cp-terminal-accent: #b11f4b;\n --cp-terminal-accent-soft: rgba(177, 31, 75, 0.08);\n --cp-terminal-danger: #dc2626;\n",ni="\n --cp-terminal-surface: #292929;\n --cp-terminal-text: #dedede;\n --cp-terminal-text-muted: #919191;\n --cp-terminal-border-strong: #5f5f5f;\n --cp-terminal-accent: #fd8ea1;\n --cp-terminal-accent-soft: rgba(253, 142, 161, 0.14);\n --cp-terminal-danger: #f87171;\n",si=`\n :host {\n ${ri}\n --cp-terminal-font-family: "Segoe UI", Aptos, Calibri, -apple-system, BlinkMacSystemFont, sans-serif;\n }\n @media (prefers-color-scheme: dark) { :host { ${ni} } }\n :host-context([data-theme="light"]) { ${ri} }\n :host-context([data-theme="dark"]) { ${ni} }\n :host([data-theme="light"]) { ${ri} }\n :host([data-theme="dark"]) { ${ni} }\n .viewport {\n ${Object.entries(Ht).map(([e,t])=>`--cp-view-scrollbar-${e}: var(--cp-scrollbar-${e}, ${t});`).join("\n")}\n --cp-view-surface: var(--cp-surface, var(--cp-terminal-surface));\n --cp-view-text: var(--cp-text, var(--cp-terminal-text));\n --cp-view-text-muted: var(--cp-text-muted, var(--cp-terminal-text-muted));\n --cp-view-border-strong: var(--cp-border-strong, var(--cp-terminal-border-strong));\n --cp-view-accent: var(--cp-accent, var(--cp-terminal-accent));\n --cp-view-accent-soft: var(--cp-accent-soft, var(--cp-terminal-accent-soft));\n --cp-view-danger: var(--cp-danger, var(--cp-terminal-danger));\n --cp-view-font-family: var(--cp-font-family, var(--cp-terminal-font-family));\n }\n`;G(),s();var oi=["status","mode","requestId","active","pending","canExtend","text","message","copying","copyError"],ai=["available","following","pending","generation","buffer","top","liveTop","totalRows","requestId","navigationError"],li=["columns","rows","cellWidth","cellHeight","mouseTracking"],ci=["row","startColumn","endColumn"],hi=(e,t,i)=>i.every(i=>e?.[i]===t?.[i]);function di(e,t,i){const r=i.width/t.columns,n=i.height/t.rows;return e.map(e=>Object.freeze({left:e.startColumn*r,top:e.row*n,width:(e.endColumn-e.startColumn)*r,height:n}))}var ui=class{#L;#A;#R;#P;#O;#z;#U;#D;#B;#q=!1;#j=!1;constructor({element:e,overlay:t,button:i,signal:n,getState:s,runAction:o,onSelectionUI:a,reportError:l}){this.#L=e,this.#A=t,this.#R=i,this.#P=n,this.#O=s,this.#z=o,this.#U=l,a&&e.addEventListener("selectionui",e=>{if(!this.#B||e!==this.#B)return;const t=this.#D;try{const e=a(this.#B);if(void 0!==e)throw r(e)&&"function"==typeof e.then&&Promise.resolve(e).catch(e=>{n.aborted||this.#D!==t||l(e)}),new TypeError("onSelectionUI must finish synchronously; use preventDefault() and signal for UI ownership")}catch(t){e.preventDefault(),l(t)}},{signal:n})}refresh(e=!1){this.#j||=e,this.#q||this.#P.aborted||(this.#q=!0,queueMicrotask(()=>{if(this.#q=!1,this.#P.aborted)return;const e=this.#O(),t=this.#j;if(this.#j=!1,!t&&(i=this.#D,r=e,i&&i.connected===r.connected&&i.readOnly===r.readOnly&&hi(i.selection,r.selection,oi)&&hi(i.viewport,r.viewport,ai)&&hi(i.geometry,r.geometry,li)&&hi(i.canvasSize,r.canvasSize,["width","height"])&&i.selection.ranges.length===r.selection.ranges.length&&i.selection.ranges.every((e,t)=>hi(e,r.selection.ranges[t],ci))))return;var i,r;const n=Object.freeze({...e,selection:Object.freeze({...e.selection,ranges:Object.freeze(e.selection.ranges.map(e=>Object.freeze({...e})))}),viewport:Object.freeze({...e.viewport,rowIds:Object.freeze([...e.viewport.rowIds??[]])}),geometry:Object.freeze({...e.geometry}),canvasSize:Object.freeze({...e.canvasSize})});this.#D=n;const s=Object.freeze({...n,overlay:this.#A,signal:this.#P,runAction:this.#z,rects:Object.freeze(di(n.selection.ranges,n.geometry,n.canvasSize))}),o=new CustomEvent("selectionui",{cancelable:!0,detail:s});if(this.#B=o,this.#U(null),this.#L.dispatchEvent(o),this.#P.aborted)return;const a=n.selection;this.#R.hidden=o.defaultPrevented||["none","unavailable"].includes(a.status),this.#R.disabled=!n.connected||"valid"!==a.status||a.copying;const l=a.copying?"Copying…":"Copy";this.#R.textContent!==l&&(this.#R.textContent=l)}))}};function pi(e){if(/[\u0000-\u0020\u007f]/u.test(e)||!URL.canParse(e))return null;const t=new URL(e);return["https:","http:","mailto:"].includes(t.protocol)?t.href:null}var fi=class{#N=new Map;update(e){this.#N.clear();const t=new Map;for(const i of e){t.has(i.uri)||t.set(i.uri,pi(i.uri));const e=t.get(i.uri);if(!e)continue;const r=this.#N.get(i.row)??[];r.push({...i,uri:e}),this.#N.set(i.row,r)}}at(e){return this.#N.get(e.y)?.find(t=>e.x>=t.startColumn&&e.xF.cells)throw new RangeError("Link grid exceeds cell limit");const s=[];let o="",a=[],l=new Set,c=0,h=0,d="unknown";const u=e=>e>=0&&!!(1024&(n[(e+1)*i-1]?.attributes??0)),p=e=>{const i=Object.freeze({text:o,mode:t,start:d,end:e}),r=new Set([o.length]);for(const e of mi.segment(o))r.add(e.index);s.push({chunk:i,spans:a,unsafe:l,boundaries:r,key:JSON.stringify([t,c,d,e,o])}),o="",a=[],l=new Set};for(let e=0;e=1&&s<=2&&t+s<=i&&!(r&&(64&r.attributes||1109742===r.text.codePointAt(0)))&&(2!==s||0===n[e*i+t+1]?.width),d=c?r?.text||" ":"\0";if(h+d.length>F.totalText||o.length+d.length>F.chunk)throw new RangeError("Link text exceeds chunk or viewport limit");const u=o.length;o+=d,h+=d.length,c?a.push({start:u,end:o.length,row:e,startColumn:t,endColumn:t+s}):(l.add(u),l.add(o.length)),c&&2===s&&t++}const s=u(e);if(s||l.add(o.length),"physicalRow"===t||"logicalLine"===t&&!s||e===r-1)p(s?"clipped":"unknown");else if(!s){if(++h>F.totalText||o.length+1>F.chunk)throw new RangeError("Link text exceeds limit");o+="\n"}}return s}function wi(e,t,i){const r=t+i,{chunk:n}=e;if(i<=0||t<0||r>n.text.length||!e.boundaries.has(t)||!e.boundaries.has(r)||0===t&&"complete"!==n.start||r===n.text.length&&"complete"!==n.end||n.text.slice(t,r).includes("\0"))return null;for(const i of e.unsafe)if(i===t||i===r||i>t&&i=r)continue;if(i.startr)return null;const e=s.at(-1);e&&e.row===i.row&&e.endColumn===i.startColumn?e.endColumn=i.endColumn:s.push({row:i.row,startColumn:i.startColumn,endColumn:i.endColumn})}return s.length?s:null}ie();var yi=class{#$;#F;#G=null;#W;#_=null;#H=null;#K=[];#V=new Set;#X=new Map;#Y=new Map;#J=0;#Z=0;#Q=0;#o=0;#ee=!1;#te=!1;#ie=!1;constructor(e){this.#$={...e,actions:new Set(e.actions)}}configure(e){const t=H({detection:e},this.#$.actions);this.#ee||(this.#F=!1!==t&&t.detection,this.#V.clear(),this.#te=!1,this.clear())}update(e){if(this.#ee||e.revisionObject.freeze({...t,activation:Object.freeze({...t.activation,revision:e})}))))}clear(){this.#oe(),this.#Z++,this.#H=null,this.#X.clear(),this.#Y.clear(),this.#J=0,this.#ee||this.#re([])}dispose(){this.#ee||(this.clear(),this.#ee=!0)}#re(e){this.#K=Object.freeze([...e]),this.#$.onChange(this.#o,this.#K)}#ne(e,t,i){this.#$.onError(Object.freeze({code:e,ruleId:t,revision:this.#o,message:i instanceof Error?i.message:String(i)}))}#oe(){clearTimeout(this.#W),this.#W=void 0,this.#G&&(this.#G.terminate(),this.#G=null),this.#_=null}#ae(e){const t=this.#_?.rule.id??null;this.#oe(),this.#te=!0,this.#re([]),this.#ne("worker",t,e)}#se(){if(!(this.#ie||this.#ee||this.#_)&&this.#H&&this.#F&&!this.#te){this.#ie=!0;try{const e=this.#H;for(const t of this.#F.rules){if(!t.enabled||this.#V.has(t.id))continue;const i=(e.chunks.get(t.text??"logicalLine")??[]).filter(e=>!this.#X.has(this.#le(t,e)));if(i.length){try{if(!this.#G){const e=new Worker(this.#$.workerUrl??Lt("link-detection"),{type:"module",name:"hex1b-link-detection"});this.#G=e,e.addEventListener("message",t=>{this.#G===e&&this.#ce(t.data)}),e.addEventListener("error",t=>{this.#G===e&&(t.preventDefault(),this.#ae(t.message))}),e.addEventListener("messageerror",()=>{this.#G===e&&this.#ae("Invalid detection worker message")})}const r=++this.#Q;this.#_={id:r,work:e,rule:t,keys:new Map(i.map(e=>[e.key,this.#le(t,e)]))},this.#W=setTimeout(()=>{this.#_?.id===r&&(this.#oe(),this.#V.add(t.id),this.#ne("timeout",t.id,`Link rule exceeded ${F.timeoutMs} ms`),this.#se())},F.timeoutMs);const n={id:r,rule:t.builtin?{builtin:t.builtin}:{source:t.pattern.source,flags:t.pattern.flags},chunks:i.map(({chunk:e,key:t})=>({key:t,text:e.text}))};this.#G.postMessage(n)}catch(e){this.#ae(e)}return}}this.#he(e)}finally{this.#ie=!1}}}#le(e,t){return JSON.stringify([e.id,t.key])}#ce(e){const t=this.#_;if(t&&e?.id===t.id&&!this.#ee){if(clearTimeout(this.#W),this.#W=void 0,this.#_=null,e.error)this.#V.add(t.rule.id),this.#ne(e.error,t.rule.id,e.message??"Link scan failed");else{if(!function(e,t){if(!Array.isArray(e.results)||e.results.length!==t.keys.size)return!1;const i=new Set;let r=0,n=0;for(const s of e.results){if(!s||!t.keys.has(s.key)||i.has(s.key)||!Array.isArray(s.matches))return!1;if(i.add(s.key),r+=s.matches.length,r>F.matches)return!1;for(const e of s.matches){if(!e||!Number.isSafeInteger(e.index)||e.index<0||"string"!=typeof e.text||!e.text.length||!Array.isArray(e.captures)||!e.captures.every(e=>void 0===e||"string"==typeof e)||!e.groups||"object"!=typeof e.groups||Array.isArray(e.groups)||!Object.values(e.groups).every(e=>void 0===e||"string"==typeof e))return!1;if(n+=X(e),n>F.resultText)return!1}}return!0}(e,t))return void this.#ae("Malformed detection response");for(const i of e.results){const e=t.keys.get(i.key),r=e.length+i.matches.reduce((e,t)=>e+X(t),0);this.#J+=r-(this.#Y.get(e)??0),this.#Y.set(e,r),this.#X.set(e,i.matches)}}this.#H!==t.work&&this.#de(),this.#se()}}#he(e){if(!this.#F||this.#H!==e)return;const t=[...e.snapshot.hyperlinks],i=[],r=e=>e.some(e=>t.some(t=>e.row===t.row&&e.startColumnt.startColumn));for(const n of this.#F.rules){if(!n.enabled||this.#V.has(n.id))continue;const s=[],o=[];try{for(const t of e.chunks.get(n.text??"logicalLine")??[])for(const a of this.#X.get(this.#le(n,t))??[]){const l=wi(t,a.index,a.text.length);if(!l||r(l))continue;const c=Object.freeze({...a,captures:Object.freeze([...a.captures]),groups:Object.freeze({...a.groups}),chunk:t.chunk}),h=n.resolve?n.resolve(c):{target:a.text};if(this.#H!==e||this.#ee)return;if(null===h)continue;if("object"!=typeof h||void 0===h||"then"in h||"string"!=typeof h.target)throw new TypeError("Resolver must return a synchronous link resolution or null");const d=void 0===h.action?n.action:h.action;if(W(d,this.#$.actions),i.length+s.length>=F.matches)throw new RangeError("Resolved link limit exceeded");const u=Object.freeze({source:"detected",ruleId:n.id,kind:n.builtin?"url"===n.builtin||"uri"===n.builtin?"uri":"path":n.kind,text:a.text,target:h.target,ranges:Object.freeze(l.map(e=>Object.freeze(e))),revision:e.revision,data:h.data});s.push(Object.freeze({id:JSON.stringify([e.identity,n.id,l[0].row,l[0].startColumn,a.index]),action:d,activation:u})),o.push(...l)}i.push(...s),t.push(...o)}catch(t){if(this.#V.add(n.id),this.#ne(t instanceof RangeError?"limit":"resolver",n.id,t),this.#H!==e||this.#ee)return}}this.#de(),this.#re(i)}#de(){for(;this.#X.size>F.cacheEntries||this.#J>F.cacheText;){const e=this.#X.keys().next().value;this.#X.delete(e),this.#J-=this.#Y.get(e),this.#Y.delete(e)}}};function vi(e,t,i){const r=e.querySelector(t);if(!(r instanceof i))throw new Error(`Missing terminal element: ${t}`);return r}V(),s();var bi=class e{element;#$;#ue;#pe;#fe;#me;#ge=()=>{this.#ee||"system"!==this.#pe||this.#we()};#G;#ye;#ve;#be;#ke;#xe;#Ee=new AbortController;#Te={width:0,height:0};#Ce=Ot();#Se={columns:80,rows:24,cellWidth:10,cellHeight:20,mouseTracking:0};#Ie={id:null,primaryId:null,isPrimary:!1};#Me=!1;#Le=!1;#Ae;#ee=!1;#Re=!1;#Pe;#Oe;#ze;#Ue;#De=Promise.withResolvers();#Be;#qe={};#je="";#Ne="";#$e=!1;#Fe={state:"none",percentage:null};#Ge={phase:"unknown",lastExitCode:null};#We={uri:null,host:null,path:null};#_e=null;#He=!1;#g;#Ke;#Ve;#Xe;#Ye;#Je;#Ze;#Qe=!1;#et=!1;#tt;#it;#rt="";#nt=0;#st=!1;#ot;#f;#at=!1;#lt=0;#ct;#ht;#dt="";#ut={width:0,height:0};#pt=new fi;#K;#ft;#mt=1;#gt=0;#wt=0;#yt;#vt=new Map;#bt=[];#kt=[];#xt=new Map;#Et;#Tt;static async mount(t,i){if(!(t instanceof HTMLElement))throw new TypeError("A terminal container HTMLElement is required");if(!i?.url)throw new TypeError("A terminal WebSocket URL is required");if(i.signal?.aborted)throw i.signal.reason;if(!("webgpu"!==p(i.renderer)||window.isSecureContext&&navigator.gpu))throw new Error("The requested WebGPU renderer requires WebGPU over HTTPS or localhost");if(!(window.Worker&&window.ResizeObserver&&window.OffscreenCanvas&&HTMLCanvasElement.prototype.transferControlToOffscreen))throw new Error("WebTerminal requires module workers, ResizeObserver, and a transferable OffscreenCanvas");const r=new e(i);try{return await Promise.all([r.#De.promise,Promise.resolve().then(()=>r.#l(t))]),r}catch(e){throw r.dispose(),e}}constructor(e){if(this.#$=e,void 0!==e.readOnly&&"boolean"!=typeof e.readOnly)throw new TypeError("readOnly must be a boolean");if(this.#Ae=e.readOnly??!1,this.#ue=p(e.renderer),this.#pe=w(e.colorMode),this.#fe={light:v(void 0===e.lightModePalette?m:e.lightModePalette),dark:v(void 0===e.darkModePalette?f:e.darkModePalette)},this.#Ve=qt(e.padding),this.#Xe=Vt(e.scrollbar),this.#Ye=Nt(this.#Te,this.#Se,!1,this.#Ce,this.#Ve,this.#Xe),!(void 0===e.workerUrl||e.workerUrl instanceof URL||"string"==typeof e.workerUrl&&e.workerUrl.trim()))throw new TypeError("workerUrl must be a nonempty URL string or URL");if(!(void 0===e.linkDetectionWorkerUrl||e.linkDetectionWorkerUrl instanceof URL||"string"==typeof e.linkDetectionWorkerUrl&&e.linkDetectionWorkerUrl.trim()))throw new TypeError("linkDetectionWorkerUrl must be a nonempty URL string or URL");if(void 0!==e.onLinkDetectionError&&"function"!=typeof e.onLinkDetectionError)throw new TypeError("onLinkDetectionError must be a function");if(void 0!==e.onSelectionUI&&("function"!=typeof e.onSelectionUI||"AsyncFunction"===e.onSelectionUI.constructor.name))throw new TypeError("onSelectionUI must be a synchronous event handler");this.#ot=new Et(e),this.#f=new Map(Object.entries(e.actions??{})),this.#K=H(e.links,new Set(this.#f.keys())),this.#ft=new yi({workerUrl:void 0===e.linkDetectionWorkerUrl?void 0:new URL(e.linkDetectionWorkerUrl,location.href),actions:new Set(this.#f.keys()),onChange:(e,t)=>{!this.#ee&&this.#Me&&e===this.#gt&&(this.#bt=t,this.#Ct([]),this.#St())},onError:e=>{if(!this.#ee)try{this.#$.onStatus?.(`Link detection: ${e.message}`,"error")}finally{this.#$.onLinkDetectionError?.(e)}}}),this.#ft.configure(!!this.#K&&this.#K.detection),this.#g=new Bt(e=>this.#e(e),()=>this.#It()),this.#Ke=new I(e=>this.#e(e),()=>this.#Mt()),this.element=document.createElement("div"),this.element.className="hex1b-terminal",this.element.tabIndex=-1,this.element.style.cssText="width:100%;height:100%;min-width:0;min-height:0;contain:strict"}get geometry(){return{...this.#Se}}get peer(){return{...this.#Ie}}get connected(){return this.#Me}get readOnly(){return this.#Ae}get title(){return this.#Ne}get progress(){return{...this.#Fe}}get shellIntegration(){return{...this.#Ge}}get workingDirectory(){return{...this.#We}}get commandMark(){return this.#_e?{...this.#_e}:null}get stats(){return{...this.#qe}}get screenText(){return this.#je}get sizing(){return{...this.#Ce}}get layout(){return this.#Ye}get padding(){return this.#Ve}get scrollbar(){return this.#Xe}get markers(){return this.#Ke.markers}get inputBindings(){return this.#ot.bindings}get viewport(){const e=this.#g.viewport;return{...e,followTail:e.following,offset:e.available?e.liveTop-e.top:0}}get selection(){const e=this.#g.selection;return{...e,active:"valid"===e.status,pending:"pending"===e.status,copying:this.#st,copyError:this.#rt}}#l(e){if(this.#$.signal?.aborted)throw this.#$.signal.reason;const t=new URL(this.#$.url,location.href);if("https:"===t.protocol&&(t.protocol="wss:"),"http:"===t.protocol&&(t.protocol="ws:"),!["ws:","wss:"].includes(t.protocol))throw new TypeError("A ws: or wss: URL is required");const i=void 0===this.#$.scale||"auto"===this.#$.scale?Math.min(3,Math.max(.5,window.devicePixelRatio||1)):this.#$.scale;if(!Number.isFinite(i)||i<.5||i>3)throw new RangeError("Backing scale must be 0.5-3 or 'auto'");const r=h(this.#$.font,location.href);this.#Ce=Ot(this.#$.sizing);const s=this.element.attachShadow({mode:"open"});s.innerHTML=`\n \n
\n \n \n \n \n
\n \n
\n \n
\n \n \n \n
`,this.#ye=vi(s,".surface",HTMLDivElement),this.#ve=vi(s,"canvas",HTMLCanvasElement),this.#be=vi(s,"textarea",HTMLTextAreaElement),this.#tt=vi(s,".highlights",HTMLDivElement),this.#it=vi(s,".inspection",HTMLDivElement),this.#ve.id=`hex1b-content-${o()}`;const a=vi(s,".scrollbar-accessibility",HTMLDivElement);a.setAttribute("aria-controls",this.#ve.id);const l=document.createElement("div");l.slot="scrollbar-tooltip",l.className="hex1b-scrollbar-tooltip-overlay",l.style.cssText="position:relative;width:100%;height:100%;pointer-events:none",this.element.append(l),this.#Ze=new ii({overlay:l,getDetails:e=>this.getCommandMarkDetails(e),reportError:e=>{this.#rt=`Scrollbar tooltip: ${n(e)}`,this.#Lt(),this.#$.onStatus?.(this.#rt,"error")}}),this.#Je=new Zt({element:vi(s,".viewport",HTMLDivElement),canvas:vi(s,".scrollbar-canvas",HTMLCanvasElement),accessibility:a,getState:()=>({layout:this.layout,viewport:this.viewport,markers:this.markers,connected:this.#Me,configuration:this.#Xe}),scrollToRow:e=>this.scrollToRow(e),scrollToLive:()=>this.scrollToLive(),scrollToMarker:e=>this.scrollToMarker(e),onMarkerHover:e=>this.#Ze?.update(e,this.layout,!!this.#Xe&&this.#Xe.tooltip),reportError:e=>{this.#rt=`Scrollbar: ${n(e)}`,this.#Lt(),this.#$.onStatus?.(this.#rt,"error")}}),this.#ht=document.createElement("div"),this.#ht.slot="selection-ui",this.#ht.className="hex1b-selection-overlay",this.#ht.style.cssText="position:relative;width:100%;height:100%;pointer-events:none",this.element.append(this.#ht),this.#ct=new ui({element:this.element,overlay:this.#ht,button:vi(this.#it,".copy-selection",HTMLButtonElement),signal:this.#Ee.signal,getState:()=>({selection:this.selection,viewport:this.viewport,geometry:this.geometry,canvasSize:this.#ut,connected:this.#Me,readOnly:this.#Ae}),runAction:this.runAction.bind(this),onSelectionUI:this.#$.onSelectionUI,reportError:e=>{this.#dt=e?`Selection UI failed: ${n(e)}`:"",this.#ht.hidden=!!e,this.#Lt(),e&&this.#$.onStatus?.(this.#dt,"error")}}),this.#ct.refresh(),this.#be.setAttribute("aria-label",this.#$.label||"Terminal input. Click outside to use page controls."),this.#be.disabled=!0,e.append(this.element);const c=e=>{try{this.#rt="",e()}catch(e){this.#rt=n(e),this.#It()}};this.#ke=Mt(this.#ve,e=>this.#At(e),()=>this.focus(),{state:()=>({historical:!this.viewport.following||this.viewport.pending,readOnly:this.#Ae,selection:this.selection}),begin:(e,t)=>c(()=>this.#g.begin(e,t)),extend:e=>c(()=>this.#g.extend(e)),scroll:(e,t)=>c(()=>{this.#g.scroll(e,t),this.#Je?.activity()}),end:e=>this.#g.endGesture(e),resolve:e=>this.#Rt(e),execute:(e,t)=>this.#Pt(e,t),hyperlink:e=>this.#Ot(e),hoverHyperlink:e=>{this.#Et=e?.id,this.#K&&this.#K.detection&&"hover"===this.#K.detection.decoration&&this.#St()},openHyperlink:(e,t)=>this.#zt(e,t)}),this.#Ut(),vi(this.#it,".return-live",HTMLButtonElement).addEventListener("click",()=>{this.runAction(gt.ScrollToLive).catch(e=>this.#Dt(e))},{signal:this.#Ee.signal}),vi(this.#it,".copy-selection",HTMLButtonElement).addEventListener("click",()=>{this.runAction(gt.CopySelection).catch(e=>this.#Dt(e))},{signal:this.#Ee.signal}),vi(s,".viewport",HTMLDivElement).addEventListener("pointerdown",e=>{e.composedPath().includes(this.#ht)||e.target instanceof Element&&e.target.closest("button")||e.target===this.#ve||e.target===this.#be||(e.preventDefault(),this.focus())},{signal:this.#Ee.signal}),this.#xe=new ResizeObserver(e=>{const{width:t,height:i}=e[0].contentRect,r=t!==this.#Te.width||i!==this.#Te.height;this.#Te={width:t,height:i},this.#Bt(),r&&this.#qt()}),this.#xe.observe(e),window.addEventListener("pagehide",()=>this.dispose(),{signal:this.#Ee.signal}),this.#$.signal?.addEventListener("abort",()=>this.dispose(),{once:!0,signal:this.#Ee.signal}),this.#Be=setTimeout(()=>this.#jt(new Error("Timed out waiting for the terminal's first frame")),3e4),this.#G=void 0===this.#$.workerUrl?new Worker(Lt("terminal"),{type:"module",name:"Hex1b WebTerminal"}):new Worker(new URL(this.#$.workerUrl,location.href),{type:"module",name:"Hex1b WebTerminal"}),this.#G.addEventListener("message",e=>this.#Nt(e.data)),this.#G.addEventListener("error",e=>{e.preventDefault(),this.#jt(new Error(e.message||"Terminal worker failed"))}),this.#G.addEventListener("messageerror",()=>this.#jt(new Error("Terminal worker message could not be decoded")));const d=this.#ve.transferControlToOffscreen();this.#me=window.matchMedia?.("(prefers-color-scheme: dark)"),this.#me?.addEventListener("change",this.#ge),this.#$t({type:"init",canvas:d,url:t.href,scale:i,font:r,renderer:this.#ue,palette:this.#fe[this.resolvedColorMode]},[d]),this.#we(),this.#Ft()}#Nt(e){if(!this.#ee)if("connected"===e.type)this.#Me=!0,this.#be.disabled=!this.#Gt();else if("closed"===e.type){if(this.#Le)return;this.#Le=!0,clearTimeout(this.#Be);try{this.#Wt(),this.#ee||this.#$.onClose?.(Object.freeze({...e.details}))}finally{this.#De.reject(new Error(`Terminal WebSocket closed (${e.details.code}${e.details.reason?`: ${e.details.reason}`:""}) before mounting completed`))}}else if("status"===e.type)"error"===e.level&&(this.#Wt(),this.#De.reject(new Error(e.message))),this.#$.onStatus?.(e.message,e.level);else if("geometry"===e.type){this.#gt=e.revision,this.#vt.clear();for(const t of e.hyperlinks){const e=this.#vt.get(t.row)??[];e.push(t),this.#vt.set(t.row,e)}this.#Ct([]),this.#Tt=void 0;const t=!this.#Re,i=t||["columns","rows","cellWidth","cellHeight","mouseTracking"].some(t=>this.#Se[t]!==e[t]);this.#Se={columns:e.columns,rows:e.rows,cellWidth:e.cellWidth,cellHeight:e.cellHeight,mouseTracking:e.mouseTracking},this.#Re=!0;const r=this.#Ie;if(this.#Ie=e.peer,this.#be.disabled=!this.#Gt(),this.#Gt()&&document.activeElement===this.element&&!this.element.shadowRoot?.activeElement&&this.focus(),this.#pt.update(e.hyperlinks),(i||r.isPrimary!==this.#Ie.isPrimary)&&this.#Bt(),this.#Ie.isPrimary||(clearTimeout(this.#Pe),this.#Pe=void 0,this.#Oe=void 0),(t||!r.isPrimary&&this.#Ie.isPrimary)&&this.#qt(!0),this.#Oe===`${e.columns}x${e.rows}`&&(this.#Oe=void 0),Object.hasOwn(e,"history")&&(this.#je=e.text,this.#Ke.accept(e.history,e.revision),this.#g.accept(e.history,e.revision)),e.linkGeneration===this.#mt&&(e.linkSnapshot?this.#_t(e.linkSnapshot):(this.#yt&&(this.#yt={...this.#yt,revision:e.revision}),this.#ft.advance(e.revision))),this.#ke?.update(e.columns,e.rows,e.mouseTracking),i&&this.#$.onGeometry?.(this.geometry),t&&this.#$.onSizingChange?.(this.sizing),r.id===this.#Ie.id&&r.primaryId===this.#Ie.primaryId&&r.isPrimary===this.#Ie.isPrimary||this.#$.onRoleChange?.(this.peer),!this.#ee&&this.#Me&&(null!==this.#Ie.id||this.#Ie.isPrimary)){const t=!this.#$e||this.#Ne!==e.title,i=!this.#He||this.#Fe.state!==e.progress.state||this.#Fe.percentage!==e.progress.percentage,r=!this.#He||this.#Ge.phase!==e.shellIntegration.phase||this.#Ge.lastExitCode!==e.shellIntegration.lastExitCode,n=!this.#He||this.#We.uri!==e.workingDirectory.uri,s=!this.#He||this.#_e?.phase!==e.commandMark?.phase||this.#_e?.exitCode!==e.commandMark?.exitCode||this.#_e?.rawParameters!==e.commandMark?.rawParameters;this.#Ne=e.title,this.#$e=!0,this.#Fe={...e.progress},this.#Ge={...e.shellIntegration},this.#We={...e.workingDirectory},this.#_e=e.commandMark?{...e.commandMark}:null,this.#He=!0,t&&this.#$.onTitleChange?.(this.#Ne),!this.#ee&&i&&this.#$.onProgressChange?.(this.progress),!this.#ee&&r&&this.#$.onShellIntegrationChange?.(this.shellIntegration),!this.#ee&&n&&this.#$.onWorkingDirectoryChange?.(this.workingDirectory),!this.#ee&&s&&this.#$.onCommandMarkChange?.(this.commandMark)}}else if("linkSnapshot"===e.type)e.generation===this.#mt&&e.snapshot.revision===this.#gt&&this.#_t(e.snapshot);else if("linkDecorations"===e.type){const t=this.#Tt;this.#Me&&t&&t.serial===e.serial&&t.generation===e.generation&&e.generation===this.#mt&&t.revision===e.revision&&e.revision===this.#gt&&(this.#Tt=void 0,this.#Ct(t.links),this.#ke?.refresh())}else"history"===e.type?(this.#je=e.text,this.#Ke.accept(e.history,e.revision),this.#g.accept(e.history,e.revision)):"stats"===e.type&&(this.#qe=e.stats,void 0!==e.text&&(this.#je=e.text),e.stats.revision>0&&this.#$e&&this.#He&&this.#Me&&(null!==this.#Ie.id||this.#Ie.isPrimary)&&(clearTimeout(this.#Be),this.#De.resolve(this)),this.#$.onStats?.(this.stats,e.text))}#Bt(){const e=Nt(this.#Te,this.#Se,this.#Ie.isPrimary,this.#Ce,this.#Ve,this.#Xe),t=JSON.stringify(e)!==JSON.stringify(this.#Ye);this.#Ye=e;const{width:i,height:r,left:n,top:s}=e.content;this.#ye.style.width=`${i}px`,this.#ye.style.height=`${r}px`,this.#ye.style.left=`${n}px`,this.#ye.style.top=`${s}px`;const o=getComputedStyle(this.#ye);this.#ut={width:Number.parseFloat(o.width),height:Number.parseFloat(o.height)},this.#ct?.refresh(),this.#Je?.refresh(),t&&!this.#Qe&&(this.#Qe=!0,queueMicrotask(()=>{this.#Qe=!1,this.#ee||this.#$.onLayoutChange?.(this.layout)}));const a=window.devicePixelRatio||1;this.#$t({type:"viewport",width:Math.ceil(i*a),height:Math.ceil(r*a)})}#Ht(){return function(e,t,i){if("fixed"===i.mode)return{columns:i.columns,rows:i.rows};if(e.width<=0||e.height<=0)return null;const r=i.fontSize/16;return{columns:Math.max(1,Math.min(300,Math.floor(e.width/(t.cellWidth*r)))),rows:Math.max(1,Math.min(100,Math.floor(e.height/(t.cellHeight*r))))}}(jt(this.#Te,this.#Ve,this.#Xe),this.#Se,this.#Ce)}#qt(e=!1){("fixed"!==this.#Ce.mode||e)&&this.#Gt()&&this.#Ie.isPrimary&&void 0===this.#Pe&&!this.#ee&&(this.#Pe=setTimeout(()=>{this.#Pe=void 0;const e=this.#Ht();if(!e||!this.#Ie.isPrimary||!this.#Gt())return;const t=`${e.columns}x${e.rows}`;e.columns===this.#Se.columns&&e.rows===this.#Se.rows||t===this.#Oe||this.resize(e.columns,e.rows)},50))}#$t(e,t=[]){this.#G?.postMessage(e,t)}#e(e){if(!this.#Me||this.#ee)throw new Error("Terminal view is not connected");if(this.#Ae&&["input","paste","key","mouse","resize","requestPrimary"].includes(e.type))throw new Error("Terminal view does not accept input");this.#$t({type:"command",command:e})}#At(e){!this.#ee&&this.#Gt()&&(!function(e){if(O.encode(JSON.stringify(e)).byteLength>R.commandBytes)throw new RangeError("Terminal input exceeds the 64 KiB message limit. Send a smaller amount of text.")}(e),this.#lt++,["input","paste","key"].includes(e.type)&&this.viewport.available&&(this.#ke?.cancel(),"none"!==this.selection.status&&this.clearSelection(),this.viewport.following&&!this.viewport.pending||this.scrollToLive()),this.#e(e))}#It(){const e=this.viewport,t=this.selection;if(this.#ke?.refresh(),"invalidated"===t.status&&this.#ke?.cancel(),this.#tt){this.#tt.replaceChildren(...t.ranges.map(e=>{const t=document.createElement("span");return t.className="highlight",t.setAttribute("part","selection-highlight"),t.style.cssText=`left:${e.startColumn/this.#Se.columns*100}%;top:${e.row/this.#Se.rows*100}%;width:${(e.endColumn-e.startColumn)/this.#Se.columns*100}%;height:${100/this.#Se.rows}%`,t}));const i=vi(this.#it,".return-live",HTMLButtonElement);i.hidden=!e.available||e.following&&!e.pending,i.disabled=!this.#Me,this.#Lt()}this.#ee||this.#ct?.refresh(),this.#Je?.refresh(),this.#$.onViewportChange?.(e),this.#$.onSelectionChange?.(t)}scrollLines(e){this.#g.scroll(e),this.#Je?.activity()}scrollToRow(e){this.#g.scrollTo(e),this.#Je?.activity()}scrollToLive(){this.#g.live(),this.#Je?.activity()}scrollToMarker(e){return this.#Je?.activity(),this.#Ke.jump(e)}addMarker(e){return this.#Ke.add(e)}removeMarker(e){return this.#Ke.remove(e)}getCommandMarkDetails(e){return this.#Ke.details(e)}#Mt(){this.#Je?.activity(),this.#et||(this.#et=!0,queueMicrotask(()=>{this.#et=!1,this.#ee||this.#$.onMarkersChange?.(this.markers)}))}setPadding(e){if(this.#ee)throw new Error("Terminal view is disposed");this.#Ve=qt(e),this.#Bt(),this.#qt(!0)}setScrollbar(e){if(this.#ee)throw new Error("Terminal view is disposed");this.#Xe=Vt(e),this.#Je?.cancel(),this.#Bt(),this.#qt(!0)}refreshScrollbar(){if(this.#ee)throw new Error("Terminal view is disposed");this.#Je?.invalidate()}clearSelection(){this.#rt="",this.#g.clear()}refreshSelectionUI(){if(this.#ee)throw new Error("Terminal view is disposed");this.#ct?.refresh(!0)}#Lt(){if(!this.#it)return;const e=this.selection,t=this.viewport,i=vi(this.#it,".inspection-message",HTMLSpanElement);i.textContent=this.#dt||this.#rt||t.navigationError||("unavailable"===e.status?"":e.message)||(t.available&&!t.following?t.liveTop-t.top+" rows above live":""),i.hidden=!i.textContent,i.dataset.level=this.#dt||this.#rt||t.navigationError||"invalidated"===e.status?"error":"info"}#Dt(e){const t=e instanceof Error?e:new Error(String(e));this.#rt=`Input action failed: ${t.message}`,this.#It(),this.#$.onInputError?.(t)}async copySelection({clear:e=!1}={}){if("boolean"!=typeof e)throw new TypeError("Copy clear must be a boolean");const t=++this.#nt,i=this.selection.requestId,r=this.viewport.generation;this.#rt="";try{if(!this.#Me||this.#ee)throw new Error("Terminal view is not connected");if(!navigator.clipboard?.write||"function"!=typeof ClipboardItem)throw new Error("Clipboard writing is unavailable. Use a secure browser context with clipboard permission.");const n=this.#g.copy();this.#st=!0,this.#It();const s=new ClipboardItem({"text/plain":n.then(e=>new Blob([e],{type:"text/plain"}))}),[o]=await Promise.all([n,navigator.clipboard.write([s])]);return e&&t===this.#nt&&"valid"===this.selection.status&&this.selection.requestId===i&&this.viewport.generation===r&&this.clearSelection(),o}catch(e){throw t===this.#nt&&(this.#g.cancelCopy(e),this.#rt=`Copy failed: ${n(e)}`),e}finally{t===this.#nt&&(this.#st=!1),this.#It()}}#Gt(){return this.#Me&&this.#Re&&!this.#Ae&&(null!==this.#Ie.id||this.#Ie.isPrimary)}get inputContext(){return Object.freeze({terminal:this,selection:this.selection,viewport:this.viewport,buffer:this.viewport.buffer??null,mouseCaptured:!this.#Ae&&0!==this.#Se.mouseTracking,historical:!this.viewport.following||this.viewport.pending,readOnly:this.#Ae,connected:this.#Me,peer:this.peer})}#Rt(e){try{const t=this.#ot.resolve(Object.freeze(e),this.inputContext);return this.#Ae&&t.route===mt.Application?{route:"pointer"===e.type||"wheel"===e.type?mt.Continue:mt.Consume}:t}catch(e){return this.#Dt(e),{route:mt.Consume}}}#Pt(e,t){this.#Kt(e.action,e.args,t).catch(e=>this.#Dt(e))}async runAction(e,t,i){return this.#Kt(e,t,i)}async#Kt(e,t,i){if(this.#ee)throw new Error("Terminal view is disposed");if(this.#rt="","function"==typeof e)return e(this.inputContext,t,i);const n=this.#f.get(e);if(n)return n(this.inputContext,t,i);switch(e){case gt.CopySelection:if(void 0===t)return this.copySelection();if(!r(t))throw new TypeError("Copy options must be an object");if(void 0!==t.clear&&"boolean"!=typeof t.clear)throw new TypeError("Copy clear must be a boolean");return this.copySelection({clear:t.clear});case gt.PasteClipboard:return this.pasteClipboard();case gt.ClearSelection:return this.clearSelection();case gt.ScrollToLive:return this.scrollToLive();case gt.ScrollLines:if("number"!=typeof t)throw new RangeError("Scroll delta must be a signed 32-bit integer");return this.scrollLines(t);case gt.CopyOrPaste:if(this.#at)throw new Error("A clipboard action is still in progress. Try again when it finishes.");this.#at=!0;try{return this.selection.active||this.selection.pending&&this.selection.canExtend?await this.copySelection({clear:!0}):this.#Ae?void 0:await this.pasteClipboard()}finally{this.#at=!1}default:throw new TypeError(`Unknown terminal action: ${e}`)}}paste(e){if("string"!=typeof e)throw new TypeError("Paste text must be a string");if(!this.#Gt())throw new Error("Terminal view does not accept input");e&&this.#At({type:"paste",text:e})}async pasteClipboard(){if(!this.#Gt())throw new Error("Terminal view does not accept input");if(!navigator.clipboard?.readText)throw new Error("Clipboard reading is unavailable. Use the browser's paste shortcut instead.");const e=this.#lt,t=this.viewport.generation,i=this.selection.requestId,r=document.activeElement,n=await navigator.clipboard.readText();if(!this.#Gt()||e!==this.#lt||t!==this.viewport.generation||i!==this.selection.requestId||document.activeElement!==r)throw new Error("Terminal input, selection, focus, or buffer changed while reading the clipboard. Paste again.");return this.paste(n),n}get colorMode(){return this.#pe}get resolvedColorMode(){return"system"===this.#pe?this.#me?.matches?"dark":"light":this.#pe}setColorMode(e){if(this.#ee)throw new Error("Terminal view is disposed");if(void 0===e)throw new TypeError("A color mode is required");this.#pe=w(e),this.#we()}setPalette(e,t){if(this.#ee)throw new Error("Terminal view is disposed");if("light"!==e&&"dark"!==e)throw new TypeError('Palette mode must be "light" or "dark"');this.#fe[e]=v(t),this.resolvedColorMode===e&&this.#we()}#we(){const e=this.#fe[this.resolvedColorMode];this.element.dataset.theme=this.resolvedColorMode,this.element.style.colorScheme=this.resolvedColorMode,this.element.style.backgroundColor=e.background,this.#$t({type:"palette",palette:e})}setReadOnly(e){if("boolean"!=typeof e)throw new TypeError("readOnly must be a boolean");if(this.#ee)throw new Error("Terminal view is disposed");if(this.#Ae===e)return;const t=document.activeElement===this.element&&(!this.element.shadowRoot?.activeElement||this.element.shadowRoot.activeElement===this.#be);this.#Ae=e,this.#lt++,this.#Ue?.(),this.#be&&(this.#be.value="",this.#be.disabled=!this.#Gt()),this.#ke?.cancel(),this.#ke?.refresh(),clearTimeout(this.#Pe),this.#Pe=void 0,this.#Oe=void 0,this.#qt(!0),t&&this.focus(),this.#ct?.refresh()}setLinks(e){if(this.#ee)throw new Error("Terminal view is disposed");if(void 0===e)throw new TypeError("Link options must be an object or false");const t=H(e,new Set(this.#f.keys()));this.#K=t,this.#mt++,this.#bt=[],this.#Ct([]),this.#Tt=void 0,this.#yt=void 0,this.#Et=void 0,this.#ft.configure(!!t&&t.detection),this.#ke?.cancel(),this.#ke?.refresh(),this.#Ft()}#Ft(){const e=!(!this.#K||!(this.#K.osc8||this.#K.detection&&this.#K.detection.rules.some(e=>!1!==e.enabled)));this.#$t({type:"linkDetection",enabled:e,generation:this.#mt})}#_t(e){this.#yt=e,this.#bt=[],this.#Ct([]),this.#Tt=void 0,this.#ft.update(e),this.#ke?.refresh()}#Vt(e){return`detected/${this.#mt}/${this.#gt}/${e.id}`}#Ct(e){this.#kt=e,this.#xt.clear();for(const t of e)for(const e of t.activation.ranges){const i=this.#xt.get(e.row)??[];i.push({startColumn:e.startColumn,endColumn:e.endColumn,link:t}),this.#xt.set(e.row,i)}}#St(){if(!this.#G||!this.#Me||!this.#gt)return;const e=this.#K&&this.#K.detection;if(!e)return;const t="none"===e.decoration?[]:"hover"===e.decoration?this.#bt.filter(e=>this.#Vt(e)===this.#Et):this.#bt;this.#Tt={revision:this.#gt,generation:this.#mt,serial:++this.#wt,links:this.#bt},this.#$t({type:"linkDecorations",revision:this.#gt,generation:this.#mt,serial:this.#wt,ranges:t.flatMap(e=>e.activation.ranges),underlineStyle:e.underlineStyle??"solid"})}#Ot(e){if(!this.#Me||this.viewport.pending||!this.#K)return null;const t=this.#vt.get(e.y)?.find(t=>e.x>=t.startColumn&&e.xe.x>=t.startColumn&&e.xthis.#Vt(t)===e.id);if(i)return void this.#Kt(i.action,i.activation,t).catch(e=>this.#Dt(e));if(!this.#K.osc8){try{window.open(e.target,"_blank","noopener,noreferrer")}catch(e){this.#Dt(e)}return}const r=this.#vt.get(t.point.y)?.find(e=>t.point.x>=e.startColumn&&t.point.xthis.#Dt(e))}#Xt(e){if("key"===e.type){if(e.meta)throw new Error("Meta has no terminal key encoding; bind this shortcut to a local action instead.");this.#At({type:"key",key:e.key,ctrl:e.ctrl,alt:e.alt,shift:e.shift})}else"paste"===e.type?this.#Gt()&&this.paste(e.text):"text"===e.type&&this.#At({type:"input",text:e.text})}#Yt(e,t,i=!0){const r=this.#Rt(e);if((i||void 0!==r.action||r.route===mt.Consume)&&r.route!==mt.Browser&&(r.route!==mt.Continue||"key"!==e.type))if(t?.preventDefault(),t?.stopPropagation(),void 0!==r.action)this.#Pt(r,e);else if(r.route!==mt.Consume)try{this.#Xt(e)}catch(e){this.#Dt(e)}}#Ut(){const e=this.#be,t={signal:this.#Ee.signal};let i=!1,r=null;this.#Ue=()=>{i=!1,r=null,clearTimeout(this.#ze)},this.element.addEventListener("keydown",t=>{if(t.defaultPrevented)return;if(t.composedPath().includes(this.#ht))return;const r=t.composedPath()[0];!this.#Me||t.isComposing||i||"Process"===t.key||"Dead"===t.key||t.getModifierState("AltGraph")||this.#Yt({type:"key",key:t.key,code:t.code,repeat:t.repeat,...Ct(t)},t,r===e||r===this.element)},{...t,capture:!0}),e.addEventListener("paste",t=>{!t.defaultPrevented&&this.#Me&&t.clipboardData&&(this.#Yt({type:"paste",text:t.clipboardData.getData("text/plain")},t),e.value="")},t),e.addEventListener("compositionstart",()=>{this.#Gt()&&(i=!0,r=null,clearTimeout(this.#ze))},t),e.addEventListener("compositionend",t=>{i&&(i=!1,r="string"==typeof t.data?t.data:e.value,this.#ze=setTimeout(()=>{r&&this.#Yt({type:"text",text:r}),r=null,e.value=""},0))},t),e.addEventListener("input",t=>{const n=t instanceof InputEvent?t:void 0;if(n?.isComposing||i)return;clearTimeout(this.#ze);const s=r??n?.data??e.value;r=null,s&&"insertFromPaste"!==n?.inputType&&this.#Yt({type:"text",text:s},t),e.value=""},t)}focus(){if(this.#ee)return;(this.#Gt()?this.#be:this.element).focus({preventScroll:!0})}requestPrimary(){if(!this.#Gt())throw new Error("Terminal view does not accept input");if(this.#Te.width<=0||this.#Te.height<=0)throw new Error("Show the terminal container before taking primary");const e=this.#Ht();if(!e)throw new Error("Show the terminal container before taking primary");if(null!==this.#Ie.id)this.#e({type:"requestPrimary",...e});else{if(!this.#Ie.isPrimary)throw new Error("Waiting for the HMP1 connection");this.resize(e.columns,e.rows)}}resize(e,t){const i=Pt(e,t);if(!this.#Gt())throw new Error("Terminal view does not accept input");if(!this.#Ie.isPrimary)throw new Error("Only the primary view can request a terminal resize");this.#e({type:"resize",...i}),this.#Oe=`${e}x${t}`}setSizing(e){const t=Ot(e,this.#Ce.fontSize);if(!this.#Me||this.#ee)throw new Error("Terminal view is not connected");if(this.#Ae)throw new Error("Terminal view does not accept input");if(!this.#Ie.isPrimary)throw new Error("Only the primary view can change terminal sizing");this.#Ce=t,clearTimeout(this.#Pe),this.#Pe=void 0,this.#Oe=void 0,this.#Bt(),this.#qt(!0),this.#$.onSizingChange?.(this.sizing)}resync(){this.#e({type:"resync"})}#Wt(){this.#lt++,this.#Me=!1,this.#pt.update([]),this.#vt.clear(),this.#bt=[],this.#Ct([]),this.#yt=void 0,this.#Tt=void 0,this.#ft.clear(),this.#be&&(this.#be.disabled=!0),this.#ke?.update(1,1,0),this.#g.disconnect(),this.#Ke.disconnect(),this.#Je?.cancel(),this.#It(),clearTimeout(this.#Pe),this.#Pe=void 0}#jt(e){this.#Wt(),this.#De.reject(e),this.#$.onStatus?.(e.message,"error"),this.#G?.terminate()}dispose(){this.#ee||(this.#ee=!0,this.#Wt(),this.#ft.dispose(),this.#De.reject(new DOMException("Terminal view was disposed","AbortError")),clearTimeout(this.#Be),clearTimeout(this.#ze),this.#xe?.disconnect(),this.#me?.removeEventListener("change",this.#ge),this.#ke?.dispose(),this.#Je?.dispose(),this.#Ze?.dispose(),this.#Ee.abort(),this.#$t({type:"stop"}),this.#G?.terminate(),this.element.remove())}};if(M(),V(),"undefined"!=typeof DedicatedWorkerGlobalScope&&globalThis instanceof DedicatedWorkerGlobalScope)switch(new URL(import.meta.url).hash){case"#hex1b-terminal-worker":await Promise.resolve().then(()=>(lt(),tt));break;case"#hex1b-link-detection-worker":await Promise.resolve().then(()=>(ut(),ct))}export{mt as InputRoute,Rt as MAX_FONT_SIZE,At as MIN_FONT_SIZE,gt as TerminalAction,bi as WebTerminal,Gt as createDefaultScrollbarRenderer,f as defaultDarkPalette,kt as defaultInputBindings,m as defaultLightPalette,ei as getCmdlineUrl,K as linkAction,Qt as parseCommandMarkParameters,_t as renderDefaultScrollbar,ti as renderDefaultScrollbarTooltip}; +// @hex1b/web-terminal 0.173.0-alpha.1824.1.98d8766; minified with Terser. See ../LICENSE. +var e=Object.defineProperty,t=Object.getOwnPropertyNames,i=(e,i,r)=>function(){if(r)throw r[0];try{return e&&(i=(0,e[t(e)[0]])(e=0)),i}catch(e){throw r=[e],e}};function r(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)}function n(e){return r(e)&&"string"==typeof e.message?e.message:String(e)}var s,o=i({"src/validation.ts"(){}});function a(e){const t=new URL(e,globalThis.location?.href);if("https:"===t.protocol&&(t.protocol="wss:"),"http:"===t.protocol&&(t.protocol="ws:"),!["ws:","wss:"].includes(t.protocol))throw new TypeError("A ws: or wss: URL is required");return t.href}function l(e){if(!(e instanceof URL||"string"==typeof e&&e.trim()))throw new TypeError("A terminal WebSocket URL is required");const t=String(e),i={connect:e=>new Promise((i,r)=>{e.signal.throwIfAborted();const n=new WebSocket(a(t));n.binaryType="arraybuffer";const s=()=>{n.close(1e3,"View detached"),r(e.signal.reason)};e.signal.addEventListener("abort",s,{once:!0}),n.addEventListener("open",()=>{e.signal.aborted||i({send(e){if(n.readyState!==WebSocket.OPEN)throw new Error("Terminal WebSocket is not open");if(n.bufferedAmount+(new TextEncoder).encode(e).byteLength>1048576)throw new Error("Terminal WebSocket control buffer exceeded 1 MiB");n.send(e)},dispose(){e.signal.removeEventListener("abort",s),n.close(1e3,"View detached")}})}),n.addEventListener("message",t=>{e.signal.aborted||(t.data instanceof ArrayBuffer?e.onFrame(t.data).catch(t=>e.onError(t)):e.onError(new Error("Expected binary HWT1 frame")))}),n.addEventListener("close",t=>{e.signal.removeEventListener("abort",s),e.signal.aborted||(e.onClose({code:t.code,reason:t.reason,wasClean:t.wasClean}),r(new Error(`Terminal WebSocket closed (${t.code})`)))})})};return s.set(i,t),Object.freeze(i)}function c(e){if(!e||void 0!==e.url==(void 0!==e.transport))throw new TypeError("Exactly one of url or transport is required");if(void 0!==e.url)return l(e.url);if(!e.transport||"function"!=typeof e.transport.connect)throw new TypeError("transport must provide connect(context)");return e.transport}var h=i({"src/websocket-transport.ts"(){s=new WeakMap}});function d(){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();const e=crypto.getRandomValues(new Uint8Array(16));e[6]=15&e[6]|64,e[8]=63&e[8]|128;const t=Array.from(e,e=>e.toString(16).padStart(2,"0")).join("");return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var u,p=i({"src/random-id.ts"(){}});function f(e){return"string"==typeof e&&/^[1-9][0-9]{0,18}$/u.test(e)&&BigInt(e)<=9223372036854775807n}function m(e){if("string"!=typeof e||!/^(?:custom|command):[A-Za-z0-9-]{1,100}$/u.test(e))throw new TypeError("Invalid terminal marker ID")}var g,w,y=i({"src/marker-state.ts"(){p(),u=class{#e;#t;#i=Object.freeze([]);#r=new Map;#n=[];#s=0;#o=0;#a=!1;constructor(e,t){this.#e=e,this.#t=t}get markers(){return this.#i}accept(e,t){if(t<=this.#o)return;this.#o=t;const i=(e?.markers??[]).map(e=>Object.freeze({...e,...this.#r.get(e.id)})),r=!this.#a||JSON.stringify(i)!==JSON.stringify(this.#i);this.#a=!0,this.#i=Object.freeze(i);const n=e?.markerResult,s=this.#n[0];n&&s&&n.requestId===s.command.requestId&&(this.#n.shift(),clearTimeout(s.timer),n.success?s.resolve(n):s.reject(new Error(n.error||"Marker operation failed")),this.#l());const o=new Set(i.map(e=>e.id));for(const e of this.#n)"add"===e.command.action&&o.add(e.command.id);for(const e of this.#r.keys())o.has(e)||this.#r.delete(e);r&&this.#t()}#c(e){const t=Promise.withResolvers();return this.#n.push({command:{type:"marker",...e,requestId:++this.#s},resolve:t.resolve,reject:t.reject}),1===this.#n.length&&this.#l(),t.promise}#l(){const e=this.#n[0];if(e)try{this.#e(e.command),e.timer=setTimeout(()=>{this.#h(new Error("Timed out waiting for the marker operation; resync before retrying."))},1e4)}catch(t){this.#n.shift(),e.reject(t instanceof Error?t:new Error(String(t))),this.#l()}}async add(e){const t=e?.position;if(!t||!f(t.generation)||!f(t.rowId)||!Number.isInteger(t.column)||t.column<0||t.column>1024)throw new TypeError("A marker requires a valid presented generation, row ID, and column");if(void 0!==e.label&&("string"!=typeof e.label||e.label.length>4096))throw new TypeError("Marker label must be at most 4096 characters");if(void 0!==e.color&&("string"!=typeof e.color||e.color.length>256||!CSS.supports("color",e.color)))throw new TypeError("Marker color must be a CSS color");const i=`custom:${d()}`;this.#r.set(i,{...void 0===e.label?{}:{label:e.label},...void 0===e.color?{}:{color:e.color}});try{await this.#c({action:"add",id:i,...t});const e=this.#i.find(e=>e.id===i);if(!e)throw new Error("Marker registration succeeded without a retained marker");return e}catch(e){throw this.#r.delete(i),e}}async remove(e){m(e),await this.#c({action:"remove",id:e}),this.#r.delete(e)}async jump(e){m(e),await this.#c({action:"jump",id:e})}async details(e){m(e);const t=await this.#c({action:"details",id:e});if(!t.details)throw new Error("Command marker details were not returned");return Object.freeze({...t.details})}disconnect(e=new Error("Terminal disconnected before the marker operation completed.")){this.#h(e),this.#r.clear()}#h(e){for(const t of this.#n.splice(0))clearTimeout(t.timer),t.reject(e)}}}});function v(e="dark"){if("light"===e||"dark"===e||"system"===e)return e;throw new TypeError('colorMode must be "light", "dark", or "system"')}function b(e,t){if("string"!=typeof e||!/^#[0-9a-f]{6}$/iu.test(e))throw new TypeError(`${t} must be a #RRGGBB color`);return e.toLowerCase()}function k(e){if(!r(e))throw new TypeError("A terminal palette object is required");if(!Array.isArray(e.ansi)||16!==e.ansi.length)throw new TypeError("palette.ansi must contain exactly 16 colors");const t=Array.from(e.ansi,(e,t)=>b(e,`palette.ansi[${t}]`)),i={};if(void 0!==e.extended){if(!r(e.extended))throw new TypeError("palette.extended must be an object");for(const[t,r]of Object.entries(e.extended)){const e=Number(t);if(!Number.isInteger(e)||e<16||e>255||String(e)!==t)throw new TypeError("palette.extended indices must be integers from 16 to 255");i[e]=b(r,`palette.extended[${t}]`)}}return Object.freeze({foreground:b(e.foreground,"palette.foreground"),background:b(e.background,"palette.background"),ansi:Object.freeze(t),...void 0===e.cursor?{}:{cursor:b(e.cursor,"palette.cursor")},...void 0===e.selectionForeground?{}:{selectionForeground:b(e.selectionForeground,"palette.selectionForeground")},...void 0===e.selectionBackground?{}:{selectionBackground:b(e.selectionBackground,"palette.selectionBackground")},...void 0===e.extended?{}:{extended:Object.freeze(i)}})}function x(e,t,i){return(e|t<<8|i<<16|4278190080)>>>0}function E(e){const t=Number.parseInt(e.slice(1),16);return x(t>>16,t>>8&255,255&t)}function T(e){const t=new Uint32Array(256);for(let i=0;i<16;i++)t[i]=E(e.ansi[i]);const i=[0,95,135,175,215,255];for(let e=16;e<232;e++){const r=e-16;t[e]=x(i[Math.floor(r/36)],i[Math.floor(r/6)%6],i[r%6])}for(let e=232;e<256;e++){const i=8+10*(e-232);t[e]=x(i,i,i)}for(const[i,r]of Object.entries(e.extended??{}))t[Number(i)]=E(r);return{foreground:E(e.foreground),background:E(e.background),selectionForeground:E(e.selectionForeground??e.background),selectionBackground:E(e.selectionBackground??e.foreground),...void 0===e.cursor?{}:{cursor:E(e.cursor)},indexed:t}}function C(e,t=!1){if(!(e>>>24==255||e>=16777216&&e<=16777471||33554432===e||50331648===e||t&&67108864===e))throw new Error("Invalid indexed-v1 color reference")}function S(e,t){switch(e>>>24){case 1:return t.indexed[255&e];case 2:return t.foreground;case 3:return t.background;default:return e}}function I(e,t,i){if("indexed-v1"!==t.colorEncoding)return e.foreground;const r=S(32&e.attributes?e.background:e.foreground,i);return 2&e.attributes?((255&r)>>1|(r>>>8&255)>>1<<8|(r>>>16&255)>>1<<16|4278190080)>>>0:r}function M(e,t,i){return"indexed-v1"!==t.colorEncoding?e.background:32&e.attributes||50331648!==e.background?S(32&e.attributes?e.foreground:e.background,i):16777215&i.background}var A,L,R,P=i({"src/terminal-palette.ts"(){o(),g=Object.freeze({foreground:"#d4d0c8",background:"#323232",selectionForeground:"#323232",selectionBackground:"#d4d0c8",ansi:Object.freeze(["#242424","#ed888f","#8dae82","#c3a150","#7baad1","#bc99c5","#64b1b5","#b7b4ae","#9a9a9a","#ff99a0","#9bc08e","#d7b156","#87bbe7","#d0a8da","#6cc4c8","#dedad3"])}),w=Object.freeze({foreground:"#323232",background:"#d4d0c8",selectionForeground:"#d4d0c8",selectionBackground:"#323232",ansi:Object.freeze(["#24262b","#9e3141","#3e6131","#705402","#255c86","#704b7a","#006367","#b7b4ae","#595959","#982338","#355a27","#664d01","#175480","#694273","#015a5e","#e3dfd7"])})}});function O(e,t,i=0,r=Number.MAX_SAFE_INTEGER){if("number"!=typeof e||!Number.isSafeInteger(e)||er)throw new Error(`Invalid ${t}: ${String(e)}`);return e}function z(e,t,i){if(!Array.isArray(e)||e.length>i)throw new Error(`Invalid ${t}`)}function U(e){if("string"!=typeof e||!e.length||e.length>1024)throw new Error("Invalid image key")}function D(e,t){if("string"!=typeof e||!/^[1-9][0-9]{0,18}$/u.test(e)||BigInt(e)>9223372036854775807n)throw new Error(`Invalid ${t}`)}function B(e){if(!r(e)||"string"!=typeof e.phase||!["unknown","prompt","commandLine","executing","finished"].includes(e.phase))throw new Error("Invalid terminal command mark");if(null!==e.exitCode&&(O(e.exitCode,"terminal command mark exit code",-2147483648,2147483647),"finished"!==e.phase))throw new Error("Non-finished terminal command mark has an exit code");if(null!==e.rawParameters&&("string"!=typeof e.rawParameters||e.rawParameters.length>A.commandMarkParameterUnits))throw new Error("Invalid terminal command mark parameters")}function q(e){if("string"!=typeof e.status||!["none","valid","invalidated"].includes(e.status)||("valid"===e.status?"string"!=typeof e.text||e.text.length>A.metadataBytes:null!==e.text))throw new Error("Invalid selection text")}function j(e){if(!r(e)||1!==e.version||"boolean"!=typeof e.full)throw new Error("Unsupported frame metadata version");if(O(e.revision,"revision",1),O(e.baseRevision,"base revision"),void 0!==e.colorEncoding&&null!==e.colorEncoding&&"indexed-v1"!==e.colorEncoding)throw new Error("Unsupported color encoding");if(void 0!==e.colorEncodings&&(z(e.colorEncodings,"color encodings",16),e.colorEncodings.some(e=>"string"!=typeof e||e.length>64)))throw new Error("Invalid color encodings");if("string"!=typeof e.title||e.title.length>A.titleUnits||/[\u0000-\u001f\u007f-\u009f\ud800-\udfff]/u.test(e.title))throw new Error("Invalid terminal title");const t=e.progress;if(!r(t)||"string"!=typeof t.state||!["none","normal","error","indeterminate","warning"].includes(t.state))throw new Error("Invalid terminal progress");if("none"===t.state||"indeterminate"===t.state){if(null!==t.percentage)throw new Error("Unexpected terminal progress percentage")}else O(t.percentage,"terminal progress percentage",0,100);const i=e.shellIntegration;if(!r(i)||"string"!=typeof i.phase||!["unknown","prompt","commandLine","executing","finished"].includes(i.phase))throw new Error("Invalid terminal shell integration");if(null!==i.lastExitCode&&O(i.lastExitCode,"terminal shell exit code",-2147483648,2147483647),"unknown"===i.phase&&null!==i.lastExitCode)throw new Error("Unknown terminal shell phase has an exit code");const n=e.workingDirectory;if(!r(n))throw new Error("Invalid terminal working directory");const s=[n.uri,n.host,n.path];if(s.every(e=>null===e));else if(s.some(e=>"string"!=typeof e||e.length>A.metadataBytes))throw new Error("Invalid terminal working directory");const o=e.commandMark;null!==o&&B(o);const a=O(e.columns,"columns",1,1024),l=O(e.rows,"rows",1,512);if(void 0!==e.lineRenditions){if(z(e.lineRenditions,"line renditions",l),e.lineRenditions.length!==l)throw new Error("Invalid line rendition count");for(const t of e.lineRenditions)O(t,"line rendition",0,3)}if("number"!=typeof e.mouseTracking||![0,9,1e3,1002,1003].includes(e.mouseTracking))throw new Error("Unsupported mouse tracking mode");if(!r(e.peer)||"boolean"!=typeof e.peer.isPrimary)throw new Error("Invalid peer state");for(const t of["id","primaryId"]){const i=e.peer[t];if(null!==i&&("string"!=typeof i||!i.length||i.length>256))throw new Error(`Invalid peer ${t}`)}if(null!==e.peer.id&&e.peer.isPrimary!==(e.peer.id===e.peer.primaryId))throw new Error("Inconsistent primary peer state");O(a*l,"cell count",1,A.cells),function(e,t,i){if(null===e)return;if(!r(e))throw new Error("Missing history metadata");if(D(e.generation,"history generation"),"main"!==e.buffer&&"alternate"!==e.buffer)throw new Error("Invalid history buffer");const n=O(e.totalRows,"history total rows",i,2147483647),s=O(e.liveTop,"history live top",0,n-i);if(s!==n-i)throw new Error("Inconsistent history extent");if(O(e.top,"history top",0,s),"boolean"!=typeof e.following||e.following&&e.top!==e.liveTop)throw new Error("Invalid history following state");if(O(e.requestId,"viewport request id"),void 0!==e.viewportError&&null!==e.viewportError&&("string"!=typeof e.viewportError||e.viewportError.length>4096))throw new Error("Invalid viewport error");if(z(e.rowIds,"viewport row ids",i),e.rowIds.length!==i||new Set(e.rowIds).size!==i)throw new Error("Invalid viewport row ids");for(const t of e.rowIds)D(t,"viewport row id");const o=e.selection;if(!r(o)||"string"!=typeof o.status||!["none","valid","invalidated"].includes(o.status))throw new Error("Invalid selection status");if(O(o.requestId,"selection request id"),"string"!=typeof o.mode||!["character","word","line","rectangle"].includes(o.mode))throw new Error("Invalid selection mode");z(o.ranges,"selection ranges",i);let a=-1;for(const e of o.ranges){if(!r(e))throw new Error("Invalid selection range");const n=O(e.row,"selection range row",a+1,i-1),s=O(e.startColumn,"selection start column",0,t-1);O(e.endColumn,"selection end column",s+1,t),a=n}if(q(o),"valid"!==o.status&&o.ranges.length)throw new Error("Inactive selection has highlight ranges");if(null!==e.copy){if(!r(e.copy))throw new Error("Missing copy metadata");O(e.copy.requestId,"copy request id",1),q(e.copy)}if(void 0!==e.markers){z(e.markers,"history markers",A.metadataBytes/48);const t=new Set;for(const i of e.markers){if(!r(i)||"string"!=typeof i.id)throw new Error("Invalid history marker");if(m(i.id),t.has(i.id))throw new Error("Duplicate history marker ID");if(t.add(i.id),"command"!==i.source&&"custom"!==i.source)throw new Error("Invalid marker source");if(!i.id.startsWith(`${i.source}:`))throw new Error("Marker source disagrees with its ID");if("main"!==i.buffer&&"alternate"!==i.buffer)throw new Error("Invalid marker buffer");if(O(i.column,"marker column",0,1024),null!==i.row&&(O(i.row,"marker row",0,n-1),i.buffer!==e.buffer))throw new Error("Inactive-buffer marker has a resolved row");if("command"===i.source)B({...i,rawParameters:null});else if(void 0!==i.phase&&null!==i.phase)throw new Error("Custom marker has a shell phase")}}if(void 0!==e.markerPage&&null!==e.markerPage){const t=e.markerPage;if(!r(t)||"string"!=typeof t.revision||!t.revision.length||t.revision.length>64)throw new Error("Invalid marker page revision");const i=O(t.total,"marker inventory count",1,Math.floor(A.metadataBytes/48)),n=O(t.offset,"marker page offset",0,i-1);if(!Array.isArray(e.markers)||!e.markers.length||n+e.markers.length>i)throw new Error("Invalid marker page length")}if(void 0!==e.markerResult&&null!==e.markerResult){const t=e.markerResult;if(!r(t)||"boolean"!=typeof t.success)throw new Error("Invalid marker result");if(O(t.requestId,"marker request ID",1),void 0!==t.error&&null!==t.error&&("string"!=typeof t.error||t.error.length>4096))throw new Error("Invalid marker error");if(void 0!==t.markerId&&null!==t.markerId){if("string"!=typeof t.markerId)throw new Error("Invalid result marker ID");m(t.markerId)}void 0!==t.details&&null!==t.details&&B(t.details)}}(e.history,a,l),z(e.hyperlinks,"hyperlinks",a*l);let c=0;for(const t of e.hyperlinks){if(!r(t))throw new Error("Invalid hyperlink");const e=O(t.row,"hyperlink row",0,l-1),i=O(t.startColumn,"hyperlink start column",0,a-1),n=O(t.endColumn,"hyperlink end column",i+1,a);if(e*a+iA.metadataBytes)throw new Error("Invalid hyperlink URI")}if(10!==e.cellWidth||20!==e.cellHeight)throw new Error("This spike requires server geometry of 10 × 20 logical pixels");for(const t of["defaultBackground","defaultForeground"])void 0!==e[t]&&O(e[t],t,0,4294967295);if(!r(e.cursor)||"boolean"!=typeof e.cursor.visible)throw new Error("Invalid cursor");O(e.cursor.x,"cursor x",-1,1024),O(e.cursor.y,"cursor y",-1,512);if("string"==typeof e.cursor.shape&&(e.cursor.shape=["Default","BlinkingBlock","SteadyBlock","BlinkingUnderline","SteadyUnderline","BlinkingBar","SteadyBar"].indexOf(e.cursor.shape)),O(e.cursor.shape,"cursor shape",0,6),z(e.images,"images",A.images),z(e.retainedImages,"retained image keys",A.images),z(e.placements,"placements",A.placements),z(e.warnings,"warnings",256),e.warnings.some(e=>"string"!=typeof e))throw new Error("Invalid warning");if(!r(e.stats))throw new Error("Invalid server metrics");for(const t of["workloadBytes","outputBatches","captureMs","elapsedMs"]){const i=e.stats[t];if("number"!=typeof i||!Number.isFinite(i)||i<0)throw new Error(`Invalid server metric ${t}`)}const h=new Set;for(const t of e.retainedImages){if(U(t),h.has(t))throw new Error("Duplicate retained image key");h.add(t)}const d=new Set;let u=0;for(const t of e.images){if(!r(t))throw new Error("Invalid image");if(U(t.key),d.has(t.key)||!h.has(t.key))throw new Error("Inconsistent new image keys");d.add(t.key);const e=O(t.width,"image width",1,16384),i=O(t.height,"image height",1,16384),n=O(t.byteLength,"image byte length",1,A.frameBytes);if("rgba"!==t.format&&"png"!==t.format)throw new Error("Unsupported image format");if("rgba"===t.format&&n!==e*i*4)throw new Error("RGBA image size mismatch");if(u+=e*i*4,u>A.textureBytes)throw new Error("New images exceed decoded texture budget")}for(const t of e.placements){if(!r(t))throw new Error("Invalid placement");if(U(t.key),!h.has(t.key))throw new Error("Placement references an unretained image");if("kgp"!==t.kind&&"sixel"!==t.kind)throw new Error("Invalid placement kind");for(const e of["x","y","width","height","sourceX","sourceY","sourceWidth","sourceHeight","clipX","clipY","clipWidth","clipHeight","z"]){const i=t[e];if("number"!=typeof i||!Number.isFinite(i)||Math.abs(i)>2147483648)throw new Error(`Invalid placement ${e}`)}for(const e of["width","height","sourceWidth","sourceHeight","clipWidth","clipHeight"]){const i=t[e];if("number"!=typeof i||i<0)throw new Error(`Negative placement ${e}`)}}}var F,N,$,G=i({"src/protocol.ts"(){o(),y(),P(),A=Object.freeze({commandBytes:65536,frameBytes:100663296,metadataBytes:8388608,titleUnits:4096,commandMarkParameterUnits:8192,cells:262144,images:4096,placements:16384,textureBytes:268435456}),L=new TextDecoder("utf-8",{fatal:!0}),R=new TextEncoder}}),W=i({"src/transport-session.ts"(){G(),o(),F=class{constructor(e){this.callbacks=e,this.#d.promise.catch(()=>{})}callbacks;#u=new AbortController;#d=Promise.withResolvers();#p;#f=!1;#m;#g=!1;#w=!1;#y=[];#v=0;get closing(){return this.#g}start(e){this.#b(e)}async#b(e){try{const t=await e.connect({signal:this.#u.signal,onFrame:e=>{const t=this.#k(e);return this.#m=t,t},onClose:e=>{if(this.#u.signal.aborted||this.#g)return;if(!e||"string"!=typeof e.reason||void 0!==e.code&&(!Number.isInteger(e.code)||"boolean"!=typeof e.wasClean))return void this.#x(new TypeError("Invalid terminal transport close details"));const t=void 0===e.code?{reason:e.reason}:{code:e.code,reason:e.reason,wasClean:e.wasClean};this.#g=!0,this.#E(t)},onError:e=>this.#x(e)});if(!t||"function"!=typeof t.send||"function"!=typeof t.dispose)throw new TypeError("transport.connect must return a connection with send and dispose");if(this.#u.signal.aborted)return void t.dispose();this.#p=t,this.callbacks.onReady(),this.#d.resolve()}catch(e){this.#x(e)}}async#k(e){try{if(this.#u.signal.throwIfAborted(),this.#g)throw new Error("Terminal transport is closing");if(this.#f)throw new Error("Concurrent terminal transport frames are not allowed");if(!(e instanceof ArrayBuffer||e instanceof Uint8Array&&e.buffer instanceof ArrayBuffer))throw new TypeError("Transport frames must be ArrayBuffer or Uint8Array backed by ArrayBuffer");if(!e.byteLength||e.byteLength>A.frameBytes)throw new RangeError("Invalid HWT1 frame size");this.#f=!0;const t=e instanceof ArrayBuffer?e:new Uint8Array(e).buffer;await this.#d.promise,this.#u.signal.throwIfAborted(),await this.callbacks.onFrame(t),this.#u.signal.throwIfAborted(),this.#f=!1}catch(e){throw this.#x(e),e}}async#E(e){try{await this.#m}catch{return}this.#u.signal.aborted||(this.dispose(),this.callbacks.onClose(e))}send(e){if(this.#u.signal.aborted)return Promise.reject(this.#u.signal.reason);if(this.#g)return Promise.reject(new DOMException("Terminal transport is closing","AbortError"));const t=(new TextEncoder).encode(e).byteLength;if(this.#y.length>=256||this.#v+t>1048576){const e=new Error("Terminal transport control queue exceeded its bounded capacity");return this.#x(e),Promise.reject(e)}const i=Promise.withResolvers();return this.#y.push({control:e,bytes:t,resolve:i.resolve,reject:i.reject}),this.#v+=t,this.#T(),i.promise}async#T(){if(!this.#w){this.#w=!0;try{for(await this.#d.promise;!this.#u.signal.aborted&&!this.#g&&this.#y.length;){const e=this.#y[0];if(await this.#p.send(e.control),this.#u.signal.aborted)return;this.#y.shift(),this.#v-=e.bytes,e.resolve()}}catch(e){this.#g||this.#x(e)}finally{this.#w=!1}}}#x(e){if(this.#u.signal.aborted)return;const t=e instanceof Error?e:new Error(n(e));this.#C(t),this.callbacks.onError(t)}dispose(){this.#C(new DOMException("Terminal transport was disposed or closed","AbortError"))}#C(e){if(!this.#u.signal.aborted){this.#u.abort(e),this.#d.reject(e);for(const t of this.#y)t.reject(e);this.#y=[],this.#v=0,this.#p?.dispose(),this.#p=void 0}}}}});function _(e,t=import.meta.url){if(e??={family:"Cascadia Mono NF",faces:[{url:N,weight:"200 700"}]},"object"!=typeof e||Array.isArray(e)||"string"!=typeof e.family||!e.family.trim()||e.family.length>256||/[\u0000-\u001f\u007f]/u.test(e.family))throw new TypeError("font.family must be a single, nonempty font family name");const i=e.faces??[];if(!Array.isArray(i)||i.length>16)throw new TypeError("font.faces must contain at most 16 font sources");return{family:e.family.trim(),faces:i.map(e=>{if(!e||"string"!=typeof e.url||!e.url.trim())throw new TypeError("Each font face requires a URL");for(const t of["weight","style"])if(void 0!==e[t]&&("string"!=typeof e[t]||e[t].length>64))throw new TypeError(`Font face ${t} must be a CSS descriptor string`);return{url:new URL(e.url,t).href,weight:e.weight??"400",style:e.style??"normal"}})}}async function H(e){const{family:t,faces:i}=_(e),n=globalThis,s="fonts"in n&&(r(o=n.fonts)&&"function"==typeof o.add&&"function"==typeof o.delete)?n.fonts:globalThis.document?.fonts;var o;if(!s||"function"!=typeof FontFace)throw new Error("Terminal font loading requires the CSS Font Loading API in the rendering context");const a=$.has(t.toLowerCase()),l=a?t.toLowerCase():JSON.stringify(t);if(a&&i.length)throw new TypeError("A generic font family cannot have downloadable faces");const c=i.length?i:a?[]:[{local:t}],h=[];for(const e of c)try{const i=new FontFace(t,"local"in e?`local(${JSON.stringify(e.local)})`:`url(${JSON.stringify(e.url)})`,{weight:e.weight??"400",style:e.style??"normal"});h.push(await i.load())}catch(i){throw new Error(`Could not load terminal font "${t}" from ${"url"in e?e.url:"local fonts"}`,{cause:i})}for(const e of h)s.add(e);return{family:t,cssFamily:l,dispose(){for(const e of h)s.delete(e)}}}var K=i({"src/terminal-font.ts"(){o(),N=new URL("./fonts/cascadia-mono-nf/CascadiaMonoNF.woff2",import.meta.url).href,$=new Set(["monospace","serif","sans-serif","system-ui"])}});function V(e="auto"){if("auto"===e||"webgpu"===e||"webgl2"===e)return e;throw new TypeError('renderer must be "auto", "webgpu", or "webgl2"')}var X,Y=i({"src/renderer-options.ts"(){}});function J(e,t){if("function"!=typeof e&&("string"!=typeof e||!t.has(e)))throw new TypeError("Link action must be a registered custom action or callback")}function Z(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)}function Q(e,t){if(!1===e)return!1;if(void 0===e)return{};if(!Z(e))throw new TypeError("Invalid links configuration");const i={};if(!1===e.osc8)i.osc8=!1;else if(void 0!==e.osc8){if(!Z(e.osc8))throw new TypeError("Invalid OSC 8 configuration");J(e.osc8.action,t),i.osc8={action:e.osc8.action}}const r=e.detection;if(!1===r)i.detection=!1;else if(void 0!==r){if(!Z(r)||!Array.isArray(r.rules))throw new TypeError("Invalid detection configuration");if(r.rules.length>X.rules)throw new TypeError("Too many link rules");if(void 0!==r.activation&&!["modifierClick","click"].includes(r.activation))throw new TypeError("Invalid link activation");if(void 0!==r.decoration&&!["always","hover","none"].includes(r.decoration))throw new TypeError("Invalid link decoration");if(void 0!==r.underlineStyle&&!["solid","dashed"].includes(r.underlineStyle))throw new TypeError("Invalid link underline style");const e=new Set,n=r.rules.map(i=>{if(!Z(i)||"string"!=typeof i.id||!i.id.length||i.id.length>256||e.has(i.id))throw new TypeError("Link rule IDs must be nonempty and unique");if(e.add(i.id),void 0!==i.enabled&&"boolean"!=typeof i.enabled)throw new TypeError("Invalid enabled flag");if(void 0!==i.text&&!["physicalRow","logicalLine","viewport"].includes(i.text))throw new TypeError("Invalid link text mode");if(void 0!==i.resolve&&"function"!=typeof i.resolve)throw new TypeError("Invalid resolver");J(i.action,t);const r={id:i.id,action:i.action,enabled:i.enabled??!0,text:i.text??"logicalLine",resolve:i.resolve};if(void 0!==i.builtin){if(!["url","uri","absolutePath","homePath"].includes(i.builtin)||void 0!==i.pattern||void 0!==i.kind)throw new TypeError("Invalid builtin rule");return{...r,builtin:i.builtin}}if(!(i.pattern instanceof RegExp&&["uri","path","custom"].includes(i.kind)))throw new TypeError("Custom rules require a RegExp and kind");if(i.pattern.source.length>X.pattern||/[^dgimsuv]/u.test(i.pattern.flags))throw new TypeError("Unsupported regex flags or oversized pattern (sticky is not supported)");return{...r,kind:i.kind,pattern:new RegExp(i.pattern.source,i.pattern.flags)}});i.detection={rules:n,activation:r.activation??"modifierClick",decoration:r.decoration??"always",underlineStyle:r.underlineStyle??"solid"}}return i}function ee(e){if("function"!=typeof e)throw new TypeError("Expected a link action callback");return(t,i,r)=>{if(!function(e){if(!Z(e))return!1;const t=e;return!(!(["detected","osc8"].includes(t.source)&&["uri","path","custom"].includes(t.kind)&&"string"==typeof t.text&&"string"==typeof t.target&&Number.isSafeInteger(t.revision))||t.revision<0||!Array.isArray(t.ranges)||0===t.ranges.length)&&("osc8"===t.source?null===t.ruleId&&"uri"===t.kind:"string"==typeof t.ruleId&&0!==t.ruleId.length)&&t.ranges.every(e=>Z(e)&&Number.isSafeInteger(e.row)&&e.row>=0&&Number.isSafeInteger(e.startColumn)&&e.startColumn>=0&&Number.isSafeInteger(e.endColumn)&&e.endColumn>e.startColumn)}(i))throw new TypeError("Expected a terminal link activation");return e(t,i,r)}}var te=i({"src/link-options.ts"(){X=Object.freeze({rules:32,pattern:8192,chunk:65536,totalText:262144,cells:262144,matches:2048,resultText:262144,cacheEntries:2048,cacheText:1048576,timeoutMs:250})}});function ie(e){let t=e.text.length+16;for(const i of e.captures)t+=8+(i?.length??0);for(const[i,r]of Object.entries(e.groups))t+=8+i.length+(r?.length??0);return t}var re,ne,se,oe,ae,le,ce=i({"src/link-worker-protocol.ts"(){}}),he=i({"src/render-backend.ts"(){re=16,ne=class extends Error{}}}),de=i({"src/webgpu-backend.ts"(){he(),se=class{constructor(e,t,i,r,n){this.device=e,this.texture=t,this.bindGroup=i,this.width=r,this.height=n}device;texture;bindGroup;width;height;writePixels(e,t,i,r=0,n=0){this.device.queue.writeTexture({texture:this.texture,origin:[r,n]},e,{bytesPerRow:4*t,rowsPerImage:i},[t,i])}writeBitmap(e){this.device.queue.copyExternalImageToTexture({source:e},{texture:this.texture,premultipliedAlpha:!1},[this.width,this.height])}destroy(){this.texture.destroy()}},oe=class e{constructor(e){this.device=e,this.format=navigator.gpu.getPreferredCanvasFormat(),this.uniform=e.createBuffer({size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.sampler=e.createSampler({minFilter:"linear",magFilter:"linear"})}device;kind="webgpu";instanceBufferBytes=0;instanceBuffer;disposed=!1;context;format;uniform;sampler;pipeline;static async create(t,i){if(!globalThis.isSecureContext)throw new ne("WebGPU requires HTTPS or localhost");if(!navigator.gpu)throw new ne("WebGPU is unavailable in this browser worker");const r=await navigator.gpu.requestAdapter();if(!r)throw new ne("No WebGPU adapter is available");let n,s;try{n=await r.requestDevice()}catch(e){if(e instanceof DOMException&&"OperationError"===e.name)throw new ne(`Could not acquire a WebGPU device: ${e.message}`,{cause:e});throw e}try{s=new e(n);const r=s;return n.lost.then(e=>{r.disposed||i(new Error(`WebGPU device lost: ${e.message||e.reason}`))}),n.addEventListener("uncapturederror",e=>{r.disposed||i(new Error(`WebGPU error: ${e.error.message}`))}),await s.initialize(t),s}catch(e){throw s?s.dispose():n.destroy(),e}}get maxTextureDimension2D(){return this.device.limits.maxTextureDimension2D}get maxCanvasDimension2D(){return this.maxTextureDimension2D}async initialize(e){const t=this.device.createShaderModule({code:"\nstruct Viewport { size: vec2f, padding: vec2f }\n@group(0) @binding(0) var viewport: Viewport;\n@group(0) @binding(1) var image: texture_2d;\n@group(0) @binding(2) var imageSampler: sampler;\n\nstruct VertexOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n @location(1) color: vec4f,\n @location(2) @interpolate(flat) mode: f32,\n}\n\n@vertex fn vertex(\n @builtin(vertex_index) index: u32,\n @location(0) rect: vec4f,\n @location(1) uvRect: vec4f,\n @location(2) color: vec4f,\n @location(3) mode: f32,\n) -> VertexOut {\n let corners = array(\n vec2f(0, 0), vec2f(1, 0), vec2f(0, 1),\n vec2f(0, 1), vec2f(1, 0), vec2f(1, 1)\n );\n let corner = corners[index];\n let position = rect.xy + corner * rect.zw;\n var out: VertexOut;\n out.position = vec4f(position / viewport.size * vec2f(2, -2) + vec2f(-1, 1), 0, 1);\n out.uv = mix(uvRect.xy, uvRect.zw, corner);\n out.color = color;\n out.mode = mode;\n return out;\n}\n\n@fragment fn fragment(in: VertexOut) -> @location(0) vec4f {\n // Explicit LOD avoids derivative-uniformity requirements across solid/mask/image batches.\n let texel = textureSampleLevel(image, imageSampler, in.uv, 0);\n if (in.mode < 0.5) { return in.color; }\n if (in.mode < 1.5) { return vec4f(in.color.rgb, in.color.a * texel.a); }\n return texel * in.color;\n}"});this.pipeline=await this.device.createRenderPipelineAsync({layout:"auto",vertex:{module:t,entryPoint:"vertex",buffers:[{arrayStride:4*re,stepMode:"instance",attributes:[{shaderLocation:0,offset:0,format:"float32x4"},{shaderLocation:1,offset:16,format:"float32x4"},{shaderLocation:2,offset:32,format:"float32x4"},{shaderLocation:3,offset:48,format:"float32"}]}]},fragment:{module:t,entryPoint:"fragment",targets:[{format:this.format,blend:{color:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha"},alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha"}}}]},primitive:{topology:"triangle-list"}});const i=e.getContext("webgpu");if(!i||!("configure"in i))throw new ne("Could not create an OffscreenCanvas WebGPU context");this.context=i,i.configure({device:this.device,format:this.format,alphaMode:"opaque"})}createTexture(e,t,i){if(e>this.maxTextureDimension2D||t>this.maxTextureDimension2D)throw new Error(`${i} exceeds the GPU texture dimension limit`);if(!this.pipeline)throw new Error("WebGPU backend is not initialized");const r=this.device.createTexture({label:i,size:[e,t],format:"rgba8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT});try{const i=this.device.createBindGroup({layout:this.pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:this.uniform}},{binding:1,resource:r.createView()},{binding:2,resource:this.sampler}]});return new se(this.device,r,i,e,t)}catch(e){throw r.destroy(),e}}resize(e,t){this.device.queue.writeBuffer(this.uniform,0,new Float32Array([e,t,0,0]))}submit(e,t,i,r){if(this.disposed||!this.context||!this.pipeline)throw new Error("WebGPU backend is not initialized");const n=t*re*4;(!this.instanceBuffer||n>this.instanceBufferBytes)&&(this.instanceBuffer?.destroy(),this.instanceBufferBytes=Math.max(256,e.byteLength),this.instanceBuffer=this.device.createBuffer({size:this.instanceBufferBytes,usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST})),n&&this.device.queue.writeBuffer(this.instanceBuffer,0,e,0,t*re);const s=this.device.createCommandEncoder(),o=s.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:r[0],g:r[1],b:r[2],a:1},loadOp:"clear",storeOp:"store"}]});o.setPipeline(this.pipeline),o.setVertexBuffer(0,this.instanceBuffer);for(const e of i){if(!(e.resource instanceof se))throw new Error("Texture belongs to a different rendering backend");o.setBindGroup(0,e.resource.bindGroup),o.draw(6,e.count,0,e.start)}o.end(),this.device.queue.submit([s.finish()])}async idle(){await this.device.queue.onSubmittedWorkDone()}dispose(){this.disposed||(this.disposed=!0,this.instanceBuffer?.destroy(),this.uniform.destroy(),this.context?.unconfigure(),this.device.destroy())}}}});function ue(e){return e instanceof Error?e:new Error(String(e))}function pe(e){return Number.isSafeInteger(e)&&e>0}function fe(e){e.pixelStorei(e.UNPACK_ALIGNMENT,1),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!1),e.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1),e.pixelStorei(e.UNPACK_COLORSPACE_CONVERSION_WEBGL,e.NONE),e.pixelStorei(e.UNPACK_ROW_LENGTH,0),e.pixelStorei(e.UNPACK_SKIP_PIXELS,0),e.pixelStorei(e.UNPACK_SKIP_ROWS,0)}var me=i({"src/webgl2-backend.ts"(){he(),ae=class{constructor(e,t,i,r,n,s,o){this.owner=e,this.gl=t,this.texture=i,this.width=r,this.height=n,this.assertActive=s,this.release=o}owner;gl;texture;width;height;assertActive;release;destroyed=!1;bind(){if(this.assertActive(),this.destroyed)throw new Error("WebGL2 texture is destroyed");this.gl.bindTexture(this.gl.TEXTURE_2D,this.texture),fe(this.gl)}writePixels(e,t,i,r=0,n=0){if(!pe(t)||!pe(i)||!Number.isSafeInteger(r)||!Number.isSafeInteger(n)||r<0||n<0||r+t>this.width||n+i>this.height||e.byteLength{const t=e.statusMessage;this.contextLost(new Error("WebGL2 context lost"+(t?`: ${t}`:"")))};static async create(t,i){const r=t.getContext("webgl2",{alpha:!1,antialias:!1,depth:!1,stencil:!1,premultipliedAlpha:!1,preserveDrawingBuffer:!1});if(!r)throw new ne("Could not create an OffscreenCanvas WebGL2 context");const n=new e(t,r,i);try{return n.initialize(),n}catch(e){throw n.dispose(),e}}get maxTextureDimension2D(){return this.textureLimit}get maxCanvasDimension2D(){return this.canvasLimit}get instanceBufferBytes(){return this.bufferBytes}initialize(){const e=this.gl;this.canvas.addEventListener("webglcontextlost",this.onContextLost),this.checkErrors("initialization"),this.textureLimit=e.getParameter(e.MAX_TEXTURE_SIZE);const t=e.getParameter(e.MAX_VIEWPORT_DIMS);if(this.canvasLimit=Math.min(this.textureLimit,e.getParameter(e.MAX_RENDERBUFFER_SIZE),t[0],t[1]),!pe(this.textureLimit)||!pe(this.canvasLimit))throw new Error("Invalid WebGL2 texture or viewport limits");const i=this.compile(e.VERTEX_SHADER,"#version 300 es\nprecision highp float;\nprecision highp int;\n\nlayout(location = 0) in vec4 rect;\nlayout(location = 1) in vec4 uvRect;\nlayout(location = 2) in vec4 color;\nlayout(location = 3) in float mode;\nuniform vec2 viewport;\nout vec2 fragmentUv;\nout vec4 fragmentTint;\nflat out float fragmentMode;\n\nvoid main() {\n const vec2 corners[6] = vec2[6](\n vec2(0, 0), vec2(1, 0), vec2(0, 1),\n vec2(0, 1), vec2(1, 0), vec2(1, 1)\n );\n vec2 corner = corners[gl_VertexID];\n vec2 position = rect.xy + corner * rect.zw;\n gl_Position = vec4(position / viewport * vec2(2, -2) + vec2(-1, 1), 0, 1);\n fragmentUv = mix(uvRect.xy, uvRect.zw, corner);\n fragmentTint = color;\n fragmentMode = mode;\n}"),r=this.compile(e.FRAGMENT_SHADER,"#version 300 es\nprecision highp float;\nprecision highp int;\n\nuniform highp sampler2D image;\nin vec2 fragmentUv;\nin vec4 fragmentTint;\nflat in float fragmentMode;\nout vec4 fragmentColor;\n\nvoid main() {\n vec4 texel = textureLod(image, fragmentUv, 0.0);\n if (fragmentMode < 0.5) {\n fragmentColor = fragmentTint;\n } else if (fragmentMode < 1.5) {\n fragmentColor = vec4(fragmentTint.rgb, fragmentTint.a * texel.a);\n } else {\n fragmentColor = texel * fragmentTint;\n }\n}"),n=e.createProgram();if(!n)throw new Error("Could not allocate a WebGL2 shader program");if(this.program=n,e.attachShader(n,i),e.attachShader(n,r),e.linkProgram(n),!e.getProgramParameter(n,e.LINK_STATUS))throw new Error(`WebGL2 shader link failed: ${e.getProgramInfoLog(n)||"No diagnostic available"}`);for(const t of this.shaders)e.detachShader(n,t),e.deleteShader(t);this.shaders.clear();const s=e.getUniformLocation(n,"viewport"),o=e.getUniformLocation(n,"image");if(null===s||null===o)throw new Error("WebGL2 shader uniforms are unavailable");this.viewport=s;const a=e.createBuffer();if(!a)throw new Error("Could not allocate a WebGL2 instance buffer");this.instanceBuffer=a;const l=e.createVertexArray();if(!l)throw new Error("Could not allocate a WebGL2 vertex array");this.vertexArray=l,e.useProgram(n),e.uniform1i(o,0),e.activeTexture(e.TEXTURE0),e.bindVertexArray(l),e.bindBuffer(e.ARRAY_BUFFER,a);for(let t=0;t<4;t++)e.enableVertexAttribArray(t),e.vertexAttribDivisor(t,1);e.enable(e.BLEND),e.blendEquationSeparate(e.FUNC_ADD,e.FUNC_ADD),e.blendFuncSeparate(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA,e.ONE,e.ONE_MINUS_SRC_ALPHA),e.disable(e.DEPTH_TEST),e.disable(e.STENCIL_TEST),e.disable(e.CULL_FACE),e.disable(e.SCISSOR_TEST),e.disable(e.DITHER),this.resize(this.canvas.width,this.canvas.height)}compile(e,t){const i=this.gl,r=i.createShader(e);if(!r)throw new Error("Could not allocate a WebGL2 shader");if(this.shaders.add(r),i.shaderSource(r,t),i.compileShader(r),!i.getShaderParameter(r,i.COMPILE_STATUS))throw new Error(`WebGL2 shader compilation failed: ${i.getShaderInfoLog(r)||"No diagnostic available"}`);return r}assertActive(){if(this.disposed)throw new Error("WebGL2 backend is disposed");if(this.lostError)throw this.lostError}contextLost(e){if(!this.disposed&&!this.lostError){this.lostError=e;for(const t of this.fences)this.settleFence(t,e);this.onFatal(e)}}checkErrors(e){this.assertActive();const t=this.gl,i=t.getError();if(i===t.CONTEXT_LOST_WEBGL||t.isContextLost()){const e=new Error("WebGL2 context lost");throw this.contextLost(e),e}if(i!==t.NO_ERROR){const r=i===t.OUT_OF_MEMORY?"OUT_OF_MEMORY":i===t.INVALID_ENUM?"INVALID_ENUM":i===t.INVALID_VALUE?"INVALID_VALUE":i===t.INVALID_OPERATION?"INVALID_OPERATION":i===t.INVALID_FRAMEBUFFER_OPERATION?"INVALID_FRAMEBUFFER_OPERATION":`0x${i.toString(16)}`;throw new Error(`WebGL2 ${e} failed: ${r}`)}}createTexture(e,t,i){if(this.assertActive(),!pe(e)||!pe(t)||e>this.textureLimit||t>this.textureLimit)throw new Error(`${i} has invalid dimensions or exceeds the WebGL2 texture dimension limit`);const r=this.gl,n=r.createTexture();if(!n)throw this.checkErrors("texture allocation"),new Error(`Could not allocate WebGL2 texture: ${i}`);try{r.bindTexture(r.TEXTURE_2D,n),fe(r),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),r.texImage2D(r.TEXTURE_2D,0,r.RGBA8,e,t,0,r.RGBA,r.UNSIGNED_BYTE,null),this.checkErrors(`texture allocation (${i})`);const s=new ae(this,r,n,e,t,()=>this.assertActive(),e=>this.textures.delete(e));return this.textures.add(s),s}catch(e){throw r.deleteTexture(n),e}}resize(e,t){if(this.assertActive(),!Number.isFinite(e)||!Number.isFinite(t)||e<=0||t<=0)throw new Error("Invalid WebGL2 logical viewport dimensions");if(!pe(this.canvas.width)||!pe(this.canvas.height)||this.canvas.width>this.canvasLimit||this.canvas.height>this.canvasLimit)throw new Error("Canvas exceeds the WebGL2 drawing buffer dimension limit");const i=this.gl;if(i.drawingBufferWidth!==this.canvas.width||i.drawingBufferHeight!==this.canvas.height)throw this.checkErrors("drawing buffer resize"),new Error("WebGL2 could not allocate the requested canvas drawing buffer");i.viewport(0,0,this.canvas.width,this.canvas.height),i.useProgram(this.program),i.uniform2f(this.viewport,e,t),this.checkErrors("resize")}submit(e,t,i,r){if(this.assertActive(),!Number.isSafeInteger(t)||t<0||t*re>e.length)throw new Error("Invalid WebGL2 instance data");for(const e of i){if(!(e.resource instanceof ae)||e.resource.owner!==this)throw new Error("Texture belongs to a different rendering backend");if(e.resource.destroyed)throw new Error("WebGL2 texture is destroyed");if(!Number.isSafeInteger(e.start)||!Number.isSafeInteger(e.count)||e.start<0||e.count<0||e.start+e.count>t)throw new Error("Invalid WebGL2 batch instance range")}const n=this.gl,s=re*Float32Array.BYTES_PER_ELEMENT,o=t*s;if(n.bindVertexArray(this.vertexArray),n.bindBuffer(n.ARRAY_BUFFER,this.instanceBuffer),!this.bufferBytes||o>this.bufferBytes){const t=Math.max(256,e.byteLength);n.bufferData(n.ARRAY_BUFFER,t,n.DYNAMIC_DRAW),this.checkErrors("instance buffer allocation"),this.bufferBytes=t}o&&n.bufferSubData(n.ARRAY_BUFFER,0,e,0,t*re),n.useProgram(this.program),n.activeTexture(n.TEXTURE0),n.clearColor(r[0],r[1],r[2],1),n.clear(n.COLOR_BUFFER_BIT);for(const e of i){if(!e.count)continue;const t=e.resource;n.bindTexture(n.TEXTURE_2D,t.texture);const i=e.start*s;n.vertexAttribPointer(0,4,n.FLOAT,!1,s,i),n.vertexAttribPointer(1,4,n.FLOAT,!1,s,i+16),n.vertexAttribPointer(2,4,n.FLOAT,!1,s,i+32),n.vertexAttribPointer(3,1,n.FLOAT,!1,s,i+48),n.drawArraysInstanced(n.TRIANGLES,0,6,e.count)}this.checkErrors("frame submission")}async idle(){this.checkErrors("GPU completion");const e=this.gl,t=e.fenceSync(e.SYNC_GPU_COMMANDS_COMPLETE,0);if(!t)throw this.checkErrors("GPU fence allocation"),new Error("Could not allocate a WebGL2 GPU fence");return new Promise((i,r)=>{const n={sync:t,resolve:i,reject:r};this.fences.add(n);try{e.flush(),this.checkErrors("GPU fence submission"),this.pollFence(n)}catch(e){this.settleFence(n,ue(e))}})}pollFence(e){if(this.fences.has(e))try{this.assertActive();const t=this.gl;if(t.isContextLost()){const e=new Error("WebGL2 context lost");throw this.contextLost(e),e}const i=t.clientWaitSync(e.sync,0,0);if(i===t.ALREADY_SIGNALED||i===t.CONDITION_SATISFIED)this.checkErrors("GPU completion"),this.settleFence(e);else{if(i!==t.TIMEOUT_EXPIRED)throw this.checkErrors("GPU fence wait"),new Error("WebGL2 GPU fence wait failed");e.timer=setTimeout(()=>{e.timer=void 0,this.pollFence(e)},0)}}catch(t){this.settleFence(e,ue(t))}}settleFence(e,t){this.fences.delete(e)&&(void 0!==e.timer&&clearTimeout(e.timer),this.gl.deleteSync(e.sync),t?e.reject(t):e.resolve())}dispose(){if(this.disposed)return;this.disposed=!0,this.canvas.removeEventListener("webglcontextlost",this.onContextLost);for(const e of this.fences)this.settleFence(e,new Error("WebGL2 backend is disposed"));for(const e of this.textures)e.destroy();const e=this.gl;this.instanceBuffer&&e.deleteBuffer(this.instanceBuffer),this.vertexArray&&e.deleteVertexArray(this.vertexArray),this.program&&e.deleteProgram(this.program);for(const t of this.shaders)e.deleteShader(t);this.shaders.clear(),this.instanceBuffer=void 0,this.vertexArray=void 0,this.program=void 0,this.bufferBytes=0,e.isContextLost()||e.getExtension("WEBGL_lose_context")?.loseContext()}}}});var ge,we,ye,ve,be,ke,xe,Ee,Te=i({"src/backend-selection.ts"(){he(),Y(),de(),me()}});function Ce(e){return[(255&e)/255,(e>>>8&255)/255,(e>>>16&255)/255,(e>>>24&255)/255]}function Se(e){return`${5&e.attributes}/${e.width}/${e.text}`}function Ie(e){return 1109742===e.text.codePointAt(0)}function Me(e,t,i,r={x:0,y:0,rowHeight:0}){let{x:n,y:s,rowHeight:o}=r;const a=[];for(const[r,l]of e){const e=Math.ceil(l.width*ge*i)+4,c=Math.ceil(we*i)+4;if(e>t||c>t)return null;if(n+e>t&&(n=0,s+=o,o=0),s+c>t)return null;a.push({key:r,cell:l,x:n,y:s,width:e,height:c}),n+=e,o=Math.max(o,c)}return{placements:a,shelf:{x:n,y:s,rowHeight:o}}}var Ae,Le,Re=i({"src/renderer.ts"(){G(),K(),Te(),he(),P(),ge=10,we=20,ye=1048576,ve=1048576,be=re,ke=[1,1,1,1],xe=T(g),Ee=class e{canvas;scale;backingScale;backend;fallbackReason;fontConfiguration;fontMetrics;images;glyphs;imageUploadBytes;imagePayloadBytes;glyphUploadBytes;atlasRebuilds;textureBytes;instances;disposed;columns;rows;font;rasterCanvas;raster;atlas;glyphKeyUnits=0;shelf={x:0,y:0,rowHeight:0};canvasLimited=!1;width=0;height=0;quadCount=0;batches=[];static async create(t,i,r,n,s="auto"){const o=_(n);if(!Number.isFinite(i)||i<.5||i>3)throw new Error("Invalid backing scale");const{backend:a,fallbackReason:l}=await async function(e,t,i="auto"){const r=V(i);if("webgl2"===r)return{backend:await le.create(e,t)};try{return{backend:await oe.create(e,t)}}catch(i){if("auto"!==r||!(i instanceof ne))throw i;try{return{backend:await le.create(e,t),fallbackReason:i.message}}catch(e){throw new AggregateError([i,e],`WebGPU unavailable (${i.message}); WebGL2 initialization failed: ${e instanceof Error?e.message:String(e)}`)}}}(t,r,s),c=new e(t,i,a,o);c.fallbackReason=l;try{return await c.initialize(),c}catch(e){throw c.dispose(),e}}constructor(e,t,i,r){if(!Number.isFinite(t)||t<.5||t>3)throw new Error("Invalid backing scale");this.canvas=e,this.scale=t,this.backingScale=t,this.backend=i,this.fontConfiguration=r,this.fontMetrics=new Map,this.images=new Map,this.glyphs=new Map,this.imageUploadBytes=0,this.imagePayloadBytes=0,this.glyphUploadBytes=0,this.atlasRebuilds=0,this.textureBytes=0,this.instances=new Float32Array(4096*be),this.disposed=!1,this.columns=0,this.rows=0}async initialize(){this.font=await H(this.fontConfiguration),this.rasterCanvas=new OffscreenCanvas(1,1);const e=this.rasterCanvas.getContext("2d",{willReadFrequently:!0});if(!e)throw new Error("Worker glyph rasterization is unavailable");this.raster=e,this.resetAtlas(Math.min(2048,this.backend.maxTextureDimension2D))}createTexture(e,t,i){return this.backend.createTexture(e,t,i)}resetAtlas(e){this.atlas?.destroy(),this.atlas=this.createTexture(e,e,"Glyph atlas"),this.glyphs.clear(),this.glyphKeyUnits=0,this.shelf={x:0,y:0,rowHeight:0}}resize(e,t,i){const r=e*ge,n=t*we,s=this.backend.maxCanvasDimension2D,o=Math.min(this.scale,i?i.width/r:1/0,i?i.height/n:1/0);this.canvasLimited=r*o>s||n*o>s,this.backingScale=Math.min(o,s/r,s/n);const a=Math.max(1,Math.min(s,Math.ceil(r*this.backingScale))),l=Math.max(1,Math.min(s,Math.ceil(n*this.backingScale)));this.columns===e&&this.rows===t&&this.canvas.width===a&&this.canvas.height===l||(this.columns=e,this.rows=t,this.width=r,this.height=n,this.canvas.width=a,this.canvas.height=l,this.backend.resize(r,n))}async updateImages(e,t){const i=new Set(t),r=new Map(e.map(e=>[e.key,e]));let n=0;for(const e of i){const t=r.get(e)||this.images.get(e);if(!t)throw new Error(`Missing retained image resource: ${e}`);n+=t.width*t.height*4}if(n>A.textureBytes)throw new Error("Retained images exceed the 256 MiB texture budget");for(const[e,t]of this.images)i.has(e)&&!r.has(e)||(t.destroy(),this.textureBytes-=t.width*t.height*4,this.images.delete(e));for(const t of e){const e=this.createTexture(t.width,t.height,`Image ${t.key}`);try{if("rgba"===t.format)e.writePixels(t.bytes,t.width,t.height);else{const i=new DataView(t.bytes.buffer,t.bytes.byteOffset,t.bytes.byteLength);if(i.byteLength<33||2303741511!==i.getUint32(0)||218765834!==i.getUint32(4)||13!==i.getUint32(8)||1229472850!==i.getUint32(12)||i.getUint32(16)!==t.width||i.getUint32(20)!==t.height)throw new Error(`PNG header dimensions do not match resource ${t.key}`);const r=await createImageBitmap(new Blob([t.bytes],{type:"image/png"}),{premultiplyAlpha:"none",colorSpaceConversion:"none"});try{if(r.width!==t.width||r.height!==t.height)throw new Error("Decoded PNG dimension mismatch");e.writeBitmap(r)}finally{r.close()}}this.images.set(t.key,e),this.textureBytes+=t.width*t.height*4,this.imageUploadBytes+=t.width*t.height*4,this.imagePayloadBytes+=t.byteLength}catch(t){throw e.destroy(),t}}}prepareGlyphs(e){const t=new Map;for(const i of e)!(i&&i.width&&i.text.trim())||64&i.attributes||Ie(i)||t.set(Se(i),i);const i=e=>[...e.keys()].reduce((e,t)=>e+t.length,0);if(t.size>16384||i(t)>ve)throw new Error("Visible glyph metadata exceeds the bounded glyph cache");const r=new Map([...t].filter(([e])=>!this.glyphs.has(e)));if(!r.size)return;let n=this.glyphs.size+r.size<=16384&&this.glyphKeyUnits+i(r)<=ve?Me(r,this.atlas.width,this.scale,this.shelf):null;if(!n){let e=this.atlas.width;const i=Math.min(4096,this.backend.maxTextureDimension2D);for(;!(n=Me(t,e,this.scale))&&e=ye)throw new Error("Frame exceeds bounded quad budget");const p=this.quadCount*be;if(p+be>this.instances.length){const e=new Float32Array(Math.min(2*this.instances.length,ye*be));e.set(this.instances),this.instances=e}const f=a[2]-a[0],m=a[3]-a[1];this.instances.set([c,h,d-c,u-h,a[0]+(c-t)/r*f,a[1]+(h-i)/n*m,a[0]+(d-t)/r*f,a[1]+(u-i)/n*m,...s,o,0,0,0],p);const g=this.batches[this.batches.length-1];g?.resource===e?g.count++:this.batches.push({resource:e,start:this.quadCount,count:1}),this.quadCount++}solid(e,t,i,r,n,s){this.quad(this.atlas,e,t,i,r,n,0,void 0,s)}placement(e){const t=this.images.get(e.key);if(!t)throw new Error(`Placement texture is missing: ${e.key}`);const{sourceX:i,sourceY:r,sourceWidth:n,sourceHeight:s}=e;if(!(n&&s&&e.width&&e.height))return;const o=e.x+Math.max(0,-i)/n*e.width,a=e.y+Math.max(0,-r)/s*e.height,l=e.x+Math.min(n,t.width-i)/n*e.width,c=e.y+Math.min(s,t.height-r)/s*e.height,h=Math.max(o,e.clipX),d=Math.max(a,e.clipY),u=Math.min(l,e.clipX+e.clipWidth),p=Math.min(c,e.clipY+e.clipHeight);this.quad(t,e.x,e.y,e.width,e.height,ke,2,[i/t.width,r/t.height,(i+n)/t.width,(r+s)/t.height],[h,d,Math.max(0,u-h),Math.max(0,p-d)])}decorations(e,t,i,r,n,s=e.underlineColor,o,a=1){128&e.attributes&&this.solid(t,i+10*a,r,a,n,o),256&e.attributes&&this.solid(t,i+a,r,a,n,o);const l=e.underlineStyle||(8&e.attributes?1:0);this.underline(l,t,i,r,Ce(s),o,a)}underline(e,t,i,r,n,s,o=1){if(1===e)this.solid(t,i+18*o,r,o,n,s);else if(2===e)this.solid(t,i+16*o,r,o,n,s),this.solid(t,i+18*o,r,o,n,s);else if(3===e)for(let e=0;e=2?2:1,d=t%this.columns;if(d>=Math.max(1,Math.floor(this.columns/c)))return;const u=d*ge*c,p=a*we,f=p-(3===l?we:0),m=Math.min(e.width*ge*c,this.width-u),g=l?o?[o[0],Math.max(p,o[1]),o[2],Math.min(p+we,o[1]+o[3])-Math.max(p,o[1])]:[0,p,this.width,we]:o,w=Ce(o?n.selectionForeground:I(e,i,n)),y=Ie(e)?void 0:this.glyphs.get(Se(e));if(y){const t=y.colored?!o&&2&e.attributes?[.5,.5,.5,1]:ke:w;this.quad(this.atlas,u,f,e.width*ge*c,we*h,t,y.colored?2:1,[y.u0,y.v0,y.u1,y.v1],g)}this.decorations(e,u,f,m,w,o?n.selectionForeground:function(e,t,i){return"indexed-v1"!==t.colorEncoding?e.underlineColor:67108864===e.underlineColor?I(e,t,i):S(e.underlineColor,i)}(e,i,n),g,h),!s||e.underlineStyle||8&e.attributes||Ie(e)||this.underline(s,u,f,m,w,g,h)}render(e,t,i,r,n=xe){const s=performance.now();this.quadCount=0,this.batches=[];const o=t.placements.map((e,t)=>({placement:e,order:t,z:"sixel"===e.kind?-1:e.z})).sort((e,t)=>e.z-t.z||e.order-t.order);for(const e of o)e.z<-1073741824&&this.placement(e.placement);for(let i=0;i=Math.max(1,Math.floor(this.columns/o))||this.solid(i%this.columns*ge*o,s*we,ge*o,we,Ce(M(r,t,n)))}for(const e of o)e.z>=-1073741824&&e.z<0&&this.placement(e.placement);for(let s=0;s=0&&this.placement(e.placement);const a=t.history?.selection;if("valid"===a?.status)for(const s of a.ranges){const o=t.lineRenditions?.[s.row]?2:1,a=[s.startColumn*ge*o,s.row*we,(s.endColumn-s.startColumn)*ge*o,we];this.solid(...a,Ce(n.selectionBackground));const l=s.row*this.columns;let c=l+s.startColumn;for(;c>l&&0===e[c]?.width;)c--;for(let o=c;o=0&&l.x=0&&l.ye!==s?.rowIds[t]))return!1;if(t.hyperlinks.length!==r.hyperlinks.length||t.hyperlinks.some((e,t)=>{const i=r.hyperlinks[t];return e.row!==i.row||e.startColumn!==i.startColumn||e.endColumn!==i.endColumn||e.uri!==i.uri}))return!1;if(e.length!==i.length)return!1;for(let t=0;tNumber.isSafeInteger(e)&&e>=0)||!Array.isArray(r)||!["solid","dashed"].includes(s))throw new Error("Invalid link decorations");const o=this.presented?.metadata;if(!this.enabled||n||!o||e!==o.revision||t!==this.generation||i<=this.lastSerial)return!1;if(r.length>o.columns*o.rows||r.some(e=>!e||!Number.isInteger(e.row)||e.row<0||e.row>=o.rows||!Number.isInteger(e.startColumn)||!Number.isInteger(e.endColumn)||e.startColumn<0||e.endColumn<=e.startColumn||e.endColumn>o.columns))throw new Error("Invalid link decoration ranges");const a=new Uint8Array(o.columns*o.rows),l="dashed"===s?5:1;for(const e of r)a.fill(l,e.row*o.columns+e.startColumn,e.row*o.columns+e.endColumn);return this.lastSerial=i,this.decorations=this.pending={revision:e,generation:t,serial:i,mask:a},!0}submission(){return{mask:this.decorations?.mask,acknowledgement:this.pending}}acknowledge(e,t){if(e&&e===this.pending&&!t&&this.enabled&&e.generation===this.generation&&e.revision===this.presented?.metadata.revision)return this.pending=void 0,{revision:e.revision,generation:e.generation,serial:e.serial}}}}}),ut=i({"src/marker-pages.ts"(){G(),Oe=class{#S;get pending(){return void 0!==this.#S}reset(){this.#S=void 0}accept(e){if(!e?.markerPage)return this.reset(),e;const{markers:t=[],markerPage:i,...r}=e,n=JSON.stringify(r);0===i.offset&&(this.#S={revision:i.revision,total:i.total,signature:n,markers:[],ids:new Set,bytes:0});const s=this.#S;if(!s||s.revision!==i.revision||s.total!==i.total||s.markers.length!==i.offset||s.signature!==n)throw new Error("Marker pages do not describe the same contiguous history snapshot");if(s.bytes+=(new TextEncoder).encode(JSON.stringify(t)).byteLength,s.bytes>A.metadataBytes)throw new Error("Retained marker inventory exceeds the 8 MiB client budget");for(const e of t){if(s.ids.has(e.id))throw new Error("Duplicate marker ID across inventory pages");s.ids.add(e.id),s.markers.push(e)}return s.markers.length{Ue?.closing||wt(e)})}function gt(e){ze&&!ze.disposed?Object.assign(ct,ze.metrics()):ze?.disposed&&Object.assign(ct,{imageCount:0,textureBytes:0,atlasGlyphs:0,atlasBytes:0,instanceBufferBytes:0}),self.postMessage({type:"stats",stats:{...ct},...void 0===e?{}:{text:e}})}function wt(e){if(je||Fe)return;je=!0;const t=n(e);ct.gpu="error",ct.connected=!1,clearInterval(tt),clearInterval(it),Ue?.dispose(),gt(),ft(t,"error"),ze?.dispose()}function yt(){Ke=!0,He||We||Ge||je||Fe||Ne&&ct.connected||!Ye||st.pending||ot||(He=!0,self.requestAnimationFrame(()=>{He=!1,Ge||We||je||Fe||Ne&&ct.connected||st.pending||ot||(et=vt())}))}async function vt(){if(!Ke||!Ye||!ze)return;Ke=!1,We=!0;const e=Qe;try{e&&nt.prepare(Je,Ye);const t=nt.submission();ze.resize(Ye.columns,Ye.rows,rt);const i=Ne||Math.floor(performance.now()/600)%2==0,r=ze.render(Je,Ye,i,t.mask,lt);if(await ze.idle(),je||Fe)return;if(Xe=i,ct.presentations++,ct.rendererCpuMs=r.cpuMs,ct.quads=r.quads,ct.drawCalls=r.drawCalls,ct.warnings=ze.canvasLimited?[...Ye.warnings,`Canvas resolution capped by the GPU; terminal grid remains ${Ye.columns}x${Ye.rows}`]:Ye.warnings,e){Qe=void 0,_e=!1,ct.frames++,e.full&&ct.fullFrames++,ct.revision=e.revision,ct.changedCells+=e.changedCells,ct.lastChangedCells=e.changedCells,ct.captureMs=Ye.stats.captureMs,ct.workloadBytes=Ye.stats.workloadBytes,ct.outputBatches=Ye.stats.outputBatches,ct.serverElapsedMs=Ye.stats.elapsedMs,ct.columns=Ye.columns,ct.rows=Ye.rows,ct.mouseTracking=Ye.mouseTracking,ct.peer=Ye.peer,ct.history=Ye.history;const t=function(e,t,i){const r=[];for(let n=0;nA.frameBytes)throw new Error("Invalid or oversized binary frame");const t=new DataView(e);let i=0;const r=e=>{if(e<0||e>t.byteLength-i)throw new Error("Truncated HWT1 frame")},n=()=>{r(4);const e=t.getUint32(i,!0);return i+=4,e};if(827610952!==n())throw new Error("Unsupported frame magic (expected HWT1)");const s=O(n(),"metadata length",2,A.metadataBytes);r(s);const o=JSON.parse(L.decode(new Uint8Array(e,i,s)));i+=s,j(o);const a=o.columns*o.rows,l=O(n(),"changed cell count",0,a);if(l>Math.floor((t.byteLength-i)/22))throw new Error("Truncated cell records");if(o.full&&l!==a)throw new Error("Incomplete full frame");const c=[],h=new Set;for(let s=0;s=a||h.has(s))throw new Error("Invalid or duplicate cell index");h.add(s);const l=n(),d=n(),u=n();"indexed-v1"===o.colorEncoding&&(C(l),C(d),C(u,!0));const p=t.getUint16(i,!0),f=t.getUint8(i+2),m=t.getUint8(i+3),g=t.getUint16(i+4,!0);if(i+=6,m>5)throw new Error("Unsupported underline style");r(g);const w=L.decode(new Uint8Array(e,i,g));i+=g,c.push({index:s,foreground:l,background:d,underlineColor:u,attributes:p,width:f,underlineStyle:m,text:w})}if(o.images.reduce((e,t)=>e+t.byteLength,0)!==t.byteLength-i)throw new Error("Image payload length mismatch");const d=o.images.map(t=>{const r=new Uint8Array(e,i,t.byteLength);return i+=t.byteLength,{...t,bytes:r}});return{metadata:o,cells:c,images:d}}(e),i=t.metadata;if(!at&&i.colorEncodings?.includes("indexed-v1")&&(at=!0,mt({type:"colorEncoding",value:"indexed-v1"})),!i.full&&(ot||i.baseRevision!==Ze||i.revision<=Ze||!Ye||i.columns!==Ye.columns||i.rows!==Ye.rows||(i.colorEncoding??null)!==(Ye.colorEncoding??null)))return ct.discardedFrames++,_e=!1,st.reset(),ot=!0,Ke=!1,Qe=void 0,mt({type:"ack",revision:i.revision}),mt({type:"resync"}),void ft(`Revision mismatch (local ${Ze}, base ${i.baseRevision}); requesting a full frame`);if(await et,je||Fe)return;if(await ze.idle(),await ze.updateImages(t.images,i.retainedImages),je||Fe)return;const r=performance.now(),n=i.full?new Array(i.columns*i.rows):Je.slice();for(const e of t.cells)n[e.index]=e;ze.prepareGlyphs(n),Je=n,Ye=i,Ze=i.revision;const s=st.pending&&!i.full,o=st.accept(i.history);if(ot=!1,Qe={revision:i.revision,full:i.full||s&&!!Qe?.full,changedCells:t.cells.length+(s?Qe?.changedCells??0:0)},Ve=Je.some(e=>e&&16&e.attributes&&!(64&e.attributes))||i.cursor.visible&&(0===i.cursor.shape||i.cursor.shape%2==1),ct.preparationCpuMs=performance.now()-r,void 0===o)return Ke=!1,_e=!1,void mt({type:"ack",revision:i.revision});i.history=o,Ke=!0}finally{Ge=!1,Ke&&yt()}}}var kt=i({"src/terminal-worker.ts"(){G(),Re(),dt(),ut(),o(),P(),h(),W(),je=!1,Fe=!1,Ne=!1,$e=!1,Ge=!1,We=!1,_e=!1,He=!1,Ke=!1,Ve=!1,Xe=!0,Je=[],Ze=0,et=Promise.resolve(),nt=new Le,st=new Oe,ot=!1,at=!1,lt=T(g),ct={revision:0,fullFrames:0,frames:0,presentations:0,changedCells:0,lastChangedCells:0,discardedFrames:0,imageCount:0,textureBytes:0,atlasGlyphs:0,atlasBytes:0,bytesReceived:0,imageUploadBytes:0,imagePayloadBytes:0,gpu:"initializing",connected:!1,warnings:[],fps:0,receivedKBps:0,workloadMBps:0,captureMs:0,rendererCpuMs:0,preparationCpuMs:0,workloadBytes:0,outputBatches:0,serverElapsedMs:0},ht={time:performance.now(),presentations:0,bytes:0,workload:0},self.addEventListener("error",e=>{e.preventDefault(),wt(e.error||new Error(e.message))}),self.addEventListener("unhandledrejection",e=>{e.preventDefault(),wt(e.reason)}),self.addEventListener("message",e=>{const t=e.data;if("init"===t.type)(async function(e){if(ze||Ue)throw new Error("Worker is already initialized");if($e=e.preserveOnDisconnect??!1,lt=T(k(void 0===e.palette?g:e.palette)),"function"!=typeof self.requestAnimationFrame)throw new Error("This browser does not support requestAnimationFrame in a dedicated OffscreenCanvas worker");if(ft("Loading terminal font and initializing renderer..."),ze=await Ee.create(e.canvas,e.scale,wt,e.font,e.renderer),je||Fe)return void ze?.dispose();ct.gpu="ready",ct.backingScale=e.scale,gt();const t="webgpu"===ze.backend.kind?"WebGPU":"WebGL2";ze.fallbackReason&&ft(`Using WebGL2: ${ze.fallbackReason}`),ft(`${t} ready. Attaching terminal view...`),Ue=new F({onReady(){je||Fe||(ct.connected=!0,self.postMessage({type:"connected"}),ft(`Connected · ${t} worker · server-authoritative cells and graphics`,"ready"),gt())},onFrame:bt,onError:wt,onClose(e){je||Fe||Ne||(Ne=!0,clearInterval(tt),clearInterval(it),async function(){if($e){if(await et,je||Fe)return;if(!(Ke||Ve&&!Xe)||st.pending||ot||(Ke=!0,await vt()),je||Fe)return}else Fe=!0,ze?.dispose();ct.connected=!1,ct.gpu="stopped",ct.fps=0,self.postMessage({type:"closed",details:e}),ft(`View disconnected (${void 0===e.code?e.reason:`${e.code}${e.reason?`: ${e.reason}`:""}`}). Attach another view to reconnect.`,"error"),gt()}().catch(wt))}}),tt=setInterval(()=>{const e=performance.now(),t=(e-ht.time)/1e3;ct.fps=(ct.presentations-ht.presentations)/t,ct.receivedKBps=(ct.bytesReceived-ht.bytes)/t/1e3,ct.workloadMBps=Math.max(0,ct.workloadBytes-ht.workload)/t/1e6,ht={time:e,presentations:ct.presentations,bytes:ct.bytesReceived,workload:ct.workloadBytes},gt()},1e3),it=setInterval(()=>{const e=Math.floor(performance.now()/600)%2==0;Ve&&e!==Xe&&yt()},100),Ue.start("websocket"===e.transport.type?l(e.transport.url):{connect:e=>(De=e,Be=Promise.withResolvers(),e.signal.addEventListener("abort",()=>{Be?.reject(e.signal.reason),qe?.reject(e.signal.reason)},{once:!0}),self.postMessage({type:"transportConnect"}),Be.promise)})})(t).catch(wt);else if("transportConnected"!==t.type||!De||je||Fe)if("transportFrame"!==t.type||!De||je||Fe)if("transportSent"!==t.type||je||Fe)if("transportClosed"===t.type)De?.onClose(t.details);else if("transportError"===t.type)De?.onError(new Error(t.message));else if("stop"===t.type)Fe=!0,clearInterval(tt),clearInterval(it),Ue?.dispose(),ze?.dispose(),self.close();else if("viewport"!==t.type||je||Fe)if("command"!==t.type||je||Fe)if("palette"!==t.type||je||Fe)if("linkDetection"!==t.type||je||Fe||Ne){if("linkDecorations"===t.type&&!je&&!Fe&&!Ne)try{nt.accept(t.revision,t.generation,t.serial,t.ranges,Ge||_e||!!Qe,t.underlineStyle)&&yt()}catch(e){wt(e)}}else try{if(!nt.configure(t.enabled,t.generation))return;if(!Ge&&!We&&!Qe){const e=nt.snapshot();e&&self.postMessage({type:"linkSnapshot",generation:nt.generation,snapshot:e})}yt()}catch(e){wt(e)}else try{lt=T(k(t.palette)),yt()}catch(e){wt(e)}else mt(t.command);else{if(!Number.isFinite(t.width)||!Number.isFinite(t.height)||t.width<0||t.height<0)return void wt(new Error("Invalid mounted viewport dimensions"));if(rt?.width===t.width&&rt?.height===t.height)return;rt={width:t.width,height:t.height},yt()}else qe?.resolve(),qe=void 0;else De.onFrame(t.buffer).then(()=>{je||Fe||self.postMessage({type:"transportReceived"})}).catch(wt);else Be?.resolve({send:e=>(qe=Promise.withResolvers(),self.postMessage({type:"transportSend",control:e}),qe.promise),dispose(){}})})}}),xt={};function Et(e){let t=e.length;const i={")":"(","]":"[","}":"{"};for(;t>0;){const r=e[t-1];if(!".,;:!?".includes(r)){if(i[r]){const n=e.slice(0,t);if(n.split(r).length>n.split(i[r]).length){t--;continue}}break}t--}return e.slice(0,t)}function Tt(e){try{const t="builtin"in e.rule?e.rule.builtin:void 0;let i;if("source"in e.rule){const t=e.rule.flags.replaceAll("g","");i=new RegExp(e.rule.source,`${t}g`)}else{const t={url:String.raw`https?:\/\/[^\s\0<>"'\x60]+`,uri:String.raw`[A-Za-z][A-Za-z0-9+.-]*:[^\s\0<>"'\x60]+`,absolutePath:String.raw`(?:[A-Za-z]:[\\/]|/)[^\s\0<>"'\x60]*`,homePath:String.raw`~/[^\s\0<>"'\x60]+`};i=new RegExp(t[e.rule.builtin],"giu")}let r=0,n=0,s=0,o=0;const a=[];for(const l of e.chunks){if(s+=l.text.length,l.text.length>X.chunk||s>X.totalText)return{id:e.id,error:"limit",message:"Link scan text limit exceeded"};i.lastIndex=0;const c=[];let h;for(;null!==(h=i.exec(l.text));){if(++n>X.totalText||r>=X.matches)return{id:e.id,error:"limit",message:"Link match limit exceeded"};if(!h[0].length){const e=l.text.codePointAt(i.lastIndex);i.lastIndex+=(i.unicode||i.unicodeSets)&&void 0!==e&&e>65535?2:1;continue}let s=h[0];if(t){const e=l.text[h.index-1];if(void 0!==e&&!/[\s([{"'<>=]/u.test(e))continue;if("uri"===t&&/^[a-z]:[\\/]/iu.test(s))continue;if(s=Et(s),!s||"absolutePath"===t&&("/"===s||/^[a-z]:[\\/]$/iu.test(s)))continue;if("url"===t)try{if(!new URL(s).hostname)continue}catch{continue}}const a={index:h.index,text:s,captures:h.slice(1),groups:{...h.groups}};if(o+=ie(a),o>X.resultText)return{id:e.id,error:"limit",message:"Link result text limit exceeded"};c.push(a),r++}a.push({key:l.key,matches:c})}return{id:e.id,results:a}}catch(t){return{id:e.id,error:"worker",message:t instanceof Error?t.message:String(t)}}}((t,i)=>{for(var r in i)e(t,r,{get:i[r],enumerable:!0})})(xt,{scanLinks:()=>Tt});var Ct=i({"src/link-detection-worker.ts"(){te(),ce(),"undefined"!=typeof self&&"function"==typeof self.postMessage&&self.addEventListener("message",e=>{self.postMessage(Tt(e.data))})}});var St=class{x=0;y=0;reset(){this.x=this.y=0}take(e,t,i){const r=t.height/i;if(!(r>0))return{x:0,y:0};const n=1===e.deltaMode?1:2===e.deltaMode?i:1/r;this.x+=e.deltaX*n,this.y+=e.deltaY*n;const s=Math.trunc(this.x),o=Math.trunc(this.y);return this.x-=s,this.y-=o,{x:Math.max(-32,Math.min(32,s)),y:Math.max(-32,Math.min(32,o))}}},It=class{owner=null;mode="character";endpoint=null;begin(e,t,{tracking:i,historical:r,readOnly:n,selection:s}){if(this.owner)return null;const o=r||n||!i||e.shiftKey;if(o&&0!==e.button)return null;if(this.owner=o?"local":"app",!o)return{owner:"app"};const a=!!e.shiftKey&&!!s&&["valid","pending"].includes(s.status)&&!1!==s.canExtend;return this.mode=a&&s?s.mode:e.altKey?"rectangle":e.detail>=3?"line":2===e.detail?"word":"character",this.endpoint={x:t.x,y:t.y},{owner:this.owner,mode:this.mode,extend:a}}move(e){return"local"!==this.owner?null:(this.endpoint={x:e.x,y:e.y},{...this.endpoint})}scrollPoint(e){if("local"===this.owner)return{x:"rectangle"===this.mode&&this.endpoint?this.endpoint.x:e.x,y:e.y}}wheelOwner(e,{tracking:t,historical:i,readOnly:r}){return this.owner??(i||r||!t||e.shiftKey?"local":"app")}end(){this.owner=null}};o();var Mt=Object.freeze({Continue:"continue",Consume:"consume",Application:"application",Browser:"browser"}),At=Object.freeze({CopySelection:"copySelection",PasteClipboard:"pasteClipboard",CopyOrPaste:"copyOrPaste",ClearSelection:"clearSelection",ScrollToLive:"scrollToLive",ScrollLines:"scrollLines"}),Lt=new Set(["l","t","v","w","+","-","=","0"]),Rt=new Set(["F1","F3","F5","F6","F7","F10","F11","F12"]),Pt=new Set(["Tab","Enter","Backspace","Delete","Escape","ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Insert",...Array.from({length:12},(e,t)=>`F${t+1}`)]),Ot=new Set(Object.values(Mt));function zt(){return[{id:"clipboard.copy-key",match:e=>"key"===e.type&&"c"===e.key.toLowerCase()&&!e.alt&&(e.meta&&!e.ctrl||e.ctrl&&e.shift&&!e.meta),action:At.CopySelection},{id:"browser.shortcuts",match:e=>"key"===e.type&&(e.meta||Rt.has(e.key)||e.ctrl&&("Tab"===e.key||"F4"===e.key||Lt.has(e.key.toLowerCase())||e.shift&&1===e.key.length)||e.alt&&!e.ctrl),route:Mt.Browser},{id:"terminal.keys",match:e=>"key"===e.type&&(Pt.has(e.key)||e.ctrl&&1===e.key.length),route:Mt.Application},{id:"clipboard.context-click",match:(e,t)=>"pointer"===e.type&&"right"===e.button&&!e.meta&&(!t.mouseCaptured||e.shift||t.historical||t.readOnly),action:At.CopyOrPaste}]}function Ut(e,t){if("function"!=typeof e||"AsyncFunction"===e.constructor.name)throw new TypeError(`${t} must be a synchronous function`)}var Dt=class{#I;#M;#A;constructor({inputBindings:e=[],onInput:t,actions:i={}}={}){if(!Array.isArray(e))throw new TypeError("inputBindings must be an array");if(!i||"object"!=typeof i||Array.isArray(i))throw new TypeError("actions must be an object");this.#A=new Set(Object.values(At));for(const[e,t]of Object.entries(i)){if(!e||this.#A.has(e)||"function"!=typeof t)throw new TypeError(`Invalid or reserved action: ${e}`);this.#A.add(e)}void 0!==t&&Ut(t,"onInput"),this.#M=t;const r=zt(),n=new Set,s=e.map(e=>{if(!e||"string"!=typeof e.id||!e.id||n.has(e.id))throw new TypeError("Each input binding needs a unique, nonempty id");if(n.add(e.id),!0===e.remove){if(!r.some(t=>t.id===e.id))throw new TypeError(`Cannot remove unknown default binding: ${e.id}`);if(Object.keys(e).some(e=>!["id","remove"].includes(e)))throw new TypeError(`Removed binding ${e.id} must contain only id and remove`);return null}return Ut(e.match,`Binding ${e.id}.match`),void 0!==e.when&&Ut(e.when,`Binding ${e.id}.when`),this.#L(e),Object.freeze({...e})}).filter(e=>null!==e);this.#I=[...s,...r.filter(e=>!n.has(e.id))]}get bindings(){return this.#I.map(e=>({...e}))}#L(e){if(Bt(e))return{route:e};if(!r(e)||"function"==typeof e.then)throw new TypeError("Input routing must synchronously return a route or action");const t=Object.hasOwn(e,"action"),i=Object.hasOwn(e,"route");if(t===i)throw new TypeError("Specify exactly one action or route");if(i){if(!Bt(e.route))throw new TypeError(`Unknown input route: ${String(e.route)}`);return{route:e.route}}if(!(function(e){return"function"==typeof e}(e.action)||"string"==typeof e.action&&this.#A.has(e.action)))throw new TypeError(`Unknown terminal action: ${String(e.action)}`);return{action:e.action,args:e.args}}resolve(e,t){const i=this.#M?.(e,t);if(void 0!==i){const e=this.#L(i);if(e.route!==Mt.Continue)return e}for(const i of this.#I){const r=i.match(e,t);if("boolean"!=typeof r)throw new TypeError(`Binding ${i.id}.match must return a boolean`);if(!r)continue;if(i.when){const r=i.when(t,e);if("boolean"!=typeof r)throw new TypeError(`Binding ${i.id}.when must return a boolean`);if(!r)continue}const n=this.#L(i);if(n.route!==Mt.Continue)return n}return{route:Mt.Continue}}};function Bt(e){return"string"==typeof e&&[...Ot].some(t=>t===e)}function qt(e){return{ctrl:!!e.ctrlKey,alt:!!e.altKey,shift:!!e.shiftKey,meta:!!e.metaKey}}var jt=[["left",1],["middle",4],["right",2]],Ft=["left","middle","right"];function Nt(e,t,i,r={}){const n=new AbortController,s={signal:n.signal};let o=1,a=1,l=0,c=null;const h=new Set;let d,u,p,f;const m=new St,g=new It;let w,y,v,b,k,x,E,T,C,S,I,M={count:0,time:0,x:-1,y:-1};const A=e.title,L=e.style.cursor,R=()=>({tracking:l,...r.state?.()});function P(t,i=!1){const n=function(e,t,i,r,n=!1){if(!(t.width>0&&t.height>0))return null;const s=Math.floor((e.clientX-t.left)*i/t.width),o=Math.floor((e.clientY-t.top)*r/t.height);return!n&&(s<0||o<0||s>=i||o>=r)?null:{x:Math.max(0,Math.min(i-1,s)),y:Math.max(0,Math.min(r-1,o)),shift:!!e.shiftKey,alt:!!e.altKey,ctrl:!!e.ctrlKey}}(t,e.getBoundingClientRect(),o,a,i);return n&&r.lineRenditions?.()[n.y]&&(n.x=Math.min(Math.max(0,Math.floor(o/2)-1),Math.floor(n.x/2))),n}function O(t=S){if(!r.hyperlink)return;S=t;const i=C&&P(C),n=i?r.hyperlink(i):null;e.title=n?`${n.target}\n${"click"===n.activation?"Click":"Ctrl/Cmd+click"} to activate link`:A,e.style.cursor=n&&t&&("click"===n.activation||t.ctrlKey||t.metaKey)&&!t.altKey&&!t.shiftKey?"pointer":L,T&&T.link.id!==n?.id&&(T.dragged=!0),I!==n?.id&&(I=n?.id,r.hoverHyperlink?.(n))}function z(){if(clearTimeout(v),v=void 0,"local"!==g.owner||!y)return;const t=e.getBoundingClientRect(),i=y.clientY=t.bottom?y.clientY-t.bottom+1:0;i&&(v=setTimeout(()=>{if(v=void 0,!y)return;const e=P(y,!0);if("local"===g.owner&&e){b&&(b.dragged=!0),M.count=0;const n=Math.sign(i)*Math.min(8,Math.max(1,Math.ceil(Math.abs(i)/(t.height/a))));r.scroll?.(n,g.scrollPoint(e)),z()}},60))}function U(){void 0!==f&&cancelAnimationFrame(f),f=void 0,u&&t(u),u=void 0}function D(e,i){for(const[r,n]of jt){const s=0!==(e.buttons&n);s!==h.has(r)&&(U(),s?h.add(r):h.delete(r),(s||9!==l)&&t({type:"mouse",action:s?"down":"up",button:r,...i}),p=void 0)}}function B(i=!0,n=!0){if(r.end?.(n),clearTimeout(v),v=void 0,y=void 0,b=void 0,k=void 0,T=void 0,g.end(),x=void 0,i?U():(void 0!==f&&cancelAnimationFrame(f),f=void 0,u=void 0),i&&9!==l&&d)for(const e of h)t({type:"mouse",action:"up",button:e,...d});h.clear();const s=c;c=null,null!==s&&e.hasPointerCapture(s)&&e.releasePointerCapture(s),p=void 0,m.reset(),w=void 0}return e.addEventListener("pointerdown",t=>{if(t.defaultPrevented&&null===c)return;if("mouse"!==t.pointerType||![0,1,2].includes(t.button))return;const n=P(t);if(!n)return;if(null!==c)return T&&(T.dragged=!0),x!==Mt.Browser&&t.preventDefault(),void("app"!==g.owner&&x!==Mt.Application||c!==t.pointerId||D(t,n));i();const s={type:"pointer",button:Ft[t.button],point:Object.freeze({...n}),...qt(t)},o=r.resolve?.(s)??{route:Mt.Continue};let a=void 0!==o.action?Mt.Consume:o.route;if(a===Mt.Continue&&0===t.button&&!t.altKey&&!t.shiftKey){const e=r.hyperlink?.(n);e&&("click"===e.activation||t.ctrlKey||t.metaKey)&&(T={link:e,input:s,x:t.clientX,y:t.clientY,dragged:!1},a=Mt.Consume)}if(E=2===t.button?a:void 0,T&&(E=Mt.Consume),C=t,O(t),a!==Mt.Continue)return k=b=void 0,M.count=0,x=a,c=t.pointerId,d=n,e.setPointerCapture(c),a!==Mt.Browser&&t.preventDefault(),a===Mt.Application?U():(void 0!==f&&cancelAnimationFrame(f),f=void 0,u=void 0),a===Mt.Application&&D(t,n),void(void 0!==o.action&&r.execute?.(o,s));if(t.preventDefault(),t.metaKey)return;const l=performance.now();M.count=l-M.time<500&&n.x===M.x&&n.y===M.y?M.count%3+1:1,M={count:M.count,time:l,x:n.x,y:n.y};const h=g.begin({button:t.button,shiftKey:t.shiftKey,altKey:t.altKey,detail:t.detail||M.count},n,R());h&&(c=t.pointerId,y=t,d=n,k=void 0,e.setPointerCapture(c),"local"===h.owner?(void 0!==f&&cancelAnimationFrame(f),f=void 0,u=void 0,p=void 0,b={point:n,mode:h.mode,extend:h.extend,dragged:!1},r.begin?.(n,h)):D(t,n))},s),e.addEventListener("pointermove",e=>{if("mouse"!==e.pointerType)return;if(null===c&&e.buttons)return;if(null!==c&&c!==e.pointerId)return;C=e,O(e),T&&Math.hypot(e.clientX-T.x,e.clientY-T.y)>=4&&(T.dragged=!0);const t=P(e,null!==c);if(!t)return;if(d=t,x&&x!==Mt.Application)return;if("local"===g.owner){y=e;const i=g.endpoint,n=g.move(t);return n&&i&&(n.x!==i.x||n.y!==i.y)&&(M.count=0,b&&(b.dragged=!0),r.extend?.(n)),void z()}if(!l||!x&&null===g.owner&&(R().historical||R().readOnly||e.shiftKey))return;if(c===e.pointerId&&D(e,t),e.metaKey||1003!==l&&(1002!==l||!h.size))return;const i={type:"mouse",action:"move",button:jt.find(([e])=>h.has(e))?.[0]??"none",...t},n=JSON.stringify(i);n!==p&&(p=n,u=i,void 0===f&&(f=requestAnimationFrame(U)))},s),e.addEventListener("pointerup",e=>{if(c!==e.pointerId)return;if(T){e.preventDefault();const t=T,i=P(e),n=0===e.button&&0===e.buttons&&!t.dragged&&Math.hypot(e.clientX-t.x,e.clientY-t.y)<4&&i&&r.hyperlink?.(i)?.id===t.link.id;return 0===e.buttons?B(!1,!1):t.dragged=!0,void(n&&r.openHyperlink?.(t.link,t.input))}const t=P(e,!0)??d;if(x)return x===Mt.Application&&t&&D(e,t),void(0===e.buttons&&B(x===Mt.Application));if("local"===g.owner){if(t&&g.endpoint&&(t.x!==g.endpoint.x||t.y!==g.endpoint.y)){b&&(b.dragged=!0);const e=g.move(t);e&&r.extend?.(e)}const e=b;return B(!1,!1),void(k=e)}t&&D(e,t),h.size||B()},s),e.addEventListener("click",e=>{const t=k;if(k=void 0,!t||t.dragged||!Number.isInteger(e.detail)||e.detail<1)return;M.count=e.detail;const i=e.detail>=3?"line":2===e.detail?"word":"character";t.extend||"rectangle"===t.mode||i===t.mode||r.begin?.(t.point,{mode:i,extend:!1})},s),e.addEventListener("pointercancel",()=>B(),s),e.addEventListener("pointerleave",()=>{C=void 0,O()},s),e.addEventListener("lostpointercapture",()=>{null!==c&&B()},s),window.addEventListener("blur",()=>{B(),C=void 0,O()},s),window.addEventListener("keydown",e=>O(e),{...s,capture:!0}),window.addEventListener("keyup",e=>O(e),{...s,capture:!0}),e.addEventListener("contextmenu",e=>{if(void 0!==E&&E!==Mt.Continue){const t=E;return E=void 0,void(t!==Mt.Browser&&e.preventDefault())}(l||"local"===g.owner)&&e.preventDefault()},s),e.addEventListener("wheel",i=>{T&&(T.dragged=!0);const n={type:"wheel",deltaX:i.deltaX,deltaY:i.deltaY,deltaMode:i.deltaMode,point:P(i),...qt(i)},s=null===c?r.resolve?.(n)??{route:Mt.Continue}:{route:x??Mt.Continue};if(s.route===Mt.Browser)return;if(void 0!==s.action||s.route===Mt.Consume)return i.preventDefault(),void(void 0!==s.action&&r.execute?.(s,n));if(s.route!==Mt.Application&&(i.ctrlKey||i.metaKey))return;const o=s.route===Mt.Application?"app":g.wheelOwner(i,R());if("app"===o&&9===l)return;const h=P("local"===g.owner&&y?y:i,"local"===g.owner);if(!h)return;i.preventDefault(),U();const d=e.getBoundingClientRect();w!==o&&m.reset(),w=o;const{x:u,y:p}=m.take(i,d,a);if("local"===o)return void(p&&(b&&(b.dragged=!0),M.count=0,r.scroll?.(p,g.scrollPoint(h))));const f=[[p,"wheelUp","wheelDown"],[u,"wheelLeft","wheelRight"]];for(const[e,i,r]of f)e&&t({type:"mouse",action:"wheel",button:e<0?i:r,count:Math.min(32,Math.abs(e)),...h})},{...s,passive:!1}),{update(e,t,i){o===e&&a===t&&l===i||(d&&(d={...d,x:Math.min(d.x,e-1),y:Math.min(d.y,t-1)}),o===e&&a===t||B(0!==i),o=e,a=t,l=i),O()},refresh(){O()},cancel(){B()},dispose(){B(!1),n.abort(),e.title=A,e.style.cursor=L}}}function $t(e){const t=new URL(import.meta.url);return t.hash=`hex1b-${e}-worker`,t}h(),W(),p(),K(),Y(),P();var Gt=Object.freeze({track:"#202020",thumb:"#999999",marker:"#aaaaaa",error:"#eeeeee",prompt:"#666666","command-line":"#888888",executing:"#bbbbbb",success:"#999999",custom:"#dddddd"});function Wt(e,t){if("custom"===e.source)return t("custom");if(null!=e.exitCode&&0!==e.exitCode)return t("error");switch(e.phase){case"prompt":return t("prompt");case"commandLine":return t("command-line");case"executing":return t("executing");case"finished":return t(0===e.exitCode?"success":"marker");default:return t("marker")}}var _t=8,Ht=32;function Kt(e,t){if(!Number.isInteger(e)||e<1||e>300||!Number.isInteger(t)||t<1||t>100)throw new RangeError("Requested grid must be 1-300 columns by 1-100 rows");return{columns:e,rows:t}}function Vt(e={mode:"auto"},t=16){if(!e||!["auto","fixed"].includes(e.mode))throw new TypeError("Sizing mode must be 'auto' or 'fixed'");const i=e.fontSize??t;if(!Number.isInteger(i)||i<8||i>32)throw new RangeError("Font size must be an integer from 8 to 32");return"fixed"===e.mode?{mode:"fixed",fontSize:i,...Kt(e.columns,e.rows)}:{mode:"auto",fontSize:i}}var Xt="Text history is not available for this terminal view",Yt="Selection expired: its text was changed, evicted, reset, or resized. Select the text again.",Jt="Selection changed before copy completed. Copy the new selection again.",Zt=class{#e;#t;#R=null;#o=0;#P=0;#O=0;#z=0;#U="character";#D="clear";#B;#q;constructor(e,t=()=>{}){this.#e=e,this.#t=t}get viewport(){if(!this.#R)return{available:!1,following:!0,pending:!1};const{selection:e,copy:t,markers:i,markerResult:r,markerPage:n,viewportError:s,...o}=this.#R;return{...o,rowIds:[...o.rowIds],available:!0,revision:this.#o,...s&&o.requestId>=this.#z?{navigationError:s}:{},pending:o.requestId({...e})),text:null,message:"Resolving selection…"}:{...e,canExtend:"valid"===e.status,ranges:e.ranges.map(e=>({...e})),revision:this.#o,message:"invalidated"===e.status?Yt:""}}accept(e,t){if(t<=this.#o)return!1;const i=this.#R?.generation;this.#R=e,this.#o=t,(!e||i&&i!==e.generation||"invalidated"===e.selection.status)&&(this.endGesture(!0),this.#j(new Error(Yt)));const r=this.#B;if(r&&e?.copy?.requestId===r.requestId&&("valid"!==e.copy.status||"valid"!==e.selection.status||e.selection.requestId!==r.selectionRequestId||e.generation!==r.generation||this.#O>r.selectionRequestId||e.copy.text!==e.selection.text?this.#j(new Error(Yt)):(clearTimeout(r.timer),this.#B=void 0,r.resolve(e.copy.text))),this.#q&&!this.viewport.pending){const e=this.#q;this.#q=void 0,this.extend(e)}return this.#t(),!0}#F(){if(!this.#R)throw new Error(Xt);return this.#R}#N(e){const t=this.#F();if(!e||!Number.isInteger(e.x)||e.x<0||e.x>1023||!Number.isInteger(e.y)||e.y<0||"string"!=typeof t.rowIds[e.y]||!t.rowIds[e.y].length)throw new Error("Terminal viewport is not ready for that selection point. Wait for the next frame and try again.");return t.rowIds[e.y]}#$(e=this.selection.mode,t="extend"){return this.#j(new Error(Jt)),this.#U=e,this.#D=t,this.#O=++this.#P,this.#O}begin(e,{mode:t,extend:i=!1}){const r=this.#N(e);this.#q=void 0;const n=this.#$(t,i?"extend":"start");this.#e({type:"selection",action:i?"extend":"start",mode:t,requestId:n,generation:this.#F().generation,rowId:r,column:e.x}),this.#t()}extend(e){if(this.#N(e),this.viewport.pending)return this.#j(new Error(Jt)),this.#U=this.selection.mode,this.#D="extend",this.#q={...e},void this.#t();this.begin(e,{mode:this.selection.mode,extend:!0})}scroll(e,t){if(this.#F(),!Number.isSafeInteger(e)||e<-2147483648||e>2147483647)throw new RangeError("Scroll delta must be a signed 32-bit integer");if(t&&this.#N(t),!e&&!t)return;t?this.#q=void 0:this.#G();const i=t?this.#$():++this.#P;this.#z=i,this.#e({type:"viewport",requestId:i,delta:e,...t?{extend:{row:t.y,column:t.x}}:{}}),this.#t()}live(){this.#F(),this.#G(),this.#z=++this.#P,this.#e({type:"viewport",live:!0,requestId:this.#z}),this.#t()}scrollTo(e){const t=this.#F();if(!Number.isSafeInteger(e)||e<0||e>2147483647)throw new RangeError("Scroll top must be a nonnegative 32-bit integer");this.#G(),this.#z=++this.#P,this.#e({type:"viewport",requestId:this.#z,top:Math.min(e,t.liveTop),generation:t.generation,originRowId:t.rowIds[0],originTop:t.top}),this.#t()}clear(){this.#F(),this.#q=void 0;const e=this.#$(this.selection.mode,"clear");this.#e({type:"selection",action:"clear",requestId:e}),this.#t()}endGesture(e=!1){e&&this.#q&&(this.#q=void 0,this.#t())}#G(){if(!this.#q)return;const e=this.#q;this.#q=void 0,this.scroll(0,e)}cancelCopy(e){this.#j(e)}copy(){const e=this.#F(),t=this.selection;if("valid"!==t.status)throw new Error(t.message||"Select text before copying.");this.#j(new Error("A newer copy request replaced this request."));const i=++this.#P,{promise:r,resolve:n,reject:s}=Promise.withResolvers(),o=setTimeout(()=>this.#j(new Error("Timed out resolving selection. Copy again.")),1e4);this.#B={requestId:i,selectionRequestId:t.requestId,generation:e.generation,resolve:n,reject:s,timer:o};try{this.#e({type:"copy",requestId:i,selectionRequestId:t.requestId,generation:e.generation})}catch(e){this.#j(e)}return r}#j(e){this.#B&&(clearTimeout(this.#B.timer),this.#B.reject(e),this.#B=void 0)}disconnect(){this.#j(new Error("Terminal view disconnected before copy completed.")),this.#q=void 0}};function Qt(e=0){if("number"!=typeof e&&!r(e))throw new TypeError("Padding must be a number or per-edge object");const t=t=>{const i="number"==typeof e?e:void 0===e[t]?0:e[t];if("number"!=typeof i||!Number.isFinite(i)||i<0)throw new RangeError(`Padding ${t} must be finite nonnegative CSS pixels`);return i};return Object.freeze({top:t("top"),right:t("right"),bottom:t("bottom"),left:t("left")})}function ei(e,t,i){return{width:Math.max(0,e.width-t.left-t.right-(i&&"beside"===i.placement?i.width:0)),height:Math.max(0,e.height-t.top-t.bottom)}}function ti(e,t,i,r,n,s){const o=ei(e,n,s),a=function(e,t,i,r){const n=i&&"auto"===r.mode?r.fontSize/16:1/0;return Math.max(0,Math.min(n,e.width/(t.columns*t.cellWidth),e.height/(t.rows*t.cellHeight)))}(o,t,i,r),l=t.columns*t.cellWidth*a,c=t.rows*t.cellHeight*a,h=Math.min(e.width,n.left)+(o.width-l)/2,d=Math.min(e.height,n.top)+(o.height-c)/2,u=s?Math.min(s.width,"overlay"===s.placement?l:Math.max(0,e.width-n.right-h-l)):0;return Object.freeze({width:e.width,height:e.height,content:Object.freeze({left:h,top:d,width:l,height:c}),scrollbar:s?Object.freeze({left:h+l-("overlay"===s.placement?u:0),top:d,width:u,height:c}):null,padding:n,cellWidth:l/t.columns,cellHeight:c/t.rows})}y(),o();var ii=/\b(?:var|env|attr|light-dark|currentcolor|inherit|initial|unset|revert|revert-layer|default|context-fill|context-stroke|accentcolor|accentcolortext|activetext|buttonborder|buttonface|buttontext|canvas|canvastext|field|fieldtext|graytext|highlight|highlighttext|linktext|mark|marktext|selecteditem|selecteditemtext|visitedtext|activeborder|activecaption|appworkspace|background|buttonhighlight|buttonshadow|captiontext|inactiveborder|inactivecaption|inactivecaptiontext|infobackground|infotext|menu|menutext|scrollbar|threeddarkshadow|threedface|threedhighlight|threedlightshadow|threedshadow|window|windowframe|windowtext)\b/i;function ri(e,t){if(null===e||"object"!=typeof e||"[object Object]"!==Object.prototype.toString.call(e))throw new TypeError(`${t} must be an options object`)}function ni(e){const t=function(e={}){let t;ri(e,"scrollbar.appearance");const i=(e,i)=>{if(void 0===e)return;if("string"!=typeof e||e.length>256||!e.trim()||/\\|\/\*|--/.test(e)||ii.test(e))throw new TypeError(`${i} must be a concrete CSS color of 1-256 characters`);const r=e.trim();if(void 0===t&&(t="undefined"!=typeof OffscreenCanvas?new OffscreenCanvas(1,1).getContext("2d"):"undefined"!=typeof document?document.createElement("canvas").getContext("2d"):null),!t)throw new TypeError(`${i} requires a browser canvas to validate CSS colors`);t.fillStyle="#010203",t.fillStyle=r;const n=t.fillStyle;if(t.fillStyle="#040506",t.fillStyle=r,n!==t.fillStyle)throw new TypeError(`${i} must be a valid concrete CSS color`);return r},r=(e,t,r)=>{void 0!==e&&ri(e,r);const{color:n,opacity:s=t}=e??{};if("number"!=typeof s||!Number.isFinite(s)||s<0||s>1)throw new RangeError(`${r}.opacity must be a finite number between 0 and 1`);return Object.freeze({color:i(n,`${r}.color`),opacity:s})},{track:n,thumb:s,markers:o}=e,a=r(o,1,"scrollbar.appearance.markers");return Object.freeze({track:r(n,.35,"scrollbar.appearance.track"),thumb:r(s,1,"scrollbar.appearance.thumb"),markers:Object.freeze({...a,errorColor:i(o?.errorColor,"scrollbar.appearance.markers.errorColor")})})}(e);return e=>{if(e.opacity<=0)return;const{context:i,track:r,thumb:n,colors:s}=e,o=i.globalAlpha*e.opacity;i.save();try{if(i.globalAlpha=o*t.track.opacity,i.fillStyle=t.track.color??s.track,r.width>0&&r.height>0){const n=2,o=6,a=12,l=Math.min(r.width,r.height)/2,c=r.left+r.width,h=r.top+r.height;let d,u=r.left;for(const{bounds:t}of e.markers){if(t.width<=0||t.height<=0||t.left>=r.left||t.top>=r.top+r.height||t.top+t.height<=r.top)continue;const e=o*Math.min(1,(r.left-t.left)/t.width),i=Math.max(0,t.left-e);if(i>=r.left)continue;u=Math.min(u,i),d??=new Float64Array(Math.ceil(r.height/n)+1);const s=Math.max(0,Math.floor((t.top-o-a-r.top)/n)),l=Math.min(d.length-1,Math.ceil((t.top+t.height+o+a-r.top)/n));for(let e=s;e<=l;e++){const s=r.top+Math.min(e*n,r.height),l=Math.max(t.top-o-s,s-t.top-t.height-o,0),c=Math.max(0,1-l/a),h=(r.left-i)*c*c*(3-2*c);d[e]=Math.max(d[e],h)}}if(d){const e=i.createLinearGradient(u,0,r.left,0);e.addColorStop(0,"transparent"),e.addColorStop(1,t.track.color??s.track),i.fillStyle=e;const a=r.left-d[0],p=r.left-d[d.length-1],f=a=h-m)continue;if(d[e-1]===d[e]&&d[e]===d[e+1]){g=!0;continue}const o=r.left-d[e],a=r.left-d[e+1];g&&(i.lineTo((r.left-d[e-1]+o)/2,t-n/2),g=!1),i.quadraticCurveTo(o,t,(o+a)/2,(t+s)/2)}i.lineTo(p,h-m),i.quadraticCurveTo(p,h,p+m,h),i.lineTo(c-l,h),i.quadraticCurveTo(c,h,c,h-l),i.lineTo(c,r.top+l),i.quadraticCurveTo(c,r.top,c-l,r.top),i.closePath(),i.fill()}else i.beginPath(),i.roundRect(r.left,r.top,r.width,r.height,l),i.fill()}i.globalAlpha=o*t.markers.opacity;for(const{marker:r,bounds:n,color:o}of e.markers)i.fillStyle=null!=r.exitCode&&0!==r.exitCode?t.markers.errorColor??o??s.error:t.markers.color??o??s.marker,r.color&&(i.fillStyle=r.color),n.width>0&&n.height>0&&(i.beginPath(),i.roundRect(n.left,n.top,n.width,n.height,Math.min(n.width,n.height)/2),i.fill());if(n.width>0&&n.height>0){const e=Math.min(2,n.width/4),r=n.width-2*e;i.globalAlpha=o*t.thumb.opacity,i.fillStyle=t.thumb.color??s.thumb,i.beginPath(),i.roundRect(n.left+e,n.top,r,n.height,Math.min(r,n.height)/2),i.fill()}}finally{i.restore()}}}var si=ni();function oi(e){si(e)}function ai(e){if(!1===e)return!1;if(void 0!==e&&(null===e||"object"!=typeof e||Array.isArray(e)))throw new TypeError("scrollbar must be false or an options object");const t=e??{},i=(e,t,i,r=0,n=1/0)=>{const s=void 0===e?t:e;if(!Number.isFinite(s)||sn)throw new RangeError(`scrollbar.${i} must be a finite number between ${r} and ${n}`);return s};if(void 0!==t.placement&&"overlay"!==t.placement&&"beside"!==t.placement)throw new TypeError("scrollbar.placement must be overlay or beside");if(void 0!==t.markers&&"boolean"!=typeof t.markers)throw new TypeError("scrollbar.markers must be boolean");if(void 0!==t.render&&("function"!=typeof t.render||"AsyncFunction"===t.render.constructor.name))throw new TypeError("scrollbar.render must be a synchronous function");if(void 0!==t.tooltip&&!1!==t.tooltip&&("function"!=typeof t.tooltip||"AsyncFunction"===t.tooltip.constructor.name))throw new TypeError("scrollbar.tooltip must be false or a synchronous function");return Object.freeze({placement:t.placement??"overlay",width:i(t.width,12,"width",4,64),proximity:i(t.proximity,24,"proximity"),hideDelay:i(t.hideDelay,900,"hideDelay"),fadeDuration:i(t.fadeDuration,300,"fadeDuration"),markers:t.markers??!0,...t.render?{render:t.render}:{},...void 0!==t.tooltip?{tooltip:t.tooltip}:{}})}var li=(e,t,i)=>Math.max(t,Math.min(i,e)),ci=(e,t,i,r=0)=>t>=e.left-r&&t=e.top-r&&in?i>=e.top-3&&i{if(!ci(e,t,i))return!1;const r=Math.min(e.width,e.height)/2;return(t-(e.left+e.width/2))**2+(i-e.top-e.height/2)**2<=r**2})(e,t,i)).sort((e,t)=>Math.abs(e.bounds.top+e.bounds.height/2-i)-Math.abs(t.bounds.top+t.bounds.height/2-i)||(e.marker.idt.marker.id?1:0))[0]?.marker}var ui=class{constructor(e){this.options=e,this.motion="function"==typeof matchMedia?matchMedia("(prefers-reduced-motion: reduce)"):void 0;const t=(e,t,i,r=!1)=>{e.addEventListener(t,i,{capture:!0,passive:r}),this.listeners.push(()=>e.removeEventListener?.(t,i,{capture:!0}))};t(e.element,"pointerdown",e=>this.pointerDown(e)),t(e.element,"pointermove",e=>this.pointerMove(e)),t(e.element,"pointerup",e=>this.pointerEnd(e,!1)),t(e.element,"pointercancel",e=>this.pointerEnd(e,!0)),t(e.element,"lostpointercapture",e=>this.pointerEnd(e,!0)),t(e.element,"pointerleave",()=>{this.near=this.hovered=!1,this.clearHover(),this.activity()}),t(e.element,"wheel",e=>this.wheel(e));for(const i of["mousedown","mouseup","click","dblclick","contextmenu"])t(e.element,i,e=>{this.contentPointers.size||this.contentClick||(null!==this.pointer||this.hit(e)||this.suppressClick&&("click"===i||"mouseup"===i))&&(this.consume(e),"click"===i&&(this.suppressClick=!1))});if(t(e.accessibility,"keydown",e=>this.keyDown(e)),t(e.accessibility,"focus",()=>{this.focused=!0,this.activity()}),t(e.accessibility,"blur",()=>{this.focused=!1,this.activity()}),this.motion){const e=()=>this.invalidate();this.motion.addEventListener("change",e),this.listeners.push(()=>this.motion?.removeEventListener("change",e))}e.accessibility.setAttribute("role","scrollbar"),e.accessibility.setAttribute("aria-label","Terminal scrollback"),e.accessibility.setAttribute("aria-orientation","vertical"),e.accessibility.style.position="absolute",e.accessibility.style.background="transparent",this.refresh()}options;context;listeners=[];motion;configuration;frameId;fadeTimer;disposed=!1;failed=!1;dormant=!1;near=!1;hovered=!1;focused=!1;lastActivityAt=-1/0;pointer=null;dragOffset=null;contentPointers=new Set;contentClick=!1;suppressClick=!1;desired=null;dragTop=null;visualTop=null;visualTarget=null;visualFrom=0;visualStartedAt=0;queued=null;requestBaseline=0;generation;wheelRemainder=0;hoverPoint=null;refresh(){if(this.disposed)return;const e=this.options.getState();e.configuration!==this.configuration&&(this.cancel(),this.configuration=e.configuration,this.failed=!1,this.lastActivityAt=this.now()),e.viewport.generation!==this.generation&&(this.endGesture(),this.desired=this.queued=null,this.visualTop=this.visualTarget=null,this.generation=e.viewport.generation),null===this.pointer&&null===this.queued&&e.viewport.available&&!e.viewport.pending&&(e.viewport.top===this.desired||e.viewport.requestId>this.requestBaseline)&&(this.desired=null),this.updateVisualPosition(e);const t=this.geometry(e);if(!t)return this.cancel(),this.options.accessibility.hidden=!0,this.options.accessibility.tabIndex=-1,void this.clear();const{track:i}=t,r=this.options.accessibility;r.hidden=!1,r.tabIndex=0,r.matches?.(":focus")&&(this.focused=!0),Object.assign(r.style,{left:`${i.left}px`,top:`${i.top}px`,width:`${i.width}px`,height:`${i.height}px`}),r.setAttribute("aria-valuemin","0"),r.setAttribute("aria-valuemax",String(e.viewport.liveTop)),r.setAttribute("aria-valuenow",String(this.desired??e.viewport.top)),r.setAttribute("aria-valuetext",e.viewport.following?"Live terminal output":`Scrollback row ${this.desired??e.viewport.top}`),r.setAttribute("aria-busy",String(e.viewport.pending)),this.refreshHover(t),this.schedule()}activity(){this.disposed||(this.dormant=!1,this.lastActivityAt=this.now(),void 0!==this.fadeTimer&&clearTimeout(this.fadeTimer),this.fadeTimer=void 0,this.schedule())}invalidate(){this.dormant=!1,this.refresh()}cancel(){this.clearHover(),this.endGesture(),this.contentPointers.clear(),this.contentClick=!1,this.desired=this.queued=null,this.visualTop=this.visualTarget=null,this.near=this.hovered=this.focused=this.suppressClick=!1,this.dormant=!1,this.wheelRemainder=0,void 0!==this.frameId&&cancelAnimationFrame(this.frameId),void 0!==this.fadeTimer&&clearTimeout(this.fadeTimer),this.frameId=this.fadeTimer=void 0,this.options.accessibility.hidden=!0,this.options.accessibility.tabIndex=-1,this.options.accessibility.title="",this.clear()}dispose(){if(!this.disposed){this.disposed=!0,this.cancel();for(const e of this.listeners)e();this.options.accessibility.hidden=!0,this.options.accessibility.tabIndex=-1}}now(){return performance.now()}updateVisualPosition(e,t=!1){if(!e.viewport.available)return;const i=li(this.dragTop??this.desired??e.viewport.top,0,e.viewport.liveTop),r=this.now();if(null===this.visualTop||null!==this.dragTop||this.motion?.matches||this.dormant)return this.visualFrom=this.visualTop=this.visualTarget=i,void(this.visualStartedAt=r);if(i!==this.visualTarget&&(this.visualFrom=this.visualTop,this.visualTarget=i,this.visualStartedAt=r),t){const e=li((r-this.visualStartedAt)/120,0,1);this.visualTop=1===e?i:this.visualFrom+(i-this.visualFrom)*(1-(1-e)**3)}}geometry(e=this.options.getState()){return e.connected&&e.configuration?function(e,t,i,r=null){const n=e.scrollbar;if(!n||n.width<=0||n.height<=0||!t.available||"main"!==t.buffer||t.liveTop<=0)return null;const s=Math.max(1,t.totalRows-t.liveTop),o=Math.min(n.height,Math.max(24,n.height*s/t.totalRows)),a=Object.freeze({left:n.left,top:n.top+(n.height-o)*li(r??t.top,0,t.liveTop)/t.liveTop,width:n.width,height:o}),l=Math.min(5,n.height,.75*(a.width-2*Math.min(2,a.width/4))),c=n.left+(n.width-l)/2,h=Math.max(0,n.left-l-2),d=Math.min(8,n.height),u=i.filter(e=>e.buffer===t.buffer&&null!==e.row&&Number.isFinite(e.row)&&e.row>=0&&e.row{const i=n.top+(n.height-l)*e.row/Math.max(1,t.totalRows-1),r=Math.max(a.top-i-l,i-a.top-a.height,0),s=1-li(r/d,0,1),o=c+s*s*(3-2*s)*(h-c);return Object.freeze({marker:Object.freeze({...e}),bounds:Object.freeze({left:o,top:i,width:l,height:l})})});return Object.freeze({track:Object.freeze({...n}),thumb:a,markers:Object.freeze(u)})}(e.layout,e.viewport,e.configuration.markers?e.markers:[],this.visualTop??this.desired):null}point(e){const t=this.options.element.getBoundingClientRect(),i=this.options.getState().layout;return{x:t.width>0?(e.clientX-t.left)*i.width/t.width:-1/0,y:t.height>0?(e.clientY-t.top)*i.height/t.height:-1/0}}hit(e){const t=this.geometry(),i=this.point(e);return!!t&&(ci(t.track,i.x,i.y)||!!this.markerAt(t,i))}markerAt(e,t){if(ci(e.thumb,t.x,t.y))return;const i=this.options.getState().configuration;return ci(e.track,t.x,t.y)||!(this.failed||!i||hi(this.now(),this.lastActivityAt,this.near||this.hovered||this.focused||null!==this.pointer,i,this.motion?.matches??!1)<=0)?di(e.markers,t.x,t.y,e.track):void 0}consume(e){e.preventDefault(),e.stopImmediatePropagation()}pointerDown(e){if(this.clearHover(),this.suppressClick=!1,this.contentClick=!1,!this.hit(e))return this.contentPointers.add(e.pointerId),void(this.contentClick=!0);if(this.contentPointers.size)return void(this.contentClick=!0);if(this.consume(e),0!==e.button||null!==this.pointer)return;const t=this.geometry(),i=this.point(e);this.pointer=e.pointerId;try{this.options.element.setPointerCapture(e.pointerId)}catch(e){return this.endGesture(),void this.report(e)}if(this.options.accessibility.setAttribute("data-pointer-active","true"),this.options.accessibility.focus({preventScroll:!0}),this.focused=!0,this.activity(),ci(t.thumb,i.x,i.y)){this.dragOffset=i.y-t.thumb.top;const e=this.options.getState().viewport;return void(e.available&&(this.dragTop=this.visualTop??this.desired??e.top))}const r=this.markerAt(t,i);if(r)return void this.jump(r.id);const n=this.options.getState().viewport;n.available&&this.navigate((this.desired??n.top)+(i.y0&&(this.dragTop=li((i.y-this.dragOffset-t.track.top)/r*e.liveTop,0,e.liveTop),this.navigate(this.dragTop))}return void this.activity()}if(this.contentPointers.size||0!==e.buttons)return void this.clearHover();const r=this.hoveredMarker(t);this.hoverPoint={clientX:e.clientX,clientY:e.clientY};const n=this.refreshHover(t);r?.marker.id!==n?.marker.id&&(this.dormant=!1,this.schedule()),this.hovered&&this.consume(e)}refreshHover(e){if(e&&this.hoverPoint&&null===this.pointer&&!this.contentPointers.size){const t=this.point(this.hoverPoint),i=this.options.getState().configuration,r=ci(e.track,t.x,t.y,i?i.proximity:0),n=ci(e.track,t.x,t.y)||!!this.markerAt(e,t);r===this.near&&n===this.hovered||(this.near=r,this.hovered=n,this.activity()),this.hovered=ci(e.track,t.x,t.y)||!!this.markerAt(e,t)}const t=this.hoveredMarker(e);return this.options.onMarkerHover?.(t),t}clearHover(){this.hoverPoint=null,this.options.onMarkerHover?.(null)}hoveredMarker(e=this.geometry()){if(!this.hoverPoint||null!==this.pointer||this.contentPointers.size)return null;if(!e)return null;const t=this.point(this.hoverPoint),i=this.markerAt(e,t);return e.markers.find(e=>e.marker.id===i?.id)??null}pointerEnd(e,t){this.contentPointers.delete(e.pointerId),this.pointer===e.pointerId&&(this.consume(e),this.suppressClick=!t,this.endGesture(),t&&(this.desired=this.queued=null,this.visualTop=this.visualTarget=null),this.updateVisualPosition(this.options.getState()),this.activity())}endGesture(){const e=this.pointer;if(this.pointer=this.dragOffset=null,this.dragTop=null,this.options.accessibility.setAttribute("data-pointer-active","false"),null!==e)try{this.options.element.hasPointerCapture(e)&&this.options.element.releasePointerCapture(e)}catch(e){this.report(e)}}wheel(e){if(this.contentPointers.size||!this.hit(e))return;this.consume(e);const{viewport:t,layout:i}=this.options.getState();if(!t.available||!Number.isFinite(e.deltaY))return;const r=Math.max(1,t.totalRows-t.liveTop),n=this.options.element.getBoundingClientRect(),s=i.height>0?n.height/i.height:1;this.wheelRemainder+=e.deltaY*(2===e.deltaMode?r:1===e.deltaMode?1:1/Math.max(1,i.cellHeight*s));const o=Math.trunc(this.wheelRemainder);this.wheelRemainder-=o,0!==o&&this.navigate((this.desired??t.top)+o),this.activity()}keyDown(e){const{viewport:t}=this.options.getState();if(!this.geometry()||!t.available||e.ctrlKey||e.metaKey)return;const i=this.desired??t.top,r=Math.max(1,t.totalRows-t.liveTop);if(e.altKey&&("ArrowDown"===e.key||"ArrowUp"===e.key)){this.consume(e);const t="ArrowDown"===e.key,r=this.geometry().markers.map(e=>e.marker).filter(e=>t?e.row>i:e.row(t?1:-1)*(e.row-i.row)||(e.idi.id?1:0));return r[0]&&this.jump(r[0].id),void this.activity()}if(e.altKey)return;const n={ArrowUp:i-1,ArrowDown:i+1,PageUp:i-r,PageDown:i+r,Home:0,End:t.liveTop};Object.hasOwn(n,e.key)&&(this.consume(e),this.navigate(n[e.key]),this.activity())}jump(e){this.desired=this.queued=null;try{this.options.scrollToMarker(e).catch(e=>this.report(e))}catch(e){this.report(e)}}navigate(e){const t=this.options.getState(),i=t.viewport;if(!i.available||!Number.isFinite(e))return;const r=Math.round(li(e,0,i.liveTop));null!==this.dragTop&&r===(this.desired??i.top)||(this.requestBaseline=i.requestId,this.queued=r),this.desired=r,this.updateVisualPosition(t),this.schedule()}schedule(){this.disposed||void 0!==this.frameId||!this.geometry()||this.dormant&&null===this.queued||"function"!=typeof requestAnimationFrame||(this.frameId=requestAnimationFrame(()=>{this.frameId=void 0,this.paint()}))}clear(){this.context&&(this.context.save(),this.context.setTransform(1,0,0,1,0,0),this.context.clearRect(0,0,this.options.canvas.width,this.options.canvas.height),this.context.restore())}paint(){if(this.disposed)return;let e=this.options.getState();if(!this.geometry(e)||!e.configuration)return;if(null!==this.queued){const t=this.queued;this.queued=null;try{e.viewport.available&&t>=e.viewport.liveTop?this.options.scrollToLive():this.options.scrollToRow(t)}catch(e){this.desired=null,this.report(e)}}e=this.options.getState(),this.updateVisualPosition(e,!0);const t=this.geometry(e);if(!t||!e.configuration)return;if(this.refreshHover(t),this.disposed||e.configuration!==this.options.getState().configuration)return;const{canvas:i,accessibility:r}=this.options;if(r.setAttribute("aria-valuenow",String(this.desired??e.viewport.top)),!this.failed&&void 0===this.context)try{if(this.context=i.getContext("2d"),!this.context)throw new Error("The scrollbar Canvas2D context is unavailable")}catch(e){this.failed=!0,this.endGesture(),this.report(e)}if(!this.context||this.failed)return;const n=this.context,s=li("number"==typeof devicePixelRatio?devicePixelRatio:1,1,3),o=Math.max(1,Math.round(e.layout.width*s)),a=Math.max(1,Math.round(e.layout.height*s));i.width!==o&&(i.width=o),i.height!==a&&(i.height=a),this.clear();const l=this.now(),c=this.near||this.hovered||this.focused||null!==this.pointer,h="overlay"===e.configuration.placement,d=this.motion?.matches??!1,u=hi(l,this.lastActivityAt,c,e.configuration,d),p="function"==typeof getComputedStyle?getComputedStyle(this.options.element):null,f=e=>((e,t)=>p?.getPropertyValue?.(e).trim()||t)(`--cp-view-scrollbar-${e}`,Gt[e]),m=Object.freeze({...e.layout,content:Object.freeze({...e.layout.content}),padding:Object.freeze({...e.layout.padding}),scrollbar:t.track}),g=Object.freeze({...e.viewport,...e.viewport.rowIds?{rowIds:Object.freeze([...e.viewport.rowIds])}:{}}),w=Object.freeze({canvas:i,context:n,layout:m,viewport:g,...t,now:l,opacity:u,pendingTarget:this.desired,markers:Object.freeze(t.markers.map(e=>Object.freeze({...e,color:Wt(e.marker,f)}))),hoveredMarker:this.hoveredMarker(t),interaction:Object.freeze({near:this.near,hovered:this.hovered,dragging:null!==this.dragOffset,focused:this.focused,lastActivityAt:this.lastActivityAt,reducedMotion:d}),colors:Object.freeze({track:f("track"),thumb:f("thumb"),marker:f("marker"),error:f("error")})});let y=!1;n.save();try{n.setTransform(s,0,0,s,0,0);const t=(e.configuration.render??oi)(w);if(void 0!==t&&"boolean"!=typeof t)throw t instanceof Promise&&t.catch(e=>{this.disposed||this.report(e)}),new TypeError("scrollbar.render must synchronously return boolean or undefined");y=t}catch(e){this.failed=!0,this.endGesture(),this.desired=this.queued=null,void 0!==this.frameId&&cancelAnimationFrame(this.frameId),void 0!==this.fadeTimer&&clearTimeout(this.fadeTimer),this.frameId=this.fadeTimer=void 0,this.report(e)}finally{n.restore()}this.failed?this.clear():this.disposed||e.configuration!==this.options.getState().configuration||(this.dormant=!c&&0===u&&!0!==y,!0===y||u>0&&this.visualTop!==this.visualTarget||h&&!c&&u>0&&l>=this.lastActivityAt+e.configuration.hideDelay?this.schedule():h&&!c&&u>0&&void 0===this.fadeTimer&&(this.fadeTimer=setTimeout(()=>{this.fadeTimer=void 0,this.schedule()},Math.min(2147483647,Math.max(0,this.lastActivityAt+e.configuration.hideDelay-l)))))}report(e){this.disposed||this.options.reportError(e)}};function pi(e){const t=new Map;if(!e)return t;for(const i of e.split(";")){const e=i.indexOf("=");e<0||t.set(i.slice(0,e),i.slice(e+1))}return t}function fi(e){return e?pi(e.rawParameters).get("cmdline_url")??null:null}function mi(e){const t=document.createElement("div");t.className="hex1b-scrollbar-tooltip",t.setAttribute("role","tooltip"),t.style.cssText="padding:8px 10px;border:1px solid var(--cp-view-text-muted,#888);border-radius:6px;background:var(--cp-view-surface,#202020);color:var(--cp-view-text,#eee);font:12px/1.4 var(--cp-view-font-family,monospace);white-space:pre-wrap;overflow-wrap:anywhere";const{marker:i,details:r}=e,n=[i.label||("custom"===i.source?"Bookmark":"Terminal command")];if("command"===i.source&&(n.push(`Phase: ${i.phase??"unknown"}`),null!=i.exitCode&&n.push(`Exit code: ${i.exitCode}`),e.loading&&n.push("Loading command details..."),e.error&&n.push(`Command details unavailable: ${e.error}`),r)){const e=fi(r);if(e)try{n.push(decodeURIComponent(e))}catch(t){if(!(t instanceof URIError))throw t;n.push(`Command (invalid URL encoding): ${e}`)}else r.rawParameters?n.push(r.rawParameters):n.push("This shell did not provide command text.")}return t.textContent=n.join("\n"),t}o();var gi=class{constructor(e){this.options=e}options;target=null;result=null;fetching=!1;lifetime;observer;element=null;failed=!1;disposed=!1;update(e,t,i=mi){if(this.disposed)return;if(!e||!1===i)return this.target=this.result=null,this.failed=!1,void this.clearView();const r=JSON.stringify({tick:e,layout:t});if(this.target?.signature===r&&this.target.renderer===i)return;const n=JSON.stringify([e.marker.id,e.marker.phase,e.marker.exitCode]);this.target?.key===n&&this.target.renderer===i||(this.result=null,this.failed=!1),this.target={tick:e,layout:t,renderer:i,key:n,signature:r},this.render(),this.loadDetails()}dispose(){this.disposed=!0,this.target=this.result=null,this.clearView()}clearView(){this.observer?.disconnect(),this.observer=void 0;const e=this.lifetime;this.lifetime=void 0,this.element=null,this.options.overlay.replaceChildren(),e?.abort()}render(){const e=this.target;if(this.clearView(),!e||this.target!==e||this.failed||this.disposed)return;const t=this.lifetime=new AbortController,i=this.result?.key===e.key?this.result:null,n=Object.freeze({marker:e.tick.marker,anchor:e.tick.bounds,layout:e.layout,details:i?.details??null,error:i?.error??null,loading:"command"===e.tick.marker.source&&!i,signal:t.signal});try{const i=e.renderer(n);if(r(i)&&"function"==typeof i.then&&Promise.resolve(i).catch(e=>{t.signal.aborted||this.options.reportError(e)}),t.signal.aborted)return;if(null!==i&&!(i instanceof HTMLElement))throw new TypeError("scrollbar.tooltip must synchronously return an HTMLElement or null");if(!i)return;this.element=i,Object.assign(i.style,{position:"absolute",boxSizing:"border-box",margin:"0",pointerEvents:"none",maxWidth:`${Math.max(0,Math.min(360,e.layout.width-16))}px`,maxHeight:`${Math.max(0,e.layout.height-16)}px`,overflow:"hidden"}),this.options.overlay.append(i),this.position(),this.observer=new ResizeObserver(()=>this.position()),this.observer.observe(i)}catch(e){this.failed=!0,this.clearView(),this.options.reportError(e)}}position(){if(!this.target||!this.element)return;const{bounds:e}=this.target.tick,{width:t,height:i}=this.target.layout,r=e.left-this.element.offsetWidth-8,n=e.top+e.height/2-this.element.offsetHeight/2;this.element.style.left=`${Math.max(0,Math.min(Math.max(8,r),t-this.element.offsetWidth-8))}px`,this.element.style.top=`${Math.max(0,Math.min(Math.max(8,n),i-this.element.offsetHeight-8))}px`}loadDetails(){const e=this.target;!e||this.fetching||this.failed||this.disposed||"command"!==e.tick.marker.source||this.result?.key===e.key||(this.fetching=!0,Promise.resolve().then(()=>this.disposed||this.target?.key!==e.key?null:this.options.getDetails(e.tick.marker.id)).then(t=>{t&&this.target?.key===e.key&&!this.disposed&&(this.result={key:e.key,details:Object.freeze({...t}),error:null},this.render())},t=>{this.target?.key!==e.key||this.disposed||(this.result={key:e.key,details:null,error:t instanceof Error?t.message:String(t)},this.render())}).finally(()=>{this.fetching=!1,this.loadDetails()}))}},wi="\n --cp-terminal-surface: #ffffff;\n --cp-terminal-text: #242424;\n --cp-terminal-text-muted: #5c5c5c;\n --cp-terminal-border-strong: #919191;\n --cp-terminal-accent: #b11f4b;\n --cp-terminal-accent-soft: rgba(177, 31, 75, 0.08);\n --cp-terminal-danger: #dc2626;\n",yi="\n --cp-terminal-surface: #292929;\n --cp-terminal-text: #dedede;\n --cp-terminal-text-muted: #919191;\n --cp-terminal-border-strong: #5f5f5f;\n --cp-terminal-accent: #fd8ea1;\n --cp-terminal-accent-soft: rgba(253, 142, 161, 0.14);\n --cp-terminal-danger: #f87171;\n",vi=`\n :host {\n ${wi}\n --cp-terminal-font-family: "Segoe UI", Aptos, Calibri, -apple-system, BlinkMacSystemFont, sans-serif;\n }\n @media (prefers-color-scheme: dark) { :host { ${yi} } }\n :host-context([data-theme="light"]) { ${wi} }\n :host-context([data-theme="dark"]) { ${yi} }\n :host([data-theme="light"]) { ${wi} }\n :host([data-theme="dark"]) { ${yi} }\n .viewport {\n ${Object.entries(Gt).map(([e,t])=>`--cp-view-scrollbar-${e}: var(--cp-scrollbar-${e}, var(--cp-terminal-scrollbar-${e}, ${t}));`).join("\n")}\n --cp-view-surface: var(--cp-surface, var(--cp-terminal-surface));\n --cp-view-text: var(--cp-text, var(--cp-terminal-text));\n --cp-view-text-muted: var(--cp-text-muted, var(--cp-terminal-text-muted));\n --cp-view-border-strong: var(--cp-border-strong, var(--cp-terminal-border-strong));\n --cp-view-accent: var(--cp-accent, var(--cp-terminal-accent));\n --cp-view-accent-soft: var(--cp-accent-soft, var(--cp-terminal-accent-soft));\n --cp-view-danger: var(--cp-danger, var(--cp-terminal-danger));\n --cp-view-font-family: var(--cp-font-family, var(--cp-terminal-font-family));\n }\n`;G(),o();var bi=["status","mode","requestId","active","pending","canExtend","text","message","copying","copyError"],ki=["available","following","pending","generation","buffer","top","liveTop","totalRows","requestId","navigationError"],xi=["columns","rows","cellWidth","cellHeight","mouseTracking"],Ei=["row","startColumn","endColumn"],Ti=(e,t,i)=>i.every(i=>e?.[i]===t?.[i]);function Ci(e,t,i){const r=i.width/t.columns,n=i.height/t.rows;return e.map(e=>Object.freeze({left:e.startColumn*r,top:e.row*n,width:(e.endColumn-e.startColumn)*r,height:n}))}var Si=class{#W;#_;#H;#K;#V;#X;#Y;#J;#Z;#Q=!1;#ee=!1;constructor({element:e,overlay:t,button:i,signal:n,getState:s,runAction:o,onSelectionUI:a,reportError:l}){this.#W=e,this.#_=t,this.#H=i,this.#K=n,this.#V=s,this.#X=o,this.#Y=l,a&&e.addEventListener("selectionui",e=>{if(!this.#Z||e!==this.#Z)return;const t=this.#J;try{const e=a(this.#Z);if(void 0!==e)throw r(e)&&"function"==typeof e.then&&Promise.resolve(e).catch(e=>{n.aborted||this.#J!==t||l(e)}),new TypeError("onSelectionUI must finish synchronously; use preventDefault() and signal for UI ownership")}catch(t){e.preventDefault(),l(t)}},{signal:n})}refresh(e=!1){this.#ee||=e,this.#Q||this.#K.aborted||(this.#Q=!0,queueMicrotask(()=>{if(this.#Q=!1,this.#K.aborted)return;const e=this.#V(),t=this.#ee;if(this.#ee=!1,!t&&(i=this.#J,r=e,i&&i.connected===r.connected&&i.readOnly===r.readOnly&&Ti(i.selection,r.selection,bi)&&Ti(i.viewport,r.viewport,ki)&&Ti(i.geometry,r.geometry,xi)&&Ti(i.canvasSize,r.canvasSize,["width","height"])&&i.selection.ranges.length===r.selection.ranges.length&&i.selection.ranges.every((e,t)=>Ti(e,r.selection.ranges[t],Ei))))return;var i,r;const n=Object.freeze({...e,selection:Object.freeze({...e.selection,ranges:Object.freeze(e.selection.ranges.map(e=>Object.freeze({...e})))}),viewport:Object.freeze({...e.viewport,rowIds:Object.freeze([...e.viewport.rowIds??[]])}),geometry:Object.freeze({...e.geometry}),canvasSize:Object.freeze({...e.canvasSize})});this.#J=n;const s=Object.freeze({...n,overlay:this.#_,signal:this.#K,runAction:this.#X,rects:Object.freeze(Ci(n.selection.ranges,n.geometry,n.canvasSize))}),o=new CustomEvent("selectionui",{cancelable:!0,detail:s});if(this.#Z=o,this.#Y(null),this.#W.dispatchEvent(o),this.#K.aborted)return;const a=n.selection;this.#H.hidden=o.defaultPrevented||["none","unavailable"].includes(a.status),this.#H.disabled=!n.connected||"valid"!==a.status||a.copying;const l=a.copying?"Copying…":"Copy";this.#H.textContent!==l&&(this.#H.textContent=l)}))}};function Ii(e){if(/[\u0000-\u0020\u007f]/u.test(e)||!URL.canParse(e))return null;const t=new URL(e);return["https:","http:","mailto:"].includes(t.protocol)?t.href:null}var Mi=class{#te=new Map;update(e){this.#te.clear();const t=new Map;for(const i of e){t.has(i.uri)||t.set(i.uri,Ii(i.uri));const e=t.get(i.uri);if(!e)continue;const r=this.#te.get(i.row)??[];r.push({...i,uri:e}),this.#te.set(i.row,r)}}at(e){return this.#te.get(e.y)?.find(t=>e.x>=t.startColumn&&e.xX.cells)throw new RangeError("Link grid exceeds cell limit");const s=[];let o="",a=[],l=new Set,c=0,h=0,d="unknown";const u=e=>e>=0&&!!(1024&(n[(e+1)*i-1]?.attributes??0)),p=e=>{const i=Object.freeze({text:o,mode:t,start:d,end:e}),r=new Set([o.length]);for(const e of Ai.segment(o))r.add(e.index);s.push({chunk:i,spans:a,unsafe:l,boundaries:r,key:JSON.stringify([t,c,d,e,o])}),o="",a=[],l=new Set};for(let e=0;e=1&&s<=2&&t+s<=i&&!(r&&(64&r.attributes||1109742===r.text.codePointAt(0)))&&(2!==s||0===n[e*i+t+1]?.width),d=c?r?.text||" ":"\0";if(h+d.length>X.totalText||o.length+d.length>X.chunk)throw new RangeError("Link text exceeds chunk or viewport limit");const u=o.length;o+=d,h+=d.length,c?a.push({start:u,end:o.length,row:e,startColumn:t,endColumn:t+s}):(l.add(u),l.add(o.length)),c&&2===s&&t++}const s=u(e);if(s||l.add(o.length),"physicalRow"===t||"logicalLine"===t&&!s||e===r-1)p(s?"clipped":"unknown");else if(!s){if(++h>X.totalText||o.length+1>X.chunk)throw new RangeError("Link text exceeds limit");o+="\n"}}return s}function Ri(e,t,i){const r=t+i,{chunk:n}=e;if(i<=0||t<0||r>n.text.length||!e.boundaries.has(t)||!e.boundaries.has(r)||0===t&&"complete"!==n.start||r===n.text.length&&"complete"!==n.end||n.text.slice(t,r).includes("\0"))return null;for(const i of e.unsafe)if(i===t||i===r||i>t&&i=r)continue;if(i.startr)return null;const e=s.at(-1);e&&e.row===i.row&&e.endColumn===i.startColumn?e.endColumn=i.endColumn:s.push({row:i.row,startColumn:i.startColumn,endColumn:i.endColumn})}return s.length?s:null}ce();var Pi=class{#ie;#re;#ne=null;#se;#oe=null;#ae=null;#le=[];#ce=new Set;#he=new Map;#de=new Map;#ue=0;#pe=0;#fe=0;#o=0;#me=!1;#ge=!1;#we=!1;constructor(e){this.#ie={...e,actions:new Set(e.actions)}}configure(e){const t=Q({detection:e},this.#ie.actions);this.#me||(this.#re=!1!==t&&t.detection,this.#ce.clear(),this.#ge=!1,this.clear())}update(e){if(this.#me||e.revisionObject.freeze({...t,activation:Object.freeze({...t.activation,revision:e})}))))}clear(){this.#ke(),this.#pe++,this.#ae=null,this.#he.clear(),this.#de.clear(),this.#ue=0,this.#me||this.#ye([])}dispose(){this.#me||(this.clear(),this.#me=!0)}#ye(e){this.#le=Object.freeze([...e]),this.#ie.onChange(this.#o,this.#le)}#ve(e,t,i){this.#ie.onError(Object.freeze({code:e,ruleId:t,revision:this.#o,message:i instanceof Error?i.message:String(i)}))}#ke(){clearTimeout(this.#se),this.#se=void 0,this.#ne&&(this.#ne.terminate(),this.#ne=null),this.#oe=null}#xe(e){const t=this.#oe?.rule.id??null;this.#ke(),this.#ge=!0,this.#ye([]),this.#ve("worker",t,e)}#be(){if(!(this.#we||this.#me||this.#oe)&&this.#ae&&this.#re&&!this.#ge){this.#we=!0;try{const e=this.#ae;for(const t of this.#re.rules){if(!t.enabled||this.#ce.has(t.id))continue;const i=(e.chunks.get(t.text??"logicalLine")??[]).filter(e=>!this.#he.has(this.#Ee(t,e)));if(i.length){try{if(!this.#ne){const e=new Worker(this.#ie.workerUrl??$t("link-detection"),{type:"module",name:"hex1b-link-detection"});this.#ne=e,e.addEventListener("message",t=>{this.#ne===e&&this.#k(t.data)}),e.addEventListener("error",t=>{this.#ne===e&&(t.preventDefault(),this.#xe(t.message))}),e.addEventListener("messageerror",()=>{this.#ne===e&&this.#xe("Invalid detection worker message")})}const r=++this.#fe;this.#oe={id:r,work:e,rule:t,keys:new Map(i.map(e=>[e.key,this.#Ee(t,e)]))},this.#se=setTimeout(()=>{this.#oe?.id===r&&(this.#ke(),this.#ce.add(t.id),this.#ve("timeout",t.id,`Link rule exceeded ${X.timeoutMs} ms`),this.#be())},X.timeoutMs);const n={id:r,rule:t.builtin?{builtin:t.builtin}:{source:t.pattern.source,flags:t.pattern.flags},chunks:i.map(({chunk:e,key:t})=>({key:t,text:e.text}))};this.#ne.postMessage(n)}catch(e){this.#xe(e)}return}}this.#Te(e)}finally{this.#we=!1}}}#Ee(e,t){return JSON.stringify([e.id,t.key])}#k(e){const t=this.#oe;if(t&&e?.id===t.id&&!this.#me){if(clearTimeout(this.#se),this.#se=void 0,this.#oe=null,e.error)this.#ce.add(t.rule.id),this.#ve(e.error,t.rule.id,e.message??"Link scan failed");else{if(!function(e,t){if(!Array.isArray(e.results)||e.results.length!==t.keys.size)return!1;const i=new Set;let r=0,n=0;for(const s of e.results){if(!s||!t.keys.has(s.key)||i.has(s.key)||!Array.isArray(s.matches))return!1;if(i.add(s.key),r+=s.matches.length,r>X.matches)return!1;for(const e of s.matches){if(!e||!Number.isSafeInteger(e.index)||e.index<0||"string"!=typeof e.text||!e.text.length||!Array.isArray(e.captures)||!e.captures.every(e=>void 0===e||"string"==typeof e)||!e.groups||"object"!=typeof e.groups||Array.isArray(e.groups)||!Object.values(e.groups).every(e=>void 0===e||"string"==typeof e))return!1;if(n+=ie(e),n>X.resultText)return!1}}return!0}(e,t))return void this.#xe("Malformed detection response");for(const i of e.results){const e=t.keys.get(i.key),r=e.length+i.matches.reduce((e,t)=>e+ie(t),0);this.#ue+=r-(this.#de.get(e)??0),this.#de.set(e,r),this.#he.set(e,i.matches)}}this.#ae!==t.work&&this.#Ce(),this.#be()}}#Te(e){if(!this.#re||this.#ae!==e)return;const t=[...e.snapshot.hyperlinks],i=[],r=e=>e.some(e=>t.some(t=>e.row===t.row&&e.startColumnt.startColumn));for(const n of this.#re.rules){if(!n.enabled||this.#ce.has(n.id))continue;const s=[],o=[];try{for(const t of e.chunks.get(n.text??"logicalLine")??[])for(const a of this.#he.get(this.#Ee(n,t))??[]){const l=Ri(t,a.index,a.text.length);if(!l||r(l))continue;const c=Object.freeze({...a,captures:Object.freeze([...a.captures]),groups:Object.freeze({...a.groups}),chunk:t.chunk}),h=n.resolve?n.resolve(c):{target:a.text};if(this.#ae!==e||this.#me)return;if(null===h)continue;if("object"!=typeof h||void 0===h||"then"in h||"string"!=typeof h.target)throw new TypeError("Resolver must return a synchronous link resolution or null");const d=void 0===h.action?n.action:h.action;if(J(d,this.#ie.actions),i.length+s.length>=X.matches)throw new RangeError("Resolved link limit exceeded");const u=Object.freeze({source:"detected",ruleId:n.id,kind:n.builtin?"url"===n.builtin||"uri"===n.builtin?"uri":"path":n.kind,text:a.text,target:h.target,ranges:Object.freeze(l.map(e=>Object.freeze(e))),revision:e.revision,data:h.data});s.push(Object.freeze({id:JSON.stringify([e.identity,n.id,l[0].row,l[0].startColumn,a.index]),action:d,activation:u})),o.push(...l)}i.push(...s),t.push(...o)}catch(t){if(this.#ce.add(n.id),this.#ve(t instanceof RangeError?"limit":"resolver",n.id,t),this.#ae!==e||this.#me)return}}this.#Ce(),this.#ye(i)}#Ce(){for(;this.#he.size>X.cacheEntries||this.#ue>X.cacheText;){const e=this.#he.keys().next().value;this.#he.delete(e),this.#ue-=this.#de.get(e),this.#de.delete(e)}}};function Oi(e,t,i){const r=e.querySelector(t);if(!(r instanceof i))throw new Error(`Missing terminal element: ${t}`);return r}te(),o();var zi=class e{element;#ie;#Se;#Ie;#Me;#Ae;#Le;#Re;#Pe;#Oe=()=>{this.#me||"system"!==this.#Le||this.#ze()};#ne;#Ue;#De;#Be;#qe;#je;#Fe=new AbortController;#Ne={width:0,height:0};#$e=Vt();#Ge={columns:80,rows:24,cellWidth:10,cellHeight:20,mouseTracking:0};#We={id:null,primaryId:null,isPrimary:!1};#_e=!1;#He=!1;#Ke;#Ve;#me=!1;#Xe=!1;#Ye;#Je;#Ze;#Qe;#d=Promise.withResolvers();#et;#tt={};#it="";#rt="";#nt=!1;#st={state:"none",percentage:null};#ot={phase:"unknown",lastExitCode:null};#at={uri:null,host:null,path:null};#lt=null;#ct=!1;#R;#ht;#dt;#ut;#pt;#ft;#mt;#gt=!1;#wt=!1;#yt;#vt;#bt="";#kt=0;#xt=!1;#Et;#A;#Tt=!1;#Ct=0;#St;#It;#Mt="";#At={width:0,height:0};#Lt=new Mi;#Rt=[];#le;#Pt;#Ot=1;#zt=0;#Ut=0;#Dt;#Bt=new Map;#qt=[];#jt=[];#Ft=new Map;#Nt;#$t;static async mount(t,i){if(!(t instanceof HTMLElement))throw new TypeError("A terminal container HTMLElement is required");const r=c(i);if(i.signal?.aborted)throw i.signal.reason;if(!("webgpu"!==V(i.renderer)||window.isSecureContext&&navigator.gpu))throw new Error("The requested WebGPU renderer requires WebGPU over HTTPS or localhost");if(!(window.Worker&&window.ResizeObserver&&window.OffscreenCanvas&&HTMLCanvasElement.prototype.transferControlToOffscreen))throw new Error("WebTerminal requires module workers, ResizeObserver, and a transferable OffscreenCanvas");const n=new e(i,r);try{return await Promise.all([n.#d.promise,Promise.resolve().then(()=>n.#l(t))]),n}catch(e){throw n.dispose(),e}}constructor(e,t=c(e)){if(this.#ie=e,this.#Se=t,void 0!==e.readOnly&&"boolean"!=typeof e.readOnly)throw new TypeError("readOnly must be a boolean");if(this.#Ke=e.readOnly??!1,void 0!==e.preserveOnDisconnect&&"boolean"!=typeof e.preserveOnDisconnect)throw new TypeError("preserveOnDisconnect must be a boolean");if(this.#Ve=e.preserveOnDisconnect??!1,this.#Ae=V(e.renderer),this.#Le=v(e.colorMode),this.#Re={light:k(void 0===e.lightModePalette?w:e.lightModePalette),dark:k(void 0===e.darkModePalette?g:e.darkModePalette)},this.#dt=Qt(e.padding),this.#ut=ai(e.scrollbar),this.#pt=ti(this.#Ne,this.#Ge,!1,this.#$e,this.#dt,this.#ut),!(void 0===e.workerUrl||e.workerUrl instanceof URL||"string"==typeof e.workerUrl&&e.workerUrl.trim()))throw new TypeError("workerUrl must be a nonempty URL string or URL");if(!(void 0===e.linkDetectionWorkerUrl||e.linkDetectionWorkerUrl instanceof URL||"string"==typeof e.linkDetectionWorkerUrl&&e.linkDetectionWorkerUrl.trim()))throw new TypeError("linkDetectionWorkerUrl must be a nonempty URL string or URL");if(void 0!==e.onLinkDetectionError&&"function"!=typeof e.onLinkDetectionError)throw new TypeError("onLinkDetectionError must be a function");if(void 0!==e.onSelectionUI&&("function"!=typeof e.onSelectionUI||"AsyncFunction"===e.onSelectionUI.constructor.name))throw new TypeError("onSelectionUI must be a synchronous event handler");this.#Et=new Dt(e),this.#A=new Map(Object.entries(e.actions??{})),this.#le=Q(e.links,new Set(this.#A.keys())),this.#Pt=new Pi({workerUrl:void 0===e.linkDetectionWorkerUrl?void 0:new URL(e.linkDetectionWorkerUrl,location.href),actions:new Set(this.#A.keys()),onChange:(e,t)=>{!this.#me&&this.#_e&&e===this.#zt&&(this.#qt=t,this.#Gt([]),this.#Wt())},onError:e=>{if(!this.#me)try{this.#ie.onStatus?.(`Link detection: ${e.message}`,"error")}finally{this.#ie.onLinkDetectionError?.(e)}}}),this.#Pt.configure(!!this.#le&&this.#le.detection),this.#R=new Zt(e=>this.#e(e),()=>this.#_t()),this.#ht=new u(e=>this.#e(e),()=>this.#Ht()),this.element=document.createElement("div"),this.element.className="hex1b-terminal",this.element.tabIndex=-1,this.element.style.cssText="width:100%;height:100%;min-width:0;min-height:0;contain:strict"}get geometry(){return{...this.#Ge}}get peer(){return{...this.#We}}get connected(){return this.#_e}get readOnly(){return this.#Ke}get title(){return this.#rt}get progress(){return{...this.#st}}get shellIntegration(){return{...this.#ot}}get workingDirectory(){return{...this.#at}}get commandMark(){return this.#lt?{...this.#lt}:null}get stats(){return{...this.#tt}}get screenText(){return this.#it}get sizing(){return{...this.#$e}}get layout(){return this.#pt}get padding(){return this.#dt}get scrollbar(){return this.#ut}get markers(){return this.#ht.markers}get inputBindings(){return this.#Et.bindings}get viewport(){const e=this.#R.viewport;return{...e,followTail:e.following,offset:e.available?e.liveTop-e.top:0}}get selection(){const e=this.#R.selection;return{...e,active:"valid"===e.status,pending:"pending"===e.status,copying:this.#xt,copyError:this.#bt}}#l(e){if(this.#ie.signal?.aborted)throw this.#ie.signal.reason;const t=function(e){const t=s.get(e);return void 0===t?void 0:a(t)}(this.#Se),i=void 0===this.#ie.scale||"auto"===this.#ie.scale?Math.min(3,Math.max(.5,window.devicePixelRatio||1)):this.#ie.scale;if(!Number.isFinite(i)||i<.5||i>3)throw new RangeError("Backing scale must be 0.5-3 or 'auto'");const r=_(this.#ie.font,location.href);this.#$e=Vt(this.#ie.sizing);const o=this.element.attachShadow({mode:"open"});o.innerHTML=`\n \n
\n \n \n \n \n
\n \n
\n \n
\n \n \n \n
`,this.#Ue=Oi(o,".surface",HTMLDivElement),this.#De=Oi(o,"canvas",HTMLCanvasElement),this.#Be=Oi(o,"textarea",HTMLTextAreaElement),this.#yt=Oi(o,".highlights",HTMLDivElement),this.#vt=Oi(o,".inspection",HTMLDivElement),this.#De.id=`hex1b-content-${d()}`;const l=Oi(o,".scrollbar-accessibility",HTMLDivElement);l.setAttribute("aria-controls",this.#De.id);const c=document.createElement("div");c.slot="scrollbar-tooltip",c.className="hex1b-scrollbar-tooltip-overlay",c.style.cssText="position:relative;width:100%;height:100%;pointer-events:none",this.element.append(c),this.#mt=new gi({overlay:c,getDetails:e=>this.getCommandMarkDetails(e),reportError:e=>{this.#bt=`Scrollbar tooltip: ${n(e)}`,this.#Kt(),this.#ie.onStatus?.(this.#bt,"error")}}),this.#ft=new ui({element:Oi(o,".viewport",HTMLDivElement),canvas:Oi(o,".scrollbar-canvas",HTMLCanvasElement),accessibility:l,getState:()=>({layout:this.layout,viewport:this.viewport,markers:this.markers,connected:this.#_e,configuration:this.#ut}),scrollToRow:e=>this.scrollToRow(e),scrollToLive:()=>this.scrollToLive(),scrollToMarker:e=>this.scrollToMarker(e),onMarkerHover:e=>this.#mt?.update(e,this.layout,!!this.#ut&&this.#ut.tooltip),reportError:e=>{this.#bt=`Scrollbar: ${n(e)}`,this.#Kt(),this.#ie.onStatus?.(this.#bt,"error")}}),this.#It=document.createElement("div"),this.#It.slot="selection-ui",this.#It.className="hex1b-selection-overlay",this.#It.style.cssText="position:relative;width:100%;height:100%;pointer-events:none",this.element.append(this.#It),this.#St=new Si({element:this.element,overlay:this.#It,button:Oi(this.#vt,".copy-selection",HTMLButtonElement),signal:this.#Fe.signal,getState:()=>({selection:this.selection,viewport:this.viewport,geometry:this.geometry,canvasSize:this.#At,connected:this.#_e,readOnly:this.#Ke}),runAction:this.runAction.bind(this),onSelectionUI:this.#ie.onSelectionUI,reportError:e=>{this.#Mt=e?`Selection UI failed: ${n(e)}`:"",this.#It.hidden=!!e,this.#Kt(),e&&this.#ie.onStatus?.(this.#Mt,"error")}}),this.#St.refresh(),this.#Be.setAttribute("aria-label",this.#ie.label||"Terminal input. Click outside to use page controls."),this.#Be.disabled=!0,e.append(this.element);const h=e=>{try{this.#bt="",e()}catch(e){this.#bt=n(e),this.#_t()}};this.#qe=Nt(this.#De,e=>this.#Vt(e),()=>this.focus(),{lineRenditions:()=>this.#Rt,state:()=>({historical:!this.viewport.following||this.viewport.pending,readOnly:this.#Ke,selection:this.selection}),begin:(e,t)=>h(()=>this.#R.begin(e,t)),extend:e=>h(()=>this.#R.extend(e)),scroll:(e,t)=>h(()=>{this.#R.scroll(e,t),this.#ft?.activity()}),end:e=>this.#R.endGesture(e),resolve:e=>this.#Xt(e),execute:(e,t)=>this.#Yt(e,t),hyperlink:e=>this.#Jt(e),hoverHyperlink:e=>{this.#Nt=e?.id,this.#le&&this.#le.detection&&"hover"===this.#le.detection.decoration&&this.#Wt()},openHyperlink:(e,t)=>this.#Zt(e,t)}),this.#Qt(),Oi(this.#vt,".return-live",HTMLButtonElement).addEventListener("click",()=>{this.runAction(At.ScrollToLive).catch(e=>this.#ei(e))},{signal:this.#Fe.signal}),Oi(this.#vt,".copy-selection",HTMLButtonElement).addEventListener("click",()=>{this.runAction(At.CopySelection).catch(e=>this.#ei(e))},{signal:this.#Fe.signal}),Oi(o,".viewport",HTMLDivElement).addEventListener("pointerdown",e=>{e.composedPath().includes(this.#It)||e.target instanceof Element&&e.target.closest("button")||e.target===this.#De||e.target===this.#Be||(e.preventDefault(),this.focus())},{signal:this.#Fe.signal}),this.#je=new ResizeObserver(e=>{const{width:t,height:i}=e[0].contentRect,r=t!==this.#Ne.width||i!==this.#Ne.height;this.#Ne={width:t,height:i},this.#ti(),r&&this.#ii()}),this.#je.observe(e),window.addEventListener("pagehide",()=>this.dispose(),{signal:this.#Fe.signal}),this.#ie.signal?.addEventListener("abort",()=>this.dispose(),{once:!0,signal:this.#Fe.signal}),this.#et=setTimeout(()=>this.#x(new Error("Timed out waiting for the terminal's first frame")),3e4),this.#ne=void 0===this.#ie.workerUrl?new Worker($t("terminal"),{type:"module",name:"Hex1b WebTerminal"}):new Worker(new URL(this.#ie.workerUrl,location.href),{type:"module",name:"Hex1b WebTerminal"}),this.#ne.addEventListener("message",e=>this.#ri(e.data)),this.#ne.addEventListener("error",e=>{e.preventDefault(),this.#x(new Error(e.message||"Terminal worker failed"))}),this.#ne.addEventListener("messageerror",()=>this.#x(new Error("Terminal worker message could not be decoded")));const u=this.#De.transferControlToOffscreen();this.#Pe=window.matchMedia?.("(prefers-color-scheme: dark)"),this.#Pe?.addEventListener("change",this.#Oe),this.#ni({type:"init",canvas:u,transport:void 0===t?{type:"custom"}:{type:"websocket",url:t},scale:i,font:r,renderer:this.#Ae,palette:this.#Re[this.resolvedColorMode],preserveOnDisconnect:this.#Ve},[u]),this.#ze(),this.#si()}#ri(e){if(!this.#me)if("transportConnect"===e.type){if(this.#Ie)return void this.#x(new Error("Transport is already initialized"));this.#Ie=new F({onReady:()=>this.#ni({type:"transportConnected"}),onFrame:e=>(this.#Me=Promise.withResolvers(),this.#ni({type:"transportFrame",buffer:e},[e]),this.#Me.promise),onClose:e=>this.#ni({type:"transportClosed",details:e}),onError:e=>this.#ni({type:"transportError",message:e.message})}),this.#Ie.start(this.#Se)}else if("transportSend"===e.type)Promise.resolve(this.#Me?.promise).then(()=>this.#Ie?.send(e.control)).then(()=>this.#ni({type:"transportSent"})).catch(e=>{this.#me||this.#Ie?.closing||this.#ni({type:"transportError",message:n(e)})});else if("transportReceived"===e.type)this.#Me?.resolve(),this.#Me=void 0;else if("connected"===e.type)this.#_e=!0,this.#Be.disabled=!this.#oi();else if("closed"===e.type){if(this.#He)return;this.#He=!0,clearTimeout(this.#et);try{if(this.#ai(),!this.#me){const t=Object.freeze({...e.details});(void 0!==this.#ie.transport||void 0!==t.code)&&this.#ie.onClose?.(t)}}finally{this.#d.reject(new Error(`Terminal ${void 0===e.details.code?"transport":"WebSocket"} closed (${void 0===e.details.code?e.details.reason:`${e.details.code}${e.details.reason?`: ${e.details.reason}`:""}`}) before mounting completed`))}}else if("status"===e.type)"error"===e.level&&(this.#li(),this.#ai(),this.#d.reject(new Error(e.message))),this.#ie.onStatus?.(e.message,e.level);else if("geometry"===e.type){this.#Rt=e.lineRenditions??[],this.#zt=e.revision,this.#Bt.clear();for(const t of e.hyperlinks){const e=this.#Bt.get(t.row)??[];e.push(t),this.#Bt.set(t.row,e)}this.#Gt([]),this.#$t=void 0;const t=!this.#Xe,i=t||["columns","rows","cellWidth","cellHeight","mouseTracking"].some(t=>this.#Ge[t]!==e[t]);this.#Ge={columns:e.columns,rows:e.rows,cellWidth:e.cellWidth,cellHeight:e.cellHeight,mouseTracking:e.mouseTracking},this.#Xe=!0;const r=this.#We;if(this.#We=e.peer,this.#Be.disabled=!this.#oi(),this.#oi()&&document.activeElement===this.element&&!this.element.shadowRoot?.activeElement&&this.focus(),this.#Lt.update(e.hyperlinks),(i||r.isPrimary!==this.#We.isPrimary)&&this.#ti(),this.#We.isPrimary||(clearTimeout(this.#Ye),this.#Ye=void 0,this.#Je=void 0),(t||!r.isPrimary&&this.#We.isPrimary)&&this.#ii(!0),this.#Je===`${e.columns}x${e.rows}`&&(this.#Je=void 0),Object.hasOwn(e,"history")&&(this.#it=e.text,this.#ht.accept(e.history,e.revision),this.#R.accept(e.history,e.revision)),e.linkGeneration===this.#Ot&&(e.linkSnapshot?this.#ci(e.linkSnapshot):(this.#Dt&&(this.#Dt={...this.#Dt,revision:e.revision}),this.#Pt.advance(e.revision))),this.#qe?.update(e.columns,e.rows,e.mouseTracking),i&&this.#ie.onGeometry?.(this.geometry),t&&this.#ie.onSizingChange?.(this.sizing),r.id===this.#We.id&&r.primaryId===this.#We.primaryId&&r.isPrimary===this.#We.isPrimary||this.#ie.onRoleChange?.(this.peer),!this.#me&&this.#_e&&(null!==this.#We.id||this.#We.isPrimary)){const t=!this.#nt||this.#rt!==e.title,i=!this.#ct||this.#st.state!==e.progress.state||this.#st.percentage!==e.progress.percentage,r=!this.#ct||this.#ot.phase!==e.shellIntegration.phase||this.#ot.lastExitCode!==e.shellIntegration.lastExitCode,n=!this.#ct||this.#at.uri!==e.workingDirectory.uri,s=!this.#ct||this.#lt?.phase!==e.commandMark?.phase||this.#lt?.exitCode!==e.commandMark?.exitCode||this.#lt?.rawParameters!==e.commandMark?.rawParameters;this.#rt=e.title,this.#nt=!0,this.#st={...e.progress},this.#ot={...e.shellIntegration},this.#at={...e.workingDirectory},this.#lt=e.commandMark?{...e.commandMark}:null,this.#ct=!0,t&&this.#ie.onTitleChange?.(this.#rt),!this.#me&&i&&this.#ie.onProgressChange?.(this.progress),!this.#me&&r&&this.#ie.onShellIntegrationChange?.(this.shellIntegration),!this.#me&&n&&this.#ie.onWorkingDirectoryChange?.(this.workingDirectory),!this.#me&&s&&this.#ie.onCommandMarkChange?.(this.commandMark)}}else if("linkSnapshot"===e.type)e.generation===this.#Ot&&e.snapshot.revision===this.#zt&&this.#ci(e.snapshot);else if("linkDecorations"===e.type){const t=this.#$t;this.#_e&&t&&t.serial===e.serial&&t.generation===e.generation&&e.generation===this.#Ot&&t.revision===e.revision&&e.revision===this.#zt&&(this.#$t=void 0,this.#Gt(t.links),this.#qe?.refresh())}else"history"===e.type?(this.#it=e.text,this.#ht.accept(e.history,e.revision),this.#R.accept(e.history,e.revision)):"stats"===e.type&&(this.#tt=e.stats,void 0!==e.text&&(this.#it=e.text),e.stats.revision>0&&this.#nt&&this.#ct&&this.#_e&&(null!==this.#We.id||this.#We.isPrimary)&&(clearTimeout(this.#et),this.#d.resolve(this)),this.#ie.onStats?.(this.stats,e.text))}#ti(){const e=ti(this.#Ne,this.#Ge,this.#We.isPrimary,this.#$e,this.#dt,this.#ut),t=JSON.stringify(e)!==JSON.stringify(this.#pt);this.#pt=e;const{width:i,height:r,left:n,top:s}=e.content;this.#Ue.style.width=`${i}px`,this.#Ue.style.height=`${r}px`,this.#Ue.style.left=`${n}px`,this.#Ue.style.top=`${s}px`;const o=getComputedStyle(this.#Ue);this.#At={width:Number.parseFloat(o.width),height:Number.parseFloat(o.height)},this.#St?.refresh(),this.#ft?.refresh(),t&&!this.#gt&&(this.#gt=!0,queueMicrotask(()=>{this.#gt=!1,this.#me||this.#ie.onLayoutChange?.(this.layout)}));const a=window.devicePixelRatio||1;this.#ni({type:"viewport",width:Math.ceil(i*a),height:Math.ceil(r*a)})}#hi(){return function(e,t,i){if("fixed"===i.mode)return{columns:i.columns,rows:i.rows};if(e.width<=0||e.height<=0)return null;const r=i.fontSize/16;return{columns:Math.max(1,Math.min(300,Math.floor(e.width/(t.cellWidth*r)))),rows:Math.max(1,Math.min(100,Math.floor(e.height/(t.cellHeight*r))))}}(ei(this.#Ne,this.#dt,this.#ut),this.#Ge,this.#$e)}#ii(e=!1){("fixed"!==this.#$e.mode||e)&&this.#oi()&&this.#We.isPrimary&&void 0===this.#Ye&&!this.#me&&(this.#Ye=setTimeout(()=>{this.#Ye=void 0;const e=this.#hi();if(!e||!this.#We.isPrimary||!this.#oi())return;const t=`${e.columns}x${e.rows}`;e.columns===this.#Ge.columns&&e.rows===this.#Ge.rows||t===this.#Je||this.resize(e.columns,e.rows)},50))}#ni(e,t=[]){this.#ne?.postMessage(e,t)}#e(e){if(!this.#_e||this.#me)throw new Error("Terminal view is not connected");if(this.#Ke&&["input","paste","key","mouse","resize","requestPrimary"].includes(e.type))throw new Error("Terminal view does not accept input");this.#ni({type:"command",command:e})}#Vt(e){!this.#me&&this.#oi()&&(!function(e){if(R.encode(JSON.stringify(e)).byteLength>A.commandBytes)throw new RangeError("Terminal input exceeds the 64 KiB message limit. Send a smaller amount of text.")}(e),this.#Ct++,["input","paste","key"].includes(e.type)&&this.viewport.available&&(this.#qe?.cancel(),"none"!==this.selection.status&&this.clearSelection(),this.viewport.following&&!this.viewport.pending||this.scrollToLive()),this.#e(e))}#_t(){const e=this.viewport,t=this.selection;if(this.#qe?.refresh(),"invalidated"===t.status&&this.#qe?.cancel(),this.#yt){this.#yt.replaceChildren(...t.ranges.map(e=>{const t=document.createElement("span");t.className="highlight",t.setAttribute("part","selection-highlight");const i=this.#Rt[e.row]?2:1;return t.style.cssText=`left:${e.startColumn*i/this.#Ge.columns*100}%;top:${e.row/this.#Ge.rows*100}%;width:${(e.endColumn-e.startColumn)*i/this.#Ge.columns*100}%;height:${100/this.#Ge.rows}%`,t}));const i=Oi(this.#vt,".return-live",HTMLButtonElement);i.hidden=!e.available||e.following&&!e.pending,i.disabled=!this.#_e,this.#Kt()}this.#me||this.#St?.refresh(),this.#ft?.refresh(),this.#ie.onViewportChange?.(e),this.#ie.onSelectionChange?.(t)}scrollLines(e){this.#R.scroll(e),this.#ft?.activity()}scrollToRow(e){this.#R.scrollTo(e),this.#ft?.activity()}scrollToLive(){this.#R.live(),this.#ft?.activity()}scrollToMarker(e){return this.#ft?.activity(),this.#ht.jump(e)}addMarker(e){return this.#ht.add(e)}removeMarker(e){return this.#ht.remove(e)}getCommandMarkDetails(e){return this.#ht.details(e)}#Ht(){this.#ft?.activity(),this.#wt||(this.#wt=!0,queueMicrotask(()=>{this.#wt=!1,this.#me||this.#ie.onMarkersChange?.(this.markers)}))}setPadding(e){if(this.#me)throw new Error("Terminal view is disposed");this.#dt=Qt(e),this.#ti(),this.#ii(!0)}setScrollbar(e){if(this.#me)throw new Error("Terminal view is disposed");this.#ut=ai(e),this.#ft?.cancel(),this.#ti(),this.#ii(!0)}refreshScrollbar(){if(this.#me)throw new Error("Terminal view is disposed");this.#ft?.invalidate()}clearSelection(){this.#bt="",this.#R.clear()}refreshSelectionUI(){if(this.#me)throw new Error("Terminal view is disposed");this.#St?.refresh(!0)}#Kt(){if(!this.#vt)return;const e=this.selection,t=this.viewport,i=Oi(this.#vt,".inspection-message",HTMLSpanElement);i.textContent=this.#Mt||this.#bt||t.navigationError||("unavailable"===e.status?"":e.message)||(t.available&&!t.following?t.liveTop-t.top+" rows above live":""),i.hidden=!i.textContent,i.dataset.level=this.#Mt||this.#bt||t.navigationError||"invalidated"===e.status?"error":"info"}#ei(e){const t=e instanceof Error?e:new Error(String(e));this.#bt=`Input action failed: ${t.message}`,this.#_t(),this.#ie.onInputError?.(t)}async copySelection({clear:e=!1}={}){if("boolean"!=typeof e)throw new TypeError("Copy clear must be a boolean");const t=++this.#kt,i=this.selection.requestId,r=this.viewport.generation;this.#bt="";try{if(!this.#_e||this.#me)throw new Error("Terminal view is not connected");if(!navigator.clipboard?.write||"function"!=typeof ClipboardItem)throw new Error("Clipboard writing is unavailable. Use a secure browser context with clipboard permission.");const n=this.#R.copy();this.#xt=!0,this.#_t();const s=new ClipboardItem({"text/plain":n.then(e=>new Blob([e],{type:"text/plain"}))}),[o]=await Promise.all([n,navigator.clipboard.write([s])]);return e&&t===this.#kt&&"valid"===this.selection.status&&this.selection.requestId===i&&this.viewport.generation===r&&this.clearSelection(),o}catch(e){throw t===this.#kt&&(this.#R.cancelCopy(e),this.#bt=`Copy failed: ${n(e)}`),e}finally{t===this.#kt&&(this.#xt=!1),this.#_t()}}#oi(){return this.#_e&&this.#Xe&&!this.#Ke&&(null!==this.#We.id||this.#We.isPrimary)}get inputContext(){return Object.freeze({terminal:this,selection:this.selection,viewport:this.viewport,buffer:this.viewport.buffer??null,mouseCaptured:!this.#Ke&&0!==this.#Ge.mouseTracking,historical:!this.viewport.following||this.viewport.pending,readOnly:this.#Ke,connected:this.#_e,peer:this.peer})}#Xt(e){try{const t=this.#Et.resolve(Object.freeze(e),this.inputContext);return this.#Ke&&t.route===Mt.Application?{route:"pointer"===e.type||"wheel"===e.type?Mt.Continue:Mt.Consume}:t}catch(e){return this.#ei(e),{route:Mt.Consume}}}#Yt(e,t){this.#di(e.action,e.args,t).catch(e=>this.#ei(e))}async runAction(e,t,i){return this.#di(e,t,i)}async#di(e,t,i){if(this.#me)throw new Error("Terminal view is disposed");if(this.#bt="","function"==typeof e)return e(this.inputContext,t,i);const n=this.#A.get(e);if(n)return n(this.inputContext,t,i);switch(e){case At.CopySelection:if(void 0===t)return this.copySelection();if(!r(t))throw new TypeError("Copy options must be an object");if(void 0!==t.clear&&"boolean"!=typeof t.clear)throw new TypeError("Copy clear must be a boolean");return this.copySelection({clear:t.clear});case At.PasteClipboard:return this.pasteClipboard();case At.ClearSelection:return this.clearSelection();case At.ScrollToLive:return this.scrollToLive();case At.ScrollLines:if("number"!=typeof t)throw new RangeError("Scroll delta must be a signed 32-bit integer");return this.scrollLines(t);case At.CopyOrPaste:if(this.#Tt)throw new Error("A clipboard action is still in progress. Try again when it finishes.");this.#Tt=!0;try{return this.selection.active||this.selection.pending&&this.selection.canExtend?await this.copySelection({clear:!0}):this.#Ke?void 0:await this.pasteClipboard()}finally{this.#Tt=!1}default:throw new TypeError(`Unknown terminal action: ${e}`)}}paste(e){if("string"!=typeof e)throw new TypeError("Paste text must be a string");if(!this.#oi())throw new Error("Terminal view does not accept input");e&&this.#Vt({type:"paste",text:e})}async pasteClipboard(){if(!this.#oi())throw new Error("Terminal view does not accept input");if(!navigator.clipboard?.readText)throw new Error("Clipboard reading is unavailable. Use the browser's paste shortcut instead.");const e=this.#Ct,t=this.viewport.generation,i=this.selection.requestId,r=document.activeElement,n=await navigator.clipboard.readText();if(!this.#oi()||e!==this.#Ct||t!==this.viewport.generation||i!==this.selection.requestId||document.activeElement!==r)throw new Error("Terminal input, selection, focus, or buffer changed while reading the clipboard. Paste again.");return this.paste(n),n}get colorMode(){return this.#Le}get resolvedColorMode(){return"system"===this.#Le?this.#Pe?.matches?"dark":"light":this.#Le}setColorMode(e){if(this.#me)throw new Error("Terminal view is disposed");if(void 0===e)throw new TypeError("A color mode is required");this.#Le=v(e),this.#ze()}setPalette(e,t){if(this.#me)throw new Error("Terminal view is disposed");if("light"!==e&&"dark"!==e)throw new TypeError('Palette mode must be "light" or "dark"');this.#Re[e]=k(t),this.resolvedColorMode===e&&this.#ze()}#ze(){const e=this.#Re[this.resolvedColorMode];this.element.dataset.theme=this.resolvedColorMode,this.element.style.colorScheme=this.resolvedColorMode,this.element.style.backgroundColor=e.background;for(const[t,i]of Object.entries(function(e){const t=[1,3,5].map(t=>parseInt(e.foreground.slice(t,t+2),16)),i=[1,3,5].map(t=>parseInt(e.background.slice(t,t+2),16)),r=e=>"#"+t.map((t,r)=>Math.round(t*e+i[r]*(1-e)).toString(16).padStart(2,"0")).join("");return Object.freeze({track:r(.5),thumb:e.foreground,marker:r(.78),error:e.foreground,prompt:r(.45),"command-line":r(.6),executing:r(.85),success:r(.7),custom:r(.95)})}(e)))this.element.style.setProperty(`--cp-terminal-scrollbar-${t}`,i);this.#ni({type:"palette",palette:e}),this.#ft?.refresh()}setReadOnly(e){if("boolean"!=typeof e)throw new TypeError("readOnly must be a boolean");if(this.#me)throw new Error("Terminal view is disposed");if(this.#Ke===e)return;const t=document.activeElement===this.element&&(!this.element.shadowRoot?.activeElement||this.element.shadowRoot.activeElement===this.#Be);this.#Ke=e,this.#Ct++,this.#Qe?.(),this.#Be&&(this.#Be.value="",this.#Be.disabled=!this.#oi()),this.#qe?.cancel(),this.#qe?.refresh(),clearTimeout(this.#Ye),this.#Ye=void 0,this.#Je=void 0,this.#ii(!0),t&&this.focus(),this.#St?.refresh()}setLinks(e){if(this.#me)throw new Error("Terminal view is disposed");if(void 0===e)throw new TypeError("Link options must be an object or false");const t=Q(e,new Set(this.#A.keys()));this.#le=t,this.#Ot++,this.#qt=[],this.#Gt([]),this.#$t=void 0,this.#Dt=void 0,this.#Nt=void 0,this.#Pt.configure(!!t&&t.detection),this.#qe?.cancel(),this.#qe?.refresh(),this.#si()}#si(){const e=!(!this.#le||!(this.#le.osc8||this.#le.detection&&this.#le.detection.rules.some(e=>!1!==e.enabled)));this.#ni({type:"linkDetection",enabled:e,generation:this.#Ot})}#ci(e){this.#Dt=e,this.#qt=[],this.#Gt([]),this.#$t=void 0,this.#Pt.update(e),this.#qe?.refresh()}#ui(e){return`detected/${this.#Ot}/${this.#zt}/${e.id}`}#Gt(e){this.#jt=e,this.#Ft.clear();for(const t of e)for(const e of t.activation.ranges){const i=this.#Ft.get(e.row)??[];i.push({startColumn:e.startColumn,endColumn:e.endColumn,link:t}),this.#Ft.set(e.row,i)}}#Wt(){if(!this.#ne||!this.#_e||!this.#zt)return;const e=this.#le&&this.#le.detection;if(!e)return;const t="none"===e.decoration?[]:"hover"===e.decoration?this.#qt.filter(e=>this.#ui(e)===this.#Nt):this.#qt;this.#$t={revision:this.#zt,generation:this.#Ot,serial:++this.#Ut,links:this.#qt},this.#ni({type:"linkDecorations",revision:this.#zt,generation:this.#Ot,serial:this.#Ut,ranges:t.flatMap(e=>e.activation.ranges),underlineStyle:e.underlineStyle??"solid"})}#Jt(e){if(!this.#_e||this.viewport.pending||!this.#le)return null;const t=this.#Bt.get(e.y)?.find(t=>e.x>=t.startColumn&&e.xe.x>=t.startColumn&&e.xthis.#ui(t)===e.id);if(i)return void this.#di(i.action,i.activation,t).catch(e=>this.#ei(e));if(!this.#le.osc8){try{window.open(e.target,"_blank","noopener,noreferrer")}catch(e){this.#ei(e)}return}const r=this.#Bt.get(t.point.y)?.find(e=>t.point.x>=e.startColumn&&t.point.xthis.#ei(e))}#pi(e){if("key"===e.type){if(e.meta)throw new Error("Meta has no terminal key encoding; bind this shortcut to a local action instead.");this.#Vt({type:"key",key:e.key,ctrl:e.ctrl,alt:e.alt,shift:e.shift})}else"paste"===e.type?this.#oi()&&this.paste(e.text):"text"===e.type&&this.#Vt({type:"input",text:e.text})}#fi(e,t,i=!0){const r=this.#Xt(e);if((i||void 0!==r.action||r.route===Mt.Consume)&&r.route!==Mt.Browser&&(r.route!==Mt.Continue||"key"!==e.type))if(t?.preventDefault(),t?.stopPropagation(),void 0!==r.action)this.#Yt(r,e);else if(r.route!==Mt.Consume)try{this.#pi(e)}catch(e){this.#ei(e)}}#Qt(){const e=this.#Be,t={signal:this.#Fe.signal};let i=!1,r=null;this.#Qe=()=>{i=!1,r=null,clearTimeout(this.#Ze)},this.element.addEventListener("keydown",t=>{if(t.defaultPrevented)return;if(t.composedPath().includes(this.#It))return;const r=t.composedPath()[0];!this.#_e||t.isComposing||i||"Process"===t.key||"Dead"===t.key||t.getModifierState("AltGraph")||this.#fi({type:"key",key:t.key,code:t.code,repeat:t.repeat,...qt(t)},t,r===e||r===this.element)},{...t,capture:!0}),e.addEventListener("paste",t=>{!t.defaultPrevented&&this.#_e&&t.clipboardData&&(this.#fi({type:"paste",text:t.clipboardData.getData("text/plain")},t),e.value="")},t),e.addEventListener("compositionstart",()=>{this.#oi()&&(i=!0,r=null,clearTimeout(this.#Ze))},t),e.addEventListener("compositionend",t=>{i&&(i=!1,r="string"==typeof t.data?t.data:e.value,this.#Ze=setTimeout(()=>{r&&this.#fi({type:"text",text:r}),r=null,e.value=""},0))},t),e.addEventListener("input",t=>{const n=t instanceof InputEvent?t:void 0;if(n?.isComposing||i)return;clearTimeout(this.#Ze);const s=r??n?.data??e.value;r=null,s&&"insertFromPaste"!==n?.inputType&&this.#fi({type:"text",text:s},t),e.value=""},t)}focus(){if(this.#me)return;(this.#oi()?this.#Be:this.element).focus({preventScroll:!0})}requestPrimary(){if(!this.#oi())throw new Error("Terminal view does not accept input");if(this.#Ne.width<=0||this.#Ne.height<=0)throw new Error("Show the terminal container before taking primary");const e=this.#hi();if(!e)throw new Error("Show the terminal container before taking primary");if(null!==this.#We.id)this.#e({type:"requestPrimary",...e});else{if(!this.#We.isPrimary)throw new Error("Waiting for the HMP1 connection");this.resize(e.columns,e.rows)}}resize(e,t){const i=Kt(e,t);if(!this.#oi())throw new Error("Terminal view does not accept input");if(!this.#We.isPrimary)throw new Error("Only the primary view can request a terminal resize");this.#e({type:"resize",...i}),this.#Je=`${e}x${t}`}setSizing(e){const t=Vt(e,this.#$e.fontSize);if(!this.#_e||this.#me)throw new Error("Terminal view is not connected");if(this.#Ke)throw new Error("Terminal view does not accept input");if(!this.#We.isPrimary)throw new Error("Only the primary view can change terminal sizing");this.#$e=t,clearTimeout(this.#Ye),this.#Ye=void 0,this.#Je=void 0,this.#ti(),this.#ii(!0),this.#ie.onSizingChange?.(this.sizing)}resync(){this.#e({type:"resync"})}#ai(){this.#Ct++,this.#_e=!1,this.#Lt.update([]),this.#Bt.clear(),this.#qt=[],this.#Gt([]),this.#Dt=void 0,this.#$t=void 0,this.#Pt.clear(),this.#Be&&(this.#Be.disabled=!0),this.#qe?.update(1,1,0),this.#R.disconnect(),this.#ht.disconnect(),this.#ft?.cancel(),this.#_t(),clearTimeout(this.#Ye),this.#Ye=void 0}#x(e){this.#me||(this.#li(),this.#ai(),this.#d.reject(e),this.#ie.onStatus?.(e.message,"error"),this.#ne?.terminate())}dispose(){this.#me||(this.#me=!0,this.#li(),this.#ai(),this.#Pt.dispose(),this.#d.reject(new DOMException("Terminal view was disposed","AbortError")),clearTimeout(this.#et),clearTimeout(this.#Ze),this.#je?.disconnect(),this.#Pe?.removeEventListener("change",this.#Oe),this.#qe?.dispose(),this.#ft?.dispose(),this.#mt?.dispose(),this.#Fe.abort(),this.#ni({type:"stop"}),this.#ne?.terminate(),this.element.remove())}#li(){this.#Ie?.dispose(),this.#Me?.reject(new DOMException("Terminal transport was disposed or closed","AbortError")),this.#Me=void 0}};if(h(),P(),te(),"undefined"!=typeof DedicatedWorkerGlobalScope&&globalThis instanceof DedicatedWorkerGlobalScope)switch(new URL(import.meta.url).hash){case"#hex1b-terminal-worker":await Promise.resolve().then(()=>(kt(),pt));break;case"#hex1b-link-detection-worker":await Promise.resolve().then(()=>(Ct(),xt))}export{Mt as InputRoute,Ht as MAX_FONT_SIZE,_t as MIN_FONT_SIZE,At as TerminalAction,zi as WebTerminal,ni as createDefaultScrollbarRenderer,l as createWebSocketTransport,g as defaultDarkPalette,zt as defaultInputBindings,w as defaultLightPalette,fi as getCmdlineUrl,ee as linkAction,pi as parseCommandMarkParameters,oi as renderDefaultScrollbar,mi as renderDefaultScrollbarTooltip}; diff --git a/src/Aspire.TerminalHost/DcpUpstreamAdapter.cs b/src/Aspire.TerminalHost/DcpUpstreamAdapter.cs index 2fda50a42db..ab1d533f7eb 100644 --- a/src/Aspire.TerminalHost/DcpUpstreamAdapter.cs +++ b/src/Aspire.TerminalHost/DcpUpstreamAdapter.cs @@ -77,6 +77,11 @@ internal sealed class DcpUpstreamAdapter : IHex1bTerminalWorkloadAdapter /// public event Action? Disconnected; + /// + /// Completes after the producer transport connects. + /// + public Task ConnectedTask => _connectedTcs.Task; + public DcpUpstreamAdapter( Func> streamFactory, ILogger logger) diff --git a/src/Aspire.TerminalHost/Hmp1UdsServerListenerFilter.cs b/src/Aspire.TerminalHost/Hmp1UdsServerListenerFilter.cs index c7f8bc2ca16..acb5c346cbc 100644 --- a/src/Aspire.TerminalHost/Hmp1UdsServerListenerFilter.cs +++ b/src/Aspire.TerminalHost/Hmp1UdsServerListenerFilter.cs @@ -18,6 +18,7 @@ internal sealed class Hmp1UdsServerListenerFilter : IHex1bTerminalPresentationFi private readonly Hmp1PresentationAdapter _presentation; private readonly ILogger _logger; private readonly Action _listenerFaulted; + private readonly Task _producerConnected; private readonly object _gate = new(); private readonly HashSet _clientTasks = []; private CancellationTokenSource? _listenerCts; @@ -28,12 +29,14 @@ public Hmp1UdsServerListenerFilter( string socketPath, Hmp1PresentationAdapter presentation, ILogger logger, - Action listenerFaulted) + Action listenerFaulted, + Task producerConnected) { _socketPath = socketPath; _presentation = presentation; _logger = logger; _listenerFaulted = listenerFaulted; + _producerConnected = producerConnected; _listener = BindListener(socketPath); } @@ -121,6 +124,11 @@ private async Task RunListenerAsync(Socket listener, CancellationToken ct) { try { + // A recycled terminal is empty until DCP reconnects. Completing a viewer + // handshake now would replace its retained screen with that empty state. + // Bind immediately for endpoint readiness, but defer handshakes until + // the producer is attached. Session cancellation still releases the listener. + await _producerConnected.WaitAsync(ct).ConfigureAwait(false); while (!ct.IsCancellationRequested) { var socket = await listener.AcceptAsync(ct).ConfigureAwait(false); diff --git a/src/Aspire.TerminalHost/TerminalReplica.cs b/src/Aspire.TerminalHost/TerminalReplica.cs index 3e4a27d6752..31a7334792b 100644 --- a/src/Aspire.TerminalHost/TerminalReplica.cs +++ b/src/Aspire.TerminalHost/TerminalReplica.cs @@ -426,7 +426,8 @@ private Hex1bTerminal BuildTerminal() ConsumerUdsPath, downstream, _consumerListenerLogger, - upstream.ReportConsumerListenerFailure); + upstream.ReportConsumerListenerFailure, + upstream.ConnectedTask); try { diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs new file mode 100644 index 00000000000..5e09f4686b7 --- /dev/null +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Threading.Channels; +using Aspire.Dashboard.Components.Controls; +using Aspire.Dashboard.Components.Tests.Shared; +using Aspire.Dashboard.Model; +using Aspire.Dashboard.Tests.Shared; +using Aspire.Tests.Shared.DashboardModel; +using Bunit; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.FluentUI.AspNetCore.Components; +using Xunit; + +namespace Aspire.Dashboard.Components.Tests.Controls; + +[UseCulture("en-US")] +public class TerminalDisconnectedBannerTests : DashboardTestContext +{ + [Theory] + [InlineData(CommandViewModel.StartCommand, ResourceCommandResponseKind.Succeeded)] + [InlineData(CommandViewModel.RestartCommand, ResourceCommandResponseKind.Succeeded)] + [InlineData(CommandViewModel.StartCommand, ResourceCommandResponseKind.Failed)] + public async Task Restart_ExecutesAdvertisedCommandForExactReplicaAndReportsResult(string commandName, ResourceCommandResponseKind resultKind) + { + var executions = new ConcurrentQueue<(string Resource, string Command)>(); + var response = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var resource = ModelTestHelpers.CreateResource(resourceName: "shell-replica", + displayName: "shell", commands: [CreateCommand(commandName)]); + var client = new TestDashboardClient(isEnabled: true, initialResources: [resource], + resourceChannelProvider: () => Channel.CreateUnbounded>(), + executeResourceCommand: (name, _, command, _, _) => + { + executions.Enqueue((name, command.Name)); + return response.Task; + }); + TerminalSetupHelpers.SetupTerminalComponents(this, client); + Render(); + var cut = Render(builder => builder.Add(p => p.ResourceName, "shell-replica")); + cut.WaitForAssertion(() => Assert.Single(cut.FindAll(".terminal-restart-resource"))); + Assert.Equal("Restart resource", cut.Find(".terminal-restart-resource").TextContent.Trim()); + Assert.Equal("Terminal disconnected. The last output has been preserved.", cut.Find("[role=status]").TextContent); + + var click = cut.InvokeAsync(() => cut.Find(".terminal-restart-resource").ClickAsync(new())); + cut.WaitForAssertion(() => Assert.Equal([("shell-replica", commandName)], executions.ToArray())); + Assert.True(cut.FindComponents().Single(b => b.Instance.Class == "terminal-restart-resource").Instance.Disabled); + response.SetResult(new ResourceCommandResponseViewModel { Kind = resultKind, Message = "Command result" }); + await click; + Assert.False(cut.FindComponents().Single(b => b.Instance.Class == "terminal-restart-resource").Instance.Disabled); + var notification = Assert.Single(Services.GetRequiredService().GetNotifications()).Entry; + Assert.Equal(resultKind == ResourceCommandResponseKind.Succeeded ? MessageBarIntent.Success : MessageBarIntent.Error, + notification.Intent); + Assert.Single(cut.FindAll("[role=status]")); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public void Restart_ReadOnlyViewsDoNotOfferResourceMutation(bool readOnly, bool clientReadOnly) + { + var resource = ModelTestHelpers.CreateResource(resourceName: "shell", + commands: [CreateCommand(CommandViewModel.RestartCommand)]); + var client = new TestDashboardClient(isEnabled: true, isReadOnly: clientReadOnly, initialResources: [resource], + resourceChannelProvider: () => Channel.CreateUnbounded>()); + TerminalSetupHelpers.SetupTerminalComponents(this, client); + var cut = Render(builder => builder + .Add(p => p.ResourceName, "shell") + .Add(p => p.ReadOnly, readOnly)); + cut.WaitForAssertion(() => Assert.Equal(1, client.ResourceSubscriptionCount)); + Assert.Equal(readOnly || clientReadOnly ? 0 : 1, cut.FindAll(".terminal-restart-resource").Count); + } + + [Fact] + public async Task Restart_TracksCommandAvailabilityAndResourceRemoval() + { + var updates = Channel.CreateUnbounded>(); + var resource = ModelTestHelpers.CreateResource(resourceName: "shell", commands: + [CreateCommand(CommandViewModel.RestartCommand, CommandViewModelState.Hidden)]); + var client = new TestDashboardClient(isEnabled: true, initialResources: [resource], resourceChannelProvider: () => updates); + TerminalSetupHelpers.SetupTerminalComponents(this, client); + var cut = Render(builder => builder.Add(p => p.ResourceName, "shell")); + cut.WaitForAssertion(() => Assert.Equal(1, client.ResourceSubscriptionCount)); + Assert.Empty(cut.FindAll(".terminal-restart-resource")); + + await updates.Writer.WriteAsync([new(ResourceViewModelChangeType.Upsert, ModelTestHelpers.CreateResource(resourceName: "shell", + commands: [CreateCommand(CommandViewModel.RestartCommand, CommandViewModelState.Disabled)]))]); + cut.WaitForAssertion(() => Assert.True(cut.FindComponents().Single(b => b.Instance.Class == "terminal-restart-resource").Instance.Disabled)); + + await updates.Writer.WriteAsync([new(ResourceViewModelChangeType.Upsert, ModelTestHelpers.CreateResource(resourceName: "shell", + commands: [CreateCommand(CommandViewModel.RestartCommand, CommandViewModelState.Disabled), + CreateCommand(CommandViewModel.StartCommand)]))]); + cut.WaitForAssertion(() => Assert.False(cut.FindComponents().Single(b => b.Instance.Class == "terminal-restart-resource").Instance.Disabled)); + + await updates.Writer.WriteAsync([new(ResourceViewModelChangeType.Delete, resource)]); + cut.WaitForAssertion(() => Assert.Empty(cut.FindAll(".terminal-restart-resource"))); + Assert.Single(cut.FindAll("[role=status]")); + } + + [Fact] + public async Task Close_InvokesOwningSurfaceWithoutSubscribingToResources() + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var closeCount = 0; + var client = TerminalSetupHelpers.CreateTerminalDashboardClient(); + TerminalSetupHelpers.SetupTerminalComponents(this, client); + var cut = Render(builder => builder.Add(p => p.OnClose, + EventCallback.Factory.Create(this, () => + { + closeCount++; + return completion.Task; + }))); + Assert.Equal("Close tab", cut.Find(".terminal-close-tab").TextContent.Trim()); + var click = cut.InvokeAsync(() => cut.Find(".terminal-close-tab").ClickAsync(new())); + cut.WaitForAssertion(() => Assert.Equal(1, closeCount)); + Assert.True(cut.FindComponents().Single(b => b.Instance.Class == "terminal-close-tab").Instance.Disabled); + completion.SetResult(); + await click; + Assert.False(cut.FindComponents().Single(b => b.Instance.Class == "terminal-close-tab").Instance.Disabled); + Assert.Equal(0, client.ResourceSubscriptionCount); + } + + private static CommandViewModel CreateCommand(string name, CommandViewModelState state = CommandViewModelState.Enabled) => + new(name, state, name, string.Empty, string.Empty, [], false, "ArrowClockwise", IconVariant.Regular); +} diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs index 3093fc29e33..4d45e4e5c51 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs @@ -54,6 +54,40 @@ public void Initialization_ProvidesLocalizedCopyAndInputControlsWithoutHostSubsc Assert.True(registry.TryGet(viewId, new Uri(Assert.IsType(invocation.Arguments[1])).PathAndQuery, out _)); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Disconnect_ShowsLocalizedStatusAndRecoversWithoutReplacingContainer(bool chromeless) + { + var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); + var initialization = module.Setup("initTerminal", _ => true); + initialization.SetResult(1); + var cut = Render(builder => builder + .Add(p => p.ResourceName, "shell") + .Add(p => p.Chromeless, chromeless)); + var state = new TerminalToolbarState { TerminalId = 1, Generation = 1, Connected = true }; + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state)); + Assert.Empty(cut.FindAll(".terminal-disconnected-banner")); + Assert.False(cut.Find(".terminal-body").ClassList.Contains("terminal-disconnected")); + + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Connected = false, Disconnected = true })); + Assert.Single(cut.FindAll(".terminal-container")); + Assert.True(cut.Find(".terminal-body").ClassList.Contains("terminal-disconnected")); + var loc = Services.GetRequiredService>(); + Assert.Equal(loc[nameof(Resources.TerminalStrings.TerminalDisconnectedMessage)].Value, + cut.Find(".terminal-disconnected-banner [role=status]").TextContent); + Assert.Empty(cut.FindAll("[role=alert]")); + + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Generation = 2 })); + Assert.Single(cut.FindAll(".terminal-container")); + Assert.Empty(cut.FindAll(".terminal-disconnected-banner")); + Assert.False(cut.Find(".terminal-body").ClassList.Contains("terminal-disconnected")); + Assert.Single(initialization.Invocations); + Assert.Equal(["initTerminal"], module.Invocations + .Where(invocation => invocation.Identifier is "initTerminal" or "reconnectTerminal") + .Select(invocation => invocation.Identifier)); + } + [Theory] [InlineData(false, true)] [InlineData(true, true)] @@ -351,6 +385,47 @@ await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalTool Assert.Single(initialization.Invocations); } + [Fact] + public async Task Disconnect_CollapsesRecoveryAndRetryErrorWithoutReplacingTerminal() + { + var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); + var initialization = module.Setup("initTerminal", _ => true); + initialization.SetResult(1); + var cut = Render(builder => builder.Add(p => p.ResourceName, "shell")); + var state = new TerminalToolbarState { TerminalId = 1, Generation = 1, Disconnected = true, Error = "disconnected" }; + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state)); + var banner = cut.FindComponent().Instance; + Assert.Single(cut.FindAll("[role=alert]")); + Assert.Equal("Show terminal", cut.Find(".terminal-show-output").TextContent.Trim()); + + await cut.Find(".terminal-show-output").ClickAsync(new()); + Assert.Empty(cut.FindAll(".terminal-disconnected-banner")); + Assert.Empty(cut.FindAll("[role=alert]")); + Assert.Single(cut.FindAll(".terminal-overlays-collapsed")); + Assert.Single(cut.FindAll(".terminal-container")); + Assert.Same(banner, cut.FindComponent().Instance); + var indicator = cut.Find(".terminal-show-banner"); + Assert.Equal("false", indicator.GetAttribute("aria-expanded")); + Assert.Equal("Terminal disconnected. Show recovery actions", indicator.GetAttribute("aria-label")); + + // Retrying the transport must not bring an intentionally collapsed banner back. + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Generation = 2 })); + Assert.Single(cut.FindAll(".terminal-show-banner")); + await cut.Find(".terminal-show-banner").ClickAsync(new()); + Assert.Single(cut.FindAll(".terminal-disconnected-banner")); + Assert.Single(cut.FindAll("[role=alert]")); + Assert.Equal("true", cut.Find(".terminal-show-output").GetAttribute("aria-expanded")); + Assert.Empty(cut.FindAll(".terminal-overlays-collapsed")); + + await cut.Find(".terminal-show-output").ClickAsync(new()); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Generation = 2, Connected = true, Disconnected = false, Error = null })); + Assert.Empty(cut.FindAll(".terminal-show-banner")); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Generation = 2 })); + Assert.Single(cut.FindAll(".terminal-disconnected-banner")); + Assert.Empty(cut.FindAll(".terminal-show-banner")); + Assert.Single(initialization.Invocations); + } + [Theory] [InlineData("mount-failed", nameof(Resources.TerminalStrings.TerminalMountFailed))] [InlineData("disconnected", nameof(Resources.TerminalStrings.TerminalDisconnected))] diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index 6944926e476..1b8e11712e4 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -121,7 +121,7 @@ beforeEach(() => { append(style) { this.styles.push(style.textContent); }, }; const client = { - element: { parentElement: element, shadowRoot: shadow, dataset: {}, contains: value => value === client.element }, + element: { parentElement: element, shadowRoot: shadow, dataset: {}, style: {}, contains: value => value === client.element }, connected: true, peer: { id: "browser-1", primaryId: "cli-1", isPrimary: false }, geometry: { columns: 100, rows: 30 }, @@ -610,6 +610,9 @@ test("palette, theme and contrast changes replace the complete overlay without r globalAlpha: 1, save() {}, restore() {}, + beginPath() {}, + roundRect() {}, + fill() { painted.push({ color: this.fillStyle, opacity: this.globalAlpha }); }, fillRect() { painted.push({ color: this.fillStyle, opacity: this.globalAlpha }); }, }; attempt.client.scrollbar.render({ @@ -994,6 +997,41 @@ test("history chrome suppression preserves errors and selection feedback and dis assert.equal(replacement.disconnected, true); }); +for (const code of [1006, 4000]) { + test(`disconnect ${code} suppresses only redundant connection status on the retained screen`, async () => { + const { id } = mount(); + const old = attempts[0]; + old.resolve(); + await settle(); + const status = old.client.element.shadowRoot.querySelector(".inspection-message"); + const observer = themeObservers.find(observer => observer.element === status); + assert.match(old.client.element.shadowRoot.styles[0], /data-aspire-disconnected-status/); + status.textContent = "Terminal view is not connected"; + status.dataset.level = "error"; + observer.callback(); + assert.equal(status.attributes.has("data-aspire-disconnected-status"), false); + + old.close(code); + await settle(); + assert.notEqual(observer.disconnected, true); + for (const [text, hidden] of [ + ["Terminal view is not connected", true], + ["Scrollbar: Terminal view is not connected", true], + ["Copy failed: Terminal view is not connected", true], + ["Input action failed: Terminal view is not connected", true], + ["Clipboard writing is unavailable", false], + ["Navigation rejected", false], + ["Selection copied", false], + ]) { + status.textContent = text; + observer.callback(); + assert.equal(status.attributes.has("data-aspire-disconnected-status"), hidden, text); + } + terminal.disposeTerminal(id); + assert.equal(observer.disconnected, true); + }); +} + test("init returns an id while mount waits for its first connected frame", async () => { const { id } = mount(); assert.equal(terminal.getToolbarState(id).connected, false); @@ -1008,7 +1046,7 @@ test("init returns an id while mount waits for its first connected frame", async attempts[0].resolve(); await settle(); assert.deepEqual(snapshots.at(-1), { - terminalId: id, generation: 1, status: "viewer", connected: true, + terminalId: id, generation: 1, status: "viewer", connected: true, disconnected: false, title: "", workingDirectory: null, workingDirectoryUri: null, progressState: "none", progressPercentage: null, isPrimary: false, canTakeControl: true, sizeMode: "font", sizeKey: "100x30", @@ -1275,16 +1313,17 @@ test("a disconnect schedules only one retry and restores focus only if still app await settle(); document.activeElement = attempts[0].client.element; attempts[0].client.connected = false; - attempts[0].options.onStatus("closed", "error"); + attempts[0].close(1006); attempts[0].options.onStatus("closed again", "error"); await settle(); assert.equal(timers.size, 1); - assert.equal(attempts[0].client.disposed, true); + assert.equal(attempts[0].client.disposed, false); retry(); document.activeElement = document.body; attempts[1].resolve(); await settle(); assert.equal(attempts[1].client.focusCalls, 1); + assert.equal(attempts[0].client.disposed, true); assert.equal(terminal.getToolbarState(id).connected, true); }); @@ -1441,7 +1480,7 @@ test("dismissing a sizing error keeps the existing connection", async () => { assert.equal(attempts.length, 1); }); -test("a delayed dismiss cannot hide a connection failure or cancel its retry", async () => { +test("a delayed dismiss cannot hide the disconnected state or cancel its retry", async () => { const { id } = mount(); attempts[0].resolve(); await settle(); @@ -1449,9 +1488,10 @@ test("a delayed dismiss cannot hide a connection failure or cancel its retry", a attempts[0].close(1006); await settle(); terminal.dismissError(id); - assert.equal(terminal.getToolbarState(id).error, "mount-failed"); + assert.equal(terminal.getToolbarState(id).error, null); + assert.equal(terminal.getToolbarState(id).disconnected, true); assert.equal(timers.size, 1); - assert.equal(attempts[0].client.disposed, true); + assert.equal(attempts[0].client.disposed, false); }); test("remote role changes authoritatively switch primary, viewer and unclaimed states", async () => { @@ -1977,6 +2017,8 @@ test("authoritative close keeps the existing presentation read-only without remo attempts[0].options.onStatus("Late transport error", "error"); await settle(); assert.equal(terminal.getTerminalSnapshot(element).ended, true); + assert.equal(terminal.getToolbarState(id).disconnected, true); + assert.equal(attempts[0].options.preserveOnDisconnect, true); assert.equal(attempts[0].client.disposed, false); assert.equal(timers.size, 0); terminal.setReadOnly(id, false); @@ -2036,6 +2078,93 @@ test("rebind ignores authoritative close from the old connection", async () => { assert.equal(timers.size, 0); }); +test("transport retries retain the last screen until a replacement frame arrives", async () => { + const { id, element } = mount(); + const original = attempts[0]; + original.client.screenText = "Final output with graphics"; + original.resolve(); + await settle(); + original.close(1006); + await settle(); + assert.equal(original.client.disposed, false); + assert.equal(original.options.signal.aborted, false); + assert.equal(terminal.getToolbarState(id).disconnected, true); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Final output with graphics"); + + retry(); + attempts[1].reject(); + await settle(); + assert.equal(original.client.disposed, false); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Final output with graphics"); + assert.equal(terminal.getToolbarState(id).disconnected, true); + terminal.setTerminalPalette("light"); + assert.equal(original.client.colorMode, "light"); + retry(); + const replacement = attempts[2]; + replacement.client.screenText = "Reconnected output"; + replacement.resolve(); + await settle(); + assert.equal(original.client.disposed, true); + assert.equal(original.options.signal.aborted, true); + assert.equal(terminal.getToolbarState(id).disconnected, false); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Reconnected output"); +}); + +for (const action of ["dispose", "rebind"]) { + test(`${action} releases a retained screen and cancels its pending replacement`, async () => { + const { id } = mount(); + attempts[0].resolve(); + await settle(); + attempts[0].close(1006); + retry(); + if (action === "dispose") { + terminal.disposeTerminal(id); + } else { + terminal.reconnectTerminal(id, "wss://dashboard/api/terminal?resource=other"); + assert.equal(terminal.getToolbarState(id).disconnected, false); + } + assert.equal(attempts[0].client.disposed, true); + assert.equal(attempts[0].options.signal.aborted, true); + assert.equal(attempts[1].options.signal.aborted, true); + attempts[1].resolve(); + await settle(); + assert.equal(attempts[1].client.disposed, true); + }); +} + +test("exhausted retries keep the final screen until the view is disposed", async () => { + const { id, element } = mount(); + attempts[0].client.screenText = "Final output"; + attempts[0].resolve(); + await settle(); + attempts[0].close(1006); + for (let i = 0; i < 30; i++) { + retry(); + attempts.at(-1).reject(); + await settle(); + } + assert.equal(timers.size, 0); + assert.equal(terminal.getToolbarState(id).error, "disconnected"); + assert.equal(terminal.getToolbarState(id).disconnected, true); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Final output"); + assert.equal(attempts[0].client.disposed, false); + terminal.disposeTerminal(id); + assert.equal(attempts[0].client.disposed, true); +}); + +test("renderer failures surface an error rather than retaining an unusable view", async () => { + const { id } = mount(); + attempts[0].resolve(); + await settle(); + attempts[0].client.connected = false; + attempts[0].options.onStatus("Renderer failed", "error"); + await settle(); + assert.equal(terminal.getToolbarState(id).error, "mount-failed"); + assert.equal(terminal.getToolbarState(id).disconnected, false); + assert.equal(attempts[0].client.disposed, true); + assert.equal(timers.size, 1); +}); + test("disposing a view ignores later native close callbacks", async () => { const { id } = mount(); terminal.disposeTerminal(id); @@ -2053,23 +2182,29 @@ test("frontend manifest, lockfile, minified bundle and backend use the exact pai const lockfile = JSON.parse(await readFile(new URL("package-lock.json", dashboard), "utf8")); const bundle = await readFile(new URL("dist/index.min.js", assets), "utf8"); const version = manifest.dependencies["@hex1b/web-terminal"]; - assert.equal(version, "0.171.0"); + assert.equal(version, "0.173.0-alpha.1824.1.98d8766"); assert.equal(bundle.split(/\r?\n/, 1)[0], `// @hex1b/web-terminal ${version}; minified with Terser. See ../LICENSE.`); assert.equal(lockfile.packages[""].dependencies["@hex1b/web-terminal"], version); assert.equal(lockfile.packages["node_modules/@hex1b/web-terminal"].version, version); + const tools = JSON.parse(await readFile(new URL("../../.config/dotnet-tools.json", dashboard), "utf8")); + assert.equal(tools.tools["hex1b.tool"].version, version); + const mcp = JSON.parse(await readFile(new URL("../../.mcp.json", dashboard), "utf8")); + assert.equal(mcp.servers.hex1b.args[0], `Hex1b.McpServer@${version}`); // Central package rows have the form: // - // Match the exact Include value, not Hex1b.Tool or Hex1b.McpServer; + // Match the three paired package IDs, not other packages sharing their prefix; // whitespace, attribute order and either XML quote style are allowed. const packages = await readFile(new URL("../../Directory.Packages.props", dashboard), "utf8"); const declarations = [...packages.matchAll(/]*\/>/g)] .map(match => match[0]) - .filter(declaration => /\bInclude\s*=\s*["']Hex1b["']/.test(declaration)); - assert.equal(declarations.length, 1, "Expected exactly one central Hex1b library version."); - const backendVersion = declarations[0].match(/\bVersion\s*=\s*["']([^"']+)["']/); - assert.ok(backendVersion, "The paired Hex1b library must have an explicit central version."); - assert.equal(backendVersion[1], version); + .filter(declaration => /\bInclude\s*=\s*["']Hex1b(?:\.Tool|\.McpServer)?["']/.test(declaration)); + assert.equal(declarations.length, 3, "Expected all three central Hex1b package versions."); + for (const declaration of declarations) { + const backendVersion = declaration.match(/\bVersion\s*=\s*["']([^"']+)["']/); + assert.ok(backendVersion, "Paired Hex1b packages must have explicit central versions."); + assert.equal(backendVersion[1], version); + } }); test("checked-in deployment contains only the minified bundle, font and required licenses without npm installation", async () => { diff --git a/tests/Aspire.Dashboard.Components.Tests/Layout/TerminalDockTests.cs b/tests/Aspire.Dashboard.Components.Tests/Layout/TerminalDockTests.cs index a62207c96b3..38b9ad27196 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Layout/TerminalDockTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Layout/TerminalDockTests.cs @@ -425,6 +425,39 @@ await updates.Writer.WriteAsync(useSnapshot await close.DefaultTimeout(); } + [Fact] + public async Task DisconnectedBanner_ClosesOwningTabAndWaitsForWatchRemoval() + { + var updates = Channel.CreateUnbounded(); + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var client = TerminalSetupHelpers.CreateTerminalDashboardClient( + terminalChannelProvider: () => updates, closeTerminal: (_, _) => completion.Task); + TerminalSetupHelpers.SetupTerminalComponents(this, client); + var cut = Render(); + await cut.InvokeAsync(cut.Instance.ToggleAsync); + await updates.Writer.WriteAsync(TerminalSetupHelpers.Snapshot("first", "second")); + cut.WaitForAssertion(() => Assert.Equal(2, cut.FindComponents().Count)); + var views = cut.FindComponents(); + await cut.InvokeAsync(() => views[0].Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 1, Disconnected = true + })); + var click = cut.InvokeAsync(() => cut.Find(".terminal-close-tab").ClickAsync(new())); + cut.WaitForAssertion(() => Assert.Equal(["first"], client.ClosedTerminals.ToArray())); + Assert.Equal(2, cut.FindAll("[role=tab]").Count); + Assert.Empty(cut.FindAll(".terminal-restart-resource")); + + await updates.Writer.WriteAsync(TerminalSetupHelpers.Change(TerminalChangeType.Removed, "first")); + cut.WaitForAssertion(() => + { + Assert.Single(cut.FindAll("[role=tab]")); + Assert.Equal("second", cut.Find("[aria-selected=true]").TextContent.Trim()); + Assert.Same(views[1].Instance, cut.FindComponent().Instance); + }); + completion.SetResult(); + await click; + } + [Fact] public async Task LastTabRemoved_EmptyDockCanReceiveAnotherAppHostTerminal() { diff --git a/tests/Aspire.Dashboard.Components.Tests/Shared/TerminalSetupHelpers.cs b/tests/Aspire.Dashboard.Components.Tests/Shared/TerminalSetupHelpers.cs index 794440eb3dd..b92007eccbf 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Shared/TerminalSetupHelpers.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Shared/TerminalSetupHelpers.cs @@ -73,6 +73,7 @@ public static BunitJSModuleInterop SetupTerminalViewModule(BunitContext context, { SetupTerminalTitle(context); context.Services.TryAddSingleton(); + context.Services.TryAddScoped(); context.JSInterop.SetupVoid("Microsoft.FluentUI.Blazor.Components.Select.Initialize", _ => true).SetVoidResult(); var module = context.JSInterop.SetupModule(modulePath); module.Setup("reconnectTerminal", _ => true).SetResult(2); diff --git a/tests/Aspire.Dashboard.Tests/Terminal/TerminalWebSocketTests.cs b/tests/Aspire.Dashboard.Tests/Terminal/TerminalWebSocketTests.cs index 0b78a4551a0..a07336a1928 100644 --- a/tests/Aspire.Dashboard.Tests/Terminal/TerminalWebSocketTests.cs +++ b/tests/Aspire.Dashboard.Tests/Terminal/TerminalWebSocketTests.cs @@ -718,6 +718,35 @@ public async Task BrowserView_AppHostEndClosesWithCompletionStatusAndReleasesMir Assert.Equal(1, host.DisposedAttachments); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BrowserView_DrainsFinalOutputAndGraphicsBeforeDisconnect(bool useGrpc) + { + await using var host = new TerminalTestHost(output, requireAuthentication: false, useGrpc); + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await host.StartAsync(timeout.Token); + using var browser = await host.ConnectBrowserAsync(timeout.Token); + // Hold the initial acknowledgement so final output must coalesce behind it. + var initial = await ReadFrameAsync(browser, timeout.Token); + host.Workload.Write(string.Join("\r\n", Enumerable.Range(0, 35).Select(i => $"Retained output {i:D2}")) + + "\r\nfinal line\r\ncompleted\u001b]2;Final screen\u0007" + + "\u001b_Ga=T,f=32,t=d,s=1,v=1,i=7300,q=2;/wAA/w==\u001b\\"); + await host.WaitForProducerTextAsync("completed", timeout.Token); + await host.EndTerminalAsync(includeHmpExit: true); + + await SendAsync(browser, $$"""{"type":"ack","revision":{{initial.GetProperty("revision").GetUInt32()}}}""", timeout.Token); + var final = await ReadUntilAsync(browser, frame => frame.GetProperty("title").GetString() == "Final screen", timeout.Token); + Assert.Single(final.GetProperty("placements").EnumerateArray()); + var image = Assert.Single(final.GetProperty("images").EnumerateArray()); + Assert.Equal(4, image.GetProperty("byteLength").GetInt32()); + Assert.Equal(37, final.GetProperty("history").GetProperty("totalRows").GetInt32()); + var close = await ReadCloseAsync(browser, timeout.Token); + Assert.Equal(useGrpc ? (WebSocketCloseStatus)4000 : WebSocketCloseStatus.EndpointUnavailable, close.CloseStatus); + await browser.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Received", timeout.Token); + await host.WaitForAttachmentsReleasedAsync(timeout.Token); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -803,31 +832,36 @@ private static Task SendAsync(WebSocket socket, string message, CancellationToke private static async Task ReadUntilAsync(WebSocket socket, Func predicate, CancellationToken cancellationToken) { - var buffer = new byte[64 * 1024]; while (true) { - using var message = new MemoryStream(); - WebSocketReceiveResult result; - do - { - result = await socket.ReceiveAsync(buffer, cancellationToken); - Assert.Equal(WebSocketMessageType.Binary, result.MessageType); - message.Write(buffer, 0, result.Count); - } - while (!result.EndOfMessage); - - // HWT1: four-byte magic, little-endian JSON byte length, JSON metadata, - // then binary cell/image sections. Inspect only metadata in these transport tests. - var bytes = message.ToArray(); - Assert.Equal("HWT1", Encoding.ASCII.GetString(bytes, 0, 4)); - var length = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4)); - using var document = JsonDocument.Parse(bytes.AsMemory(8, length)); - var frame = document.RootElement; + var frame = await ReadFrameAsync(socket, cancellationToken); await SendAsync(socket, $$"""{"type":"ack","revision":{{frame.GetProperty("revision").GetUInt32()}}}""", cancellationToken); if (predicate(frame)) { - return frame.Clone(); + return frame; } } } + + private static async Task ReadFrameAsync(WebSocket socket, CancellationToken cancellationToken) + { + var buffer = new byte[64 * 1024]; + using var message = new MemoryStream(); + WebSocketReceiveResult result; + do + { + result = await socket.ReceiveAsync(buffer, cancellationToken); + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + message.Write(buffer, 0, result.Count); + } + while (!result.EndOfMessage); + + // HWT1: four-byte magic, little-endian JSON byte length, JSON metadata, + // then binary cell/image sections. Inspect only metadata in these transport tests. + var bytes = message.ToArray(); + Assert.Equal("HWT1", Encoding.ASCII.GetString(bytes, 0, 4)); + var length = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4)); + using var document = JsonDocument.Parse(bytes.AsMemory(8, length)); + return document.RootElement.Clone(); + } } diff --git a/tests/Aspire.TerminalHost.Tests/TerminalHostAppTests.cs b/tests/Aspire.TerminalHost.Tests/TerminalHostAppTests.cs index 965b64fdd21..ee718198aa5 100644 --- a/tests/Aspire.TerminalHost.Tests/TerminalHostAppTests.cs +++ b/tests/Aspire.TerminalHost.Tests/TerminalHostAppTests.cs @@ -398,6 +398,71 @@ await WaitForAsync( } } + [Fact] + public async Task ConsumerHandshakeWaitsForProducerBeforeAndAfterRecycle() + { + var (args, workspace, control) = BuildArgs(); + using var disp = workspace; + await using var app = new TerminalHostApp(args, NullLoggerFactory.Instance); + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var hostTask = app.RunAsync(timeout.Token); + + try + { + await WaitForFileAsync(control, TimeSpan.FromSeconds(10)); + for (var cycle = 0; cycle < 2; cycle++) + { + await WaitForFileAsync(args.ProducerUdsPath, TimeSpan.FromSeconds(10)); + await WaitForFileAsync(args.ConsumerUdsPath, TimeSpan.FromSeconds(10)); + var transportConnected = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await using var consumer = new Hmp1WorkloadAdapter(new Hmp1ClientOptions + { + StreamFactory = async ct => + { + var stream = await Hmp1Transports.ConnectUnixSocket(args.ConsumerUdsPath, ct); + transportConnected.TrySetResult(); + return stream; + }, + DefaultRole = Hmp1Role.Secondary + }); + var handshake = consumer.ConnectAsync(timeout.Token); + try + { + await transportConnected.Task.WaitAsync(timeout.Token); + // The socket is ready, but no live screen exists yet. A successful + // handshake here would overwrite the browser's retained screen. + await Assert.ThrowsAsync(() => handshake.WaitAsync(TimeSpan.FromMilliseconds(200))); + Assert.Equal(0, app.SnapshotSession().AttachedPeerCount); + + await using var producer = await ConnectProducerAsync(args.ProducerUdsPath, TimeSpan.FromSeconds(5)); + await producer.SendHelloAsync(80, 24, timeout.Token); + await handshake.WaitAsync(timeout.Token); + Assert.True(consumer.IsConnected); + await WaitForAsync(() => app.SnapshotSession().AttachedPeerCount == 1, + TimeSpan.FromSeconds(5), "The viewer should attach after its producer connects."); + } + finally + { + if (!handshake.IsCompleted) + { + await timeout.CancelAsync(); + } + await handshake; + } + + await consumer.DisconnectedTask.WaitAsync(timeout.Token); + await WaitForAsync(() => app.SnapshotSession().RestartCount == cycle + 1, + TimeSpan.FromSeconds(10), "The replica should recycle after producer disconnect."); + } + } + finally + { + app.RequestShutdown(); + await timeout.CancelAsync(); + await hostTask.WaitAsync(TimeSpan.FromSeconds(10)); + } + } + [Fact] public async Task GracefulCancellationDeletesProducerAndConsumerSockets() { @@ -706,7 +771,7 @@ public async Task ConsumerListenerBindFailureIsReportedBeforeTerminalStarts() Path.Combine(parentFile, "consumer.sock"), presentation, NullLogger.Instance, - _ => { })); + _ => { }, Task.CompletedTask)); } [Fact] @@ -727,7 +792,7 @@ public async Task ConsumerListenerWaitsForAcceptedClientHandshakeDuringTeardown( socketPath, presentation, NullLogger.Instance, - _ => { }); + _ => { }, Task.CompletedTask); await listener.OnSessionStartAsync(80, 24, DateTimeOffset.UtcNow); Task? endTask = null; @@ -1230,7 +1295,7 @@ public async Task ConsumerSocketIsRestrictedToOwningUser() args.ConsumerUdsPath, presentation, NullLogger.Instance, - ex => connected.TrySetException(ex)); + ex => connected.TrySetException(ex), Task.CompletedTask); AssertSocketIsRestrictedToOwningUser(args.ConsumerUdsPath); await listener.OnSessionStartAsync(80, 24, DateTimeOffset.UtcNow); @@ -1319,7 +1384,7 @@ public async Task ListenersRejectPermissiveDirectoryWithoutChangingPermissionsOr { await using var presentation = new Hmp1PresentationAdapter(); Assert.Throws(() => new Hmp1UdsServerListenerFilter( - path, presentation, NullLogger.Instance, _ => { })); + path, presentation, NullLogger.Instance, _ => { }, Task.CompletedTask)); } Assert.Equal("not our socket", await File.ReadAllTextAsync(path)); From b4d7d37a6b02ebbf400c07d986c198ddd20ba316 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 5 Oct 2026 10:12:22 +1100 Subject: [PATCH 02/21] Show reported resource exit codes in terminal recovery banners Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specs/with-terminal.md | 5 ++- .../Controls/TerminalDisconnectedBanner.razor | 6 ++-- .../TerminalDisconnectedBanner.razor.cs | 10 ++++++ .../Resources/TerminalStrings.Designer.cs | 12 +++++++ .../Resources/TerminalStrings.resx | 6 ++++ .../Resources/xlf/TerminalStrings.cs.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.de.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.es.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.fr.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.it.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.ja.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.ko.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.pl.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.pt-BR.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.ru.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.tr.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.zh-Hans.xlf | 10 ++++++ .../Resources/xlf/TerminalStrings.zh-Hant.xlf | 10 ++++++ .../TerminalDisconnectedBannerTests.cs | 33 +++++++++++++++++++ 19 files changed, 198 insertions(+), 4 deletions(-) diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index 0443ef4f144..92a39792d8b 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -331,7 +331,10 @@ producer as ended. The browser enables Hex1b's `preserveOnDisconnect` option to retain the last received screen, including Sixel and Kitty graphics. Disconnected views dim their output and show a localized banner over the retained screen. Resource -terminals offer **Restart resource** using the resource's advertised Restart +terminal banners include the exit code when reported in the resource protocol; +an absent code is not treated as success. Docked-terminal completion does not +currently expose the workload exit code through the pinned Hex1b adapter. +Resource terminals offer **Restart resource** using the resource's advertised Restart or Start command, with the usual command availability, confirmation and error reporting. Docked terminals offer **Close tab** through the existing dock close action. Other surfaces show the disconnected message without a close-tab action. diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor index 40e9c3e4580..8ec59930a7f 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor @@ -7,8 +7,8 @@ Appearance="ButtonAppearance.Default" IconOnly="true" Class="terminal-show-banner" - Title="@Loc[nameof(Resources.TerminalStrings.TerminalShowDisconnectedBanner)]" - aria-label="@Loc[nameof(Resources.TerminalStrings.TerminalShowDisconnectedBanner)]" + Title="@ShowBannerLabel" + aria-label="@ShowBannerLabel" aria-expanded="false" OnClick="@(() => SetCollapsedAsync(false))"> @@ -19,7 +19,7 @@ else {
- @Loc[nameof(Resources.TerminalStrings.TerminalDisconnectedMessage)] + @Message @if (RestartCommand is not null) { ResourceName is not null ? _resources.GetValueOrDefault(ResourceName) : null; + private int? ExitCode => Resource is { } resource && resource.TryGetExitCode(out var exitCode) ? exitCode : null; + + private string Message => ExitCode is { } exitCode + ? Loc[nameof(Resources.TerminalStrings.TerminalDisconnectedWithExitCode), exitCode].Value + : Loc[nameof(Resources.TerminalStrings.TerminalDisconnectedMessage)].Value; + + private string ShowBannerLabel => ExitCode is { } exitCode + ? Loc[nameof(Resources.TerminalStrings.TerminalShowDisconnectedBannerWithExitCode), exitCode].Value + : Loc[nameof(Resources.TerminalStrings.TerminalShowDisconnectedBanner)].Value; + private CommandViewModel? RestartCommand { get diff --git a/src/Aspire.Dashboard/Resources/TerminalStrings.Designer.cs b/src/Aspire.Dashboard/Resources/TerminalStrings.Designer.cs index 33dfea42f2a..71dec712c7d 100644 --- a/src/Aspire.Dashboard/Resources/TerminalStrings.Designer.cs +++ b/src/Aspire.Dashboard/Resources/TerminalStrings.Designer.cs @@ -187,6 +187,12 @@ public static string TerminalDisconnectedMessage { } } + public static string TerminalDisconnectedWithExitCode { + get { + return ResourceManager.GetString("TerminalDisconnectedWithExitCode", resourceCulture); + } + } + public static string TerminalRestartResource { get { return ResourceManager.GetString("TerminalRestartResource", resourceCulture); @@ -211,6 +217,12 @@ public static string TerminalShowDisconnectedBanner { } } + public static string TerminalShowDisconnectedBannerWithExitCode { + get { + return ResourceManager.GetString("TerminalShowDisconnectedBannerWithExitCode", resourceCulture); + } + } + public static string TerminalInputFailed { get { return ResourceManager.GetString("TerminalInputFailed", resourceCulture); diff --git a/src/Aspire.Dashboard/Resources/TerminalStrings.resx b/src/Aspire.Dashboard/Resources/TerminalStrings.resx index 77f622f8db8..886e2f9127a 100644 --- a/src/Aspire.Dashboard/Resources/TerminalStrings.resx +++ b/src/Aspire.Dashboard/Resources/TerminalStrings.resx @@ -189,6 +189,9 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Restart resource @@ -201,6 +204,9 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + The terminal could not complete the input or clipboard action. Check browser clipboard permissions and try again. diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.cs.xlf index 5257f30da86..21b31be1aeb 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.cs.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.cs.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.de.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.de.xlf index a2d1f8d2283..b01bc7db42f 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.de.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.de.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.es.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.es.xlf index 0721f123080..0931df7b5c3 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.es.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.es.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.fr.xlf index 60e837e3121..4be72bc09aa 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.fr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.fr.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.it.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.it.xlf index 70861ba09da..ac90fe546f2 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.it.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.it.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ja.xlf index 8d45a53c8d1..d4eb6f0a611 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ja.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ja.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ko.xlf index a4656118052..0384838bc99 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ko.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ko.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pl.xlf index 9631254e477..55eef19cfa4 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pl.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pl.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pt-BR.xlf index b5f92f899b9..210390a9b9a 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pt-BR.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.pt-BR.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ru.xlf index 56edb88f31f..f91e65d9071 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ru.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.ru.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.tr.xlf index 1d9ceef0506..5aec3806e29 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.tr.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.tr.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hans.xlf index 762e3e8e650..e26a936fd6c 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hans.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hans.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hant.xlf index 41f1cfa35ec..e0917298e03 100644 --- a/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hant.xlf +++ b/src/Aspire.Dashboard/Resources/xlf/TerminalStrings.zh-Hant.xlf @@ -37,6 +37,11 @@ Terminal disconnected. The last output has been preserved. + + Terminal disconnected (exit code {0}). The last output has been preserved. + Terminal disconnected (exit code {0}). The last output has been preserved. + + Dismiss Dismiss @@ -212,6 +217,11 @@ Terminal disconnected. Show recovery actions + + Terminal disconnected (exit code {0}). Show recovery actions + Terminal disconnected (exit code {0}). Show recovery actions + + Show terminal Show terminal diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs index 5e09f4686b7..f31a3ae4a75 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs @@ -9,6 +9,7 @@ using Aspire.Dashboard.Tests.Shared; using Aspire.Tests.Shared.DashboardModel; using Bunit; +using Google.Protobuf.WellKnownTypes; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.FluentUI.AspNetCore.Components; @@ -19,6 +20,38 @@ namespace Aspire.Dashboard.Components.Tests.Controls; [UseCulture("en-US")] public class TerminalDisconnectedBannerTests : DashboardTestContext { + [Theory] + [InlineData("0", "Terminal disconnected (exit code 0). The last output has been preserved.", "Terminal disconnected (exit code 0). Show recovery actions")] + [InlineData("42", "Terminal disconnected (exit code 42). The last output has been preserved.", "Terminal disconnected (exit code 42). Show recovery actions")] + [InlineData("-1", "Terminal disconnected (exit code -1). The last output has been preserved.", "Terminal disconnected (exit code -1). Show recovery actions")] + [InlineData(null, "Terminal disconnected. The last output has been preserved.", "Terminal disconnected. Show recovery actions")] + public async Task Message_TracksReportedExitCodeAndClearsWhenResourceIsRemoved(string? exitCode, string message, string label) + { + var updates = Channel.CreateUnbounded>(); + var initial = ModelTestHelpers.CreateResource(resourceName: "shell"); + var client = new TestDashboardClient(isEnabled: true, initialResources: [initial], resourceChannelProvider: () => updates); + TerminalSetupHelpers.SetupTerminalComponents(this, client); + var cut = Render(builder => builder.Add(p => p.ResourceName, "shell")); + cut.WaitForAssertion(() => Assert.Equal(1, client.ResourceSubscriptionCount)); + Assert.Equal("Terminal disconnected. The last output has been preserved.", cut.Find("[role=status]").TextContent); + + var resource = ModelTestHelpers.CreateResource(resourceName: "shell", state: KnownResourceState.Finished, properties: exitCode is null ? [] : new() + { + [KnownProperties.Resource.ExitCode] = new(KnownProperties.Resource.ExitCode, Value.ForString(exitCode), + isValueSensitive: false, knownProperty: null, sortOrder: 0, displayName: null, isHighlighted: false) + }); + await updates.Writer.WriteAsync([new(ResourceViewModelChangeType.Upsert, resource)]); + cut.WaitForAssertion(() => Assert.Equal(message, cut.Find("[role=status]").TextContent)); + await cut.Find(".terminal-show-output").ClickAsync(new()); + Assert.Equal(label, cut.Find(".terminal-show-banner").GetAttribute("aria-label")); + Assert.Equal(label, cut.Find(".terminal-show-banner").GetAttribute("title")); + + await updates.Writer.WriteAsync([new(ResourceViewModelChangeType.Delete, resource)]); + cut.WaitForAssertion(() => Assert.Equal("Terminal disconnected. Show recovery actions", cut.Find(".terminal-show-banner").GetAttribute("aria-label"))); + await cut.Find(".terminal-show-banner").ClickAsync(new()); + Assert.Equal("Terminal disconnected. The last output has been preserved.", cut.Find("[role=status]").TextContent); + } + [Theory] [InlineData(CommandViewModel.StartCommand, ResourceCommandResponseKind.Succeeded)] [InlineData(CommandViewModel.RestartCommand, ResourceCommandResponseKind.Succeeded)] From 16c20fb126a0ec548dbfb8d96d1d81742b3280e5 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Thu, 8 Oct 2026 23:01:52 +1100 Subject: [PATCH 03/21] Repoint Hex1b dependencies to stable 0.173.0 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- .mcp.json | 2 +- Directory.Packages.props | 6 +++--- docs/specs/with-terminal.md | 4 ++-- src/Aspire.Dashboard/package-lock.json | 8 ++++---- src/Aspire.Dashboard/package.json | 2 +- src/Aspire.Dashboard/wwwroot/js/README.md | 2 +- .../wwwroot/js/hex1b-web-terminal/dist/index.min.js | 2 +- .../JavaScript/TerminalView.test.mjs | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 83959c32817..4d48301219c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "hex1b.tool": { - "version": "0.173.0-alpha.1824.1.98d8766", + "version": "0.173.0", "commands": [ "hex1b" ] diff --git a/.mcp.json b/.mcp.json index b460f4571b8..9d006478640 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,7 +4,7 @@ "type": "stdio", "command": "dnx", "args": [ - "Hex1b.McpServer@0.173.0-alpha.1824.1.98d8766", + "Hex1b.McpServer@0.173.0", "--yes" ] }, diff --git a/Directory.Packages.props b/Directory.Packages.props index e04e37ebe6c..655e410f04c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -118,9 +118,9 @@ - - - + + + diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index 92a39792d8b..0818c29cbd1 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -346,7 +346,7 @@ a successful reconnect. The floating icon is inset from the terminal's focus border, and the overlay intercepts input only on its visible controls. Hex1b's redundant "Terminal view is not connected" status is suppressed while disconnected; unrelated errors and selection feedback remain visible. -The current Hex1b alpha does not support selecting or copying output while +Hex1b 0.173.0 does not support selecting or copying output while disconnected; that requires upstream offline-selection support, not merely collapsing the banner. Retryable transport loss keeps the retained view above replacement mounts until @@ -374,7 +374,7 @@ it does not lock the terminal, its creator's automation, or other viewers. ### Browser requirements and package pairing The dashboard uses `@hex1b/web-terminal` and the `Hex1b` NuGet package at -exactly `0.173.0-alpha.1824.1.98d8766`. HWT1 is experimental state transfer +exactly `0.173.0`. HWT1 is experimental state transfer between these paired packages, not a stable wire contract implemented by Aspire. Upgrade both together. The vendored runtime consists of one bundled JavaScript file for the client and both workers, a font, and two licenses. diff --git a/src/Aspire.Dashboard/package-lock.json b/src/Aspire.Dashboard/package-lock.json index 6af634fdfee..aad37d94922 100644 --- a/src/Aspire.Dashboard/package-lock.json +++ b/src/Aspire.Dashboard/package-lock.json @@ -6,16 +6,16 @@ "": { "name": "aspire-dashboard-assets", "dependencies": { - "@hex1b/web-terminal": "0.173.0-alpha.1824.1.98d8766" + "@hex1b/web-terminal": "0.173.0" }, "devDependencies": { "terser": "5.51.2" } }, "node_modules/@hex1b/web-terminal": { - "version": "0.173.0-alpha.1824.1.98d8766", - "resolved": "https://registry.npmjs.org/@hex1b/web-terminal/-/web-terminal-0.173.0-alpha.1824.1.98d8766.tgz", - "integrity": "sha512-C6eqmH/CbbcUABwBZ/N0Of5w9ue32bPJ+2rkxSScW2TOOqGFDbN7IBjJ8VlfHLa8gH0yzserxEfUkNzetVEENw==", + "version": "0.173.0", + "resolved": "https://registry.npmjs.org/@hex1b/web-terminal/-/web-terminal-0.173.0.tgz", + "integrity": "sha512-E0b+hy0djl5QrQaurlSOSwaKcJwxkDVyMjsHB7E0AP0pHDqDKOs4pO8junX0twhdSWpURUaRfXjYBwM0oK4Ffw==", "license": "MIT", "engines": { "node": ">=22" diff --git a/src/Aspire.Dashboard/package.json b/src/Aspire.Dashboard/package.json index 79e8049c453..996aa537e55 100644 --- a/src/Aspire.Dashboard/package.json +++ b/src/Aspire.Dashboard/package.json @@ -8,7 +8,7 @@ "test": "node --test ../../tests/Aspire.Dashboard.Components.Tests/JavaScript/*.test.mjs" }, "dependencies": { - "@hex1b/web-terminal": "0.173.0-alpha.1824.1.98d8766" + "@hex1b/web-terminal": "0.173.0" }, "devDependencies": { "terser": "5.51.2" diff --git a/src/Aspire.Dashboard/wwwroot/js/README.md b/src/Aspire.Dashboard/wwwroot/js/README.md index d5d906b6158..cd67f0d2e47 100644 --- a/src/Aspire.Dashboard/wwwroot/js/README.md +++ b/src/Aspire.Dashboard/wwwroot/js/README.md @@ -18,7 +18,7 @@ If we ever want to show more chart types than those, we'll need to change the bu ## Hex1b web terminal -`hex1b-web-terminal/` vendors the published `@hex1b/web-terminal` **0.173.0-alpha.1824.1.98d8766** alpha, +`hex1b-web-terminal/` vendors the published `@hex1b/web-terminal` **0.173.0** release, paired with the Hex1b, Hex1b.McpServer, and Hex1b.Tool NuGet packages and the repository-local `hex1b` tool at the same version. The client and server use the evolving HWT1 presentation transport and must be updated together. Do not substitute a diff --git a/src/Aspire.Dashboard/wwwroot/js/hex1b-web-terminal/dist/index.min.js b/src/Aspire.Dashboard/wwwroot/js/hex1b-web-terminal/dist/index.min.js index 5134676547b..78c250c49d3 100644 --- a/src/Aspire.Dashboard/wwwroot/js/hex1b-web-terminal/dist/index.min.js +++ b/src/Aspire.Dashboard/wwwroot/js/hex1b-web-terminal/dist/index.min.js @@ -1,2 +1,2 @@ -// @hex1b/web-terminal 0.173.0-alpha.1824.1.98d8766; minified with Terser. See ../LICENSE. +// @hex1b/web-terminal 0.173.0; minified with Terser. See ../LICENSE. var e=Object.defineProperty,t=Object.getOwnPropertyNames,i=(e,i,r)=>function(){if(r)throw r[0];try{return e&&(i=(0,e[t(e)[0]])(e=0)),i}catch(e){throw r=[e],e}};function r(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)}function n(e){return r(e)&&"string"==typeof e.message?e.message:String(e)}var s,o=i({"src/validation.ts"(){}});function a(e){const t=new URL(e,globalThis.location?.href);if("https:"===t.protocol&&(t.protocol="wss:"),"http:"===t.protocol&&(t.protocol="ws:"),!["ws:","wss:"].includes(t.protocol))throw new TypeError("A ws: or wss: URL is required");return t.href}function l(e){if(!(e instanceof URL||"string"==typeof e&&e.trim()))throw new TypeError("A terminal WebSocket URL is required");const t=String(e),i={connect:e=>new Promise((i,r)=>{e.signal.throwIfAborted();const n=new WebSocket(a(t));n.binaryType="arraybuffer";const s=()=>{n.close(1e3,"View detached"),r(e.signal.reason)};e.signal.addEventListener("abort",s,{once:!0}),n.addEventListener("open",()=>{e.signal.aborted||i({send(e){if(n.readyState!==WebSocket.OPEN)throw new Error("Terminal WebSocket is not open");if(n.bufferedAmount+(new TextEncoder).encode(e).byteLength>1048576)throw new Error("Terminal WebSocket control buffer exceeded 1 MiB");n.send(e)},dispose(){e.signal.removeEventListener("abort",s),n.close(1e3,"View detached")}})}),n.addEventListener("message",t=>{e.signal.aborted||(t.data instanceof ArrayBuffer?e.onFrame(t.data).catch(t=>e.onError(t)):e.onError(new Error("Expected binary HWT1 frame")))}),n.addEventListener("close",t=>{e.signal.removeEventListener("abort",s),e.signal.aborted||(e.onClose({code:t.code,reason:t.reason,wasClean:t.wasClean}),r(new Error(`Terminal WebSocket closed (${t.code})`)))})})};return s.set(i,t),Object.freeze(i)}function c(e){if(!e||void 0!==e.url==(void 0!==e.transport))throw new TypeError("Exactly one of url or transport is required");if(void 0!==e.url)return l(e.url);if(!e.transport||"function"!=typeof e.transport.connect)throw new TypeError("transport must provide connect(context)");return e.transport}var h=i({"src/websocket-transport.ts"(){s=new WeakMap}});function d(){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();const e=crypto.getRandomValues(new Uint8Array(16));e[6]=15&e[6]|64,e[8]=63&e[8]|128;const t=Array.from(e,e=>e.toString(16).padStart(2,"0")).join("");return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var u,p=i({"src/random-id.ts"(){}});function f(e){return"string"==typeof e&&/^[1-9][0-9]{0,18}$/u.test(e)&&BigInt(e)<=9223372036854775807n}function m(e){if("string"!=typeof e||!/^(?:custom|command):[A-Za-z0-9-]{1,100}$/u.test(e))throw new TypeError("Invalid terminal marker ID")}var g,w,y=i({"src/marker-state.ts"(){p(),u=class{#e;#t;#i=Object.freeze([]);#r=new Map;#n=[];#s=0;#o=0;#a=!1;constructor(e,t){this.#e=e,this.#t=t}get markers(){return this.#i}accept(e,t){if(t<=this.#o)return;this.#o=t;const i=(e?.markers??[]).map(e=>Object.freeze({...e,...this.#r.get(e.id)})),r=!this.#a||JSON.stringify(i)!==JSON.stringify(this.#i);this.#a=!0,this.#i=Object.freeze(i);const n=e?.markerResult,s=this.#n[0];n&&s&&n.requestId===s.command.requestId&&(this.#n.shift(),clearTimeout(s.timer),n.success?s.resolve(n):s.reject(new Error(n.error||"Marker operation failed")),this.#l());const o=new Set(i.map(e=>e.id));for(const e of this.#n)"add"===e.command.action&&o.add(e.command.id);for(const e of this.#r.keys())o.has(e)||this.#r.delete(e);r&&this.#t()}#c(e){const t=Promise.withResolvers();return this.#n.push({command:{type:"marker",...e,requestId:++this.#s},resolve:t.resolve,reject:t.reject}),1===this.#n.length&&this.#l(),t.promise}#l(){const e=this.#n[0];if(e)try{this.#e(e.command),e.timer=setTimeout(()=>{this.#h(new Error("Timed out waiting for the marker operation; resync before retrying."))},1e4)}catch(t){this.#n.shift(),e.reject(t instanceof Error?t:new Error(String(t))),this.#l()}}async add(e){const t=e?.position;if(!t||!f(t.generation)||!f(t.rowId)||!Number.isInteger(t.column)||t.column<0||t.column>1024)throw new TypeError("A marker requires a valid presented generation, row ID, and column");if(void 0!==e.label&&("string"!=typeof e.label||e.label.length>4096))throw new TypeError("Marker label must be at most 4096 characters");if(void 0!==e.color&&("string"!=typeof e.color||e.color.length>256||!CSS.supports("color",e.color)))throw new TypeError("Marker color must be a CSS color");const i=`custom:${d()}`;this.#r.set(i,{...void 0===e.label?{}:{label:e.label},...void 0===e.color?{}:{color:e.color}});try{await this.#c({action:"add",id:i,...t});const e=this.#i.find(e=>e.id===i);if(!e)throw new Error("Marker registration succeeded without a retained marker");return e}catch(e){throw this.#r.delete(i),e}}async remove(e){m(e),await this.#c({action:"remove",id:e}),this.#r.delete(e)}async jump(e){m(e),await this.#c({action:"jump",id:e})}async details(e){m(e);const t=await this.#c({action:"details",id:e});if(!t.details)throw new Error("Command marker details were not returned");return Object.freeze({...t.details})}disconnect(e=new Error("Terminal disconnected before the marker operation completed.")){this.#h(e),this.#r.clear()}#h(e){for(const t of this.#n.splice(0))clearTimeout(t.timer),t.reject(e)}}}});function v(e="dark"){if("light"===e||"dark"===e||"system"===e)return e;throw new TypeError('colorMode must be "light", "dark", or "system"')}function b(e,t){if("string"!=typeof e||!/^#[0-9a-f]{6}$/iu.test(e))throw new TypeError(`${t} must be a #RRGGBB color`);return e.toLowerCase()}function k(e){if(!r(e))throw new TypeError("A terminal palette object is required");if(!Array.isArray(e.ansi)||16!==e.ansi.length)throw new TypeError("palette.ansi must contain exactly 16 colors");const t=Array.from(e.ansi,(e,t)=>b(e,`palette.ansi[${t}]`)),i={};if(void 0!==e.extended){if(!r(e.extended))throw new TypeError("palette.extended must be an object");for(const[t,r]of Object.entries(e.extended)){const e=Number(t);if(!Number.isInteger(e)||e<16||e>255||String(e)!==t)throw new TypeError("palette.extended indices must be integers from 16 to 255");i[e]=b(r,`palette.extended[${t}]`)}}return Object.freeze({foreground:b(e.foreground,"palette.foreground"),background:b(e.background,"palette.background"),ansi:Object.freeze(t),...void 0===e.cursor?{}:{cursor:b(e.cursor,"palette.cursor")},...void 0===e.selectionForeground?{}:{selectionForeground:b(e.selectionForeground,"palette.selectionForeground")},...void 0===e.selectionBackground?{}:{selectionBackground:b(e.selectionBackground,"palette.selectionBackground")},...void 0===e.extended?{}:{extended:Object.freeze(i)}})}function x(e,t,i){return(e|t<<8|i<<16|4278190080)>>>0}function E(e){const t=Number.parseInt(e.slice(1),16);return x(t>>16,t>>8&255,255&t)}function T(e){const t=new Uint32Array(256);for(let i=0;i<16;i++)t[i]=E(e.ansi[i]);const i=[0,95,135,175,215,255];for(let e=16;e<232;e++){const r=e-16;t[e]=x(i[Math.floor(r/36)],i[Math.floor(r/6)%6],i[r%6])}for(let e=232;e<256;e++){const i=8+10*(e-232);t[e]=x(i,i,i)}for(const[i,r]of Object.entries(e.extended??{}))t[Number(i)]=E(r);return{foreground:E(e.foreground),background:E(e.background),selectionForeground:E(e.selectionForeground??e.background),selectionBackground:E(e.selectionBackground??e.foreground),...void 0===e.cursor?{}:{cursor:E(e.cursor)},indexed:t}}function C(e,t=!1){if(!(e>>>24==255||e>=16777216&&e<=16777471||33554432===e||50331648===e||t&&67108864===e))throw new Error("Invalid indexed-v1 color reference")}function S(e,t){switch(e>>>24){case 1:return t.indexed[255&e];case 2:return t.foreground;case 3:return t.background;default:return e}}function I(e,t,i){if("indexed-v1"!==t.colorEncoding)return e.foreground;const r=S(32&e.attributes?e.background:e.foreground,i);return 2&e.attributes?((255&r)>>1|(r>>>8&255)>>1<<8|(r>>>16&255)>>1<<16|4278190080)>>>0:r}function M(e,t,i){return"indexed-v1"!==t.colorEncoding?e.background:32&e.attributes||50331648!==e.background?S(32&e.attributes?e.foreground:e.background,i):16777215&i.background}var A,L,R,P=i({"src/terminal-palette.ts"(){o(),g=Object.freeze({foreground:"#d4d0c8",background:"#323232",selectionForeground:"#323232",selectionBackground:"#d4d0c8",ansi:Object.freeze(["#242424","#ed888f","#8dae82","#c3a150","#7baad1","#bc99c5","#64b1b5","#b7b4ae","#9a9a9a","#ff99a0","#9bc08e","#d7b156","#87bbe7","#d0a8da","#6cc4c8","#dedad3"])}),w=Object.freeze({foreground:"#323232",background:"#d4d0c8",selectionForeground:"#d4d0c8",selectionBackground:"#323232",ansi:Object.freeze(["#24262b","#9e3141","#3e6131","#705402","#255c86","#704b7a","#006367","#b7b4ae","#595959","#982338","#355a27","#664d01","#175480","#694273","#015a5e","#e3dfd7"])})}});function O(e,t,i=0,r=Number.MAX_SAFE_INTEGER){if("number"!=typeof e||!Number.isSafeInteger(e)||er)throw new Error(`Invalid ${t}: ${String(e)}`);return e}function z(e,t,i){if(!Array.isArray(e)||e.length>i)throw new Error(`Invalid ${t}`)}function U(e){if("string"!=typeof e||!e.length||e.length>1024)throw new Error("Invalid image key")}function D(e,t){if("string"!=typeof e||!/^[1-9][0-9]{0,18}$/u.test(e)||BigInt(e)>9223372036854775807n)throw new Error(`Invalid ${t}`)}function B(e){if(!r(e)||"string"!=typeof e.phase||!["unknown","prompt","commandLine","executing","finished"].includes(e.phase))throw new Error("Invalid terminal command mark");if(null!==e.exitCode&&(O(e.exitCode,"terminal command mark exit code",-2147483648,2147483647),"finished"!==e.phase))throw new Error("Non-finished terminal command mark has an exit code");if(null!==e.rawParameters&&("string"!=typeof e.rawParameters||e.rawParameters.length>A.commandMarkParameterUnits))throw new Error("Invalid terminal command mark parameters")}function q(e){if("string"!=typeof e.status||!["none","valid","invalidated"].includes(e.status)||("valid"===e.status?"string"!=typeof e.text||e.text.length>A.metadataBytes:null!==e.text))throw new Error("Invalid selection text")}function j(e){if(!r(e)||1!==e.version||"boolean"!=typeof e.full)throw new Error("Unsupported frame metadata version");if(O(e.revision,"revision",1),O(e.baseRevision,"base revision"),void 0!==e.colorEncoding&&null!==e.colorEncoding&&"indexed-v1"!==e.colorEncoding)throw new Error("Unsupported color encoding");if(void 0!==e.colorEncodings&&(z(e.colorEncodings,"color encodings",16),e.colorEncodings.some(e=>"string"!=typeof e||e.length>64)))throw new Error("Invalid color encodings");if("string"!=typeof e.title||e.title.length>A.titleUnits||/[\u0000-\u001f\u007f-\u009f\ud800-\udfff]/u.test(e.title))throw new Error("Invalid terminal title");const t=e.progress;if(!r(t)||"string"!=typeof t.state||!["none","normal","error","indeterminate","warning"].includes(t.state))throw new Error("Invalid terminal progress");if("none"===t.state||"indeterminate"===t.state){if(null!==t.percentage)throw new Error("Unexpected terminal progress percentage")}else O(t.percentage,"terminal progress percentage",0,100);const i=e.shellIntegration;if(!r(i)||"string"!=typeof i.phase||!["unknown","prompt","commandLine","executing","finished"].includes(i.phase))throw new Error("Invalid terminal shell integration");if(null!==i.lastExitCode&&O(i.lastExitCode,"terminal shell exit code",-2147483648,2147483647),"unknown"===i.phase&&null!==i.lastExitCode)throw new Error("Unknown terminal shell phase has an exit code");const n=e.workingDirectory;if(!r(n))throw new Error("Invalid terminal working directory");const s=[n.uri,n.host,n.path];if(s.every(e=>null===e));else if(s.some(e=>"string"!=typeof e||e.length>A.metadataBytes))throw new Error("Invalid terminal working directory");const o=e.commandMark;null!==o&&B(o);const a=O(e.columns,"columns",1,1024),l=O(e.rows,"rows",1,512);if(void 0!==e.lineRenditions){if(z(e.lineRenditions,"line renditions",l),e.lineRenditions.length!==l)throw new Error("Invalid line rendition count");for(const t of e.lineRenditions)O(t,"line rendition",0,3)}if("number"!=typeof e.mouseTracking||![0,9,1e3,1002,1003].includes(e.mouseTracking))throw new Error("Unsupported mouse tracking mode");if(!r(e.peer)||"boolean"!=typeof e.peer.isPrimary)throw new Error("Invalid peer state");for(const t of["id","primaryId"]){const i=e.peer[t];if(null!==i&&("string"!=typeof i||!i.length||i.length>256))throw new Error(`Invalid peer ${t}`)}if(null!==e.peer.id&&e.peer.isPrimary!==(e.peer.id===e.peer.primaryId))throw new Error("Inconsistent primary peer state");O(a*l,"cell count",1,A.cells),function(e,t,i){if(null===e)return;if(!r(e))throw new Error("Missing history metadata");if(D(e.generation,"history generation"),"main"!==e.buffer&&"alternate"!==e.buffer)throw new Error("Invalid history buffer");const n=O(e.totalRows,"history total rows",i,2147483647),s=O(e.liveTop,"history live top",0,n-i);if(s!==n-i)throw new Error("Inconsistent history extent");if(O(e.top,"history top",0,s),"boolean"!=typeof e.following||e.following&&e.top!==e.liveTop)throw new Error("Invalid history following state");if(O(e.requestId,"viewport request id"),void 0!==e.viewportError&&null!==e.viewportError&&("string"!=typeof e.viewportError||e.viewportError.length>4096))throw new Error("Invalid viewport error");if(z(e.rowIds,"viewport row ids",i),e.rowIds.length!==i||new Set(e.rowIds).size!==i)throw new Error("Invalid viewport row ids");for(const t of e.rowIds)D(t,"viewport row id");const o=e.selection;if(!r(o)||"string"!=typeof o.status||!["none","valid","invalidated"].includes(o.status))throw new Error("Invalid selection status");if(O(o.requestId,"selection request id"),"string"!=typeof o.mode||!["character","word","line","rectangle"].includes(o.mode))throw new Error("Invalid selection mode");z(o.ranges,"selection ranges",i);let a=-1;for(const e of o.ranges){if(!r(e))throw new Error("Invalid selection range");const n=O(e.row,"selection range row",a+1,i-1),s=O(e.startColumn,"selection start column",0,t-1);O(e.endColumn,"selection end column",s+1,t),a=n}if(q(o),"valid"!==o.status&&o.ranges.length)throw new Error("Inactive selection has highlight ranges");if(null!==e.copy){if(!r(e.copy))throw new Error("Missing copy metadata");O(e.copy.requestId,"copy request id",1),q(e.copy)}if(void 0!==e.markers){z(e.markers,"history markers",A.metadataBytes/48);const t=new Set;for(const i of e.markers){if(!r(i)||"string"!=typeof i.id)throw new Error("Invalid history marker");if(m(i.id),t.has(i.id))throw new Error("Duplicate history marker ID");if(t.add(i.id),"command"!==i.source&&"custom"!==i.source)throw new Error("Invalid marker source");if(!i.id.startsWith(`${i.source}:`))throw new Error("Marker source disagrees with its ID");if("main"!==i.buffer&&"alternate"!==i.buffer)throw new Error("Invalid marker buffer");if(O(i.column,"marker column",0,1024),null!==i.row&&(O(i.row,"marker row",0,n-1),i.buffer!==e.buffer))throw new Error("Inactive-buffer marker has a resolved row");if("command"===i.source)B({...i,rawParameters:null});else if(void 0!==i.phase&&null!==i.phase)throw new Error("Custom marker has a shell phase")}}if(void 0!==e.markerPage&&null!==e.markerPage){const t=e.markerPage;if(!r(t)||"string"!=typeof t.revision||!t.revision.length||t.revision.length>64)throw new Error("Invalid marker page revision");const i=O(t.total,"marker inventory count",1,Math.floor(A.metadataBytes/48)),n=O(t.offset,"marker page offset",0,i-1);if(!Array.isArray(e.markers)||!e.markers.length||n+e.markers.length>i)throw new Error("Invalid marker page length")}if(void 0!==e.markerResult&&null!==e.markerResult){const t=e.markerResult;if(!r(t)||"boolean"!=typeof t.success)throw new Error("Invalid marker result");if(O(t.requestId,"marker request ID",1),void 0!==t.error&&null!==t.error&&("string"!=typeof t.error||t.error.length>4096))throw new Error("Invalid marker error");if(void 0!==t.markerId&&null!==t.markerId){if("string"!=typeof t.markerId)throw new Error("Invalid result marker ID");m(t.markerId)}void 0!==t.details&&null!==t.details&&B(t.details)}}(e.history,a,l),z(e.hyperlinks,"hyperlinks",a*l);let c=0;for(const t of e.hyperlinks){if(!r(t))throw new Error("Invalid hyperlink");const e=O(t.row,"hyperlink row",0,l-1),i=O(t.startColumn,"hyperlink start column",0,a-1),n=O(t.endColumn,"hyperlink end column",i+1,a);if(e*a+iA.metadataBytes)throw new Error("Invalid hyperlink URI")}if(10!==e.cellWidth||20!==e.cellHeight)throw new Error("This spike requires server geometry of 10 × 20 logical pixels");for(const t of["defaultBackground","defaultForeground"])void 0!==e[t]&&O(e[t],t,0,4294967295);if(!r(e.cursor)||"boolean"!=typeof e.cursor.visible)throw new Error("Invalid cursor");O(e.cursor.x,"cursor x",-1,1024),O(e.cursor.y,"cursor y",-1,512);if("string"==typeof e.cursor.shape&&(e.cursor.shape=["Default","BlinkingBlock","SteadyBlock","BlinkingUnderline","SteadyUnderline","BlinkingBar","SteadyBar"].indexOf(e.cursor.shape)),O(e.cursor.shape,"cursor shape",0,6),z(e.images,"images",A.images),z(e.retainedImages,"retained image keys",A.images),z(e.placements,"placements",A.placements),z(e.warnings,"warnings",256),e.warnings.some(e=>"string"!=typeof e))throw new Error("Invalid warning");if(!r(e.stats))throw new Error("Invalid server metrics");for(const t of["workloadBytes","outputBatches","captureMs","elapsedMs"]){const i=e.stats[t];if("number"!=typeof i||!Number.isFinite(i)||i<0)throw new Error(`Invalid server metric ${t}`)}const h=new Set;for(const t of e.retainedImages){if(U(t),h.has(t))throw new Error("Duplicate retained image key");h.add(t)}const d=new Set;let u=0;for(const t of e.images){if(!r(t))throw new Error("Invalid image");if(U(t.key),d.has(t.key)||!h.has(t.key))throw new Error("Inconsistent new image keys");d.add(t.key);const e=O(t.width,"image width",1,16384),i=O(t.height,"image height",1,16384),n=O(t.byteLength,"image byte length",1,A.frameBytes);if("rgba"!==t.format&&"png"!==t.format)throw new Error("Unsupported image format");if("rgba"===t.format&&n!==e*i*4)throw new Error("RGBA image size mismatch");if(u+=e*i*4,u>A.textureBytes)throw new Error("New images exceed decoded texture budget")}for(const t of e.placements){if(!r(t))throw new Error("Invalid placement");if(U(t.key),!h.has(t.key))throw new Error("Placement references an unretained image");if("kgp"!==t.kind&&"sixel"!==t.kind)throw new Error("Invalid placement kind");for(const e of["x","y","width","height","sourceX","sourceY","sourceWidth","sourceHeight","clipX","clipY","clipWidth","clipHeight","z"]){const i=t[e];if("number"!=typeof i||!Number.isFinite(i)||Math.abs(i)>2147483648)throw new Error(`Invalid placement ${e}`)}for(const e of["width","height","sourceWidth","sourceHeight","clipWidth","clipHeight"]){const i=t[e];if("number"!=typeof i||i<0)throw new Error(`Negative placement ${e}`)}}}var F,N,$,G=i({"src/protocol.ts"(){o(),y(),P(),A=Object.freeze({commandBytes:65536,frameBytes:100663296,metadataBytes:8388608,titleUnits:4096,commandMarkParameterUnits:8192,cells:262144,images:4096,placements:16384,textureBytes:268435456}),L=new TextDecoder("utf-8",{fatal:!0}),R=new TextEncoder}}),W=i({"src/transport-session.ts"(){G(),o(),F=class{constructor(e){this.callbacks=e,this.#d.promise.catch(()=>{})}callbacks;#u=new AbortController;#d=Promise.withResolvers();#p;#f=!1;#m;#g=!1;#w=!1;#y=[];#v=0;get closing(){return this.#g}start(e){this.#b(e)}async#b(e){try{const t=await e.connect({signal:this.#u.signal,onFrame:e=>{const t=this.#k(e);return this.#m=t,t},onClose:e=>{if(this.#u.signal.aborted||this.#g)return;if(!e||"string"!=typeof e.reason||void 0!==e.code&&(!Number.isInteger(e.code)||"boolean"!=typeof e.wasClean))return void this.#x(new TypeError("Invalid terminal transport close details"));const t=void 0===e.code?{reason:e.reason}:{code:e.code,reason:e.reason,wasClean:e.wasClean};this.#g=!0,this.#E(t)},onError:e=>this.#x(e)});if(!t||"function"!=typeof t.send||"function"!=typeof t.dispose)throw new TypeError("transport.connect must return a connection with send and dispose");if(this.#u.signal.aborted)return void t.dispose();this.#p=t,this.callbacks.onReady(),this.#d.resolve()}catch(e){this.#x(e)}}async#k(e){try{if(this.#u.signal.throwIfAborted(),this.#g)throw new Error("Terminal transport is closing");if(this.#f)throw new Error("Concurrent terminal transport frames are not allowed");if(!(e instanceof ArrayBuffer||e instanceof Uint8Array&&e.buffer instanceof ArrayBuffer))throw new TypeError("Transport frames must be ArrayBuffer or Uint8Array backed by ArrayBuffer");if(!e.byteLength||e.byteLength>A.frameBytes)throw new RangeError("Invalid HWT1 frame size");this.#f=!0;const t=e instanceof ArrayBuffer?e:new Uint8Array(e).buffer;await this.#d.promise,this.#u.signal.throwIfAborted(),await this.callbacks.onFrame(t),this.#u.signal.throwIfAborted(),this.#f=!1}catch(e){throw this.#x(e),e}}async#E(e){try{await this.#m}catch{return}this.#u.signal.aborted||(this.dispose(),this.callbacks.onClose(e))}send(e){if(this.#u.signal.aborted)return Promise.reject(this.#u.signal.reason);if(this.#g)return Promise.reject(new DOMException("Terminal transport is closing","AbortError"));const t=(new TextEncoder).encode(e).byteLength;if(this.#y.length>=256||this.#v+t>1048576){const e=new Error("Terminal transport control queue exceeded its bounded capacity");return this.#x(e),Promise.reject(e)}const i=Promise.withResolvers();return this.#y.push({control:e,bytes:t,resolve:i.resolve,reject:i.reject}),this.#v+=t,this.#T(),i.promise}async#T(){if(!this.#w){this.#w=!0;try{for(await this.#d.promise;!this.#u.signal.aborted&&!this.#g&&this.#y.length;){const e=this.#y[0];if(await this.#p.send(e.control),this.#u.signal.aborted)return;this.#y.shift(),this.#v-=e.bytes,e.resolve()}}catch(e){this.#g||this.#x(e)}finally{this.#w=!1}}}#x(e){if(this.#u.signal.aborted)return;const t=e instanceof Error?e:new Error(n(e));this.#C(t),this.callbacks.onError(t)}dispose(){this.#C(new DOMException("Terminal transport was disposed or closed","AbortError"))}#C(e){if(!this.#u.signal.aborted){this.#u.abort(e),this.#d.reject(e);for(const t of this.#y)t.reject(e);this.#y=[],this.#v=0,this.#p?.dispose(),this.#p=void 0}}}}});function _(e,t=import.meta.url){if(e??={family:"Cascadia Mono NF",faces:[{url:N,weight:"200 700"}]},"object"!=typeof e||Array.isArray(e)||"string"!=typeof e.family||!e.family.trim()||e.family.length>256||/[\u0000-\u001f\u007f]/u.test(e.family))throw new TypeError("font.family must be a single, nonempty font family name");const i=e.faces??[];if(!Array.isArray(i)||i.length>16)throw new TypeError("font.faces must contain at most 16 font sources");return{family:e.family.trim(),faces:i.map(e=>{if(!e||"string"!=typeof e.url||!e.url.trim())throw new TypeError("Each font face requires a URL");for(const t of["weight","style"])if(void 0!==e[t]&&("string"!=typeof e[t]||e[t].length>64))throw new TypeError(`Font face ${t} must be a CSS descriptor string`);return{url:new URL(e.url,t).href,weight:e.weight??"400",style:e.style??"normal"}})}}async function H(e){const{family:t,faces:i}=_(e),n=globalThis,s="fonts"in n&&(r(o=n.fonts)&&"function"==typeof o.add&&"function"==typeof o.delete)?n.fonts:globalThis.document?.fonts;var o;if(!s||"function"!=typeof FontFace)throw new Error("Terminal font loading requires the CSS Font Loading API in the rendering context");const a=$.has(t.toLowerCase()),l=a?t.toLowerCase():JSON.stringify(t);if(a&&i.length)throw new TypeError("A generic font family cannot have downloadable faces");const c=i.length?i:a?[]:[{local:t}],h=[];for(const e of c)try{const i=new FontFace(t,"local"in e?`local(${JSON.stringify(e.local)})`:`url(${JSON.stringify(e.url)})`,{weight:e.weight??"400",style:e.style??"normal"});h.push(await i.load())}catch(i){throw new Error(`Could not load terminal font "${t}" from ${"url"in e?e.url:"local fonts"}`,{cause:i})}for(const e of h)s.add(e);return{family:t,cssFamily:l,dispose(){for(const e of h)s.delete(e)}}}var K=i({"src/terminal-font.ts"(){o(),N=new URL("./fonts/cascadia-mono-nf/CascadiaMonoNF.woff2",import.meta.url).href,$=new Set(["monospace","serif","sans-serif","system-ui"])}});function V(e="auto"){if("auto"===e||"webgpu"===e||"webgl2"===e)return e;throw new TypeError('renderer must be "auto", "webgpu", or "webgl2"')}var X,Y=i({"src/renderer-options.ts"(){}});function J(e,t){if("function"!=typeof e&&("string"!=typeof e||!t.has(e)))throw new TypeError("Link action must be a registered custom action or callback")}function Z(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)}function Q(e,t){if(!1===e)return!1;if(void 0===e)return{};if(!Z(e))throw new TypeError("Invalid links configuration");const i={};if(!1===e.osc8)i.osc8=!1;else if(void 0!==e.osc8){if(!Z(e.osc8))throw new TypeError("Invalid OSC 8 configuration");J(e.osc8.action,t),i.osc8={action:e.osc8.action}}const r=e.detection;if(!1===r)i.detection=!1;else if(void 0!==r){if(!Z(r)||!Array.isArray(r.rules))throw new TypeError("Invalid detection configuration");if(r.rules.length>X.rules)throw new TypeError("Too many link rules");if(void 0!==r.activation&&!["modifierClick","click"].includes(r.activation))throw new TypeError("Invalid link activation");if(void 0!==r.decoration&&!["always","hover","none"].includes(r.decoration))throw new TypeError("Invalid link decoration");if(void 0!==r.underlineStyle&&!["solid","dashed"].includes(r.underlineStyle))throw new TypeError("Invalid link underline style");const e=new Set,n=r.rules.map(i=>{if(!Z(i)||"string"!=typeof i.id||!i.id.length||i.id.length>256||e.has(i.id))throw new TypeError("Link rule IDs must be nonempty and unique");if(e.add(i.id),void 0!==i.enabled&&"boolean"!=typeof i.enabled)throw new TypeError("Invalid enabled flag");if(void 0!==i.text&&!["physicalRow","logicalLine","viewport"].includes(i.text))throw new TypeError("Invalid link text mode");if(void 0!==i.resolve&&"function"!=typeof i.resolve)throw new TypeError("Invalid resolver");J(i.action,t);const r={id:i.id,action:i.action,enabled:i.enabled??!0,text:i.text??"logicalLine",resolve:i.resolve};if(void 0!==i.builtin){if(!["url","uri","absolutePath","homePath"].includes(i.builtin)||void 0!==i.pattern||void 0!==i.kind)throw new TypeError("Invalid builtin rule");return{...r,builtin:i.builtin}}if(!(i.pattern instanceof RegExp&&["uri","path","custom"].includes(i.kind)))throw new TypeError("Custom rules require a RegExp and kind");if(i.pattern.source.length>X.pattern||/[^dgimsuv]/u.test(i.pattern.flags))throw new TypeError("Unsupported regex flags or oversized pattern (sticky is not supported)");return{...r,kind:i.kind,pattern:new RegExp(i.pattern.source,i.pattern.flags)}});i.detection={rules:n,activation:r.activation??"modifierClick",decoration:r.decoration??"always",underlineStyle:r.underlineStyle??"solid"}}return i}function ee(e){if("function"!=typeof e)throw new TypeError("Expected a link action callback");return(t,i,r)=>{if(!function(e){if(!Z(e))return!1;const t=e;return!(!(["detected","osc8"].includes(t.source)&&["uri","path","custom"].includes(t.kind)&&"string"==typeof t.text&&"string"==typeof t.target&&Number.isSafeInteger(t.revision))||t.revision<0||!Array.isArray(t.ranges)||0===t.ranges.length)&&("osc8"===t.source?null===t.ruleId&&"uri"===t.kind:"string"==typeof t.ruleId&&0!==t.ruleId.length)&&t.ranges.every(e=>Z(e)&&Number.isSafeInteger(e.row)&&e.row>=0&&Number.isSafeInteger(e.startColumn)&&e.startColumn>=0&&Number.isSafeInteger(e.endColumn)&&e.endColumn>e.startColumn)}(i))throw new TypeError("Expected a terminal link activation");return e(t,i,r)}}var te=i({"src/link-options.ts"(){X=Object.freeze({rules:32,pattern:8192,chunk:65536,totalText:262144,cells:262144,matches:2048,resultText:262144,cacheEntries:2048,cacheText:1048576,timeoutMs:250})}});function ie(e){let t=e.text.length+16;for(const i of e.captures)t+=8+(i?.length??0);for(const[i,r]of Object.entries(e.groups))t+=8+i.length+(r?.length??0);return t}var re,ne,se,oe,ae,le,ce=i({"src/link-worker-protocol.ts"(){}}),he=i({"src/render-backend.ts"(){re=16,ne=class extends Error{}}}),de=i({"src/webgpu-backend.ts"(){he(),se=class{constructor(e,t,i,r,n){this.device=e,this.texture=t,this.bindGroup=i,this.width=r,this.height=n}device;texture;bindGroup;width;height;writePixels(e,t,i,r=0,n=0){this.device.queue.writeTexture({texture:this.texture,origin:[r,n]},e,{bytesPerRow:4*t,rowsPerImage:i},[t,i])}writeBitmap(e){this.device.queue.copyExternalImageToTexture({source:e},{texture:this.texture,premultipliedAlpha:!1},[this.width,this.height])}destroy(){this.texture.destroy()}},oe=class e{constructor(e){this.device=e,this.format=navigator.gpu.getPreferredCanvasFormat(),this.uniform=e.createBuffer({size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.sampler=e.createSampler({minFilter:"linear",magFilter:"linear"})}device;kind="webgpu";instanceBufferBytes=0;instanceBuffer;disposed=!1;context;format;uniform;sampler;pipeline;static async create(t,i){if(!globalThis.isSecureContext)throw new ne("WebGPU requires HTTPS or localhost");if(!navigator.gpu)throw new ne("WebGPU is unavailable in this browser worker");const r=await navigator.gpu.requestAdapter();if(!r)throw new ne("No WebGPU adapter is available");let n,s;try{n=await r.requestDevice()}catch(e){if(e instanceof DOMException&&"OperationError"===e.name)throw new ne(`Could not acquire a WebGPU device: ${e.message}`,{cause:e});throw e}try{s=new e(n);const r=s;return n.lost.then(e=>{r.disposed||i(new Error(`WebGPU device lost: ${e.message||e.reason}`))}),n.addEventListener("uncapturederror",e=>{r.disposed||i(new Error(`WebGPU error: ${e.error.message}`))}),await s.initialize(t),s}catch(e){throw s?s.dispose():n.destroy(),e}}get maxTextureDimension2D(){return this.device.limits.maxTextureDimension2D}get maxCanvasDimension2D(){return this.maxTextureDimension2D}async initialize(e){const t=this.device.createShaderModule({code:"\nstruct Viewport { size: vec2f, padding: vec2f }\n@group(0) @binding(0) var viewport: Viewport;\n@group(0) @binding(1) var image: texture_2d;\n@group(0) @binding(2) var imageSampler: sampler;\n\nstruct VertexOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n @location(1) color: vec4f,\n @location(2) @interpolate(flat) mode: f32,\n}\n\n@vertex fn vertex(\n @builtin(vertex_index) index: u32,\n @location(0) rect: vec4f,\n @location(1) uvRect: vec4f,\n @location(2) color: vec4f,\n @location(3) mode: f32,\n) -> VertexOut {\n let corners = array(\n vec2f(0, 0), vec2f(1, 0), vec2f(0, 1),\n vec2f(0, 1), vec2f(1, 0), vec2f(1, 1)\n );\n let corner = corners[index];\n let position = rect.xy + corner * rect.zw;\n var out: VertexOut;\n out.position = vec4f(position / viewport.size * vec2f(2, -2) + vec2f(-1, 1), 0, 1);\n out.uv = mix(uvRect.xy, uvRect.zw, corner);\n out.color = color;\n out.mode = mode;\n return out;\n}\n\n@fragment fn fragment(in: VertexOut) -> @location(0) vec4f {\n // Explicit LOD avoids derivative-uniformity requirements across solid/mask/image batches.\n let texel = textureSampleLevel(image, imageSampler, in.uv, 0);\n if (in.mode < 0.5) { return in.color; }\n if (in.mode < 1.5) { return vec4f(in.color.rgb, in.color.a * texel.a); }\n return texel * in.color;\n}"});this.pipeline=await this.device.createRenderPipelineAsync({layout:"auto",vertex:{module:t,entryPoint:"vertex",buffers:[{arrayStride:4*re,stepMode:"instance",attributes:[{shaderLocation:0,offset:0,format:"float32x4"},{shaderLocation:1,offset:16,format:"float32x4"},{shaderLocation:2,offset:32,format:"float32x4"},{shaderLocation:3,offset:48,format:"float32"}]}]},fragment:{module:t,entryPoint:"fragment",targets:[{format:this.format,blend:{color:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha"},alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha"}}}]},primitive:{topology:"triangle-list"}});const i=e.getContext("webgpu");if(!i||!("configure"in i))throw new ne("Could not create an OffscreenCanvas WebGPU context");this.context=i,i.configure({device:this.device,format:this.format,alphaMode:"opaque"})}createTexture(e,t,i){if(e>this.maxTextureDimension2D||t>this.maxTextureDimension2D)throw new Error(`${i} exceeds the GPU texture dimension limit`);if(!this.pipeline)throw new Error("WebGPU backend is not initialized");const r=this.device.createTexture({label:i,size:[e,t],format:"rgba8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT});try{const i=this.device.createBindGroup({layout:this.pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:this.uniform}},{binding:1,resource:r.createView()},{binding:2,resource:this.sampler}]});return new se(this.device,r,i,e,t)}catch(e){throw r.destroy(),e}}resize(e,t){this.device.queue.writeBuffer(this.uniform,0,new Float32Array([e,t,0,0]))}submit(e,t,i,r){if(this.disposed||!this.context||!this.pipeline)throw new Error("WebGPU backend is not initialized");const n=t*re*4;(!this.instanceBuffer||n>this.instanceBufferBytes)&&(this.instanceBuffer?.destroy(),this.instanceBufferBytes=Math.max(256,e.byteLength),this.instanceBuffer=this.device.createBuffer({size:this.instanceBufferBytes,usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST})),n&&this.device.queue.writeBuffer(this.instanceBuffer,0,e,0,t*re);const s=this.device.createCommandEncoder(),o=s.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:r[0],g:r[1],b:r[2],a:1},loadOp:"clear",storeOp:"store"}]});o.setPipeline(this.pipeline),o.setVertexBuffer(0,this.instanceBuffer);for(const e of i){if(!(e.resource instanceof se))throw new Error("Texture belongs to a different rendering backend");o.setBindGroup(0,e.resource.bindGroup),o.draw(6,e.count,0,e.start)}o.end(),this.device.queue.submit([s.finish()])}async idle(){await this.device.queue.onSubmittedWorkDone()}dispose(){this.disposed||(this.disposed=!0,this.instanceBuffer?.destroy(),this.uniform.destroy(),this.context?.unconfigure(),this.device.destroy())}}}});function ue(e){return e instanceof Error?e:new Error(String(e))}function pe(e){return Number.isSafeInteger(e)&&e>0}function fe(e){e.pixelStorei(e.UNPACK_ALIGNMENT,1),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!1),e.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1),e.pixelStorei(e.UNPACK_COLORSPACE_CONVERSION_WEBGL,e.NONE),e.pixelStorei(e.UNPACK_ROW_LENGTH,0),e.pixelStorei(e.UNPACK_SKIP_PIXELS,0),e.pixelStorei(e.UNPACK_SKIP_ROWS,0)}var me=i({"src/webgl2-backend.ts"(){he(),ae=class{constructor(e,t,i,r,n,s,o){this.owner=e,this.gl=t,this.texture=i,this.width=r,this.height=n,this.assertActive=s,this.release=o}owner;gl;texture;width;height;assertActive;release;destroyed=!1;bind(){if(this.assertActive(),this.destroyed)throw new Error("WebGL2 texture is destroyed");this.gl.bindTexture(this.gl.TEXTURE_2D,this.texture),fe(this.gl)}writePixels(e,t,i,r=0,n=0){if(!pe(t)||!pe(i)||!Number.isSafeInteger(r)||!Number.isSafeInteger(n)||r<0||n<0||r+t>this.width||n+i>this.height||e.byteLength{const t=e.statusMessage;this.contextLost(new Error("WebGL2 context lost"+(t?`: ${t}`:"")))};static async create(t,i){const r=t.getContext("webgl2",{alpha:!1,antialias:!1,depth:!1,stencil:!1,premultipliedAlpha:!1,preserveDrawingBuffer:!1});if(!r)throw new ne("Could not create an OffscreenCanvas WebGL2 context");const n=new e(t,r,i);try{return n.initialize(),n}catch(e){throw n.dispose(),e}}get maxTextureDimension2D(){return this.textureLimit}get maxCanvasDimension2D(){return this.canvasLimit}get instanceBufferBytes(){return this.bufferBytes}initialize(){const e=this.gl;this.canvas.addEventListener("webglcontextlost",this.onContextLost),this.checkErrors("initialization"),this.textureLimit=e.getParameter(e.MAX_TEXTURE_SIZE);const t=e.getParameter(e.MAX_VIEWPORT_DIMS);if(this.canvasLimit=Math.min(this.textureLimit,e.getParameter(e.MAX_RENDERBUFFER_SIZE),t[0],t[1]),!pe(this.textureLimit)||!pe(this.canvasLimit))throw new Error("Invalid WebGL2 texture or viewport limits");const i=this.compile(e.VERTEX_SHADER,"#version 300 es\nprecision highp float;\nprecision highp int;\n\nlayout(location = 0) in vec4 rect;\nlayout(location = 1) in vec4 uvRect;\nlayout(location = 2) in vec4 color;\nlayout(location = 3) in float mode;\nuniform vec2 viewport;\nout vec2 fragmentUv;\nout vec4 fragmentTint;\nflat out float fragmentMode;\n\nvoid main() {\n const vec2 corners[6] = vec2[6](\n vec2(0, 0), vec2(1, 0), vec2(0, 1),\n vec2(0, 1), vec2(1, 0), vec2(1, 1)\n );\n vec2 corner = corners[gl_VertexID];\n vec2 position = rect.xy + corner * rect.zw;\n gl_Position = vec4(position / viewport * vec2(2, -2) + vec2(-1, 1), 0, 1);\n fragmentUv = mix(uvRect.xy, uvRect.zw, corner);\n fragmentTint = color;\n fragmentMode = mode;\n}"),r=this.compile(e.FRAGMENT_SHADER,"#version 300 es\nprecision highp float;\nprecision highp int;\n\nuniform highp sampler2D image;\nin vec2 fragmentUv;\nin vec4 fragmentTint;\nflat in float fragmentMode;\nout vec4 fragmentColor;\n\nvoid main() {\n vec4 texel = textureLod(image, fragmentUv, 0.0);\n if (fragmentMode < 0.5) {\n fragmentColor = fragmentTint;\n } else if (fragmentMode < 1.5) {\n fragmentColor = vec4(fragmentTint.rgb, fragmentTint.a * texel.a);\n } else {\n fragmentColor = texel * fragmentTint;\n }\n}"),n=e.createProgram();if(!n)throw new Error("Could not allocate a WebGL2 shader program");if(this.program=n,e.attachShader(n,i),e.attachShader(n,r),e.linkProgram(n),!e.getProgramParameter(n,e.LINK_STATUS))throw new Error(`WebGL2 shader link failed: ${e.getProgramInfoLog(n)||"No diagnostic available"}`);for(const t of this.shaders)e.detachShader(n,t),e.deleteShader(t);this.shaders.clear();const s=e.getUniformLocation(n,"viewport"),o=e.getUniformLocation(n,"image");if(null===s||null===o)throw new Error("WebGL2 shader uniforms are unavailable");this.viewport=s;const a=e.createBuffer();if(!a)throw new Error("Could not allocate a WebGL2 instance buffer");this.instanceBuffer=a;const l=e.createVertexArray();if(!l)throw new Error("Could not allocate a WebGL2 vertex array");this.vertexArray=l,e.useProgram(n),e.uniform1i(o,0),e.activeTexture(e.TEXTURE0),e.bindVertexArray(l),e.bindBuffer(e.ARRAY_BUFFER,a);for(let t=0;t<4;t++)e.enableVertexAttribArray(t),e.vertexAttribDivisor(t,1);e.enable(e.BLEND),e.blendEquationSeparate(e.FUNC_ADD,e.FUNC_ADD),e.blendFuncSeparate(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA,e.ONE,e.ONE_MINUS_SRC_ALPHA),e.disable(e.DEPTH_TEST),e.disable(e.STENCIL_TEST),e.disable(e.CULL_FACE),e.disable(e.SCISSOR_TEST),e.disable(e.DITHER),this.resize(this.canvas.width,this.canvas.height)}compile(e,t){const i=this.gl,r=i.createShader(e);if(!r)throw new Error("Could not allocate a WebGL2 shader");if(this.shaders.add(r),i.shaderSource(r,t),i.compileShader(r),!i.getShaderParameter(r,i.COMPILE_STATUS))throw new Error(`WebGL2 shader compilation failed: ${i.getShaderInfoLog(r)||"No diagnostic available"}`);return r}assertActive(){if(this.disposed)throw new Error("WebGL2 backend is disposed");if(this.lostError)throw this.lostError}contextLost(e){if(!this.disposed&&!this.lostError){this.lostError=e;for(const t of this.fences)this.settleFence(t,e);this.onFatal(e)}}checkErrors(e){this.assertActive();const t=this.gl,i=t.getError();if(i===t.CONTEXT_LOST_WEBGL||t.isContextLost()){const e=new Error("WebGL2 context lost");throw this.contextLost(e),e}if(i!==t.NO_ERROR){const r=i===t.OUT_OF_MEMORY?"OUT_OF_MEMORY":i===t.INVALID_ENUM?"INVALID_ENUM":i===t.INVALID_VALUE?"INVALID_VALUE":i===t.INVALID_OPERATION?"INVALID_OPERATION":i===t.INVALID_FRAMEBUFFER_OPERATION?"INVALID_FRAMEBUFFER_OPERATION":`0x${i.toString(16)}`;throw new Error(`WebGL2 ${e} failed: ${r}`)}}createTexture(e,t,i){if(this.assertActive(),!pe(e)||!pe(t)||e>this.textureLimit||t>this.textureLimit)throw new Error(`${i} has invalid dimensions or exceeds the WebGL2 texture dimension limit`);const r=this.gl,n=r.createTexture();if(!n)throw this.checkErrors("texture allocation"),new Error(`Could not allocate WebGL2 texture: ${i}`);try{r.bindTexture(r.TEXTURE_2D,n),fe(r),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),r.texImage2D(r.TEXTURE_2D,0,r.RGBA8,e,t,0,r.RGBA,r.UNSIGNED_BYTE,null),this.checkErrors(`texture allocation (${i})`);const s=new ae(this,r,n,e,t,()=>this.assertActive(),e=>this.textures.delete(e));return this.textures.add(s),s}catch(e){throw r.deleteTexture(n),e}}resize(e,t){if(this.assertActive(),!Number.isFinite(e)||!Number.isFinite(t)||e<=0||t<=0)throw new Error("Invalid WebGL2 logical viewport dimensions");if(!pe(this.canvas.width)||!pe(this.canvas.height)||this.canvas.width>this.canvasLimit||this.canvas.height>this.canvasLimit)throw new Error("Canvas exceeds the WebGL2 drawing buffer dimension limit");const i=this.gl;if(i.drawingBufferWidth!==this.canvas.width||i.drawingBufferHeight!==this.canvas.height)throw this.checkErrors("drawing buffer resize"),new Error("WebGL2 could not allocate the requested canvas drawing buffer");i.viewport(0,0,this.canvas.width,this.canvas.height),i.useProgram(this.program),i.uniform2f(this.viewport,e,t),this.checkErrors("resize")}submit(e,t,i,r){if(this.assertActive(),!Number.isSafeInteger(t)||t<0||t*re>e.length)throw new Error("Invalid WebGL2 instance data");for(const e of i){if(!(e.resource instanceof ae)||e.resource.owner!==this)throw new Error("Texture belongs to a different rendering backend");if(e.resource.destroyed)throw new Error("WebGL2 texture is destroyed");if(!Number.isSafeInteger(e.start)||!Number.isSafeInteger(e.count)||e.start<0||e.count<0||e.start+e.count>t)throw new Error("Invalid WebGL2 batch instance range")}const n=this.gl,s=re*Float32Array.BYTES_PER_ELEMENT,o=t*s;if(n.bindVertexArray(this.vertexArray),n.bindBuffer(n.ARRAY_BUFFER,this.instanceBuffer),!this.bufferBytes||o>this.bufferBytes){const t=Math.max(256,e.byteLength);n.bufferData(n.ARRAY_BUFFER,t,n.DYNAMIC_DRAW),this.checkErrors("instance buffer allocation"),this.bufferBytes=t}o&&n.bufferSubData(n.ARRAY_BUFFER,0,e,0,t*re),n.useProgram(this.program),n.activeTexture(n.TEXTURE0),n.clearColor(r[0],r[1],r[2],1),n.clear(n.COLOR_BUFFER_BIT);for(const e of i){if(!e.count)continue;const t=e.resource;n.bindTexture(n.TEXTURE_2D,t.texture);const i=e.start*s;n.vertexAttribPointer(0,4,n.FLOAT,!1,s,i),n.vertexAttribPointer(1,4,n.FLOAT,!1,s,i+16),n.vertexAttribPointer(2,4,n.FLOAT,!1,s,i+32),n.vertexAttribPointer(3,1,n.FLOAT,!1,s,i+48),n.drawArraysInstanced(n.TRIANGLES,0,6,e.count)}this.checkErrors("frame submission")}async idle(){this.checkErrors("GPU completion");const e=this.gl,t=e.fenceSync(e.SYNC_GPU_COMMANDS_COMPLETE,0);if(!t)throw this.checkErrors("GPU fence allocation"),new Error("Could not allocate a WebGL2 GPU fence");return new Promise((i,r)=>{const n={sync:t,resolve:i,reject:r};this.fences.add(n);try{e.flush(),this.checkErrors("GPU fence submission"),this.pollFence(n)}catch(e){this.settleFence(n,ue(e))}})}pollFence(e){if(this.fences.has(e))try{this.assertActive();const t=this.gl;if(t.isContextLost()){const e=new Error("WebGL2 context lost");throw this.contextLost(e),e}const i=t.clientWaitSync(e.sync,0,0);if(i===t.ALREADY_SIGNALED||i===t.CONDITION_SATISFIED)this.checkErrors("GPU completion"),this.settleFence(e);else{if(i!==t.TIMEOUT_EXPIRED)throw this.checkErrors("GPU fence wait"),new Error("WebGL2 GPU fence wait failed");e.timer=setTimeout(()=>{e.timer=void 0,this.pollFence(e)},0)}}catch(t){this.settleFence(e,ue(t))}}settleFence(e,t){this.fences.delete(e)&&(void 0!==e.timer&&clearTimeout(e.timer),this.gl.deleteSync(e.sync),t?e.reject(t):e.resolve())}dispose(){if(this.disposed)return;this.disposed=!0,this.canvas.removeEventListener("webglcontextlost",this.onContextLost);for(const e of this.fences)this.settleFence(e,new Error("WebGL2 backend is disposed"));for(const e of this.textures)e.destroy();const e=this.gl;this.instanceBuffer&&e.deleteBuffer(this.instanceBuffer),this.vertexArray&&e.deleteVertexArray(this.vertexArray),this.program&&e.deleteProgram(this.program);for(const t of this.shaders)e.deleteShader(t);this.shaders.clear(),this.instanceBuffer=void 0,this.vertexArray=void 0,this.program=void 0,this.bufferBytes=0,e.isContextLost()||e.getExtension("WEBGL_lose_context")?.loseContext()}}}});var ge,we,ye,ve,be,ke,xe,Ee,Te=i({"src/backend-selection.ts"(){he(),Y(),de(),me()}});function Ce(e){return[(255&e)/255,(e>>>8&255)/255,(e>>>16&255)/255,(e>>>24&255)/255]}function Se(e){return`${5&e.attributes}/${e.width}/${e.text}`}function Ie(e){return 1109742===e.text.codePointAt(0)}function Me(e,t,i,r={x:0,y:0,rowHeight:0}){let{x:n,y:s,rowHeight:o}=r;const a=[];for(const[r,l]of e){const e=Math.ceil(l.width*ge*i)+4,c=Math.ceil(we*i)+4;if(e>t||c>t)return null;if(n+e>t&&(n=0,s+=o,o=0),s+c>t)return null;a.push({key:r,cell:l,x:n,y:s,width:e,height:c}),n+=e,o=Math.max(o,c)}return{placements:a,shelf:{x:n,y:s,rowHeight:o}}}var Ae,Le,Re=i({"src/renderer.ts"(){G(),K(),Te(),he(),P(),ge=10,we=20,ye=1048576,ve=1048576,be=re,ke=[1,1,1,1],xe=T(g),Ee=class e{canvas;scale;backingScale;backend;fallbackReason;fontConfiguration;fontMetrics;images;glyphs;imageUploadBytes;imagePayloadBytes;glyphUploadBytes;atlasRebuilds;textureBytes;instances;disposed;columns;rows;font;rasterCanvas;raster;atlas;glyphKeyUnits=0;shelf={x:0,y:0,rowHeight:0};canvasLimited=!1;width=0;height=0;quadCount=0;batches=[];static async create(t,i,r,n,s="auto"){const o=_(n);if(!Number.isFinite(i)||i<.5||i>3)throw new Error("Invalid backing scale");const{backend:a,fallbackReason:l}=await async function(e,t,i="auto"){const r=V(i);if("webgl2"===r)return{backend:await le.create(e,t)};try{return{backend:await oe.create(e,t)}}catch(i){if("auto"!==r||!(i instanceof ne))throw i;try{return{backend:await le.create(e,t),fallbackReason:i.message}}catch(e){throw new AggregateError([i,e],`WebGPU unavailable (${i.message}); WebGL2 initialization failed: ${e instanceof Error?e.message:String(e)}`)}}}(t,r,s),c=new e(t,i,a,o);c.fallbackReason=l;try{return await c.initialize(),c}catch(e){throw c.dispose(),e}}constructor(e,t,i,r){if(!Number.isFinite(t)||t<.5||t>3)throw new Error("Invalid backing scale");this.canvas=e,this.scale=t,this.backingScale=t,this.backend=i,this.fontConfiguration=r,this.fontMetrics=new Map,this.images=new Map,this.glyphs=new Map,this.imageUploadBytes=0,this.imagePayloadBytes=0,this.glyphUploadBytes=0,this.atlasRebuilds=0,this.textureBytes=0,this.instances=new Float32Array(4096*be),this.disposed=!1,this.columns=0,this.rows=0}async initialize(){this.font=await H(this.fontConfiguration),this.rasterCanvas=new OffscreenCanvas(1,1);const e=this.rasterCanvas.getContext("2d",{willReadFrequently:!0});if(!e)throw new Error("Worker glyph rasterization is unavailable");this.raster=e,this.resetAtlas(Math.min(2048,this.backend.maxTextureDimension2D))}createTexture(e,t,i){return this.backend.createTexture(e,t,i)}resetAtlas(e){this.atlas?.destroy(),this.atlas=this.createTexture(e,e,"Glyph atlas"),this.glyphs.clear(),this.glyphKeyUnits=0,this.shelf={x:0,y:0,rowHeight:0}}resize(e,t,i){const r=e*ge,n=t*we,s=this.backend.maxCanvasDimension2D,o=Math.min(this.scale,i?i.width/r:1/0,i?i.height/n:1/0);this.canvasLimited=r*o>s||n*o>s,this.backingScale=Math.min(o,s/r,s/n);const a=Math.max(1,Math.min(s,Math.ceil(r*this.backingScale))),l=Math.max(1,Math.min(s,Math.ceil(n*this.backingScale)));this.columns===e&&this.rows===t&&this.canvas.width===a&&this.canvas.height===l||(this.columns=e,this.rows=t,this.width=r,this.height=n,this.canvas.width=a,this.canvas.height=l,this.backend.resize(r,n))}async updateImages(e,t){const i=new Set(t),r=new Map(e.map(e=>[e.key,e]));let n=0;for(const e of i){const t=r.get(e)||this.images.get(e);if(!t)throw new Error(`Missing retained image resource: ${e}`);n+=t.width*t.height*4}if(n>A.textureBytes)throw new Error("Retained images exceed the 256 MiB texture budget");for(const[e,t]of this.images)i.has(e)&&!r.has(e)||(t.destroy(),this.textureBytes-=t.width*t.height*4,this.images.delete(e));for(const t of e){const e=this.createTexture(t.width,t.height,`Image ${t.key}`);try{if("rgba"===t.format)e.writePixels(t.bytes,t.width,t.height);else{const i=new DataView(t.bytes.buffer,t.bytes.byteOffset,t.bytes.byteLength);if(i.byteLength<33||2303741511!==i.getUint32(0)||218765834!==i.getUint32(4)||13!==i.getUint32(8)||1229472850!==i.getUint32(12)||i.getUint32(16)!==t.width||i.getUint32(20)!==t.height)throw new Error(`PNG header dimensions do not match resource ${t.key}`);const r=await createImageBitmap(new Blob([t.bytes],{type:"image/png"}),{premultiplyAlpha:"none",colorSpaceConversion:"none"});try{if(r.width!==t.width||r.height!==t.height)throw new Error("Decoded PNG dimension mismatch");e.writeBitmap(r)}finally{r.close()}}this.images.set(t.key,e),this.textureBytes+=t.width*t.height*4,this.imageUploadBytes+=t.width*t.height*4,this.imagePayloadBytes+=t.byteLength}catch(t){throw e.destroy(),t}}}prepareGlyphs(e){const t=new Map;for(const i of e)!(i&&i.width&&i.text.trim())||64&i.attributes||Ie(i)||t.set(Se(i),i);const i=e=>[...e.keys()].reduce((e,t)=>e+t.length,0);if(t.size>16384||i(t)>ve)throw new Error("Visible glyph metadata exceeds the bounded glyph cache");const r=new Map([...t].filter(([e])=>!this.glyphs.has(e)));if(!r.size)return;let n=this.glyphs.size+r.size<=16384&&this.glyphKeyUnits+i(r)<=ve?Me(r,this.atlas.width,this.scale,this.shelf):null;if(!n){let e=this.atlas.width;const i=Math.min(4096,this.backend.maxTextureDimension2D);for(;!(n=Me(t,e,this.scale))&&e=ye)throw new Error("Frame exceeds bounded quad budget");const p=this.quadCount*be;if(p+be>this.instances.length){const e=new Float32Array(Math.min(2*this.instances.length,ye*be));e.set(this.instances),this.instances=e}const f=a[2]-a[0],m=a[3]-a[1];this.instances.set([c,h,d-c,u-h,a[0]+(c-t)/r*f,a[1]+(h-i)/n*m,a[0]+(d-t)/r*f,a[1]+(u-i)/n*m,...s,o,0,0,0],p);const g=this.batches[this.batches.length-1];g?.resource===e?g.count++:this.batches.push({resource:e,start:this.quadCount,count:1}),this.quadCount++}solid(e,t,i,r,n,s){this.quad(this.atlas,e,t,i,r,n,0,void 0,s)}placement(e){const t=this.images.get(e.key);if(!t)throw new Error(`Placement texture is missing: ${e.key}`);const{sourceX:i,sourceY:r,sourceWidth:n,sourceHeight:s}=e;if(!(n&&s&&e.width&&e.height))return;const o=e.x+Math.max(0,-i)/n*e.width,a=e.y+Math.max(0,-r)/s*e.height,l=e.x+Math.min(n,t.width-i)/n*e.width,c=e.y+Math.min(s,t.height-r)/s*e.height,h=Math.max(o,e.clipX),d=Math.max(a,e.clipY),u=Math.min(l,e.clipX+e.clipWidth),p=Math.min(c,e.clipY+e.clipHeight);this.quad(t,e.x,e.y,e.width,e.height,ke,2,[i/t.width,r/t.height,(i+n)/t.width,(r+s)/t.height],[h,d,Math.max(0,u-h),Math.max(0,p-d)])}decorations(e,t,i,r,n,s=e.underlineColor,o,a=1){128&e.attributes&&this.solid(t,i+10*a,r,a,n,o),256&e.attributes&&this.solid(t,i+a,r,a,n,o);const l=e.underlineStyle||(8&e.attributes?1:0);this.underline(l,t,i,r,Ce(s),o,a)}underline(e,t,i,r,n,s,o=1){if(1===e)this.solid(t,i+18*o,r,o,n,s);else if(2===e)this.solid(t,i+16*o,r,o,n,s),this.solid(t,i+18*o,r,o,n,s);else if(3===e)for(let e=0;e=2?2:1,d=t%this.columns;if(d>=Math.max(1,Math.floor(this.columns/c)))return;const u=d*ge*c,p=a*we,f=p-(3===l?we:0),m=Math.min(e.width*ge*c,this.width-u),g=l?o?[o[0],Math.max(p,o[1]),o[2],Math.min(p+we,o[1]+o[3])-Math.max(p,o[1])]:[0,p,this.width,we]:o,w=Ce(o?n.selectionForeground:I(e,i,n)),y=Ie(e)?void 0:this.glyphs.get(Se(e));if(y){const t=y.colored?!o&&2&e.attributes?[.5,.5,.5,1]:ke:w;this.quad(this.atlas,u,f,e.width*ge*c,we*h,t,y.colored?2:1,[y.u0,y.v0,y.u1,y.v1],g)}this.decorations(e,u,f,m,w,o?n.selectionForeground:function(e,t,i){return"indexed-v1"!==t.colorEncoding?e.underlineColor:67108864===e.underlineColor?I(e,t,i):S(e.underlineColor,i)}(e,i,n),g,h),!s||e.underlineStyle||8&e.attributes||Ie(e)||this.underline(s,u,f,m,w,g,h)}render(e,t,i,r,n=xe){const s=performance.now();this.quadCount=0,this.batches=[];const o=t.placements.map((e,t)=>({placement:e,order:t,z:"sixel"===e.kind?-1:e.z})).sort((e,t)=>e.z-t.z||e.order-t.order);for(const e of o)e.z<-1073741824&&this.placement(e.placement);for(let i=0;i=Math.max(1,Math.floor(this.columns/o))||this.solid(i%this.columns*ge*o,s*we,ge*o,we,Ce(M(r,t,n)))}for(const e of o)e.z>=-1073741824&&e.z<0&&this.placement(e.placement);for(let s=0;s=0&&this.placement(e.placement);const a=t.history?.selection;if("valid"===a?.status)for(const s of a.ranges){const o=t.lineRenditions?.[s.row]?2:1,a=[s.startColumn*ge*o,s.row*we,(s.endColumn-s.startColumn)*ge*o,we];this.solid(...a,Ce(n.selectionBackground));const l=s.row*this.columns;let c=l+s.startColumn;for(;c>l&&0===e[c]?.width;)c--;for(let o=c;o=0&&l.x=0&&l.ye!==s?.rowIds[t]))return!1;if(t.hyperlinks.length!==r.hyperlinks.length||t.hyperlinks.some((e,t)=>{const i=r.hyperlinks[t];return e.row!==i.row||e.startColumn!==i.startColumn||e.endColumn!==i.endColumn||e.uri!==i.uri}))return!1;if(e.length!==i.length)return!1;for(let t=0;tNumber.isSafeInteger(e)&&e>=0)||!Array.isArray(r)||!["solid","dashed"].includes(s))throw new Error("Invalid link decorations");const o=this.presented?.metadata;if(!this.enabled||n||!o||e!==o.revision||t!==this.generation||i<=this.lastSerial)return!1;if(r.length>o.columns*o.rows||r.some(e=>!e||!Number.isInteger(e.row)||e.row<0||e.row>=o.rows||!Number.isInteger(e.startColumn)||!Number.isInteger(e.endColumn)||e.startColumn<0||e.endColumn<=e.startColumn||e.endColumn>o.columns))throw new Error("Invalid link decoration ranges");const a=new Uint8Array(o.columns*o.rows),l="dashed"===s?5:1;for(const e of r)a.fill(l,e.row*o.columns+e.startColumn,e.row*o.columns+e.endColumn);return this.lastSerial=i,this.decorations=this.pending={revision:e,generation:t,serial:i,mask:a},!0}submission(){return{mask:this.decorations?.mask,acknowledgement:this.pending}}acknowledge(e,t){if(e&&e===this.pending&&!t&&this.enabled&&e.generation===this.generation&&e.revision===this.presented?.metadata.revision)return this.pending=void 0,{revision:e.revision,generation:e.generation,serial:e.serial}}}}}),ut=i({"src/marker-pages.ts"(){G(),Oe=class{#S;get pending(){return void 0!==this.#S}reset(){this.#S=void 0}accept(e){if(!e?.markerPage)return this.reset(),e;const{markers:t=[],markerPage:i,...r}=e,n=JSON.stringify(r);0===i.offset&&(this.#S={revision:i.revision,total:i.total,signature:n,markers:[],ids:new Set,bytes:0});const s=this.#S;if(!s||s.revision!==i.revision||s.total!==i.total||s.markers.length!==i.offset||s.signature!==n)throw new Error("Marker pages do not describe the same contiguous history snapshot");if(s.bytes+=(new TextEncoder).encode(JSON.stringify(t)).byteLength,s.bytes>A.metadataBytes)throw new Error("Retained marker inventory exceeds the 8 MiB client budget");for(const e of t){if(s.ids.has(e.id))throw new Error("Duplicate marker ID across inventory pages");s.ids.add(e.id),s.markers.push(e)}return s.markers.length{Ue?.closing||wt(e)})}function gt(e){ze&&!ze.disposed?Object.assign(ct,ze.metrics()):ze?.disposed&&Object.assign(ct,{imageCount:0,textureBytes:0,atlasGlyphs:0,atlasBytes:0,instanceBufferBytes:0}),self.postMessage({type:"stats",stats:{...ct},...void 0===e?{}:{text:e}})}function wt(e){if(je||Fe)return;je=!0;const t=n(e);ct.gpu="error",ct.connected=!1,clearInterval(tt),clearInterval(it),Ue?.dispose(),gt(),ft(t,"error"),ze?.dispose()}function yt(){Ke=!0,He||We||Ge||je||Fe||Ne&&ct.connected||!Ye||st.pending||ot||(He=!0,self.requestAnimationFrame(()=>{He=!1,Ge||We||je||Fe||Ne&&ct.connected||st.pending||ot||(et=vt())}))}async function vt(){if(!Ke||!Ye||!ze)return;Ke=!1,We=!0;const e=Qe;try{e&&nt.prepare(Je,Ye);const t=nt.submission();ze.resize(Ye.columns,Ye.rows,rt);const i=Ne||Math.floor(performance.now()/600)%2==0,r=ze.render(Je,Ye,i,t.mask,lt);if(await ze.idle(),je||Fe)return;if(Xe=i,ct.presentations++,ct.rendererCpuMs=r.cpuMs,ct.quads=r.quads,ct.drawCalls=r.drawCalls,ct.warnings=ze.canvasLimited?[...Ye.warnings,`Canvas resolution capped by the GPU; terminal grid remains ${Ye.columns}x${Ye.rows}`]:Ye.warnings,e){Qe=void 0,_e=!1,ct.frames++,e.full&&ct.fullFrames++,ct.revision=e.revision,ct.changedCells+=e.changedCells,ct.lastChangedCells=e.changedCells,ct.captureMs=Ye.stats.captureMs,ct.workloadBytes=Ye.stats.workloadBytes,ct.outputBatches=Ye.stats.outputBatches,ct.serverElapsedMs=Ye.stats.elapsedMs,ct.columns=Ye.columns,ct.rows=Ye.rows,ct.mouseTracking=Ye.mouseTracking,ct.peer=Ye.peer,ct.history=Ye.history;const t=function(e,t,i){const r=[];for(let n=0;nA.frameBytes)throw new Error("Invalid or oversized binary frame");const t=new DataView(e);let i=0;const r=e=>{if(e<0||e>t.byteLength-i)throw new Error("Truncated HWT1 frame")},n=()=>{r(4);const e=t.getUint32(i,!0);return i+=4,e};if(827610952!==n())throw new Error("Unsupported frame magic (expected HWT1)");const s=O(n(),"metadata length",2,A.metadataBytes);r(s);const o=JSON.parse(L.decode(new Uint8Array(e,i,s)));i+=s,j(o);const a=o.columns*o.rows,l=O(n(),"changed cell count",0,a);if(l>Math.floor((t.byteLength-i)/22))throw new Error("Truncated cell records");if(o.full&&l!==a)throw new Error("Incomplete full frame");const c=[],h=new Set;for(let s=0;s=a||h.has(s))throw new Error("Invalid or duplicate cell index");h.add(s);const l=n(),d=n(),u=n();"indexed-v1"===o.colorEncoding&&(C(l),C(d),C(u,!0));const p=t.getUint16(i,!0),f=t.getUint8(i+2),m=t.getUint8(i+3),g=t.getUint16(i+4,!0);if(i+=6,m>5)throw new Error("Unsupported underline style");r(g);const w=L.decode(new Uint8Array(e,i,g));i+=g,c.push({index:s,foreground:l,background:d,underlineColor:u,attributes:p,width:f,underlineStyle:m,text:w})}if(o.images.reduce((e,t)=>e+t.byteLength,0)!==t.byteLength-i)throw new Error("Image payload length mismatch");const d=o.images.map(t=>{const r=new Uint8Array(e,i,t.byteLength);return i+=t.byteLength,{...t,bytes:r}});return{metadata:o,cells:c,images:d}}(e),i=t.metadata;if(!at&&i.colorEncodings?.includes("indexed-v1")&&(at=!0,mt({type:"colorEncoding",value:"indexed-v1"})),!i.full&&(ot||i.baseRevision!==Ze||i.revision<=Ze||!Ye||i.columns!==Ye.columns||i.rows!==Ye.rows||(i.colorEncoding??null)!==(Ye.colorEncoding??null)))return ct.discardedFrames++,_e=!1,st.reset(),ot=!0,Ke=!1,Qe=void 0,mt({type:"ack",revision:i.revision}),mt({type:"resync"}),void ft(`Revision mismatch (local ${Ze}, base ${i.baseRevision}); requesting a full frame`);if(await et,je||Fe)return;if(await ze.idle(),await ze.updateImages(t.images,i.retainedImages),je||Fe)return;const r=performance.now(),n=i.full?new Array(i.columns*i.rows):Je.slice();for(const e of t.cells)n[e.index]=e;ze.prepareGlyphs(n),Je=n,Ye=i,Ze=i.revision;const s=st.pending&&!i.full,o=st.accept(i.history);if(ot=!1,Qe={revision:i.revision,full:i.full||s&&!!Qe?.full,changedCells:t.cells.length+(s?Qe?.changedCells??0:0)},Ve=Je.some(e=>e&&16&e.attributes&&!(64&e.attributes))||i.cursor.visible&&(0===i.cursor.shape||i.cursor.shape%2==1),ct.preparationCpuMs=performance.now()-r,void 0===o)return Ke=!1,_e=!1,void mt({type:"ack",revision:i.revision});i.history=o,Ke=!0}finally{Ge=!1,Ke&&yt()}}}var kt=i({"src/terminal-worker.ts"(){G(),Re(),dt(),ut(),o(),P(),h(),W(),je=!1,Fe=!1,Ne=!1,$e=!1,Ge=!1,We=!1,_e=!1,He=!1,Ke=!1,Ve=!1,Xe=!0,Je=[],Ze=0,et=Promise.resolve(),nt=new Le,st=new Oe,ot=!1,at=!1,lt=T(g),ct={revision:0,fullFrames:0,frames:0,presentations:0,changedCells:0,lastChangedCells:0,discardedFrames:0,imageCount:0,textureBytes:0,atlasGlyphs:0,atlasBytes:0,bytesReceived:0,imageUploadBytes:0,imagePayloadBytes:0,gpu:"initializing",connected:!1,warnings:[],fps:0,receivedKBps:0,workloadMBps:0,captureMs:0,rendererCpuMs:0,preparationCpuMs:0,workloadBytes:0,outputBatches:0,serverElapsedMs:0},ht={time:performance.now(),presentations:0,bytes:0,workload:0},self.addEventListener("error",e=>{e.preventDefault(),wt(e.error||new Error(e.message))}),self.addEventListener("unhandledrejection",e=>{e.preventDefault(),wt(e.reason)}),self.addEventListener("message",e=>{const t=e.data;if("init"===t.type)(async function(e){if(ze||Ue)throw new Error("Worker is already initialized");if($e=e.preserveOnDisconnect??!1,lt=T(k(void 0===e.palette?g:e.palette)),"function"!=typeof self.requestAnimationFrame)throw new Error("This browser does not support requestAnimationFrame in a dedicated OffscreenCanvas worker");if(ft("Loading terminal font and initializing renderer..."),ze=await Ee.create(e.canvas,e.scale,wt,e.font,e.renderer),je||Fe)return void ze?.dispose();ct.gpu="ready",ct.backingScale=e.scale,gt();const t="webgpu"===ze.backend.kind?"WebGPU":"WebGL2";ze.fallbackReason&&ft(`Using WebGL2: ${ze.fallbackReason}`),ft(`${t} ready. Attaching terminal view...`),Ue=new F({onReady(){je||Fe||(ct.connected=!0,self.postMessage({type:"connected"}),ft(`Connected · ${t} worker · server-authoritative cells and graphics`,"ready"),gt())},onFrame:bt,onError:wt,onClose(e){je||Fe||Ne||(Ne=!0,clearInterval(tt),clearInterval(it),async function(){if($e){if(await et,je||Fe)return;if(!(Ke||Ve&&!Xe)||st.pending||ot||(Ke=!0,await vt()),je||Fe)return}else Fe=!0,ze?.dispose();ct.connected=!1,ct.gpu="stopped",ct.fps=0,self.postMessage({type:"closed",details:e}),ft(`View disconnected (${void 0===e.code?e.reason:`${e.code}${e.reason?`: ${e.reason}`:""}`}). Attach another view to reconnect.`,"error"),gt()}().catch(wt))}}),tt=setInterval(()=>{const e=performance.now(),t=(e-ht.time)/1e3;ct.fps=(ct.presentations-ht.presentations)/t,ct.receivedKBps=(ct.bytesReceived-ht.bytes)/t/1e3,ct.workloadMBps=Math.max(0,ct.workloadBytes-ht.workload)/t/1e6,ht={time:e,presentations:ct.presentations,bytes:ct.bytesReceived,workload:ct.workloadBytes},gt()},1e3),it=setInterval(()=>{const e=Math.floor(performance.now()/600)%2==0;Ve&&e!==Xe&&yt()},100),Ue.start("websocket"===e.transport.type?l(e.transport.url):{connect:e=>(De=e,Be=Promise.withResolvers(),e.signal.addEventListener("abort",()=>{Be?.reject(e.signal.reason),qe?.reject(e.signal.reason)},{once:!0}),self.postMessage({type:"transportConnect"}),Be.promise)})})(t).catch(wt);else if("transportConnected"!==t.type||!De||je||Fe)if("transportFrame"!==t.type||!De||je||Fe)if("transportSent"!==t.type||je||Fe)if("transportClosed"===t.type)De?.onClose(t.details);else if("transportError"===t.type)De?.onError(new Error(t.message));else if("stop"===t.type)Fe=!0,clearInterval(tt),clearInterval(it),Ue?.dispose(),ze?.dispose(),self.close();else if("viewport"!==t.type||je||Fe)if("command"!==t.type||je||Fe)if("palette"!==t.type||je||Fe)if("linkDetection"!==t.type||je||Fe||Ne){if("linkDecorations"===t.type&&!je&&!Fe&&!Ne)try{nt.accept(t.revision,t.generation,t.serial,t.ranges,Ge||_e||!!Qe,t.underlineStyle)&&yt()}catch(e){wt(e)}}else try{if(!nt.configure(t.enabled,t.generation))return;if(!Ge&&!We&&!Qe){const e=nt.snapshot();e&&self.postMessage({type:"linkSnapshot",generation:nt.generation,snapshot:e})}yt()}catch(e){wt(e)}else try{lt=T(k(t.palette)),yt()}catch(e){wt(e)}else mt(t.command);else{if(!Number.isFinite(t.width)||!Number.isFinite(t.height)||t.width<0||t.height<0)return void wt(new Error("Invalid mounted viewport dimensions"));if(rt?.width===t.width&&rt?.height===t.height)return;rt={width:t.width,height:t.height},yt()}else qe?.resolve(),qe=void 0;else De.onFrame(t.buffer).then(()=>{je||Fe||self.postMessage({type:"transportReceived"})}).catch(wt);else Be?.resolve({send:e=>(qe=Promise.withResolvers(),self.postMessage({type:"transportSend",control:e}),qe.promise),dispose(){}})})}}),xt={};function Et(e){let t=e.length;const i={")":"(","]":"[","}":"{"};for(;t>0;){const r=e[t-1];if(!".,;:!?".includes(r)){if(i[r]){const n=e.slice(0,t);if(n.split(r).length>n.split(i[r]).length){t--;continue}}break}t--}return e.slice(0,t)}function Tt(e){try{const t="builtin"in e.rule?e.rule.builtin:void 0;let i;if("source"in e.rule){const t=e.rule.flags.replaceAll("g","");i=new RegExp(e.rule.source,`${t}g`)}else{const t={url:String.raw`https?:\/\/[^\s\0<>"'\x60]+`,uri:String.raw`[A-Za-z][A-Za-z0-9+.-]*:[^\s\0<>"'\x60]+`,absolutePath:String.raw`(?:[A-Za-z]:[\\/]|/)[^\s\0<>"'\x60]*`,homePath:String.raw`~/[^\s\0<>"'\x60]+`};i=new RegExp(t[e.rule.builtin],"giu")}let r=0,n=0,s=0,o=0;const a=[];for(const l of e.chunks){if(s+=l.text.length,l.text.length>X.chunk||s>X.totalText)return{id:e.id,error:"limit",message:"Link scan text limit exceeded"};i.lastIndex=0;const c=[];let h;for(;null!==(h=i.exec(l.text));){if(++n>X.totalText||r>=X.matches)return{id:e.id,error:"limit",message:"Link match limit exceeded"};if(!h[0].length){const e=l.text.codePointAt(i.lastIndex);i.lastIndex+=(i.unicode||i.unicodeSets)&&void 0!==e&&e>65535?2:1;continue}let s=h[0];if(t){const e=l.text[h.index-1];if(void 0!==e&&!/[\s([{"'<>=]/u.test(e))continue;if("uri"===t&&/^[a-z]:[\\/]/iu.test(s))continue;if(s=Et(s),!s||"absolutePath"===t&&("/"===s||/^[a-z]:[\\/]$/iu.test(s)))continue;if("url"===t)try{if(!new URL(s).hostname)continue}catch{continue}}const a={index:h.index,text:s,captures:h.slice(1),groups:{...h.groups}};if(o+=ie(a),o>X.resultText)return{id:e.id,error:"limit",message:"Link result text limit exceeded"};c.push(a),r++}a.push({key:l.key,matches:c})}return{id:e.id,results:a}}catch(t){return{id:e.id,error:"worker",message:t instanceof Error?t.message:String(t)}}}((t,i)=>{for(var r in i)e(t,r,{get:i[r],enumerable:!0})})(xt,{scanLinks:()=>Tt});var Ct=i({"src/link-detection-worker.ts"(){te(),ce(),"undefined"!=typeof self&&"function"==typeof self.postMessage&&self.addEventListener("message",e=>{self.postMessage(Tt(e.data))})}});var St=class{x=0;y=0;reset(){this.x=this.y=0}take(e,t,i){const r=t.height/i;if(!(r>0))return{x:0,y:0};const n=1===e.deltaMode?1:2===e.deltaMode?i:1/r;this.x+=e.deltaX*n,this.y+=e.deltaY*n;const s=Math.trunc(this.x),o=Math.trunc(this.y);return this.x-=s,this.y-=o,{x:Math.max(-32,Math.min(32,s)),y:Math.max(-32,Math.min(32,o))}}},It=class{owner=null;mode="character";endpoint=null;begin(e,t,{tracking:i,historical:r,readOnly:n,selection:s}){if(this.owner)return null;const o=r||n||!i||e.shiftKey;if(o&&0!==e.button)return null;if(this.owner=o?"local":"app",!o)return{owner:"app"};const a=!!e.shiftKey&&!!s&&["valid","pending"].includes(s.status)&&!1!==s.canExtend;return this.mode=a&&s?s.mode:e.altKey?"rectangle":e.detail>=3?"line":2===e.detail?"word":"character",this.endpoint={x:t.x,y:t.y},{owner:this.owner,mode:this.mode,extend:a}}move(e){return"local"!==this.owner?null:(this.endpoint={x:e.x,y:e.y},{...this.endpoint})}scrollPoint(e){if("local"===this.owner)return{x:"rectangle"===this.mode&&this.endpoint?this.endpoint.x:e.x,y:e.y}}wheelOwner(e,{tracking:t,historical:i,readOnly:r}){return this.owner??(i||r||!t||e.shiftKey?"local":"app")}end(){this.owner=null}};o();var Mt=Object.freeze({Continue:"continue",Consume:"consume",Application:"application",Browser:"browser"}),At=Object.freeze({CopySelection:"copySelection",PasteClipboard:"pasteClipboard",CopyOrPaste:"copyOrPaste",ClearSelection:"clearSelection",ScrollToLive:"scrollToLive",ScrollLines:"scrollLines"}),Lt=new Set(["l","t","v","w","+","-","=","0"]),Rt=new Set(["F1","F3","F5","F6","F7","F10","F11","F12"]),Pt=new Set(["Tab","Enter","Backspace","Delete","Escape","ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Insert",...Array.from({length:12},(e,t)=>`F${t+1}`)]),Ot=new Set(Object.values(Mt));function zt(){return[{id:"clipboard.copy-key",match:e=>"key"===e.type&&"c"===e.key.toLowerCase()&&!e.alt&&(e.meta&&!e.ctrl||e.ctrl&&e.shift&&!e.meta),action:At.CopySelection},{id:"browser.shortcuts",match:e=>"key"===e.type&&(e.meta||Rt.has(e.key)||e.ctrl&&("Tab"===e.key||"F4"===e.key||Lt.has(e.key.toLowerCase())||e.shift&&1===e.key.length)||e.alt&&!e.ctrl),route:Mt.Browser},{id:"terminal.keys",match:e=>"key"===e.type&&(Pt.has(e.key)||e.ctrl&&1===e.key.length),route:Mt.Application},{id:"clipboard.context-click",match:(e,t)=>"pointer"===e.type&&"right"===e.button&&!e.meta&&(!t.mouseCaptured||e.shift||t.historical||t.readOnly),action:At.CopyOrPaste}]}function Ut(e,t){if("function"!=typeof e||"AsyncFunction"===e.constructor.name)throw new TypeError(`${t} must be a synchronous function`)}var Dt=class{#I;#M;#A;constructor({inputBindings:e=[],onInput:t,actions:i={}}={}){if(!Array.isArray(e))throw new TypeError("inputBindings must be an array");if(!i||"object"!=typeof i||Array.isArray(i))throw new TypeError("actions must be an object");this.#A=new Set(Object.values(At));for(const[e,t]of Object.entries(i)){if(!e||this.#A.has(e)||"function"!=typeof t)throw new TypeError(`Invalid or reserved action: ${e}`);this.#A.add(e)}void 0!==t&&Ut(t,"onInput"),this.#M=t;const r=zt(),n=new Set,s=e.map(e=>{if(!e||"string"!=typeof e.id||!e.id||n.has(e.id))throw new TypeError("Each input binding needs a unique, nonempty id");if(n.add(e.id),!0===e.remove){if(!r.some(t=>t.id===e.id))throw new TypeError(`Cannot remove unknown default binding: ${e.id}`);if(Object.keys(e).some(e=>!["id","remove"].includes(e)))throw new TypeError(`Removed binding ${e.id} must contain only id and remove`);return null}return Ut(e.match,`Binding ${e.id}.match`),void 0!==e.when&&Ut(e.when,`Binding ${e.id}.when`),this.#L(e),Object.freeze({...e})}).filter(e=>null!==e);this.#I=[...s,...r.filter(e=>!n.has(e.id))]}get bindings(){return this.#I.map(e=>({...e}))}#L(e){if(Bt(e))return{route:e};if(!r(e)||"function"==typeof e.then)throw new TypeError("Input routing must synchronously return a route or action");const t=Object.hasOwn(e,"action"),i=Object.hasOwn(e,"route");if(t===i)throw new TypeError("Specify exactly one action or route");if(i){if(!Bt(e.route))throw new TypeError(`Unknown input route: ${String(e.route)}`);return{route:e.route}}if(!(function(e){return"function"==typeof e}(e.action)||"string"==typeof e.action&&this.#A.has(e.action)))throw new TypeError(`Unknown terminal action: ${String(e.action)}`);return{action:e.action,args:e.args}}resolve(e,t){const i=this.#M?.(e,t);if(void 0!==i){const e=this.#L(i);if(e.route!==Mt.Continue)return e}for(const i of this.#I){const r=i.match(e,t);if("boolean"!=typeof r)throw new TypeError(`Binding ${i.id}.match must return a boolean`);if(!r)continue;if(i.when){const r=i.when(t,e);if("boolean"!=typeof r)throw new TypeError(`Binding ${i.id}.when must return a boolean`);if(!r)continue}const n=this.#L(i);if(n.route!==Mt.Continue)return n}return{route:Mt.Continue}}};function Bt(e){return"string"==typeof e&&[...Ot].some(t=>t===e)}function qt(e){return{ctrl:!!e.ctrlKey,alt:!!e.altKey,shift:!!e.shiftKey,meta:!!e.metaKey}}var jt=[["left",1],["middle",4],["right",2]],Ft=["left","middle","right"];function Nt(e,t,i,r={}){const n=new AbortController,s={signal:n.signal};let o=1,a=1,l=0,c=null;const h=new Set;let d,u,p,f;const m=new St,g=new It;let w,y,v,b,k,x,E,T,C,S,I,M={count:0,time:0,x:-1,y:-1};const A=e.title,L=e.style.cursor,R=()=>({tracking:l,...r.state?.()});function P(t,i=!1){const n=function(e,t,i,r,n=!1){if(!(t.width>0&&t.height>0))return null;const s=Math.floor((e.clientX-t.left)*i/t.width),o=Math.floor((e.clientY-t.top)*r/t.height);return!n&&(s<0||o<0||s>=i||o>=r)?null:{x:Math.max(0,Math.min(i-1,s)),y:Math.max(0,Math.min(r-1,o)),shift:!!e.shiftKey,alt:!!e.altKey,ctrl:!!e.ctrlKey}}(t,e.getBoundingClientRect(),o,a,i);return n&&r.lineRenditions?.()[n.y]&&(n.x=Math.min(Math.max(0,Math.floor(o/2)-1),Math.floor(n.x/2))),n}function O(t=S){if(!r.hyperlink)return;S=t;const i=C&&P(C),n=i?r.hyperlink(i):null;e.title=n?`${n.target}\n${"click"===n.activation?"Click":"Ctrl/Cmd+click"} to activate link`:A,e.style.cursor=n&&t&&("click"===n.activation||t.ctrlKey||t.metaKey)&&!t.altKey&&!t.shiftKey?"pointer":L,T&&T.link.id!==n?.id&&(T.dragged=!0),I!==n?.id&&(I=n?.id,r.hoverHyperlink?.(n))}function z(){if(clearTimeout(v),v=void 0,"local"!==g.owner||!y)return;const t=e.getBoundingClientRect(),i=y.clientY=t.bottom?y.clientY-t.bottom+1:0;i&&(v=setTimeout(()=>{if(v=void 0,!y)return;const e=P(y,!0);if("local"===g.owner&&e){b&&(b.dragged=!0),M.count=0;const n=Math.sign(i)*Math.min(8,Math.max(1,Math.ceil(Math.abs(i)/(t.height/a))));r.scroll?.(n,g.scrollPoint(e)),z()}},60))}function U(){void 0!==f&&cancelAnimationFrame(f),f=void 0,u&&t(u),u=void 0}function D(e,i){for(const[r,n]of jt){const s=0!==(e.buttons&n);s!==h.has(r)&&(U(),s?h.add(r):h.delete(r),(s||9!==l)&&t({type:"mouse",action:s?"down":"up",button:r,...i}),p=void 0)}}function B(i=!0,n=!0){if(r.end?.(n),clearTimeout(v),v=void 0,y=void 0,b=void 0,k=void 0,T=void 0,g.end(),x=void 0,i?U():(void 0!==f&&cancelAnimationFrame(f),f=void 0,u=void 0),i&&9!==l&&d)for(const e of h)t({type:"mouse",action:"up",button:e,...d});h.clear();const s=c;c=null,null!==s&&e.hasPointerCapture(s)&&e.releasePointerCapture(s),p=void 0,m.reset(),w=void 0}return e.addEventListener("pointerdown",t=>{if(t.defaultPrevented&&null===c)return;if("mouse"!==t.pointerType||![0,1,2].includes(t.button))return;const n=P(t);if(!n)return;if(null!==c)return T&&(T.dragged=!0),x!==Mt.Browser&&t.preventDefault(),void("app"!==g.owner&&x!==Mt.Application||c!==t.pointerId||D(t,n));i();const s={type:"pointer",button:Ft[t.button],point:Object.freeze({...n}),...qt(t)},o=r.resolve?.(s)??{route:Mt.Continue};let a=void 0!==o.action?Mt.Consume:o.route;if(a===Mt.Continue&&0===t.button&&!t.altKey&&!t.shiftKey){const e=r.hyperlink?.(n);e&&("click"===e.activation||t.ctrlKey||t.metaKey)&&(T={link:e,input:s,x:t.clientX,y:t.clientY,dragged:!1},a=Mt.Consume)}if(E=2===t.button?a:void 0,T&&(E=Mt.Consume),C=t,O(t),a!==Mt.Continue)return k=b=void 0,M.count=0,x=a,c=t.pointerId,d=n,e.setPointerCapture(c),a!==Mt.Browser&&t.preventDefault(),a===Mt.Application?U():(void 0!==f&&cancelAnimationFrame(f),f=void 0,u=void 0),a===Mt.Application&&D(t,n),void(void 0!==o.action&&r.execute?.(o,s));if(t.preventDefault(),t.metaKey)return;const l=performance.now();M.count=l-M.time<500&&n.x===M.x&&n.y===M.y?M.count%3+1:1,M={count:M.count,time:l,x:n.x,y:n.y};const h=g.begin({button:t.button,shiftKey:t.shiftKey,altKey:t.altKey,detail:t.detail||M.count},n,R());h&&(c=t.pointerId,y=t,d=n,k=void 0,e.setPointerCapture(c),"local"===h.owner?(void 0!==f&&cancelAnimationFrame(f),f=void 0,u=void 0,p=void 0,b={point:n,mode:h.mode,extend:h.extend,dragged:!1},r.begin?.(n,h)):D(t,n))},s),e.addEventListener("pointermove",e=>{if("mouse"!==e.pointerType)return;if(null===c&&e.buttons)return;if(null!==c&&c!==e.pointerId)return;C=e,O(e),T&&Math.hypot(e.clientX-T.x,e.clientY-T.y)>=4&&(T.dragged=!0);const t=P(e,null!==c);if(!t)return;if(d=t,x&&x!==Mt.Application)return;if("local"===g.owner){y=e;const i=g.endpoint,n=g.move(t);return n&&i&&(n.x!==i.x||n.y!==i.y)&&(M.count=0,b&&(b.dragged=!0),r.extend?.(n)),void z()}if(!l||!x&&null===g.owner&&(R().historical||R().readOnly||e.shiftKey))return;if(c===e.pointerId&&D(e,t),e.metaKey||1003!==l&&(1002!==l||!h.size))return;const i={type:"mouse",action:"move",button:jt.find(([e])=>h.has(e))?.[0]??"none",...t},n=JSON.stringify(i);n!==p&&(p=n,u=i,void 0===f&&(f=requestAnimationFrame(U)))},s),e.addEventListener("pointerup",e=>{if(c!==e.pointerId)return;if(T){e.preventDefault();const t=T,i=P(e),n=0===e.button&&0===e.buttons&&!t.dragged&&Math.hypot(e.clientX-t.x,e.clientY-t.y)<4&&i&&r.hyperlink?.(i)?.id===t.link.id;return 0===e.buttons?B(!1,!1):t.dragged=!0,void(n&&r.openHyperlink?.(t.link,t.input))}const t=P(e,!0)??d;if(x)return x===Mt.Application&&t&&D(e,t),void(0===e.buttons&&B(x===Mt.Application));if("local"===g.owner){if(t&&g.endpoint&&(t.x!==g.endpoint.x||t.y!==g.endpoint.y)){b&&(b.dragged=!0);const e=g.move(t);e&&r.extend?.(e)}const e=b;return B(!1,!1),void(k=e)}t&&D(e,t),h.size||B()},s),e.addEventListener("click",e=>{const t=k;if(k=void 0,!t||t.dragged||!Number.isInteger(e.detail)||e.detail<1)return;M.count=e.detail;const i=e.detail>=3?"line":2===e.detail?"word":"character";t.extend||"rectangle"===t.mode||i===t.mode||r.begin?.(t.point,{mode:i,extend:!1})},s),e.addEventListener("pointercancel",()=>B(),s),e.addEventListener("pointerleave",()=>{C=void 0,O()},s),e.addEventListener("lostpointercapture",()=>{null!==c&&B()},s),window.addEventListener("blur",()=>{B(),C=void 0,O()},s),window.addEventListener("keydown",e=>O(e),{...s,capture:!0}),window.addEventListener("keyup",e=>O(e),{...s,capture:!0}),e.addEventListener("contextmenu",e=>{if(void 0!==E&&E!==Mt.Continue){const t=E;return E=void 0,void(t!==Mt.Browser&&e.preventDefault())}(l||"local"===g.owner)&&e.preventDefault()},s),e.addEventListener("wheel",i=>{T&&(T.dragged=!0);const n={type:"wheel",deltaX:i.deltaX,deltaY:i.deltaY,deltaMode:i.deltaMode,point:P(i),...qt(i)},s=null===c?r.resolve?.(n)??{route:Mt.Continue}:{route:x??Mt.Continue};if(s.route===Mt.Browser)return;if(void 0!==s.action||s.route===Mt.Consume)return i.preventDefault(),void(void 0!==s.action&&r.execute?.(s,n));if(s.route!==Mt.Application&&(i.ctrlKey||i.metaKey))return;const o=s.route===Mt.Application?"app":g.wheelOwner(i,R());if("app"===o&&9===l)return;const h=P("local"===g.owner&&y?y:i,"local"===g.owner);if(!h)return;i.preventDefault(),U();const d=e.getBoundingClientRect();w!==o&&m.reset(),w=o;const{x:u,y:p}=m.take(i,d,a);if("local"===o)return void(p&&(b&&(b.dragged=!0),M.count=0,r.scroll?.(p,g.scrollPoint(h))));const f=[[p,"wheelUp","wheelDown"],[u,"wheelLeft","wheelRight"]];for(const[e,i,r]of f)e&&t({type:"mouse",action:"wheel",button:e<0?i:r,count:Math.min(32,Math.abs(e)),...h})},{...s,passive:!1}),{update(e,t,i){o===e&&a===t&&l===i||(d&&(d={...d,x:Math.min(d.x,e-1),y:Math.min(d.y,t-1)}),o===e&&a===t||B(0!==i),o=e,a=t,l=i),O()},refresh(){O()},cancel(){B()},dispose(){B(!1),n.abort(),e.title=A,e.style.cursor=L}}}function $t(e){const t=new URL(import.meta.url);return t.hash=`hex1b-${e}-worker`,t}h(),W(),p(),K(),Y(),P();var Gt=Object.freeze({track:"#202020",thumb:"#999999",marker:"#aaaaaa",error:"#eeeeee",prompt:"#666666","command-line":"#888888",executing:"#bbbbbb",success:"#999999",custom:"#dddddd"});function Wt(e,t){if("custom"===e.source)return t("custom");if(null!=e.exitCode&&0!==e.exitCode)return t("error");switch(e.phase){case"prompt":return t("prompt");case"commandLine":return t("command-line");case"executing":return t("executing");case"finished":return t(0===e.exitCode?"success":"marker");default:return t("marker")}}var _t=8,Ht=32;function Kt(e,t){if(!Number.isInteger(e)||e<1||e>300||!Number.isInteger(t)||t<1||t>100)throw new RangeError("Requested grid must be 1-300 columns by 1-100 rows");return{columns:e,rows:t}}function Vt(e={mode:"auto"},t=16){if(!e||!["auto","fixed"].includes(e.mode))throw new TypeError("Sizing mode must be 'auto' or 'fixed'");const i=e.fontSize??t;if(!Number.isInteger(i)||i<8||i>32)throw new RangeError("Font size must be an integer from 8 to 32");return"fixed"===e.mode?{mode:"fixed",fontSize:i,...Kt(e.columns,e.rows)}:{mode:"auto",fontSize:i}}var Xt="Text history is not available for this terminal view",Yt="Selection expired: its text was changed, evicted, reset, or resized. Select the text again.",Jt="Selection changed before copy completed. Copy the new selection again.",Zt=class{#e;#t;#R=null;#o=0;#P=0;#O=0;#z=0;#U="character";#D="clear";#B;#q;constructor(e,t=()=>{}){this.#e=e,this.#t=t}get viewport(){if(!this.#R)return{available:!1,following:!0,pending:!1};const{selection:e,copy:t,markers:i,markerResult:r,markerPage:n,viewportError:s,...o}=this.#R;return{...o,rowIds:[...o.rowIds],available:!0,revision:this.#o,...s&&o.requestId>=this.#z?{navigationError:s}:{},pending:o.requestId({...e})),text:null,message:"Resolving selection…"}:{...e,canExtend:"valid"===e.status,ranges:e.ranges.map(e=>({...e})),revision:this.#o,message:"invalidated"===e.status?Yt:""}}accept(e,t){if(t<=this.#o)return!1;const i=this.#R?.generation;this.#R=e,this.#o=t,(!e||i&&i!==e.generation||"invalidated"===e.selection.status)&&(this.endGesture(!0),this.#j(new Error(Yt)));const r=this.#B;if(r&&e?.copy?.requestId===r.requestId&&("valid"!==e.copy.status||"valid"!==e.selection.status||e.selection.requestId!==r.selectionRequestId||e.generation!==r.generation||this.#O>r.selectionRequestId||e.copy.text!==e.selection.text?this.#j(new Error(Yt)):(clearTimeout(r.timer),this.#B=void 0,r.resolve(e.copy.text))),this.#q&&!this.viewport.pending){const e=this.#q;this.#q=void 0,this.extend(e)}return this.#t(),!0}#F(){if(!this.#R)throw new Error(Xt);return this.#R}#N(e){const t=this.#F();if(!e||!Number.isInteger(e.x)||e.x<0||e.x>1023||!Number.isInteger(e.y)||e.y<0||"string"!=typeof t.rowIds[e.y]||!t.rowIds[e.y].length)throw new Error("Terminal viewport is not ready for that selection point. Wait for the next frame and try again.");return t.rowIds[e.y]}#$(e=this.selection.mode,t="extend"){return this.#j(new Error(Jt)),this.#U=e,this.#D=t,this.#O=++this.#P,this.#O}begin(e,{mode:t,extend:i=!1}){const r=this.#N(e);this.#q=void 0;const n=this.#$(t,i?"extend":"start");this.#e({type:"selection",action:i?"extend":"start",mode:t,requestId:n,generation:this.#F().generation,rowId:r,column:e.x}),this.#t()}extend(e){if(this.#N(e),this.viewport.pending)return this.#j(new Error(Jt)),this.#U=this.selection.mode,this.#D="extend",this.#q={...e},void this.#t();this.begin(e,{mode:this.selection.mode,extend:!0})}scroll(e,t){if(this.#F(),!Number.isSafeInteger(e)||e<-2147483648||e>2147483647)throw new RangeError("Scroll delta must be a signed 32-bit integer");if(t&&this.#N(t),!e&&!t)return;t?this.#q=void 0:this.#G();const i=t?this.#$():++this.#P;this.#z=i,this.#e({type:"viewport",requestId:i,delta:e,...t?{extend:{row:t.y,column:t.x}}:{}}),this.#t()}live(){this.#F(),this.#G(),this.#z=++this.#P,this.#e({type:"viewport",live:!0,requestId:this.#z}),this.#t()}scrollTo(e){const t=this.#F();if(!Number.isSafeInteger(e)||e<0||e>2147483647)throw new RangeError("Scroll top must be a nonnegative 32-bit integer");this.#G(),this.#z=++this.#P,this.#e({type:"viewport",requestId:this.#z,top:Math.min(e,t.liveTop),generation:t.generation,originRowId:t.rowIds[0],originTop:t.top}),this.#t()}clear(){this.#F(),this.#q=void 0;const e=this.#$(this.selection.mode,"clear");this.#e({type:"selection",action:"clear",requestId:e}),this.#t()}endGesture(e=!1){e&&this.#q&&(this.#q=void 0,this.#t())}#G(){if(!this.#q)return;const e=this.#q;this.#q=void 0,this.scroll(0,e)}cancelCopy(e){this.#j(e)}copy(){const e=this.#F(),t=this.selection;if("valid"!==t.status)throw new Error(t.message||"Select text before copying.");this.#j(new Error("A newer copy request replaced this request."));const i=++this.#P,{promise:r,resolve:n,reject:s}=Promise.withResolvers(),o=setTimeout(()=>this.#j(new Error("Timed out resolving selection. Copy again.")),1e4);this.#B={requestId:i,selectionRequestId:t.requestId,generation:e.generation,resolve:n,reject:s,timer:o};try{this.#e({type:"copy",requestId:i,selectionRequestId:t.requestId,generation:e.generation})}catch(e){this.#j(e)}return r}#j(e){this.#B&&(clearTimeout(this.#B.timer),this.#B.reject(e),this.#B=void 0)}disconnect(){this.#j(new Error("Terminal view disconnected before copy completed.")),this.#q=void 0}};function Qt(e=0){if("number"!=typeof e&&!r(e))throw new TypeError("Padding must be a number or per-edge object");const t=t=>{const i="number"==typeof e?e:void 0===e[t]?0:e[t];if("number"!=typeof i||!Number.isFinite(i)||i<0)throw new RangeError(`Padding ${t} must be finite nonnegative CSS pixels`);return i};return Object.freeze({top:t("top"),right:t("right"),bottom:t("bottom"),left:t("left")})}function ei(e,t,i){return{width:Math.max(0,e.width-t.left-t.right-(i&&"beside"===i.placement?i.width:0)),height:Math.max(0,e.height-t.top-t.bottom)}}function ti(e,t,i,r,n,s){const o=ei(e,n,s),a=function(e,t,i,r){const n=i&&"auto"===r.mode?r.fontSize/16:1/0;return Math.max(0,Math.min(n,e.width/(t.columns*t.cellWidth),e.height/(t.rows*t.cellHeight)))}(o,t,i,r),l=t.columns*t.cellWidth*a,c=t.rows*t.cellHeight*a,h=Math.min(e.width,n.left)+(o.width-l)/2,d=Math.min(e.height,n.top)+(o.height-c)/2,u=s?Math.min(s.width,"overlay"===s.placement?l:Math.max(0,e.width-n.right-h-l)):0;return Object.freeze({width:e.width,height:e.height,content:Object.freeze({left:h,top:d,width:l,height:c}),scrollbar:s?Object.freeze({left:h+l-("overlay"===s.placement?u:0),top:d,width:u,height:c}):null,padding:n,cellWidth:l/t.columns,cellHeight:c/t.rows})}y(),o();var ii=/\b(?:var|env|attr|light-dark|currentcolor|inherit|initial|unset|revert|revert-layer|default|context-fill|context-stroke|accentcolor|accentcolortext|activetext|buttonborder|buttonface|buttontext|canvas|canvastext|field|fieldtext|graytext|highlight|highlighttext|linktext|mark|marktext|selecteditem|selecteditemtext|visitedtext|activeborder|activecaption|appworkspace|background|buttonhighlight|buttonshadow|captiontext|inactiveborder|inactivecaption|inactivecaptiontext|infobackground|infotext|menu|menutext|scrollbar|threeddarkshadow|threedface|threedhighlight|threedlightshadow|threedshadow|window|windowframe|windowtext)\b/i;function ri(e,t){if(null===e||"object"!=typeof e||"[object Object]"!==Object.prototype.toString.call(e))throw new TypeError(`${t} must be an options object`)}function ni(e){const t=function(e={}){let t;ri(e,"scrollbar.appearance");const i=(e,i)=>{if(void 0===e)return;if("string"!=typeof e||e.length>256||!e.trim()||/\\|\/\*|--/.test(e)||ii.test(e))throw new TypeError(`${i} must be a concrete CSS color of 1-256 characters`);const r=e.trim();if(void 0===t&&(t="undefined"!=typeof OffscreenCanvas?new OffscreenCanvas(1,1).getContext("2d"):"undefined"!=typeof document?document.createElement("canvas").getContext("2d"):null),!t)throw new TypeError(`${i} requires a browser canvas to validate CSS colors`);t.fillStyle="#010203",t.fillStyle=r;const n=t.fillStyle;if(t.fillStyle="#040506",t.fillStyle=r,n!==t.fillStyle)throw new TypeError(`${i} must be a valid concrete CSS color`);return r},r=(e,t,r)=>{void 0!==e&&ri(e,r);const{color:n,opacity:s=t}=e??{};if("number"!=typeof s||!Number.isFinite(s)||s<0||s>1)throw new RangeError(`${r}.opacity must be a finite number between 0 and 1`);return Object.freeze({color:i(n,`${r}.color`),opacity:s})},{track:n,thumb:s,markers:o}=e,a=r(o,1,"scrollbar.appearance.markers");return Object.freeze({track:r(n,.35,"scrollbar.appearance.track"),thumb:r(s,1,"scrollbar.appearance.thumb"),markers:Object.freeze({...a,errorColor:i(o?.errorColor,"scrollbar.appearance.markers.errorColor")})})}(e);return e=>{if(e.opacity<=0)return;const{context:i,track:r,thumb:n,colors:s}=e,o=i.globalAlpha*e.opacity;i.save();try{if(i.globalAlpha=o*t.track.opacity,i.fillStyle=t.track.color??s.track,r.width>0&&r.height>0){const n=2,o=6,a=12,l=Math.min(r.width,r.height)/2,c=r.left+r.width,h=r.top+r.height;let d,u=r.left;for(const{bounds:t}of e.markers){if(t.width<=0||t.height<=0||t.left>=r.left||t.top>=r.top+r.height||t.top+t.height<=r.top)continue;const e=o*Math.min(1,(r.left-t.left)/t.width),i=Math.max(0,t.left-e);if(i>=r.left)continue;u=Math.min(u,i),d??=new Float64Array(Math.ceil(r.height/n)+1);const s=Math.max(0,Math.floor((t.top-o-a-r.top)/n)),l=Math.min(d.length-1,Math.ceil((t.top+t.height+o+a-r.top)/n));for(let e=s;e<=l;e++){const s=r.top+Math.min(e*n,r.height),l=Math.max(t.top-o-s,s-t.top-t.height-o,0),c=Math.max(0,1-l/a),h=(r.left-i)*c*c*(3-2*c);d[e]=Math.max(d[e],h)}}if(d){const e=i.createLinearGradient(u,0,r.left,0);e.addColorStop(0,"transparent"),e.addColorStop(1,t.track.color??s.track),i.fillStyle=e;const a=r.left-d[0],p=r.left-d[d.length-1],f=a=h-m)continue;if(d[e-1]===d[e]&&d[e]===d[e+1]){g=!0;continue}const o=r.left-d[e],a=r.left-d[e+1];g&&(i.lineTo((r.left-d[e-1]+o)/2,t-n/2),g=!1),i.quadraticCurveTo(o,t,(o+a)/2,(t+s)/2)}i.lineTo(p,h-m),i.quadraticCurveTo(p,h,p+m,h),i.lineTo(c-l,h),i.quadraticCurveTo(c,h,c,h-l),i.lineTo(c,r.top+l),i.quadraticCurveTo(c,r.top,c-l,r.top),i.closePath(),i.fill()}else i.beginPath(),i.roundRect(r.left,r.top,r.width,r.height,l),i.fill()}i.globalAlpha=o*t.markers.opacity;for(const{marker:r,bounds:n,color:o}of e.markers)i.fillStyle=null!=r.exitCode&&0!==r.exitCode?t.markers.errorColor??o??s.error:t.markers.color??o??s.marker,r.color&&(i.fillStyle=r.color),n.width>0&&n.height>0&&(i.beginPath(),i.roundRect(n.left,n.top,n.width,n.height,Math.min(n.width,n.height)/2),i.fill());if(n.width>0&&n.height>0){const e=Math.min(2,n.width/4),r=n.width-2*e;i.globalAlpha=o*t.thumb.opacity,i.fillStyle=t.thumb.color??s.thumb,i.beginPath(),i.roundRect(n.left+e,n.top,r,n.height,Math.min(r,n.height)/2),i.fill()}}finally{i.restore()}}}var si=ni();function oi(e){si(e)}function ai(e){if(!1===e)return!1;if(void 0!==e&&(null===e||"object"!=typeof e||Array.isArray(e)))throw new TypeError("scrollbar must be false or an options object");const t=e??{},i=(e,t,i,r=0,n=1/0)=>{const s=void 0===e?t:e;if(!Number.isFinite(s)||sn)throw new RangeError(`scrollbar.${i} must be a finite number between ${r} and ${n}`);return s};if(void 0!==t.placement&&"overlay"!==t.placement&&"beside"!==t.placement)throw new TypeError("scrollbar.placement must be overlay or beside");if(void 0!==t.markers&&"boolean"!=typeof t.markers)throw new TypeError("scrollbar.markers must be boolean");if(void 0!==t.render&&("function"!=typeof t.render||"AsyncFunction"===t.render.constructor.name))throw new TypeError("scrollbar.render must be a synchronous function");if(void 0!==t.tooltip&&!1!==t.tooltip&&("function"!=typeof t.tooltip||"AsyncFunction"===t.tooltip.constructor.name))throw new TypeError("scrollbar.tooltip must be false or a synchronous function");return Object.freeze({placement:t.placement??"overlay",width:i(t.width,12,"width",4,64),proximity:i(t.proximity,24,"proximity"),hideDelay:i(t.hideDelay,900,"hideDelay"),fadeDuration:i(t.fadeDuration,300,"fadeDuration"),markers:t.markers??!0,...t.render?{render:t.render}:{},...void 0!==t.tooltip?{tooltip:t.tooltip}:{}})}var li=(e,t,i)=>Math.max(t,Math.min(i,e)),ci=(e,t,i,r=0)=>t>=e.left-r&&t=e.top-r&&in?i>=e.top-3&&i{if(!ci(e,t,i))return!1;const r=Math.min(e.width,e.height)/2;return(t-(e.left+e.width/2))**2+(i-e.top-e.height/2)**2<=r**2})(e,t,i)).sort((e,t)=>Math.abs(e.bounds.top+e.bounds.height/2-i)-Math.abs(t.bounds.top+t.bounds.height/2-i)||(e.marker.idt.marker.id?1:0))[0]?.marker}var ui=class{constructor(e){this.options=e,this.motion="function"==typeof matchMedia?matchMedia("(prefers-reduced-motion: reduce)"):void 0;const t=(e,t,i,r=!1)=>{e.addEventListener(t,i,{capture:!0,passive:r}),this.listeners.push(()=>e.removeEventListener?.(t,i,{capture:!0}))};t(e.element,"pointerdown",e=>this.pointerDown(e)),t(e.element,"pointermove",e=>this.pointerMove(e)),t(e.element,"pointerup",e=>this.pointerEnd(e,!1)),t(e.element,"pointercancel",e=>this.pointerEnd(e,!0)),t(e.element,"lostpointercapture",e=>this.pointerEnd(e,!0)),t(e.element,"pointerleave",()=>{this.near=this.hovered=!1,this.clearHover(),this.activity()}),t(e.element,"wheel",e=>this.wheel(e));for(const i of["mousedown","mouseup","click","dblclick","contextmenu"])t(e.element,i,e=>{this.contentPointers.size||this.contentClick||(null!==this.pointer||this.hit(e)||this.suppressClick&&("click"===i||"mouseup"===i))&&(this.consume(e),"click"===i&&(this.suppressClick=!1))});if(t(e.accessibility,"keydown",e=>this.keyDown(e)),t(e.accessibility,"focus",()=>{this.focused=!0,this.activity()}),t(e.accessibility,"blur",()=>{this.focused=!1,this.activity()}),this.motion){const e=()=>this.invalidate();this.motion.addEventListener("change",e),this.listeners.push(()=>this.motion?.removeEventListener("change",e))}e.accessibility.setAttribute("role","scrollbar"),e.accessibility.setAttribute("aria-label","Terminal scrollback"),e.accessibility.setAttribute("aria-orientation","vertical"),e.accessibility.style.position="absolute",e.accessibility.style.background="transparent",this.refresh()}options;context;listeners=[];motion;configuration;frameId;fadeTimer;disposed=!1;failed=!1;dormant=!1;near=!1;hovered=!1;focused=!1;lastActivityAt=-1/0;pointer=null;dragOffset=null;contentPointers=new Set;contentClick=!1;suppressClick=!1;desired=null;dragTop=null;visualTop=null;visualTarget=null;visualFrom=0;visualStartedAt=0;queued=null;requestBaseline=0;generation;wheelRemainder=0;hoverPoint=null;refresh(){if(this.disposed)return;const e=this.options.getState();e.configuration!==this.configuration&&(this.cancel(),this.configuration=e.configuration,this.failed=!1,this.lastActivityAt=this.now()),e.viewport.generation!==this.generation&&(this.endGesture(),this.desired=this.queued=null,this.visualTop=this.visualTarget=null,this.generation=e.viewport.generation),null===this.pointer&&null===this.queued&&e.viewport.available&&!e.viewport.pending&&(e.viewport.top===this.desired||e.viewport.requestId>this.requestBaseline)&&(this.desired=null),this.updateVisualPosition(e);const t=this.geometry(e);if(!t)return this.cancel(),this.options.accessibility.hidden=!0,this.options.accessibility.tabIndex=-1,void this.clear();const{track:i}=t,r=this.options.accessibility;r.hidden=!1,r.tabIndex=0,r.matches?.(":focus")&&(this.focused=!0),Object.assign(r.style,{left:`${i.left}px`,top:`${i.top}px`,width:`${i.width}px`,height:`${i.height}px`}),r.setAttribute("aria-valuemin","0"),r.setAttribute("aria-valuemax",String(e.viewport.liveTop)),r.setAttribute("aria-valuenow",String(this.desired??e.viewport.top)),r.setAttribute("aria-valuetext",e.viewport.following?"Live terminal output":`Scrollback row ${this.desired??e.viewport.top}`),r.setAttribute("aria-busy",String(e.viewport.pending)),this.refreshHover(t),this.schedule()}activity(){this.disposed||(this.dormant=!1,this.lastActivityAt=this.now(),void 0!==this.fadeTimer&&clearTimeout(this.fadeTimer),this.fadeTimer=void 0,this.schedule())}invalidate(){this.dormant=!1,this.refresh()}cancel(){this.clearHover(),this.endGesture(),this.contentPointers.clear(),this.contentClick=!1,this.desired=this.queued=null,this.visualTop=this.visualTarget=null,this.near=this.hovered=this.focused=this.suppressClick=!1,this.dormant=!1,this.wheelRemainder=0,void 0!==this.frameId&&cancelAnimationFrame(this.frameId),void 0!==this.fadeTimer&&clearTimeout(this.fadeTimer),this.frameId=this.fadeTimer=void 0,this.options.accessibility.hidden=!0,this.options.accessibility.tabIndex=-1,this.options.accessibility.title="",this.clear()}dispose(){if(!this.disposed){this.disposed=!0,this.cancel();for(const e of this.listeners)e();this.options.accessibility.hidden=!0,this.options.accessibility.tabIndex=-1}}now(){return performance.now()}updateVisualPosition(e,t=!1){if(!e.viewport.available)return;const i=li(this.dragTop??this.desired??e.viewport.top,0,e.viewport.liveTop),r=this.now();if(null===this.visualTop||null!==this.dragTop||this.motion?.matches||this.dormant)return this.visualFrom=this.visualTop=this.visualTarget=i,void(this.visualStartedAt=r);if(i!==this.visualTarget&&(this.visualFrom=this.visualTop,this.visualTarget=i,this.visualStartedAt=r),t){const e=li((r-this.visualStartedAt)/120,0,1);this.visualTop=1===e?i:this.visualFrom+(i-this.visualFrom)*(1-(1-e)**3)}}geometry(e=this.options.getState()){return e.connected&&e.configuration?function(e,t,i,r=null){const n=e.scrollbar;if(!n||n.width<=0||n.height<=0||!t.available||"main"!==t.buffer||t.liveTop<=0)return null;const s=Math.max(1,t.totalRows-t.liveTop),o=Math.min(n.height,Math.max(24,n.height*s/t.totalRows)),a=Object.freeze({left:n.left,top:n.top+(n.height-o)*li(r??t.top,0,t.liveTop)/t.liveTop,width:n.width,height:o}),l=Math.min(5,n.height,.75*(a.width-2*Math.min(2,a.width/4))),c=n.left+(n.width-l)/2,h=Math.max(0,n.left-l-2),d=Math.min(8,n.height),u=i.filter(e=>e.buffer===t.buffer&&null!==e.row&&Number.isFinite(e.row)&&e.row>=0&&e.row{const i=n.top+(n.height-l)*e.row/Math.max(1,t.totalRows-1),r=Math.max(a.top-i-l,i-a.top-a.height,0),s=1-li(r/d,0,1),o=c+s*s*(3-2*s)*(h-c);return Object.freeze({marker:Object.freeze({...e}),bounds:Object.freeze({left:o,top:i,width:l,height:l})})});return Object.freeze({track:Object.freeze({...n}),thumb:a,markers:Object.freeze(u)})}(e.layout,e.viewport,e.configuration.markers?e.markers:[],this.visualTop??this.desired):null}point(e){const t=this.options.element.getBoundingClientRect(),i=this.options.getState().layout;return{x:t.width>0?(e.clientX-t.left)*i.width/t.width:-1/0,y:t.height>0?(e.clientY-t.top)*i.height/t.height:-1/0}}hit(e){const t=this.geometry(),i=this.point(e);return!!t&&(ci(t.track,i.x,i.y)||!!this.markerAt(t,i))}markerAt(e,t){if(ci(e.thumb,t.x,t.y))return;const i=this.options.getState().configuration;return ci(e.track,t.x,t.y)||!(this.failed||!i||hi(this.now(),this.lastActivityAt,this.near||this.hovered||this.focused||null!==this.pointer,i,this.motion?.matches??!1)<=0)?di(e.markers,t.x,t.y,e.track):void 0}consume(e){e.preventDefault(),e.stopImmediatePropagation()}pointerDown(e){if(this.clearHover(),this.suppressClick=!1,this.contentClick=!1,!this.hit(e))return this.contentPointers.add(e.pointerId),void(this.contentClick=!0);if(this.contentPointers.size)return void(this.contentClick=!0);if(this.consume(e),0!==e.button||null!==this.pointer)return;const t=this.geometry(),i=this.point(e);this.pointer=e.pointerId;try{this.options.element.setPointerCapture(e.pointerId)}catch(e){return this.endGesture(),void this.report(e)}if(this.options.accessibility.setAttribute("data-pointer-active","true"),this.options.accessibility.focus({preventScroll:!0}),this.focused=!0,this.activity(),ci(t.thumb,i.x,i.y)){this.dragOffset=i.y-t.thumb.top;const e=this.options.getState().viewport;return void(e.available&&(this.dragTop=this.visualTop??this.desired??e.top))}const r=this.markerAt(t,i);if(r)return void this.jump(r.id);const n=this.options.getState().viewport;n.available&&this.navigate((this.desired??n.top)+(i.y0&&(this.dragTop=li((i.y-this.dragOffset-t.track.top)/r*e.liveTop,0,e.liveTop),this.navigate(this.dragTop))}return void this.activity()}if(this.contentPointers.size||0!==e.buttons)return void this.clearHover();const r=this.hoveredMarker(t);this.hoverPoint={clientX:e.clientX,clientY:e.clientY};const n=this.refreshHover(t);r?.marker.id!==n?.marker.id&&(this.dormant=!1,this.schedule()),this.hovered&&this.consume(e)}refreshHover(e){if(e&&this.hoverPoint&&null===this.pointer&&!this.contentPointers.size){const t=this.point(this.hoverPoint),i=this.options.getState().configuration,r=ci(e.track,t.x,t.y,i?i.proximity:0),n=ci(e.track,t.x,t.y)||!!this.markerAt(e,t);r===this.near&&n===this.hovered||(this.near=r,this.hovered=n,this.activity()),this.hovered=ci(e.track,t.x,t.y)||!!this.markerAt(e,t)}const t=this.hoveredMarker(e);return this.options.onMarkerHover?.(t),t}clearHover(){this.hoverPoint=null,this.options.onMarkerHover?.(null)}hoveredMarker(e=this.geometry()){if(!this.hoverPoint||null!==this.pointer||this.contentPointers.size)return null;if(!e)return null;const t=this.point(this.hoverPoint),i=this.markerAt(e,t);return e.markers.find(e=>e.marker.id===i?.id)??null}pointerEnd(e,t){this.contentPointers.delete(e.pointerId),this.pointer===e.pointerId&&(this.consume(e),this.suppressClick=!t,this.endGesture(),t&&(this.desired=this.queued=null,this.visualTop=this.visualTarget=null),this.updateVisualPosition(this.options.getState()),this.activity())}endGesture(){const e=this.pointer;if(this.pointer=this.dragOffset=null,this.dragTop=null,this.options.accessibility.setAttribute("data-pointer-active","false"),null!==e)try{this.options.element.hasPointerCapture(e)&&this.options.element.releasePointerCapture(e)}catch(e){this.report(e)}}wheel(e){if(this.contentPointers.size||!this.hit(e))return;this.consume(e);const{viewport:t,layout:i}=this.options.getState();if(!t.available||!Number.isFinite(e.deltaY))return;const r=Math.max(1,t.totalRows-t.liveTop),n=this.options.element.getBoundingClientRect(),s=i.height>0?n.height/i.height:1;this.wheelRemainder+=e.deltaY*(2===e.deltaMode?r:1===e.deltaMode?1:1/Math.max(1,i.cellHeight*s));const o=Math.trunc(this.wheelRemainder);this.wheelRemainder-=o,0!==o&&this.navigate((this.desired??t.top)+o),this.activity()}keyDown(e){const{viewport:t}=this.options.getState();if(!this.geometry()||!t.available||e.ctrlKey||e.metaKey)return;const i=this.desired??t.top,r=Math.max(1,t.totalRows-t.liveTop);if(e.altKey&&("ArrowDown"===e.key||"ArrowUp"===e.key)){this.consume(e);const t="ArrowDown"===e.key,r=this.geometry().markers.map(e=>e.marker).filter(e=>t?e.row>i:e.row(t?1:-1)*(e.row-i.row)||(e.idi.id?1:0));return r[0]&&this.jump(r[0].id),void this.activity()}if(e.altKey)return;const n={ArrowUp:i-1,ArrowDown:i+1,PageUp:i-r,PageDown:i+r,Home:0,End:t.liveTop};Object.hasOwn(n,e.key)&&(this.consume(e),this.navigate(n[e.key]),this.activity())}jump(e){this.desired=this.queued=null;try{this.options.scrollToMarker(e).catch(e=>this.report(e))}catch(e){this.report(e)}}navigate(e){const t=this.options.getState(),i=t.viewport;if(!i.available||!Number.isFinite(e))return;const r=Math.round(li(e,0,i.liveTop));null!==this.dragTop&&r===(this.desired??i.top)||(this.requestBaseline=i.requestId,this.queued=r),this.desired=r,this.updateVisualPosition(t),this.schedule()}schedule(){this.disposed||void 0!==this.frameId||!this.geometry()||this.dormant&&null===this.queued||"function"!=typeof requestAnimationFrame||(this.frameId=requestAnimationFrame(()=>{this.frameId=void 0,this.paint()}))}clear(){this.context&&(this.context.save(),this.context.setTransform(1,0,0,1,0,0),this.context.clearRect(0,0,this.options.canvas.width,this.options.canvas.height),this.context.restore())}paint(){if(this.disposed)return;let e=this.options.getState();if(!this.geometry(e)||!e.configuration)return;if(null!==this.queued){const t=this.queued;this.queued=null;try{e.viewport.available&&t>=e.viewport.liveTop?this.options.scrollToLive():this.options.scrollToRow(t)}catch(e){this.desired=null,this.report(e)}}e=this.options.getState(),this.updateVisualPosition(e,!0);const t=this.geometry(e);if(!t||!e.configuration)return;if(this.refreshHover(t),this.disposed||e.configuration!==this.options.getState().configuration)return;const{canvas:i,accessibility:r}=this.options;if(r.setAttribute("aria-valuenow",String(this.desired??e.viewport.top)),!this.failed&&void 0===this.context)try{if(this.context=i.getContext("2d"),!this.context)throw new Error("The scrollbar Canvas2D context is unavailable")}catch(e){this.failed=!0,this.endGesture(),this.report(e)}if(!this.context||this.failed)return;const n=this.context,s=li("number"==typeof devicePixelRatio?devicePixelRatio:1,1,3),o=Math.max(1,Math.round(e.layout.width*s)),a=Math.max(1,Math.round(e.layout.height*s));i.width!==o&&(i.width=o),i.height!==a&&(i.height=a),this.clear();const l=this.now(),c=this.near||this.hovered||this.focused||null!==this.pointer,h="overlay"===e.configuration.placement,d=this.motion?.matches??!1,u=hi(l,this.lastActivityAt,c,e.configuration,d),p="function"==typeof getComputedStyle?getComputedStyle(this.options.element):null,f=e=>((e,t)=>p?.getPropertyValue?.(e).trim()||t)(`--cp-view-scrollbar-${e}`,Gt[e]),m=Object.freeze({...e.layout,content:Object.freeze({...e.layout.content}),padding:Object.freeze({...e.layout.padding}),scrollbar:t.track}),g=Object.freeze({...e.viewport,...e.viewport.rowIds?{rowIds:Object.freeze([...e.viewport.rowIds])}:{}}),w=Object.freeze({canvas:i,context:n,layout:m,viewport:g,...t,now:l,opacity:u,pendingTarget:this.desired,markers:Object.freeze(t.markers.map(e=>Object.freeze({...e,color:Wt(e.marker,f)}))),hoveredMarker:this.hoveredMarker(t),interaction:Object.freeze({near:this.near,hovered:this.hovered,dragging:null!==this.dragOffset,focused:this.focused,lastActivityAt:this.lastActivityAt,reducedMotion:d}),colors:Object.freeze({track:f("track"),thumb:f("thumb"),marker:f("marker"),error:f("error")})});let y=!1;n.save();try{n.setTransform(s,0,0,s,0,0);const t=(e.configuration.render??oi)(w);if(void 0!==t&&"boolean"!=typeof t)throw t instanceof Promise&&t.catch(e=>{this.disposed||this.report(e)}),new TypeError("scrollbar.render must synchronously return boolean or undefined");y=t}catch(e){this.failed=!0,this.endGesture(),this.desired=this.queued=null,void 0!==this.frameId&&cancelAnimationFrame(this.frameId),void 0!==this.fadeTimer&&clearTimeout(this.fadeTimer),this.frameId=this.fadeTimer=void 0,this.report(e)}finally{n.restore()}this.failed?this.clear():this.disposed||e.configuration!==this.options.getState().configuration||(this.dormant=!c&&0===u&&!0!==y,!0===y||u>0&&this.visualTop!==this.visualTarget||h&&!c&&u>0&&l>=this.lastActivityAt+e.configuration.hideDelay?this.schedule():h&&!c&&u>0&&void 0===this.fadeTimer&&(this.fadeTimer=setTimeout(()=>{this.fadeTimer=void 0,this.schedule()},Math.min(2147483647,Math.max(0,this.lastActivityAt+e.configuration.hideDelay-l)))))}report(e){this.disposed||this.options.reportError(e)}};function pi(e){const t=new Map;if(!e)return t;for(const i of e.split(";")){const e=i.indexOf("=");e<0||t.set(i.slice(0,e),i.slice(e+1))}return t}function fi(e){return e?pi(e.rawParameters).get("cmdline_url")??null:null}function mi(e){const t=document.createElement("div");t.className="hex1b-scrollbar-tooltip",t.setAttribute("role","tooltip"),t.style.cssText="padding:8px 10px;border:1px solid var(--cp-view-text-muted,#888);border-radius:6px;background:var(--cp-view-surface,#202020);color:var(--cp-view-text,#eee);font:12px/1.4 var(--cp-view-font-family,monospace);white-space:pre-wrap;overflow-wrap:anywhere";const{marker:i,details:r}=e,n=[i.label||("custom"===i.source?"Bookmark":"Terminal command")];if("command"===i.source&&(n.push(`Phase: ${i.phase??"unknown"}`),null!=i.exitCode&&n.push(`Exit code: ${i.exitCode}`),e.loading&&n.push("Loading command details..."),e.error&&n.push(`Command details unavailable: ${e.error}`),r)){const e=fi(r);if(e)try{n.push(decodeURIComponent(e))}catch(t){if(!(t instanceof URIError))throw t;n.push(`Command (invalid URL encoding): ${e}`)}else r.rawParameters?n.push(r.rawParameters):n.push("This shell did not provide command text.")}return t.textContent=n.join("\n"),t}o();var gi=class{constructor(e){this.options=e}options;target=null;result=null;fetching=!1;lifetime;observer;element=null;failed=!1;disposed=!1;update(e,t,i=mi){if(this.disposed)return;if(!e||!1===i)return this.target=this.result=null,this.failed=!1,void this.clearView();const r=JSON.stringify({tick:e,layout:t});if(this.target?.signature===r&&this.target.renderer===i)return;const n=JSON.stringify([e.marker.id,e.marker.phase,e.marker.exitCode]);this.target?.key===n&&this.target.renderer===i||(this.result=null,this.failed=!1),this.target={tick:e,layout:t,renderer:i,key:n,signature:r},this.render(),this.loadDetails()}dispose(){this.disposed=!0,this.target=this.result=null,this.clearView()}clearView(){this.observer?.disconnect(),this.observer=void 0;const e=this.lifetime;this.lifetime=void 0,this.element=null,this.options.overlay.replaceChildren(),e?.abort()}render(){const e=this.target;if(this.clearView(),!e||this.target!==e||this.failed||this.disposed)return;const t=this.lifetime=new AbortController,i=this.result?.key===e.key?this.result:null,n=Object.freeze({marker:e.tick.marker,anchor:e.tick.bounds,layout:e.layout,details:i?.details??null,error:i?.error??null,loading:"command"===e.tick.marker.source&&!i,signal:t.signal});try{const i=e.renderer(n);if(r(i)&&"function"==typeof i.then&&Promise.resolve(i).catch(e=>{t.signal.aborted||this.options.reportError(e)}),t.signal.aborted)return;if(null!==i&&!(i instanceof HTMLElement))throw new TypeError("scrollbar.tooltip must synchronously return an HTMLElement or null");if(!i)return;this.element=i,Object.assign(i.style,{position:"absolute",boxSizing:"border-box",margin:"0",pointerEvents:"none",maxWidth:`${Math.max(0,Math.min(360,e.layout.width-16))}px`,maxHeight:`${Math.max(0,e.layout.height-16)}px`,overflow:"hidden"}),this.options.overlay.append(i),this.position(),this.observer=new ResizeObserver(()=>this.position()),this.observer.observe(i)}catch(e){this.failed=!0,this.clearView(),this.options.reportError(e)}}position(){if(!this.target||!this.element)return;const{bounds:e}=this.target.tick,{width:t,height:i}=this.target.layout,r=e.left-this.element.offsetWidth-8,n=e.top+e.height/2-this.element.offsetHeight/2;this.element.style.left=`${Math.max(0,Math.min(Math.max(8,r),t-this.element.offsetWidth-8))}px`,this.element.style.top=`${Math.max(0,Math.min(Math.max(8,n),i-this.element.offsetHeight-8))}px`}loadDetails(){const e=this.target;!e||this.fetching||this.failed||this.disposed||"command"!==e.tick.marker.source||this.result?.key===e.key||(this.fetching=!0,Promise.resolve().then(()=>this.disposed||this.target?.key!==e.key?null:this.options.getDetails(e.tick.marker.id)).then(t=>{t&&this.target?.key===e.key&&!this.disposed&&(this.result={key:e.key,details:Object.freeze({...t}),error:null},this.render())},t=>{this.target?.key!==e.key||this.disposed||(this.result={key:e.key,details:null,error:t instanceof Error?t.message:String(t)},this.render())}).finally(()=>{this.fetching=!1,this.loadDetails()}))}},wi="\n --cp-terminal-surface: #ffffff;\n --cp-terminal-text: #242424;\n --cp-terminal-text-muted: #5c5c5c;\n --cp-terminal-border-strong: #919191;\n --cp-terminal-accent: #b11f4b;\n --cp-terminal-accent-soft: rgba(177, 31, 75, 0.08);\n --cp-terminal-danger: #dc2626;\n",yi="\n --cp-terminal-surface: #292929;\n --cp-terminal-text: #dedede;\n --cp-terminal-text-muted: #919191;\n --cp-terminal-border-strong: #5f5f5f;\n --cp-terminal-accent: #fd8ea1;\n --cp-terminal-accent-soft: rgba(253, 142, 161, 0.14);\n --cp-terminal-danger: #f87171;\n",vi=`\n :host {\n ${wi}\n --cp-terminal-font-family: "Segoe UI", Aptos, Calibri, -apple-system, BlinkMacSystemFont, sans-serif;\n }\n @media (prefers-color-scheme: dark) { :host { ${yi} } }\n :host-context([data-theme="light"]) { ${wi} }\n :host-context([data-theme="dark"]) { ${yi} }\n :host([data-theme="light"]) { ${wi} }\n :host([data-theme="dark"]) { ${yi} }\n .viewport {\n ${Object.entries(Gt).map(([e,t])=>`--cp-view-scrollbar-${e}: var(--cp-scrollbar-${e}, var(--cp-terminal-scrollbar-${e}, ${t}));`).join("\n")}\n --cp-view-surface: var(--cp-surface, var(--cp-terminal-surface));\n --cp-view-text: var(--cp-text, var(--cp-terminal-text));\n --cp-view-text-muted: var(--cp-text-muted, var(--cp-terminal-text-muted));\n --cp-view-border-strong: var(--cp-border-strong, var(--cp-terminal-border-strong));\n --cp-view-accent: var(--cp-accent, var(--cp-terminal-accent));\n --cp-view-accent-soft: var(--cp-accent-soft, var(--cp-terminal-accent-soft));\n --cp-view-danger: var(--cp-danger, var(--cp-terminal-danger));\n --cp-view-font-family: var(--cp-font-family, var(--cp-terminal-font-family));\n }\n`;G(),o();var bi=["status","mode","requestId","active","pending","canExtend","text","message","copying","copyError"],ki=["available","following","pending","generation","buffer","top","liveTop","totalRows","requestId","navigationError"],xi=["columns","rows","cellWidth","cellHeight","mouseTracking"],Ei=["row","startColumn","endColumn"],Ti=(e,t,i)=>i.every(i=>e?.[i]===t?.[i]);function Ci(e,t,i){const r=i.width/t.columns,n=i.height/t.rows;return e.map(e=>Object.freeze({left:e.startColumn*r,top:e.row*n,width:(e.endColumn-e.startColumn)*r,height:n}))}var Si=class{#W;#_;#H;#K;#V;#X;#Y;#J;#Z;#Q=!1;#ee=!1;constructor({element:e,overlay:t,button:i,signal:n,getState:s,runAction:o,onSelectionUI:a,reportError:l}){this.#W=e,this.#_=t,this.#H=i,this.#K=n,this.#V=s,this.#X=o,this.#Y=l,a&&e.addEventListener("selectionui",e=>{if(!this.#Z||e!==this.#Z)return;const t=this.#J;try{const e=a(this.#Z);if(void 0!==e)throw r(e)&&"function"==typeof e.then&&Promise.resolve(e).catch(e=>{n.aborted||this.#J!==t||l(e)}),new TypeError("onSelectionUI must finish synchronously; use preventDefault() and signal for UI ownership")}catch(t){e.preventDefault(),l(t)}},{signal:n})}refresh(e=!1){this.#ee||=e,this.#Q||this.#K.aborted||(this.#Q=!0,queueMicrotask(()=>{if(this.#Q=!1,this.#K.aborted)return;const e=this.#V(),t=this.#ee;if(this.#ee=!1,!t&&(i=this.#J,r=e,i&&i.connected===r.connected&&i.readOnly===r.readOnly&&Ti(i.selection,r.selection,bi)&&Ti(i.viewport,r.viewport,ki)&&Ti(i.geometry,r.geometry,xi)&&Ti(i.canvasSize,r.canvasSize,["width","height"])&&i.selection.ranges.length===r.selection.ranges.length&&i.selection.ranges.every((e,t)=>Ti(e,r.selection.ranges[t],Ei))))return;var i,r;const n=Object.freeze({...e,selection:Object.freeze({...e.selection,ranges:Object.freeze(e.selection.ranges.map(e=>Object.freeze({...e})))}),viewport:Object.freeze({...e.viewport,rowIds:Object.freeze([...e.viewport.rowIds??[]])}),geometry:Object.freeze({...e.geometry}),canvasSize:Object.freeze({...e.canvasSize})});this.#J=n;const s=Object.freeze({...n,overlay:this.#_,signal:this.#K,runAction:this.#X,rects:Object.freeze(Ci(n.selection.ranges,n.geometry,n.canvasSize))}),o=new CustomEvent("selectionui",{cancelable:!0,detail:s});if(this.#Z=o,this.#Y(null),this.#W.dispatchEvent(o),this.#K.aborted)return;const a=n.selection;this.#H.hidden=o.defaultPrevented||["none","unavailable"].includes(a.status),this.#H.disabled=!n.connected||"valid"!==a.status||a.copying;const l=a.copying?"Copying…":"Copy";this.#H.textContent!==l&&(this.#H.textContent=l)}))}};function Ii(e){if(/[\u0000-\u0020\u007f]/u.test(e)||!URL.canParse(e))return null;const t=new URL(e);return["https:","http:","mailto:"].includes(t.protocol)?t.href:null}var Mi=class{#te=new Map;update(e){this.#te.clear();const t=new Map;for(const i of e){t.has(i.uri)||t.set(i.uri,Ii(i.uri));const e=t.get(i.uri);if(!e)continue;const r=this.#te.get(i.row)??[];r.push({...i,uri:e}),this.#te.set(i.row,r)}}at(e){return this.#te.get(e.y)?.find(t=>e.x>=t.startColumn&&e.xX.cells)throw new RangeError("Link grid exceeds cell limit");const s=[];let o="",a=[],l=new Set,c=0,h=0,d="unknown";const u=e=>e>=0&&!!(1024&(n[(e+1)*i-1]?.attributes??0)),p=e=>{const i=Object.freeze({text:o,mode:t,start:d,end:e}),r=new Set([o.length]);for(const e of Ai.segment(o))r.add(e.index);s.push({chunk:i,spans:a,unsafe:l,boundaries:r,key:JSON.stringify([t,c,d,e,o])}),o="",a=[],l=new Set};for(let e=0;e=1&&s<=2&&t+s<=i&&!(r&&(64&r.attributes||1109742===r.text.codePointAt(0)))&&(2!==s||0===n[e*i+t+1]?.width),d=c?r?.text||" ":"\0";if(h+d.length>X.totalText||o.length+d.length>X.chunk)throw new RangeError("Link text exceeds chunk or viewport limit");const u=o.length;o+=d,h+=d.length,c?a.push({start:u,end:o.length,row:e,startColumn:t,endColumn:t+s}):(l.add(u),l.add(o.length)),c&&2===s&&t++}const s=u(e);if(s||l.add(o.length),"physicalRow"===t||"logicalLine"===t&&!s||e===r-1)p(s?"clipped":"unknown");else if(!s){if(++h>X.totalText||o.length+1>X.chunk)throw new RangeError("Link text exceeds limit");o+="\n"}}return s}function Ri(e,t,i){const r=t+i,{chunk:n}=e;if(i<=0||t<0||r>n.text.length||!e.boundaries.has(t)||!e.boundaries.has(r)||0===t&&"complete"!==n.start||r===n.text.length&&"complete"!==n.end||n.text.slice(t,r).includes("\0"))return null;for(const i of e.unsafe)if(i===t||i===r||i>t&&i=r)continue;if(i.startr)return null;const e=s.at(-1);e&&e.row===i.row&&e.endColumn===i.startColumn?e.endColumn=i.endColumn:s.push({row:i.row,startColumn:i.startColumn,endColumn:i.endColumn})}return s.length?s:null}ce();var Pi=class{#ie;#re;#ne=null;#se;#oe=null;#ae=null;#le=[];#ce=new Set;#he=new Map;#de=new Map;#ue=0;#pe=0;#fe=0;#o=0;#me=!1;#ge=!1;#we=!1;constructor(e){this.#ie={...e,actions:new Set(e.actions)}}configure(e){const t=Q({detection:e},this.#ie.actions);this.#me||(this.#re=!1!==t&&t.detection,this.#ce.clear(),this.#ge=!1,this.clear())}update(e){if(this.#me||e.revisionObject.freeze({...t,activation:Object.freeze({...t.activation,revision:e})}))))}clear(){this.#ke(),this.#pe++,this.#ae=null,this.#he.clear(),this.#de.clear(),this.#ue=0,this.#me||this.#ye([])}dispose(){this.#me||(this.clear(),this.#me=!0)}#ye(e){this.#le=Object.freeze([...e]),this.#ie.onChange(this.#o,this.#le)}#ve(e,t,i){this.#ie.onError(Object.freeze({code:e,ruleId:t,revision:this.#o,message:i instanceof Error?i.message:String(i)}))}#ke(){clearTimeout(this.#se),this.#se=void 0,this.#ne&&(this.#ne.terminate(),this.#ne=null),this.#oe=null}#xe(e){const t=this.#oe?.rule.id??null;this.#ke(),this.#ge=!0,this.#ye([]),this.#ve("worker",t,e)}#be(){if(!(this.#we||this.#me||this.#oe)&&this.#ae&&this.#re&&!this.#ge){this.#we=!0;try{const e=this.#ae;for(const t of this.#re.rules){if(!t.enabled||this.#ce.has(t.id))continue;const i=(e.chunks.get(t.text??"logicalLine")??[]).filter(e=>!this.#he.has(this.#Ee(t,e)));if(i.length){try{if(!this.#ne){const e=new Worker(this.#ie.workerUrl??$t("link-detection"),{type:"module",name:"hex1b-link-detection"});this.#ne=e,e.addEventListener("message",t=>{this.#ne===e&&this.#k(t.data)}),e.addEventListener("error",t=>{this.#ne===e&&(t.preventDefault(),this.#xe(t.message))}),e.addEventListener("messageerror",()=>{this.#ne===e&&this.#xe("Invalid detection worker message")})}const r=++this.#fe;this.#oe={id:r,work:e,rule:t,keys:new Map(i.map(e=>[e.key,this.#Ee(t,e)]))},this.#se=setTimeout(()=>{this.#oe?.id===r&&(this.#ke(),this.#ce.add(t.id),this.#ve("timeout",t.id,`Link rule exceeded ${X.timeoutMs} ms`),this.#be())},X.timeoutMs);const n={id:r,rule:t.builtin?{builtin:t.builtin}:{source:t.pattern.source,flags:t.pattern.flags},chunks:i.map(({chunk:e,key:t})=>({key:t,text:e.text}))};this.#ne.postMessage(n)}catch(e){this.#xe(e)}return}}this.#Te(e)}finally{this.#we=!1}}}#Ee(e,t){return JSON.stringify([e.id,t.key])}#k(e){const t=this.#oe;if(t&&e?.id===t.id&&!this.#me){if(clearTimeout(this.#se),this.#se=void 0,this.#oe=null,e.error)this.#ce.add(t.rule.id),this.#ve(e.error,t.rule.id,e.message??"Link scan failed");else{if(!function(e,t){if(!Array.isArray(e.results)||e.results.length!==t.keys.size)return!1;const i=new Set;let r=0,n=0;for(const s of e.results){if(!s||!t.keys.has(s.key)||i.has(s.key)||!Array.isArray(s.matches))return!1;if(i.add(s.key),r+=s.matches.length,r>X.matches)return!1;for(const e of s.matches){if(!e||!Number.isSafeInteger(e.index)||e.index<0||"string"!=typeof e.text||!e.text.length||!Array.isArray(e.captures)||!e.captures.every(e=>void 0===e||"string"==typeof e)||!e.groups||"object"!=typeof e.groups||Array.isArray(e.groups)||!Object.values(e.groups).every(e=>void 0===e||"string"==typeof e))return!1;if(n+=ie(e),n>X.resultText)return!1}}return!0}(e,t))return void this.#xe("Malformed detection response");for(const i of e.results){const e=t.keys.get(i.key),r=e.length+i.matches.reduce((e,t)=>e+ie(t),0);this.#ue+=r-(this.#de.get(e)??0),this.#de.set(e,r),this.#he.set(e,i.matches)}}this.#ae!==t.work&&this.#Ce(),this.#be()}}#Te(e){if(!this.#re||this.#ae!==e)return;const t=[...e.snapshot.hyperlinks],i=[],r=e=>e.some(e=>t.some(t=>e.row===t.row&&e.startColumnt.startColumn));for(const n of this.#re.rules){if(!n.enabled||this.#ce.has(n.id))continue;const s=[],o=[];try{for(const t of e.chunks.get(n.text??"logicalLine")??[])for(const a of this.#he.get(this.#Ee(n,t))??[]){const l=Ri(t,a.index,a.text.length);if(!l||r(l))continue;const c=Object.freeze({...a,captures:Object.freeze([...a.captures]),groups:Object.freeze({...a.groups}),chunk:t.chunk}),h=n.resolve?n.resolve(c):{target:a.text};if(this.#ae!==e||this.#me)return;if(null===h)continue;if("object"!=typeof h||void 0===h||"then"in h||"string"!=typeof h.target)throw new TypeError("Resolver must return a synchronous link resolution or null");const d=void 0===h.action?n.action:h.action;if(J(d,this.#ie.actions),i.length+s.length>=X.matches)throw new RangeError("Resolved link limit exceeded");const u=Object.freeze({source:"detected",ruleId:n.id,kind:n.builtin?"url"===n.builtin||"uri"===n.builtin?"uri":"path":n.kind,text:a.text,target:h.target,ranges:Object.freeze(l.map(e=>Object.freeze(e))),revision:e.revision,data:h.data});s.push(Object.freeze({id:JSON.stringify([e.identity,n.id,l[0].row,l[0].startColumn,a.index]),action:d,activation:u})),o.push(...l)}i.push(...s),t.push(...o)}catch(t){if(this.#ce.add(n.id),this.#ve(t instanceof RangeError?"limit":"resolver",n.id,t),this.#ae!==e||this.#me)return}}this.#Ce(),this.#ye(i)}#Ce(){for(;this.#he.size>X.cacheEntries||this.#ue>X.cacheText;){const e=this.#he.keys().next().value;this.#he.delete(e),this.#ue-=this.#de.get(e),this.#de.delete(e)}}};function Oi(e,t,i){const r=e.querySelector(t);if(!(r instanceof i))throw new Error(`Missing terminal element: ${t}`);return r}te(),o();var zi=class e{element;#ie;#Se;#Ie;#Me;#Ae;#Le;#Re;#Pe;#Oe=()=>{this.#me||"system"!==this.#Le||this.#ze()};#ne;#Ue;#De;#Be;#qe;#je;#Fe=new AbortController;#Ne={width:0,height:0};#$e=Vt();#Ge={columns:80,rows:24,cellWidth:10,cellHeight:20,mouseTracking:0};#We={id:null,primaryId:null,isPrimary:!1};#_e=!1;#He=!1;#Ke;#Ve;#me=!1;#Xe=!1;#Ye;#Je;#Ze;#Qe;#d=Promise.withResolvers();#et;#tt={};#it="";#rt="";#nt=!1;#st={state:"none",percentage:null};#ot={phase:"unknown",lastExitCode:null};#at={uri:null,host:null,path:null};#lt=null;#ct=!1;#R;#ht;#dt;#ut;#pt;#ft;#mt;#gt=!1;#wt=!1;#yt;#vt;#bt="";#kt=0;#xt=!1;#Et;#A;#Tt=!1;#Ct=0;#St;#It;#Mt="";#At={width:0,height:0};#Lt=new Mi;#Rt=[];#le;#Pt;#Ot=1;#zt=0;#Ut=0;#Dt;#Bt=new Map;#qt=[];#jt=[];#Ft=new Map;#Nt;#$t;static async mount(t,i){if(!(t instanceof HTMLElement))throw new TypeError("A terminal container HTMLElement is required");const r=c(i);if(i.signal?.aborted)throw i.signal.reason;if(!("webgpu"!==V(i.renderer)||window.isSecureContext&&navigator.gpu))throw new Error("The requested WebGPU renderer requires WebGPU over HTTPS or localhost");if(!(window.Worker&&window.ResizeObserver&&window.OffscreenCanvas&&HTMLCanvasElement.prototype.transferControlToOffscreen))throw new Error("WebTerminal requires module workers, ResizeObserver, and a transferable OffscreenCanvas");const n=new e(i,r);try{return await Promise.all([n.#d.promise,Promise.resolve().then(()=>n.#l(t))]),n}catch(e){throw n.dispose(),e}}constructor(e,t=c(e)){if(this.#ie=e,this.#Se=t,void 0!==e.readOnly&&"boolean"!=typeof e.readOnly)throw new TypeError("readOnly must be a boolean");if(this.#Ke=e.readOnly??!1,void 0!==e.preserveOnDisconnect&&"boolean"!=typeof e.preserveOnDisconnect)throw new TypeError("preserveOnDisconnect must be a boolean");if(this.#Ve=e.preserveOnDisconnect??!1,this.#Ae=V(e.renderer),this.#Le=v(e.colorMode),this.#Re={light:k(void 0===e.lightModePalette?w:e.lightModePalette),dark:k(void 0===e.darkModePalette?g:e.darkModePalette)},this.#dt=Qt(e.padding),this.#ut=ai(e.scrollbar),this.#pt=ti(this.#Ne,this.#Ge,!1,this.#$e,this.#dt,this.#ut),!(void 0===e.workerUrl||e.workerUrl instanceof URL||"string"==typeof e.workerUrl&&e.workerUrl.trim()))throw new TypeError("workerUrl must be a nonempty URL string or URL");if(!(void 0===e.linkDetectionWorkerUrl||e.linkDetectionWorkerUrl instanceof URL||"string"==typeof e.linkDetectionWorkerUrl&&e.linkDetectionWorkerUrl.trim()))throw new TypeError("linkDetectionWorkerUrl must be a nonempty URL string or URL");if(void 0!==e.onLinkDetectionError&&"function"!=typeof e.onLinkDetectionError)throw new TypeError("onLinkDetectionError must be a function");if(void 0!==e.onSelectionUI&&("function"!=typeof e.onSelectionUI||"AsyncFunction"===e.onSelectionUI.constructor.name))throw new TypeError("onSelectionUI must be a synchronous event handler");this.#Et=new Dt(e),this.#A=new Map(Object.entries(e.actions??{})),this.#le=Q(e.links,new Set(this.#A.keys())),this.#Pt=new Pi({workerUrl:void 0===e.linkDetectionWorkerUrl?void 0:new URL(e.linkDetectionWorkerUrl,location.href),actions:new Set(this.#A.keys()),onChange:(e,t)=>{!this.#me&&this.#_e&&e===this.#zt&&(this.#qt=t,this.#Gt([]),this.#Wt())},onError:e=>{if(!this.#me)try{this.#ie.onStatus?.(`Link detection: ${e.message}`,"error")}finally{this.#ie.onLinkDetectionError?.(e)}}}),this.#Pt.configure(!!this.#le&&this.#le.detection),this.#R=new Zt(e=>this.#e(e),()=>this.#_t()),this.#ht=new u(e=>this.#e(e),()=>this.#Ht()),this.element=document.createElement("div"),this.element.className="hex1b-terminal",this.element.tabIndex=-1,this.element.style.cssText="width:100%;height:100%;min-width:0;min-height:0;contain:strict"}get geometry(){return{...this.#Ge}}get peer(){return{...this.#We}}get connected(){return this.#_e}get readOnly(){return this.#Ke}get title(){return this.#rt}get progress(){return{...this.#st}}get shellIntegration(){return{...this.#ot}}get workingDirectory(){return{...this.#at}}get commandMark(){return this.#lt?{...this.#lt}:null}get stats(){return{...this.#tt}}get screenText(){return this.#it}get sizing(){return{...this.#$e}}get layout(){return this.#pt}get padding(){return this.#dt}get scrollbar(){return this.#ut}get markers(){return this.#ht.markers}get inputBindings(){return this.#Et.bindings}get viewport(){const e=this.#R.viewport;return{...e,followTail:e.following,offset:e.available?e.liveTop-e.top:0}}get selection(){const e=this.#R.selection;return{...e,active:"valid"===e.status,pending:"pending"===e.status,copying:this.#xt,copyError:this.#bt}}#l(e){if(this.#ie.signal?.aborted)throw this.#ie.signal.reason;const t=function(e){const t=s.get(e);return void 0===t?void 0:a(t)}(this.#Se),i=void 0===this.#ie.scale||"auto"===this.#ie.scale?Math.min(3,Math.max(.5,window.devicePixelRatio||1)):this.#ie.scale;if(!Number.isFinite(i)||i<.5||i>3)throw new RangeError("Backing scale must be 0.5-3 or 'auto'");const r=_(this.#ie.font,location.href);this.#$e=Vt(this.#ie.sizing);const o=this.element.attachShadow({mode:"open"});o.innerHTML=`\n \n
\n \n \n \n \n
\n \n
\n \n
\n \n \n \n
`,this.#Ue=Oi(o,".surface",HTMLDivElement),this.#De=Oi(o,"canvas",HTMLCanvasElement),this.#Be=Oi(o,"textarea",HTMLTextAreaElement),this.#yt=Oi(o,".highlights",HTMLDivElement),this.#vt=Oi(o,".inspection",HTMLDivElement),this.#De.id=`hex1b-content-${d()}`;const l=Oi(o,".scrollbar-accessibility",HTMLDivElement);l.setAttribute("aria-controls",this.#De.id);const c=document.createElement("div");c.slot="scrollbar-tooltip",c.className="hex1b-scrollbar-tooltip-overlay",c.style.cssText="position:relative;width:100%;height:100%;pointer-events:none",this.element.append(c),this.#mt=new gi({overlay:c,getDetails:e=>this.getCommandMarkDetails(e),reportError:e=>{this.#bt=`Scrollbar tooltip: ${n(e)}`,this.#Kt(),this.#ie.onStatus?.(this.#bt,"error")}}),this.#ft=new ui({element:Oi(o,".viewport",HTMLDivElement),canvas:Oi(o,".scrollbar-canvas",HTMLCanvasElement),accessibility:l,getState:()=>({layout:this.layout,viewport:this.viewport,markers:this.markers,connected:this.#_e,configuration:this.#ut}),scrollToRow:e=>this.scrollToRow(e),scrollToLive:()=>this.scrollToLive(),scrollToMarker:e=>this.scrollToMarker(e),onMarkerHover:e=>this.#mt?.update(e,this.layout,!!this.#ut&&this.#ut.tooltip),reportError:e=>{this.#bt=`Scrollbar: ${n(e)}`,this.#Kt(),this.#ie.onStatus?.(this.#bt,"error")}}),this.#It=document.createElement("div"),this.#It.slot="selection-ui",this.#It.className="hex1b-selection-overlay",this.#It.style.cssText="position:relative;width:100%;height:100%;pointer-events:none",this.element.append(this.#It),this.#St=new Si({element:this.element,overlay:this.#It,button:Oi(this.#vt,".copy-selection",HTMLButtonElement),signal:this.#Fe.signal,getState:()=>({selection:this.selection,viewport:this.viewport,geometry:this.geometry,canvasSize:this.#At,connected:this.#_e,readOnly:this.#Ke}),runAction:this.runAction.bind(this),onSelectionUI:this.#ie.onSelectionUI,reportError:e=>{this.#Mt=e?`Selection UI failed: ${n(e)}`:"",this.#It.hidden=!!e,this.#Kt(),e&&this.#ie.onStatus?.(this.#Mt,"error")}}),this.#St.refresh(),this.#Be.setAttribute("aria-label",this.#ie.label||"Terminal input. Click outside to use page controls."),this.#Be.disabled=!0,e.append(this.element);const h=e=>{try{this.#bt="",e()}catch(e){this.#bt=n(e),this.#_t()}};this.#qe=Nt(this.#De,e=>this.#Vt(e),()=>this.focus(),{lineRenditions:()=>this.#Rt,state:()=>({historical:!this.viewport.following||this.viewport.pending,readOnly:this.#Ke,selection:this.selection}),begin:(e,t)=>h(()=>this.#R.begin(e,t)),extend:e=>h(()=>this.#R.extend(e)),scroll:(e,t)=>h(()=>{this.#R.scroll(e,t),this.#ft?.activity()}),end:e=>this.#R.endGesture(e),resolve:e=>this.#Xt(e),execute:(e,t)=>this.#Yt(e,t),hyperlink:e=>this.#Jt(e),hoverHyperlink:e=>{this.#Nt=e?.id,this.#le&&this.#le.detection&&"hover"===this.#le.detection.decoration&&this.#Wt()},openHyperlink:(e,t)=>this.#Zt(e,t)}),this.#Qt(),Oi(this.#vt,".return-live",HTMLButtonElement).addEventListener("click",()=>{this.runAction(At.ScrollToLive).catch(e=>this.#ei(e))},{signal:this.#Fe.signal}),Oi(this.#vt,".copy-selection",HTMLButtonElement).addEventListener("click",()=>{this.runAction(At.CopySelection).catch(e=>this.#ei(e))},{signal:this.#Fe.signal}),Oi(o,".viewport",HTMLDivElement).addEventListener("pointerdown",e=>{e.composedPath().includes(this.#It)||e.target instanceof Element&&e.target.closest("button")||e.target===this.#De||e.target===this.#Be||(e.preventDefault(),this.focus())},{signal:this.#Fe.signal}),this.#je=new ResizeObserver(e=>{const{width:t,height:i}=e[0].contentRect,r=t!==this.#Ne.width||i!==this.#Ne.height;this.#Ne={width:t,height:i},this.#ti(),r&&this.#ii()}),this.#je.observe(e),window.addEventListener("pagehide",()=>this.dispose(),{signal:this.#Fe.signal}),this.#ie.signal?.addEventListener("abort",()=>this.dispose(),{once:!0,signal:this.#Fe.signal}),this.#et=setTimeout(()=>this.#x(new Error("Timed out waiting for the terminal's first frame")),3e4),this.#ne=void 0===this.#ie.workerUrl?new Worker($t("terminal"),{type:"module",name:"Hex1b WebTerminal"}):new Worker(new URL(this.#ie.workerUrl,location.href),{type:"module",name:"Hex1b WebTerminal"}),this.#ne.addEventListener("message",e=>this.#ri(e.data)),this.#ne.addEventListener("error",e=>{e.preventDefault(),this.#x(new Error(e.message||"Terminal worker failed"))}),this.#ne.addEventListener("messageerror",()=>this.#x(new Error("Terminal worker message could not be decoded")));const u=this.#De.transferControlToOffscreen();this.#Pe=window.matchMedia?.("(prefers-color-scheme: dark)"),this.#Pe?.addEventListener("change",this.#Oe),this.#ni({type:"init",canvas:u,transport:void 0===t?{type:"custom"}:{type:"websocket",url:t},scale:i,font:r,renderer:this.#Ae,palette:this.#Re[this.resolvedColorMode],preserveOnDisconnect:this.#Ve},[u]),this.#ze(),this.#si()}#ri(e){if(!this.#me)if("transportConnect"===e.type){if(this.#Ie)return void this.#x(new Error("Transport is already initialized"));this.#Ie=new F({onReady:()=>this.#ni({type:"transportConnected"}),onFrame:e=>(this.#Me=Promise.withResolvers(),this.#ni({type:"transportFrame",buffer:e},[e]),this.#Me.promise),onClose:e=>this.#ni({type:"transportClosed",details:e}),onError:e=>this.#ni({type:"transportError",message:e.message})}),this.#Ie.start(this.#Se)}else if("transportSend"===e.type)Promise.resolve(this.#Me?.promise).then(()=>this.#Ie?.send(e.control)).then(()=>this.#ni({type:"transportSent"})).catch(e=>{this.#me||this.#Ie?.closing||this.#ni({type:"transportError",message:n(e)})});else if("transportReceived"===e.type)this.#Me?.resolve(),this.#Me=void 0;else if("connected"===e.type)this.#_e=!0,this.#Be.disabled=!this.#oi();else if("closed"===e.type){if(this.#He)return;this.#He=!0,clearTimeout(this.#et);try{if(this.#ai(),!this.#me){const t=Object.freeze({...e.details});(void 0!==this.#ie.transport||void 0!==t.code)&&this.#ie.onClose?.(t)}}finally{this.#d.reject(new Error(`Terminal ${void 0===e.details.code?"transport":"WebSocket"} closed (${void 0===e.details.code?e.details.reason:`${e.details.code}${e.details.reason?`: ${e.details.reason}`:""}`}) before mounting completed`))}}else if("status"===e.type)"error"===e.level&&(this.#li(),this.#ai(),this.#d.reject(new Error(e.message))),this.#ie.onStatus?.(e.message,e.level);else if("geometry"===e.type){this.#Rt=e.lineRenditions??[],this.#zt=e.revision,this.#Bt.clear();for(const t of e.hyperlinks){const e=this.#Bt.get(t.row)??[];e.push(t),this.#Bt.set(t.row,e)}this.#Gt([]),this.#$t=void 0;const t=!this.#Xe,i=t||["columns","rows","cellWidth","cellHeight","mouseTracking"].some(t=>this.#Ge[t]!==e[t]);this.#Ge={columns:e.columns,rows:e.rows,cellWidth:e.cellWidth,cellHeight:e.cellHeight,mouseTracking:e.mouseTracking},this.#Xe=!0;const r=this.#We;if(this.#We=e.peer,this.#Be.disabled=!this.#oi(),this.#oi()&&document.activeElement===this.element&&!this.element.shadowRoot?.activeElement&&this.focus(),this.#Lt.update(e.hyperlinks),(i||r.isPrimary!==this.#We.isPrimary)&&this.#ti(),this.#We.isPrimary||(clearTimeout(this.#Ye),this.#Ye=void 0,this.#Je=void 0),(t||!r.isPrimary&&this.#We.isPrimary)&&this.#ii(!0),this.#Je===`${e.columns}x${e.rows}`&&(this.#Je=void 0),Object.hasOwn(e,"history")&&(this.#it=e.text,this.#ht.accept(e.history,e.revision),this.#R.accept(e.history,e.revision)),e.linkGeneration===this.#Ot&&(e.linkSnapshot?this.#ci(e.linkSnapshot):(this.#Dt&&(this.#Dt={...this.#Dt,revision:e.revision}),this.#Pt.advance(e.revision))),this.#qe?.update(e.columns,e.rows,e.mouseTracking),i&&this.#ie.onGeometry?.(this.geometry),t&&this.#ie.onSizingChange?.(this.sizing),r.id===this.#We.id&&r.primaryId===this.#We.primaryId&&r.isPrimary===this.#We.isPrimary||this.#ie.onRoleChange?.(this.peer),!this.#me&&this.#_e&&(null!==this.#We.id||this.#We.isPrimary)){const t=!this.#nt||this.#rt!==e.title,i=!this.#ct||this.#st.state!==e.progress.state||this.#st.percentage!==e.progress.percentage,r=!this.#ct||this.#ot.phase!==e.shellIntegration.phase||this.#ot.lastExitCode!==e.shellIntegration.lastExitCode,n=!this.#ct||this.#at.uri!==e.workingDirectory.uri,s=!this.#ct||this.#lt?.phase!==e.commandMark?.phase||this.#lt?.exitCode!==e.commandMark?.exitCode||this.#lt?.rawParameters!==e.commandMark?.rawParameters;this.#rt=e.title,this.#nt=!0,this.#st={...e.progress},this.#ot={...e.shellIntegration},this.#at={...e.workingDirectory},this.#lt=e.commandMark?{...e.commandMark}:null,this.#ct=!0,t&&this.#ie.onTitleChange?.(this.#rt),!this.#me&&i&&this.#ie.onProgressChange?.(this.progress),!this.#me&&r&&this.#ie.onShellIntegrationChange?.(this.shellIntegration),!this.#me&&n&&this.#ie.onWorkingDirectoryChange?.(this.workingDirectory),!this.#me&&s&&this.#ie.onCommandMarkChange?.(this.commandMark)}}else if("linkSnapshot"===e.type)e.generation===this.#Ot&&e.snapshot.revision===this.#zt&&this.#ci(e.snapshot);else if("linkDecorations"===e.type){const t=this.#$t;this.#_e&&t&&t.serial===e.serial&&t.generation===e.generation&&e.generation===this.#Ot&&t.revision===e.revision&&e.revision===this.#zt&&(this.#$t=void 0,this.#Gt(t.links),this.#qe?.refresh())}else"history"===e.type?(this.#it=e.text,this.#ht.accept(e.history,e.revision),this.#R.accept(e.history,e.revision)):"stats"===e.type&&(this.#tt=e.stats,void 0!==e.text&&(this.#it=e.text),e.stats.revision>0&&this.#nt&&this.#ct&&this.#_e&&(null!==this.#We.id||this.#We.isPrimary)&&(clearTimeout(this.#et),this.#d.resolve(this)),this.#ie.onStats?.(this.stats,e.text))}#ti(){const e=ti(this.#Ne,this.#Ge,this.#We.isPrimary,this.#$e,this.#dt,this.#ut),t=JSON.stringify(e)!==JSON.stringify(this.#pt);this.#pt=e;const{width:i,height:r,left:n,top:s}=e.content;this.#Ue.style.width=`${i}px`,this.#Ue.style.height=`${r}px`,this.#Ue.style.left=`${n}px`,this.#Ue.style.top=`${s}px`;const o=getComputedStyle(this.#Ue);this.#At={width:Number.parseFloat(o.width),height:Number.parseFloat(o.height)},this.#St?.refresh(),this.#ft?.refresh(),t&&!this.#gt&&(this.#gt=!0,queueMicrotask(()=>{this.#gt=!1,this.#me||this.#ie.onLayoutChange?.(this.layout)}));const a=window.devicePixelRatio||1;this.#ni({type:"viewport",width:Math.ceil(i*a),height:Math.ceil(r*a)})}#hi(){return function(e,t,i){if("fixed"===i.mode)return{columns:i.columns,rows:i.rows};if(e.width<=0||e.height<=0)return null;const r=i.fontSize/16;return{columns:Math.max(1,Math.min(300,Math.floor(e.width/(t.cellWidth*r)))),rows:Math.max(1,Math.min(100,Math.floor(e.height/(t.cellHeight*r))))}}(ei(this.#Ne,this.#dt,this.#ut),this.#Ge,this.#$e)}#ii(e=!1){("fixed"!==this.#$e.mode||e)&&this.#oi()&&this.#We.isPrimary&&void 0===this.#Ye&&!this.#me&&(this.#Ye=setTimeout(()=>{this.#Ye=void 0;const e=this.#hi();if(!e||!this.#We.isPrimary||!this.#oi())return;const t=`${e.columns}x${e.rows}`;e.columns===this.#Ge.columns&&e.rows===this.#Ge.rows||t===this.#Je||this.resize(e.columns,e.rows)},50))}#ni(e,t=[]){this.#ne?.postMessage(e,t)}#e(e){if(!this.#_e||this.#me)throw new Error("Terminal view is not connected");if(this.#Ke&&["input","paste","key","mouse","resize","requestPrimary"].includes(e.type))throw new Error("Terminal view does not accept input");this.#ni({type:"command",command:e})}#Vt(e){!this.#me&&this.#oi()&&(!function(e){if(R.encode(JSON.stringify(e)).byteLength>A.commandBytes)throw new RangeError("Terminal input exceeds the 64 KiB message limit. Send a smaller amount of text.")}(e),this.#Ct++,["input","paste","key"].includes(e.type)&&this.viewport.available&&(this.#qe?.cancel(),"none"!==this.selection.status&&this.clearSelection(),this.viewport.following&&!this.viewport.pending||this.scrollToLive()),this.#e(e))}#_t(){const e=this.viewport,t=this.selection;if(this.#qe?.refresh(),"invalidated"===t.status&&this.#qe?.cancel(),this.#yt){this.#yt.replaceChildren(...t.ranges.map(e=>{const t=document.createElement("span");t.className="highlight",t.setAttribute("part","selection-highlight");const i=this.#Rt[e.row]?2:1;return t.style.cssText=`left:${e.startColumn*i/this.#Ge.columns*100}%;top:${e.row/this.#Ge.rows*100}%;width:${(e.endColumn-e.startColumn)*i/this.#Ge.columns*100}%;height:${100/this.#Ge.rows}%`,t}));const i=Oi(this.#vt,".return-live",HTMLButtonElement);i.hidden=!e.available||e.following&&!e.pending,i.disabled=!this.#_e,this.#Kt()}this.#me||this.#St?.refresh(),this.#ft?.refresh(),this.#ie.onViewportChange?.(e),this.#ie.onSelectionChange?.(t)}scrollLines(e){this.#R.scroll(e),this.#ft?.activity()}scrollToRow(e){this.#R.scrollTo(e),this.#ft?.activity()}scrollToLive(){this.#R.live(),this.#ft?.activity()}scrollToMarker(e){return this.#ft?.activity(),this.#ht.jump(e)}addMarker(e){return this.#ht.add(e)}removeMarker(e){return this.#ht.remove(e)}getCommandMarkDetails(e){return this.#ht.details(e)}#Ht(){this.#ft?.activity(),this.#wt||(this.#wt=!0,queueMicrotask(()=>{this.#wt=!1,this.#me||this.#ie.onMarkersChange?.(this.markers)}))}setPadding(e){if(this.#me)throw new Error("Terminal view is disposed");this.#dt=Qt(e),this.#ti(),this.#ii(!0)}setScrollbar(e){if(this.#me)throw new Error("Terminal view is disposed");this.#ut=ai(e),this.#ft?.cancel(),this.#ti(),this.#ii(!0)}refreshScrollbar(){if(this.#me)throw new Error("Terminal view is disposed");this.#ft?.invalidate()}clearSelection(){this.#bt="",this.#R.clear()}refreshSelectionUI(){if(this.#me)throw new Error("Terminal view is disposed");this.#St?.refresh(!0)}#Kt(){if(!this.#vt)return;const e=this.selection,t=this.viewport,i=Oi(this.#vt,".inspection-message",HTMLSpanElement);i.textContent=this.#Mt||this.#bt||t.navigationError||("unavailable"===e.status?"":e.message)||(t.available&&!t.following?t.liveTop-t.top+" rows above live":""),i.hidden=!i.textContent,i.dataset.level=this.#Mt||this.#bt||t.navigationError||"invalidated"===e.status?"error":"info"}#ei(e){const t=e instanceof Error?e:new Error(String(e));this.#bt=`Input action failed: ${t.message}`,this.#_t(),this.#ie.onInputError?.(t)}async copySelection({clear:e=!1}={}){if("boolean"!=typeof e)throw new TypeError("Copy clear must be a boolean");const t=++this.#kt,i=this.selection.requestId,r=this.viewport.generation;this.#bt="";try{if(!this.#_e||this.#me)throw new Error("Terminal view is not connected");if(!navigator.clipboard?.write||"function"!=typeof ClipboardItem)throw new Error("Clipboard writing is unavailable. Use a secure browser context with clipboard permission.");const n=this.#R.copy();this.#xt=!0,this.#_t();const s=new ClipboardItem({"text/plain":n.then(e=>new Blob([e],{type:"text/plain"}))}),[o]=await Promise.all([n,navigator.clipboard.write([s])]);return e&&t===this.#kt&&"valid"===this.selection.status&&this.selection.requestId===i&&this.viewport.generation===r&&this.clearSelection(),o}catch(e){throw t===this.#kt&&(this.#R.cancelCopy(e),this.#bt=`Copy failed: ${n(e)}`),e}finally{t===this.#kt&&(this.#xt=!1),this.#_t()}}#oi(){return this.#_e&&this.#Xe&&!this.#Ke&&(null!==this.#We.id||this.#We.isPrimary)}get inputContext(){return Object.freeze({terminal:this,selection:this.selection,viewport:this.viewport,buffer:this.viewport.buffer??null,mouseCaptured:!this.#Ke&&0!==this.#Ge.mouseTracking,historical:!this.viewport.following||this.viewport.pending,readOnly:this.#Ke,connected:this.#_e,peer:this.peer})}#Xt(e){try{const t=this.#Et.resolve(Object.freeze(e),this.inputContext);return this.#Ke&&t.route===Mt.Application?{route:"pointer"===e.type||"wheel"===e.type?Mt.Continue:Mt.Consume}:t}catch(e){return this.#ei(e),{route:Mt.Consume}}}#Yt(e,t){this.#di(e.action,e.args,t).catch(e=>this.#ei(e))}async runAction(e,t,i){return this.#di(e,t,i)}async#di(e,t,i){if(this.#me)throw new Error("Terminal view is disposed");if(this.#bt="","function"==typeof e)return e(this.inputContext,t,i);const n=this.#A.get(e);if(n)return n(this.inputContext,t,i);switch(e){case At.CopySelection:if(void 0===t)return this.copySelection();if(!r(t))throw new TypeError("Copy options must be an object");if(void 0!==t.clear&&"boolean"!=typeof t.clear)throw new TypeError("Copy clear must be a boolean");return this.copySelection({clear:t.clear});case At.PasteClipboard:return this.pasteClipboard();case At.ClearSelection:return this.clearSelection();case At.ScrollToLive:return this.scrollToLive();case At.ScrollLines:if("number"!=typeof t)throw new RangeError("Scroll delta must be a signed 32-bit integer");return this.scrollLines(t);case At.CopyOrPaste:if(this.#Tt)throw new Error("A clipboard action is still in progress. Try again when it finishes.");this.#Tt=!0;try{return this.selection.active||this.selection.pending&&this.selection.canExtend?await this.copySelection({clear:!0}):this.#Ke?void 0:await this.pasteClipboard()}finally{this.#Tt=!1}default:throw new TypeError(`Unknown terminal action: ${e}`)}}paste(e){if("string"!=typeof e)throw new TypeError("Paste text must be a string");if(!this.#oi())throw new Error("Terminal view does not accept input");e&&this.#Vt({type:"paste",text:e})}async pasteClipboard(){if(!this.#oi())throw new Error("Terminal view does not accept input");if(!navigator.clipboard?.readText)throw new Error("Clipboard reading is unavailable. Use the browser's paste shortcut instead.");const e=this.#Ct,t=this.viewport.generation,i=this.selection.requestId,r=document.activeElement,n=await navigator.clipboard.readText();if(!this.#oi()||e!==this.#Ct||t!==this.viewport.generation||i!==this.selection.requestId||document.activeElement!==r)throw new Error("Terminal input, selection, focus, or buffer changed while reading the clipboard. Paste again.");return this.paste(n),n}get colorMode(){return this.#Le}get resolvedColorMode(){return"system"===this.#Le?this.#Pe?.matches?"dark":"light":this.#Le}setColorMode(e){if(this.#me)throw new Error("Terminal view is disposed");if(void 0===e)throw new TypeError("A color mode is required");this.#Le=v(e),this.#ze()}setPalette(e,t){if(this.#me)throw new Error("Terminal view is disposed");if("light"!==e&&"dark"!==e)throw new TypeError('Palette mode must be "light" or "dark"');this.#Re[e]=k(t),this.resolvedColorMode===e&&this.#ze()}#ze(){const e=this.#Re[this.resolvedColorMode];this.element.dataset.theme=this.resolvedColorMode,this.element.style.colorScheme=this.resolvedColorMode,this.element.style.backgroundColor=e.background;for(const[t,i]of Object.entries(function(e){const t=[1,3,5].map(t=>parseInt(e.foreground.slice(t,t+2),16)),i=[1,3,5].map(t=>parseInt(e.background.slice(t,t+2),16)),r=e=>"#"+t.map((t,r)=>Math.round(t*e+i[r]*(1-e)).toString(16).padStart(2,"0")).join("");return Object.freeze({track:r(.5),thumb:e.foreground,marker:r(.78),error:e.foreground,prompt:r(.45),"command-line":r(.6),executing:r(.85),success:r(.7),custom:r(.95)})}(e)))this.element.style.setProperty(`--cp-terminal-scrollbar-${t}`,i);this.#ni({type:"palette",palette:e}),this.#ft?.refresh()}setReadOnly(e){if("boolean"!=typeof e)throw new TypeError("readOnly must be a boolean");if(this.#me)throw new Error("Terminal view is disposed");if(this.#Ke===e)return;const t=document.activeElement===this.element&&(!this.element.shadowRoot?.activeElement||this.element.shadowRoot.activeElement===this.#Be);this.#Ke=e,this.#Ct++,this.#Qe?.(),this.#Be&&(this.#Be.value="",this.#Be.disabled=!this.#oi()),this.#qe?.cancel(),this.#qe?.refresh(),clearTimeout(this.#Ye),this.#Ye=void 0,this.#Je=void 0,this.#ii(!0),t&&this.focus(),this.#St?.refresh()}setLinks(e){if(this.#me)throw new Error("Terminal view is disposed");if(void 0===e)throw new TypeError("Link options must be an object or false");const t=Q(e,new Set(this.#A.keys()));this.#le=t,this.#Ot++,this.#qt=[],this.#Gt([]),this.#$t=void 0,this.#Dt=void 0,this.#Nt=void 0,this.#Pt.configure(!!t&&t.detection),this.#qe?.cancel(),this.#qe?.refresh(),this.#si()}#si(){const e=!(!this.#le||!(this.#le.osc8||this.#le.detection&&this.#le.detection.rules.some(e=>!1!==e.enabled)));this.#ni({type:"linkDetection",enabled:e,generation:this.#Ot})}#ci(e){this.#Dt=e,this.#qt=[],this.#Gt([]),this.#$t=void 0,this.#Pt.update(e),this.#qe?.refresh()}#ui(e){return`detected/${this.#Ot}/${this.#zt}/${e.id}`}#Gt(e){this.#jt=e,this.#Ft.clear();for(const t of e)for(const e of t.activation.ranges){const i=this.#Ft.get(e.row)??[];i.push({startColumn:e.startColumn,endColumn:e.endColumn,link:t}),this.#Ft.set(e.row,i)}}#Wt(){if(!this.#ne||!this.#_e||!this.#zt)return;const e=this.#le&&this.#le.detection;if(!e)return;const t="none"===e.decoration?[]:"hover"===e.decoration?this.#qt.filter(e=>this.#ui(e)===this.#Nt):this.#qt;this.#$t={revision:this.#zt,generation:this.#Ot,serial:++this.#Ut,links:this.#qt},this.#ni({type:"linkDecorations",revision:this.#zt,generation:this.#Ot,serial:this.#Ut,ranges:t.flatMap(e=>e.activation.ranges),underlineStyle:e.underlineStyle??"solid"})}#Jt(e){if(!this.#_e||this.viewport.pending||!this.#le)return null;const t=this.#Bt.get(e.y)?.find(t=>e.x>=t.startColumn&&e.xe.x>=t.startColumn&&e.xthis.#ui(t)===e.id);if(i)return void this.#di(i.action,i.activation,t).catch(e=>this.#ei(e));if(!this.#le.osc8){try{window.open(e.target,"_blank","noopener,noreferrer")}catch(e){this.#ei(e)}return}const r=this.#Bt.get(t.point.y)?.find(e=>t.point.x>=e.startColumn&&t.point.xthis.#ei(e))}#pi(e){if("key"===e.type){if(e.meta)throw new Error("Meta has no terminal key encoding; bind this shortcut to a local action instead.");this.#Vt({type:"key",key:e.key,ctrl:e.ctrl,alt:e.alt,shift:e.shift})}else"paste"===e.type?this.#oi()&&this.paste(e.text):"text"===e.type&&this.#Vt({type:"input",text:e.text})}#fi(e,t,i=!0){const r=this.#Xt(e);if((i||void 0!==r.action||r.route===Mt.Consume)&&r.route!==Mt.Browser&&(r.route!==Mt.Continue||"key"!==e.type))if(t?.preventDefault(),t?.stopPropagation(),void 0!==r.action)this.#Yt(r,e);else if(r.route!==Mt.Consume)try{this.#pi(e)}catch(e){this.#ei(e)}}#Qt(){const e=this.#Be,t={signal:this.#Fe.signal};let i=!1,r=null;this.#Qe=()=>{i=!1,r=null,clearTimeout(this.#Ze)},this.element.addEventListener("keydown",t=>{if(t.defaultPrevented)return;if(t.composedPath().includes(this.#It))return;const r=t.composedPath()[0];!this.#_e||t.isComposing||i||"Process"===t.key||"Dead"===t.key||t.getModifierState("AltGraph")||this.#fi({type:"key",key:t.key,code:t.code,repeat:t.repeat,...qt(t)},t,r===e||r===this.element)},{...t,capture:!0}),e.addEventListener("paste",t=>{!t.defaultPrevented&&this.#_e&&t.clipboardData&&(this.#fi({type:"paste",text:t.clipboardData.getData("text/plain")},t),e.value="")},t),e.addEventListener("compositionstart",()=>{this.#oi()&&(i=!0,r=null,clearTimeout(this.#Ze))},t),e.addEventListener("compositionend",t=>{i&&(i=!1,r="string"==typeof t.data?t.data:e.value,this.#Ze=setTimeout(()=>{r&&this.#fi({type:"text",text:r}),r=null,e.value=""},0))},t),e.addEventListener("input",t=>{const n=t instanceof InputEvent?t:void 0;if(n?.isComposing||i)return;clearTimeout(this.#Ze);const s=r??n?.data??e.value;r=null,s&&"insertFromPaste"!==n?.inputType&&this.#fi({type:"text",text:s},t),e.value=""},t)}focus(){if(this.#me)return;(this.#oi()?this.#Be:this.element).focus({preventScroll:!0})}requestPrimary(){if(!this.#oi())throw new Error("Terminal view does not accept input");if(this.#Ne.width<=0||this.#Ne.height<=0)throw new Error("Show the terminal container before taking primary");const e=this.#hi();if(!e)throw new Error("Show the terminal container before taking primary");if(null!==this.#We.id)this.#e({type:"requestPrimary",...e});else{if(!this.#We.isPrimary)throw new Error("Waiting for the HMP1 connection");this.resize(e.columns,e.rows)}}resize(e,t){const i=Kt(e,t);if(!this.#oi())throw new Error("Terminal view does not accept input");if(!this.#We.isPrimary)throw new Error("Only the primary view can request a terminal resize");this.#e({type:"resize",...i}),this.#Je=`${e}x${t}`}setSizing(e){const t=Vt(e,this.#$e.fontSize);if(!this.#_e||this.#me)throw new Error("Terminal view is not connected");if(this.#Ke)throw new Error("Terminal view does not accept input");if(!this.#We.isPrimary)throw new Error("Only the primary view can change terminal sizing");this.#$e=t,clearTimeout(this.#Ye),this.#Ye=void 0,this.#Je=void 0,this.#ti(),this.#ii(!0),this.#ie.onSizingChange?.(this.sizing)}resync(){this.#e({type:"resync"})}#ai(){this.#Ct++,this.#_e=!1,this.#Lt.update([]),this.#Bt.clear(),this.#qt=[],this.#Gt([]),this.#Dt=void 0,this.#$t=void 0,this.#Pt.clear(),this.#Be&&(this.#Be.disabled=!0),this.#qe?.update(1,1,0),this.#R.disconnect(),this.#ht.disconnect(),this.#ft?.cancel(),this.#_t(),clearTimeout(this.#Ye),this.#Ye=void 0}#x(e){this.#me||(this.#li(),this.#ai(),this.#d.reject(e),this.#ie.onStatus?.(e.message,"error"),this.#ne?.terminate())}dispose(){this.#me||(this.#me=!0,this.#li(),this.#ai(),this.#Pt.dispose(),this.#d.reject(new DOMException("Terminal view was disposed","AbortError")),clearTimeout(this.#et),clearTimeout(this.#Ze),this.#je?.disconnect(),this.#Pe?.removeEventListener("change",this.#Oe),this.#qe?.dispose(),this.#ft?.dispose(),this.#mt?.dispose(),this.#Fe.abort(),this.#ni({type:"stop"}),this.#ne?.terminate(),this.element.remove())}#li(){this.#Ie?.dispose(),this.#Me?.reject(new DOMException("Terminal transport was disposed or closed","AbortError")),this.#Me=void 0}};if(h(),P(),te(),"undefined"!=typeof DedicatedWorkerGlobalScope&&globalThis instanceof DedicatedWorkerGlobalScope)switch(new URL(import.meta.url).hash){case"#hex1b-terminal-worker":await Promise.resolve().then(()=>(kt(),pt));break;case"#hex1b-link-detection-worker":await Promise.resolve().then(()=>(Ct(),xt))}export{Mt as InputRoute,Ht as MAX_FONT_SIZE,_t as MIN_FONT_SIZE,At as TerminalAction,zi as WebTerminal,ni as createDefaultScrollbarRenderer,l as createWebSocketTransport,g as defaultDarkPalette,zt as defaultInputBindings,w as defaultLightPalette,fi as getCmdlineUrl,ee as linkAction,pi as parseCommandMarkParameters,oi as renderDefaultScrollbar,mi as renderDefaultScrollbarTooltip}; diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index 1b8e11712e4..fb5b2e0e7d9 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -2182,7 +2182,7 @@ test("frontend manifest, lockfile, minified bundle and backend use the exact pai const lockfile = JSON.parse(await readFile(new URL("package-lock.json", dashboard), "utf8")); const bundle = await readFile(new URL("dist/index.min.js", assets), "utf8"); const version = manifest.dependencies["@hex1b/web-terminal"]; - assert.equal(version, "0.173.0-alpha.1824.1.98d8766"); + assert.equal(version, "0.173.0"); assert.equal(bundle.split(/\r?\n/, 1)[0], `// @hex1b/web-terminal ${version}; minified with Terser. See ../LICENSE.`); assert.equal(lockfile.packages[""].dependencies["@hex1b/web-terminal"], version); assert.equal(lockfile.packages["node_modules/@hex1b/web-terminal"].version, version); From 2a6583634e5fae91536b6b5112e6813f67dd2dce Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 08:58:18 +1100 Subject: [PATCH 04/21] Remove temporary Hex1b feed now stable packages are mirrored Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- NuGet.config | 7 ------- 1 file changed, 7 deletions(-) diff --git a/NuGet.config b/NuGet.config index f4507eb8d07..b6a9c24227a 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,8 +2,6 @@ - - @@ -31,11 +29,6 @@ - - - - - From 6dfdbdb5213a0b45bbbfeb20cb1bbc034af2aec1 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 09:11:12 +1100 Subject: [PATCH 05/21] Address terminal recovery and CI routing review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specs/with-terminal.md | 5 +- eng/github-ci/ci-skip-entirely-patterns.txt | 1 - eng/github-ci/test-trigger-map.yml | 2 + .../TerminalDisconnectedBanner.razor.cs | 12 ++- .../Components/Controls/TerminalView.razor | 1 + .../Components/Controls/TerminalView.razor.cs | 2 + .../Components/Controls/TerminalView.razor.js | 16 +++- .../Model/DashboardCommandExecutor.cs | 18 +++- src/Aspire.Dashboard/wwwroot/js/README.md | 6 +- .../TerminalDisconnectedBannerTests.cs | 51 +++++++++- .../Controls/TerminalViewTests.cs | 29 ++++++ .../JavaScript/TerminalView.test.mjs | 95 ++++++++++++++++--- .../Model/DashboardCommandExecutorTests.cs | 29 ++++++ .../SelectTestsAcceptanceTests.cs | 5 +- .../TestTriggerMap/TestTriggerMapTests.cs | 2 + 15 files changed, 247 insertions(+), 27 deletions(-) diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index 0818c29cbd1..f6b48eaf9e4 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -336,7 +336,10 @@ an absent code is not treated as success. Docked-terminal completion does not currently expose the workload exit code through the pinned Hex1b adapter. Resource terminals offer **Restart resource** using the resource's advertised Restart or Start command, with the usual command availability, confirmation and error -reporting. Docked terminals offer **Close tab** through the existing dock close +reporting. A successful resource restart explicitly reconnects the view and resets +its retry budget, including after producer completion or exhausted automatic retries. +The retained screen remains visible until the replacement receives a frame. +Docked terminals offer **Close tab** through the existing dock close action. Other surfaces show the disconnected message without a close-tab action. All banners offer **Show terminal**, which hides the banner and accompanying retry error behind a floating disconnected icon at the top right. The icon diff --git a/eng/github-ci/ci-skip-entirely-patterns.txt b/eng/github-ci/ci-skip-entirely-patterns.txt index fd86b1773cd..666adca2abb 100644 --- a/eng/github-ci/ci-skip-entirely-patterns.txt +++ b/eng/github-ci/ci-skip-entirely-patterns.txt @@ -55,7 +55,6 @@ Aspire-Core.slnf .github/extensions/** # Dev/AzDO/scheduled-only infrastructure with no regular GitHub PR CI consumer. -.config/dotnet-tools.json eng/Version.Details.xml .github/actions/create-pull-request/** .github/actions/preload-azure-cli-requests/** diff --git a/eng/github-ci/test-trigger-map.yml b/eng/github-ci/test-trigger-map.yml index a156df200fb..f34cf93c7d2 100644 --- a/eng/github-ci/test-trigger-map.yml +++ b/eng/github-ci/test-trigger-map.yml @@ -135,6 +135,8 @@ path_rules: reason: solution membership can add an existing project without touching that project's files, so run everything - paths: + - .config/dotnet-tools.json + - .mcp.json - src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js - src/Aspire.Dashboard/Components/Controls/TerminalTitle.razor.js - src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.js diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs index 03c55019800..2c4e7e88f70 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs @@ -34,6 +34,10 @@ public sealed partial class TerminalDisconnectedBanner : ComponentBase, IAsyncDi [Parameter] public EventCallback OnClose { get; set; } + /// Raised after a resource starts or restarts successfully so its terminal can reconnect. + [Parameter] + public EventCallback OnRestarted { get; set; } + /// Raised when the banner is collapsed or expanded over the retained output. [Parameter] public EventCallback OnCollapsedChanged { get; set; } @@ -172,7 +176,13 @@ private async Task RestartResourceAsync() _executing = true; try { - await CommandExecutor.ExecuteAsync(resource, command, r => ResourceViewModel.GetResourceName(r, _resources)); + // The executor uses this callback off the renderer thread, where the resource watch + // may be changing the dictionary. Resolve the name before yielding. + var resourceName = ResourceViewModel.GetResourceName(resource, _resources); + if (await CommandExecutor.ExecuteAsync(resource, command, _ => resourceName) && !_disposed) + { + await OnRestarted.InvokeAsync(); + } } finally { diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index 40fd7dddbe0..81a2d2eb942 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -35,6 +35,7 @@ @if (_state.Disconnected) { } @if (_terminalError is not null && !(_state.Disconnected && _bannerCollapsed)) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs index 90c1907617f..82a4fb58f41 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs @@ -449,6 +449,8 @@ private string BuildWebSocketUrl(string pathAndQuery) private Task DismissErrorAsync() => InvokeTerminalAsync("dismissError"); + private Task ReconnectAfterRestartAsync() => InvokeTerminalAsync("restartTerminal"); + private async Task RetryAsync() { if (_reconciling || _disposed) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index a94ed6988ac..492754bd5b3 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -741,11 +741,13 @@ async function mountClient(state, generation, controller) { clearInvalidatedSelection(state); // Policy can change while mount is waiting for its first frame. client.setReadOnly(state.readOnly || state.ended); + // Mount resolves after a replacement frame arrives, even if completion was + // reported in the same turn. The old projection must not obscure that frame. + releaseRetainedClient(state); if (state.ended) { notifyToolbar(state); return; } - releaseRetainedClient(state); state.connected = client.connected; state.disconnected = !client.connected; state.peer = client.peer; @@ -910,6 +912,18 @@ export function initTerminal(element, wsUrl, dotNetRef, options, selectionTempla return id; } +export function restartTerminal(id) { + const state = terminals.get(id); + if (!state || state.client?.connected) { + return; + } + // Only an explicit successful resource restart can reopen an ended endpoint. + // Preserve its final projection while waiting for the restarted producer. + releaseClient(state, true); + state.ended = false; + reconnectTerminal(id, state.wsUrl); +} + export function reconnectTerminal(id, wsUrl) { const state = terminals.get(id); if (!state || (state.ended && state.wsUrl === wsUrl)) { diff --git a/src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs b/src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs index 55f901ceb23..8ce0b81c4ff 100644 --- a/src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs +++ b/src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs @@ -33,7 +33,9 @@ public bool IsExecuting(string resourceName, string commandName) } } - public async Task ExecuteAsync(ResourceViewModel resource, CommandViewModel command, Func getResourceName) + /// Executes a resource command with confirmation and result notifications. + /// True only when the command completes successfully. + public async Task ExecuteAsync(ResourceViewModel resource, CommandViewModel command, Func getResourceName) { var executingCommandKey = (resource.Name, command.Name); lock (_lock) @@ -52,12 +54,14 @@ public async Task ExecuteAsync(ResourceViewModel resource, CommandViewModel comm try { - await ExecuteAsyncCore(resource, command, getResourceName).ConfigureAwait(false); + var succeeded = await ExecuteAsyncCore(resource, command, getResourceName).ConfigureAwait(false); if (operationId is not null) { telemetryService.EndOperation(operationId, TelemetryResult.Success); } + + return succeeded; } catch (Exception ex) { @@ -65,6 +69,8 @@ public async Task ExecuteAsync(ResourceViewModel resource, CommandViewModel comm { telemetryService.EndOperation(operationId, TelemetryResult.Failure, ex.Message); } + + return false; } finally { @@ -85,14 +91,14 @@ public async Task ExecuteAsync(ResourceViewModel resource, CommandViewModel comm } } - public async Task ExecuteAsyncCore(ResourceViewModel resource, CommandViewModel command, Func getResourceName) + public async Task ExecuteAsyncCore(ResourceViewModel resource, CommandViewModel command, Func getResourceName) { if (!string.IsNullOrWhiteSpace(command.ConfirmationMessage)) { var result = await dialogService.ShowConfirmationAsync(command.ConfirmationMessage).ConfigureAwait(false); if (result.Cancelled) { - return; + return false; } } @@ -195,7 +201,7 @@ public async Task ExecuteAsyncCore(ResourceViewModel resource, CommandViewModel Body = response.Message, Intent = FluentMessageIntent.Info, }); - return; + return false; } else { @@ -301,6 +307,8 @@ string GetProgressNotificationId() { return progressNotificationId ?? throw new InvalidOperationException("The progress notification has not been created."); } + + return response.Kind == ResourceCommandResponseKind.Succeeded; } // Copied from FluentUI. diff --git a/src/Aspire.Dashboard/wwwroot/js/README.md b/src/Aspire.Dashboard/wwwroot/js/README.md index cd67f0d2e47..8634d3d656d 100644 --- a/src/Aspire.Dashboard/wwwroot/js/README.md +++ b/src/Aspire.Dashboard/wwwroot/js/README.md @@ -60,9 +60,9 @@ path `woff2/CascadiaMonoNF.woff2`. Its relative path under `dist/fonts/cascadia-mono-nf/` is preserved. Do not hand-edit generated assets. `TerminalView.razor.js` imports only the minified public entry point. -For this alpha trial, `NuGet.config` temporarily maps only the three Hex1b packages -to nuget.org. Remove that temporary source and mapping once these versions are -available through an approved internal feed. +The paired Hex1b NuGet packages must be available through the approved internal +feeds in `NuGet.config`. Mirror new versions before updating the paired packages; +do not add an external restore source. Desktop and Android Firefox terminals use `renderer: "webgl2"` until the [WebGPU performance issue](https://bugzilla.mozilla.org/show_bug.cgi?id=1870699) diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs index f31a3ae4a75..b1a51c38964 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalDisconnectedBannerTests.cs @@ -56,6 +56,7 @@ public async Task Message_TracksReportedExitCodeAndClearsWhenResourceIsRemoved(s [InlineData(CommandViewModel.StartCommand, ResourceCommandResponseKind.Succeeded)] [InlineData(CommandViewModel.RestartCommand, ResourceCommandResponseKind.Succeeded)] [InlineData(CommandViewModel.StartCommand, ResourceCommandResponseKind.Failed)] + [InlineData(CommandViewModel.RestartCommand, ResourceCommandResponseKind.Cancelled)] public async Task Restart_ExecutesAdvertisedCommandForExactReplicaAndReportsResult(string commandName, ResourceCommandResponseKind resultKind) { var executions = new ConcurrentQueue<(string Resource, string Command)>(); @@ -71,7 +72,10 @@ public async Task Restart_ExecutesAdvertisedCommandForExactReplicaAndReportsResu }); TerminalSetupHelpers.SetupTerminalComponents(this, client); Render(); - var cut = Render(builder => builder.Add(p => p.ResourceName, "shell-replica")); + var restarted = 0; + var cut = Render(builder => builder + .Add(p => p.ResourceName, "shell-replica") + .Add(p => p.OnRestarted, () => restarted++)); cut.WaitForAssertion(() => Assert.Single(cut.FindAll(".terminal-restart-resource"))); Assert.Equal("Restart resource", cut.Find(".terminal-restart-resource").TextContent.Trim()); Assert.Equal("Terminal disconnected. The last output has been preserved.", cut.Find("[role=status]").TextContent); @@ -83,11 +87,54 @@ public async Task Restart_ExecutesAdvertisedCommandForExactReplicaAndReportsResu await click; Assert.False(cut.FindComponents().Single(b => b.Instance.Class == "terminal-restart-resource").Instance.Disabled); var notification = Assert.Single(Services.GetRequiredService().GetNotifications()).Entry; - Assert.Equal(resultKind == ResourceCommandResponseKind.Succeeded ? MessageBarIntent.Success : MessageBarIntent.Error, + Assert.Equal(resultKind switch + { + ResourceCommandResponseKind.Succeeded => MessageBarIntent.Success, + ResourceCommandResponseKind.Cancelled => MessageBarIntent.Info, + _ => MessageBarIntent.Error + }, notification.Intent); + Assert.Equal(resultKind == ResourceCommandResponseKind.Succeeded ? 1 : 0, restarted); Assert.Single(cut.FindAll("[role=status]")); } + [Fact] + public async Task Restart_UsesCapturedResourceNameWhileResourcesChange() + { + var updates = Channel.CreateUnbounded>(); + var response = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var resource = ModelTestHelpers.CreateResource(resourceName: "shell-replica", + displayName: "shell", commands: [CreateCommand(CommandViewModel.RestartCommand)]); + var client = new TestDashboardClient(isEnabled: true, initialResources: [resource], + resourceChannelProvider: () => updates, executeResourceCommand: (_, _, _, _, _) => response.Task); + TerminalSetupHelpers.SetupTerminalComponents(this, client); + var toasts = Render(); + var cut = Render(builder => builder.Add(p => p.ResourceName, "shell-replica")); + cut.WaitForAssertion(() => Assert.Single(cut.FindAll(".terminal-restart-resource"))); + + var click = cut.InvokeAsync(() => cut.Find(".terminal-restart-resource").ClickAsync(new())); + cut.WaitForAssertion(() => Assert.True(cut.FindComponents() + .Single(b => b.Instance.Class == "terminal-restart-resource").Instance.Disabled)); + var updated = ModelTestHelpers.CreateResource(resourceName: "shell-replica", displayName: "shell", + commands: [CreateCommand(CommandViewModel.RestartCommand)], state: KnownResourceState.Finished, properties: new() + { + [KnownProperties.Resource.ExitCode] = new(KnownProperties.Resource.ExitCode, Value.ForString("42"), + isValueSensitive: false, knownProperty: null, sortOrder: 0, displayName: null, isHighlighted: false) + }); + await updates.Writer.WriteAsync([ + new(ResourceViewModelChangeType.Upsert, updated), + new(ResourceViewModelChangeType.Upsert, ModelTestHelpers.CreateResource(resourceName: "shell-other", displayName: "shell"))]); + cut.WaitForAssertion(() => Assert.Equal("Terminal disconnected (exit code 42). The last output has been preserved.", + cut.Find("[role=status]").TextContent)); + response.SetResult(new ResourceCommandResponseViewModel { Kind = ResourceCommandResponseKind.Succeeded }); + await click; + + // MainLayout rerenders the provider when a command replaces its notification. + toasts.Render(); + toasts.WaitForAssertion(() => Assert.Equal($"shell \"{CommandViewModel.RestartCommand}\" succeeded", + toasts.FindComponent().Instance.Title)); + } + [Theory] [InlineData(false, false)] [InlineData(true, false)] diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs index 4d45e4e5c51..4d9b321eaa9 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs @@ -426,6 +426,35 @@ public async Task Disconnect_CollapsesRecoveryAndRetryErrorWithoutReplacingTermi Assert.Single(initialization.Invocations); } + [Fact] + public async Task Disconnect_SuccessfulResourceRestartReconnectsExhaustedViewWithoutReplacingContainer() + { + var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); + var initialization = module.Setup("initTerminal", _ => true); + initialization.SetResult(1); + module.SetupVoid("restartTerminal", 1).SetVoidResult(); + var cut = Render(builder => builder.Add(p => p.ResourceName, "shell")); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 31, Disconnected = true, Error = "disconnected" + })); + var banner = cut.FindComponent(); + + await cut.InvokeAsync(banner.Instance.OnRestarted.InvokeAsync); + + Assert.Single(module.Invocations, i => i.Identifier == "restartTerminal"); + Assert.Single(initialization.Invocations); + Assert.Single(cut.FindAll(".terminal-container")); + Assert.Single(cut.FindAll(".terminal-disconnected-banner")); + + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 32, Connected = true + })); + Assert.Empty(cut.FindAll(".terminal-disconnected-banner")); + Assert.Empty(cut.FindAll("[role=alert]")); + } + [Theory] [InlineData("mount-failed", nameof(Resources.TerminalStrings.TerminalMountFailed))] [InlineData("disconnected", nameof(Resources.TerminalStrings.TerminalDisconnected))] diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index fb5b2e0e7d9..8b65120fec8 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -2030,18 +2030,37 @@ test("authoritative close keeps the existing presentation read-only without remo assert.equal(attempts[0].client.primaryRequests, 0); }); -test("completion before the mount continuation cannot revive the connected state", async () => { - const { id, element } = mount({ options: { autoFit: true } }); - attempts[0].resolve(); - attempts[0].close(4000); - await settle(); - assert.equal(terminal.getTerminalSnapshot(element).ended, true); - assert.equal(terminal.getToolbarState(id).connected, false); - assert.equal(terminal.getToolbarState(id).error, null); - assert.equal(attempts[0].client.readOnly, true); - assert.equal(attempts[0].client.primaryRequests, 0); - assert.equal(timers.size, 0); -}); +for (const reconnect of [false, true]) { + test(`completion before the ${reconnect ? "replacement" : "initial"} mount continuation retains only the latest frame`, async () => { + const { id, element } = mount({ options: { autoFit: true } }); + const original = attempts[0]; + if (reconnect) { + original.client.screenText = "Old screen"; + original.resolve(); + await settle(); + original.close(1006); + retry(); + assert.equal(original.client.disposed, false); + } + const replacement = attempts.at(-1); + replacement.client.screenText = "Newest final frame"; + replacement.resolve(); + replacement.close(4000); + await settle(); + assert.equal(terminal.getTerminalSnapshot(element).ended, true); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Newest final frame"); + assert.equal(terminal.getToolbarState(id).connected, false); + assert.equal(terminal.getToolbarState(id).error, null); + assert.equal(replacement.client.readOnly, true); + assert.equal(replacement.client.primaryRequests, 0); + assert.equal(replacement.client.disposed, false); + if (reconnect) { + assert.equal(original.client.disposed, true); + assert.equal(original.options.signal.aborted, true); + } + assert.equal(timers.size, 0); + }); +} for (const code of [1000, 1001, 1006]) { for (const mounted of [false, true]) { @@ -2152,6 +2171,58 @@ test("exhausted retries keep the final screen until the view is disposed", async assert.equal(attempts[0].client.disposed, true); }); +test("successful resource restart resets exhausted retries and preserves the screen until a new frame", async () => { + const { id, element } = mount(); + const original = attempts[0]; + original.client.screenText = "Previous final output"; + original.resolve(); + await settle(); + original.close(1006); + for (let i = 0; i < 30; i++) { + retry(); + attempts.at(-1).reject(); + await settle(); + } + assert.equal(timers.size, 0); + terminal.restartTerminal(id); + assert.equal(attempts.length, 32); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Previous final output"); + assert.equal(original.client.disposed, false); + attempts.at(-1).reject(); + await settle(); + assert.equal(retry(), 500, "A restarted workload gets a fresh retry budget"); + attempts.at(-1).client.screenText = "Restarted output"; + attempts.at(-1).resolve(); + await settle(); + assert.equal(terminal.getToolbarState(id).connected, true); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Restarted output"); + assert.equal(original.client.disposed, true); +}); + +test("successful resource restart explicitly reopens an ended endpoint without clearing its final frame", async () => { + const { id, element } = mount(); + const original = attempts[0]; + original.client.screenText = "Completed workload output"; + original.resolve(); + await settle(); + original.close(4000); + terminal.reconnectTerminal(id, original.options.url); + assert.equal(attempts.length, 1, "Ordinary reconnects cannot reopen an ended endpoint"); + terminal.restartTerminal(id); + assert.equal(attempts.length, 2); + assert.equal(attempts[1].options.url, original.options.url); + assert.equal(terminal.getTerminalSnapshot(element).ended, false); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Completed workload output"); + assert.equal(original.client.disposed, false); + assert.equal(original.options.signal.aborted, false); + attempts[1].resolve(); + await settle(); + assert.equal(terminal.getToolbarState(id).connected, true); + assert.equal(original.client.disposed, true); + terminal.restartTerminal(id); + assert.equal(attempts.length, 2, "A view that already recovered is not remounted"); +}); + test("renderer failures surface an error rather than retaining an unusable view", async () => { const { id } = mount(); attempts[0].resolve(); diff --git a/tests/Aspire.Dashboard.Tests/Model/DashboardCommandExecutorTests.cs b/tests/Aspire.Dashboard.Tests/Model/DashboardCommandExecutorTests.cs index c866ed1496d..0cb70e42a9b 100644 --- a/tests/Aspire.Dashboard.Tests/Model/DashboardCommandExecutorTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/DashboardCommandExecutorTests.cs @@ -24,6 +24,35 @@ namespace Aspire.Dashboard.Tests.Model; public sealed class DashboardCommandExecutorTests { + [Theory] + [InlineData(ResourceCommandResponseKind.Succeeded, true)] + [InlineData(ResourceCommandResponseKind.Failed, false)] + [InlineData(ResourceCommandResponseKind.Cancelled, false)] + public async Task ExecuteAsync_ReportsWhetherTheCommandSucceeded(ResourceCommandResponseKind kind, bool expected) + { + var client = new TestDashboardClient(isEnabled: true, + executeResourceCommand: (_, _, _, _, _) => Task.FromResult(new ResourceCommandResponseViewModel { Kind = kind })); + var executor = CreateExecutor(client, out _, out _); + var command = CreateCommand(); + var resource = ModelTestHelpers.CreateResource(resourceName: "api", commands: [command]); + + Assert.Equal(expected, await executor.ExecuteAsync(resource, command, r => r.DisplayName)); + Assert.False(executor.IsExecuting(resource.Name, command.Name)); + } + + [Fact] + public async Task ExecuteAsync_DoesNotReportSuccessWhenExecutionThrows() + { + var client = new TestDashboardClient(isEnabled: true, + executeResourceCommand: (_, _, _, _, _) => Task.FromException(new InvalidOperationException("Command failed"))); + var executor = CreateExecutor(client, out _, out _); + var command = CreateCommand(); + var resource = ModelTestHelpers.CreateResource(resourceName: "api", commands: [command]); + + Assert.False(await executor.ExecuteAsync(resource, command, r => r.DisplayName)); + Assert.False(executor.IsExecuting(resource.Name, command.Name)); + } + [Fact] public async Task ExecuteAsyncCore_CancelNotificationAction_CancelsCommandAndUpdatesNotification() { diff --git a/tests/Infrastructure.Tests/TestTriggerMap/SelectTestsAcceptanceTests.cs b/tests/Infrastructure.Tests/TestTriggerMap/SelectTestsAcceptanceTests.cs index 52e6a6c8361..8434567dfad 100644 --- a/tests/Infrastructure.Tests/TestTriggerMap/SelectTestsAcceptanceTests.cs +++ b/tests/Infrastructure.Tests/TestTriggerMap/SelectTestsAcceptanceTests.cs @@ -853,9 +853,12 @@ public void PrefilterReadsPatternsFileAndHonorsKeepRouted() Assert.True(filter.IsExcluded("pyrightconfig.json")); // Dev/AzDO-only inputs with no regular GitHub PR CI consumer. - Assert.True(filter.IsExcluded(".config/dotnet-tools.json")); Assert.True(filter.IsExcluded("eng/Version.Details.xml")); + // Terminal package-pairing checks consume these manifests at runtime. + Assert.False(filter.IsExcluded(".config/dotnet-tools.json")); + Assert.False(filter.IsExcluded(".mcp.json")); + // Generated API/ATS baselines under src/*/api/: no PR CI is needed when these are the only // changed files. Assert.True(filter.IsExcluded("src/Aspire.Hosting/api/Aspire.Hosting.cs")); diff --git a/tests/Infrastructure.Tests/TestTriggerMap/TestTriggerMapTests.cs b/tests/Infrastructure.Tests/TestTriggerMap/TestTriggerMapTests.cs index d3069909548..8096600b565 100644 --- a/tests/Infrastructure.Tests/TestTriggerMap/TestTriggerMapTests.cs +++ b/tests/Infrastructure.Tests/TestTriggerMap/TestTriggerMapTests.cs @@ -768,6 +768,8 @@ public void ExtensionJavaE2eDiscoveryInputsAreNotGitIgnored() } [Theory] + [InlineData(".config/dotnet-tools.json")] + [InlineData(".mcp.json")] [InlineData("src/Aspire.Dashboard/Components/Controls/TerminalTitle.razor.js")] [InlineData("src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.js")] [InlineData("src/Aspire.Dashboard/wwwroot/js/app-terminalwindow.js")] From d892cef02cb646142aeaf7d8e8cdf86acc8fc99b Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 10:43:41 +1100 Subject: [PATCH 06/21] Keep terminal action errors visible when recovery is collapsed Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Components/Controls/TerminalView.razor | 2 +- .../Controls/TerminalViewTests.cs | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index 81a2d2eb942..52cf4cdba61 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -38,7 +38,7 @@ OnRestarted="@ReconnectAfterRestartAsync" OnCollapsedChanged="@(collapsed => _bannerCollapsed = collapsed)" /> } - @if (_terminalError is not null && !(_state.Disconnected && _bannerCollapsed)) + @if (_terminalError is not null && !(_state.Disconnected && _bannerCollapsed && _terminalError is "disconnected" or "mount-failed")) {
@GetErrorMessage() diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs index 4d9b321eaa9..8520a700a2b 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs @@ -426,6 +426,37 @@ public async Task Disconnect_CollapsesRecoveryAndRetryErrorWithoutReplacingTermi Assert.Single(initialization.Invocations); } + [Fact] + public async Task Disconnect_CollapsedRecoveryKeepsPaletteSaveFailureVisibleAndDismissible() + { + var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); + module.Setup("initTerminal", _ => true).SetResult(1); + module.SetupVoid("dismissError", 1).SetVoidResult(); + var cut = Render(builder => builder.Add(p => p.ResourceName, "shell")); + var state = new TerminalToolbarState { TerminalId = 1, Generation = 1, Disconnected = true, Error = "disconnected" }; + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state)); + await cut.Find(".terminal-show-output").ClickAsync(new()); + + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Error = "mount-failed" })); + Assert.Empty(cut.FindAll("[role=alert]")); + + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Error = "palette-failed" })); + + Assert.Equal(Resources.TerminalStrings.TerminalPaletteSaveFailed, cut.Find("[role=alert]").TextContent); + Assert.Single(cut.FindAll(".terminal-show-banner")); + Assert.Empty(cut.FindAll(".terminal-disconnected-banner")); + var dismiss = cut.Find(".terminal-error fluent-button"); + Assert.Equal(Resources.TerminalStrings.TerminalDismissError, dismiss.TextContent.Trim()); + await dismiss.ClickAsync(new()); + Assert.Single(module.Invocations, i => i.Identifier == "dismissError"); + + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Error = null })); + Assert.Empty(cut.FindAll("[role=alert]")); + Assert.Single(cut.FindAll(".terminal-show-banner")); + Assert.Single(cut.FindAll(".terminal-container")); + Assert.Single(module.Invocations, i => i.Identifier == "initTerminal"); + } + [Fact] public async Task Disconnect_SuccessfulResourceRestartReconnectsExhaustedViewWithoutReplacingContainer() { From 46873c98b7ea863f4cf1b91c3a0c782c576d8525 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 13:01:16 +1100 Subject: [PATCH 07/21] Avoid duplicate terminal recovery banners after refresh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specs/with-terminal.md | 9 +++- .../Components/Controls/TerminalView.razor | 6 +-- .../Components/Controls/TerminalView.razor.cs | 4 ++ .../Components/Controls/TerminalView.razor.js | 2 +- .../Controls/TerminalViewTests.cs | 43 ++++++++++++++++--- .../JavaScript/TerminalView.test.mjs | 18 +++++++- 6 files changed, 70 insertions(+), 12 deletions(-) diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index 5ceae80020c..d6cc0209f77 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -348,8 +348,13 @@ its retry budget, including after producer completion or exhausted automatic ret The retained screen remains visible until the replacement receives a frame. Docked terminals offer **Close tab** through the existing dock close action. Other surfaces show the disconnected message without a close-tab action. -All banners offer **Show terminal**, which hides the banner and accompanying -retry error behind a floating disconnected icon at the top right. The icon +Connection or renderer initialization failures show a single error with +**Reconnect terminal**, rather than also claiming that output was preserved. +The failure stays visible after automatic retries are exhausted. A disconnected +recovery banner replaces the redundant transport error when output is retained; +unrelated action failures remain visible alongside recovery. +All banners offer **Show terminal**, which hides the banner +behind a floating disconnected icon at the top right. The icon reopens the recovery actions without reconnecting, restarting, or replacing the retained renderer. Collapse persists through transport retries and resets after a successful reconnect. The floating icon is inset from the terminal's focus diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index 52cf4cdba61..e0aacca2b42 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -31,14 +31,14 @@
@if (_state.Disconnected || _terminalError is not null) { -
- @if (_state.Disconnected) +
+ @if (ShowDisconnectedRecovery) { } - @if (_terminalError is not null && !(_state.Disconnected && _bannerCollapsed && _terminalError is "disconnected" or "mount-failed")) + @if (_terminalError is not null && !(ShowDisconnectedRecovery && _terminalError is "disconnected")) {
@GetErrorMessage() diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs index 82a4fb58f41..dafff103ccf 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs @@ -438,6 +438,10 @@ private string BuildWebSocketUrl(string pathAndQuery) return $"{scheme}://{endpoint.Authority}{boundPathAndQuery}"; } + // A failed mount may follow a transport close before any frame was received. + // Show its connection/renderer error, not a claim that output was preserved. + private bool ShowDisconnectedRecovery => _state.Disconnected && _terminalError is not "mount-failed"; + private string GetErrorMessage() => Loc[_terminalError switch { "disconnected" => nameof(Resources.TerminalStrings.TerminalDisconnected), diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index 739bf3559dc..ee581825e9f 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -334,7 +334,7 @@ function scheduleReconnect(state, generation) { return; } if (state.attempts >= MAX_RECONNECT_ATTEMPTS) { - state.error = "disconnected"; + state.error ??= "disconnected"; notifyToolbar(state); return; } diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs index 8520a700a2b..3ced581a02e 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs @@ -395,7 +395,7 @@ public async Task Disconnect_CollapsesRecoveryAndRetryErrorWithoutReplacingTermi var state = new TerminalToolbarState { TerminalId = 1, Generation = 1, Disconnected = true, Error = "disconnected" }; await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state)); var banner = cut.FindComponent().Instance; - Assert.Single(cut.FindAll("[role=alert]")); + Assert.Empty(cut.FindAll("[role=alert]")); Assert.Equal("Show terminal", cut.Find(".terminal-show-output").TextContent.Trim()); await cut.Find(".terminal-show-output").ClickAsync(new()); @@ -413,7 +413,7 @@ public async Task Disconnect_CollapsesRecoveryAndRetryErrorWithoutReplacingTermi Assert.Single(cut.FindAll(".terminal-show-banner")); await cut.Find(".terminal-show-banner").ClickAsync(new()); Assert.Single(cut.FindAll(".terminal-disconnected-banner")); - Assert.Single(cut.FindAll("[role=alert]")); + Assert.Empty(cut.FindAll("[role=alert]")); Assert.Equal("true", cut.Find(".terminal-show-output").GetAttribute("aria-expanded")); Assert.Empty(cut.FindAll(".terminal-overlays-collapsed")); @@ -437,9 +437,6 @@ public async Task Disconnect_CollapsedRecoveryKeepsPaletteSaveFailureVisibleAndD await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state)); await cut.Find(".terminal-show-output").ClickAsync(new()); - await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Error = "mount-failed" })); - Assert.Empty(cut.FindAll("[role=alert]")); - await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Error = "palette-failed" })); Assert.Equal(Resources.TerminalStrings.TerminalPaletteSaveFailed, cut.Find("[role=alert]").TextContent); @@ -457,6 +454,42 @@ public async Task Disconnect_CollapsedRecoveryKeepsPaletteSaveFailureVisibleAndD Assert.Single(module.Invocations, i => i.Identifier == "initTerminal"); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Disconnect_BeforeFirstFrameShowsOnlyMountFailureAndCanRetry(bool collapsed) + { + var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); + module.Setup("initTerminal", _ => true).SetResult(1); + module.Setup("reconnectTerminal", _ => true).SetResult(2); + var cut = Render(builder => builder.Add(p => p.ResourceName, "shell")); + + if (collapsed) + { + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 1, Disconnected = true, Error = "disconnected" + })); + await cut.Find(".terminal-show-output").ClickAsync(new()); + } + + for (var generation = 1; generation <= 3; generation++) + { + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = generation, Disconnected = true, Error = "mount-failed" + })); + + Assert.Equal(Resources.TerminalStrings.TerminalMountFailed, cut.Find("[role=alert]").TextContent); + Assert.Empty(cut.FindAll(".terminal-disconnected-banner")); + Assert.Empty(cut.FindAll(".terminal-show-banner")); + Assert.Single(cut.FindAll(".terminal-error")); + } + + await cut.Find(".terminal-error fluent-button").ClickAsync(new()); + Assert.Single(module.Invocations, i => i.Identifier == "reconnectTerminal"); + } + [Fact] public async Task Disconnect_SuccessfulResourceRestartReconnectsExhaustedViewWithoutReplacingContainer() { diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index 56f56544718..b9704e0d78b 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -1609,13 +1609,29 @@ test("automatic retries are bounded and explicit reconnect resets the exhausted } assert.equal(attempts.length, 31); assert.equal(timers.size, 0); - assert.equal(terminal.getToolbarState(id).error, "disconnected"); + assert.equal(terminal.getToolbarState(id).error, "mount-failed"); terminal.reconnectTerminal(id, "wss://dashboard/api/terminal?resource=app"); attempts.at(-1).reject(); await settle(); assert.equal(retry(), 500); }); +test("refreshing an unavailable terminal preserves its mount error through transport retries and exhaustion", async () => { + const { id } = mount(); + for (let i = 0; i <= 30; i++) { + attempts.at(-1).close(1006); + attempts.at(-1).reject(); + await settle(); + assert.equal(terminal.getToolbarState(id).disconnected, true); + assert.equal(terminal.getToolbarState(id).error, "mount-failed"); + if (i < 30) { + retry(); + } + } + assert.equal(timers.size, 0); + assert.equal(attempts.length, 31); +}); + function clickFooter(footer, control, { selectOption = false, ...options } = {}) { const option = { matches: selector => selector === "fluent-option" }; const event = Object.assign(new Event("click"), { From 738a258ee3cebbd33783d692e92c4c2b61f9bd92 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 13:12:38 +1100 Subject: [PATCH 08/21] Offer non-mutating terminal reconnect after retry exhaustion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specs/with-terminal.md | 2 + .../Controls/TerminalDisconnectedBanner.razor | 8 ++++ .../TerminalDisconnectedBanner.razor.cs | 4 ++ .../Components/Controls/TerminalView.razor | 1 + .../Controls/TerminalViewTests.cs | 43 +++++++++++++++++++ 5 files changed, 58 insertions(+) diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index d6cc0209f77..790f67bcba7 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -353,6 +353,8 @@ Connection or renderer initialization failures show a single error with The failure stays visible after automatic retries are exhausted. A disconnected recovery banner replaces the redundant transport error when output is retained; unrelated action failures remain visible alongside recovery. +After automatic transport retries are exhausted, recovery offers **Reconnect terminal** +without restarting the resource, including in read-only and docked views. All banners offer **Show terminal**, which hides the banner behind a floating disconnected icon at the top right. The icon reopens the recovery actions without reconnecting, restarting, or replacing the diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor index 8ec59930a7f..014ce771750 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor @@ -20,6 +20,14 @@ else
@Message + @if (OnReconnect.HasDelegate) + { + + @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] + + } @if (RestartCommand is not null) { Raised to retry an exhausted transport connection without restarting the resource. + [Parameter] + public EventCallback OnReconnect { get; set; } + /// Raised when the banner is collapsed or expanded over the retained output. [Parameter] public EventCallback OnCollapsedChanged { get; set; } diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index e0aacca2b42..871775b9fed 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -36,6 +36,7 @@ { } @if (_terminalError is not null && !(ShowDisconnectedRecovery && _terminalError is "disconnected")) diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs index 3ced581a02e..9dfc3fa332c 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs @@ -454,6 +454,49 @@ public async Task Disconnect_CollapsedRecoveryKeepsPaletteSaveFailureVisibleAndD Assert.Single(module.Invocations, i => i.Identifier == "initTerminal"); } + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task Disconnect_ExhaustedRetriesOfferReconnectWithoutRestartingResource(bool docked, bool readOnly) + { + var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); + module.Setup("initTerminal", _ => true).SetResult(1); + module.Setup("reconnectTerminal", _ => true).SetResult(32); + var cut = Render(builder => + { + builder.Add(p => p.ReadOnly, readOnly); + if (docked) + { + builder.Add(p => p.EndpointPathAndQuery, "/api/apphost-terminal?terminalId=shell"); + builder.Add(p => p.OnClose, () => { }); + } + else + { + builder.Add(p => p.ResourceName, "shell"); + } + }); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 31, Disconnected = true, Error = "disconnected" + })); + + Assert.Empty(cut.FindAll(".terminal-error")); + Assert.Equal(Resources.TerminalStrings.TerminalRetry, cut.Find(".terminal-reconnect").TextContent.Trim()); + await cut.Find(".terminal-reconnect").ClickAsync(new()); + Assert.Single(module.Invocations, i => i.Identifier == "reconnectTerminal"); + Assert.Single(module.Invocations, i => i.Identifier == "initTerminal"); + Assert.Equal(["initTerminal", "getSizePresets", "reconnectTerminal"], module.Invocations.Select(i => i.Identifier)); + Assert.Single(cut.FindAll(".terminal-container")); + + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 32, Disconnected = true + })); + Assert.Empty(cut.FindAll(".terminal-reconnect")); + } + [Theory] [InlineData(false)] [InlineData(true)] From 156f1e731a740fed8fcc9fe563d83277dd9699ef Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 13:19:55 +1100 Subject: [PATCH 09/21] Soften terminal recovery banner styling Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Controls/TerminalDisconnectedBanner.razor | 10 +++++----- .../TerminalDisconnectedBanner.razor.css | 19 +++++++++++++----- .../Components/Controls/TerminalView.razor | 4 ++-- .../Controls/TerminalView.razor.css | 20 +++++++++++++++++-- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor index 014ce771750..b4a03e2ddd8 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor @@ -4,7 +4,7 @@ {
@Message @if (OnReconnect.HasDelegate) { - @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] @@ -30,7 +30,7 @@ else } @if (RestartCommand is not null) { - @@ -39,7 +39,7 @@ else } @if (OnClose.HasDelegate) { - @@ -47,7 +47,7 @@ else } diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css index 7247116139e..25daa4210a4 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css @@ -5,15 +5,24 @@ gap: 8px; padding: 12px; border: 1px solid var(--colorNeutralStroke1); - border-radius: var(--borderRadiusMedium); - background: var(--colorNeutralBackground1); + border-radius: var(--aspire-control-radius); + background: var(--colorNeutralBackground2); color: var(--colorNeutralForeground1); - box-shadow: var(--shadow4); } .terminal-disconnected-banner span { flex: 1 1 200px; min-width: 0; + overflow-wrap: anywhere; +} + +.terminal-disconnected-banner ::deep fluent-button { + flex: 0 0 auto; + white-space: nowrap; +} + +.terminal-disconnected-banner ::deep fluent-button::part(control) { + white-space: nowrap; } @media (forced-colors: active) { @@ -31,6 +40,6 @@ .terminal-disconnected-indicator ::deep .terminal-show-banner { width: 36px; height: 36px; - border-radius: var(--borderRadiusMedium); - box-shadow: var(--shadow4); + border-radius: var(--aspire-control-radius); + background: var(--colorNeutralBackground2); } diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index 871775b9fed..45210c90a74 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -45,11 +45,11 @@ @GetErrorMessage() @if (_terminalError is "input-failed" or "sizing-failed" or "palette-failed") { - @Loc[nameof(Resources.TerminalStrings.TerminalDismissError)] + @Loc[nameof(Resources.TerminalStrings.TerminalDismissError)] } else { - @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] + @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] }
} diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css index d5ce2cfb16b..ef64b8d8590 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css @@ -275,12 +275,28 @@ .terminal-error { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 12px; border: 1px solid var(--colorNeutralStroke1); - border-radius: var(--border-radius); - background: var(--colorNeutralBackground1); + border-radius: var(--aspire-control-radius); + background: var(--colorNeutralBackground2); color: var(--colorNeutralForeground1); } + +.terminal-error span { + flex: 1 1 200px; + min-width: 0; + overflow-wrap: anywhere; +} + +.terminal-error ::deep fluent-button { + flex: 0 0 auto; + white-space: nowrap; +} + +.terminal-error ::deep fluent-button::part(control) { + white-space: nowrap; +} From d92be3e3dc9fb61aa0fd3def898ae299d050d504 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 13:31:38 +1100 Subject: [PATCH 10/21] Preserve first frame when transport closes before mount continuation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Components/Controls/TerminalView.razor.js | 9 +- .../JavaScript/TerminalView.test.mjs | 85 +++++++++++++------ 2 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index ee581825e9f..2ddb271073c 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -370,7 +370,14 @@ function connectionClosed(state, generation, details) { if (details.code !== TERMINAL_ENDED_CLOSE_CODE) { // Normal closure (1000), missing close frames (1006), reasons and wasClean // describe transport state, never whether the producer has completed. - connectionFailed(state, generation, new Error(`Terminal WebSocket closed (${details.code}).`), true); + const fail = () => connectionFailed(state, generation, new Error(`Terminal WebSocket closed (${details.code}).`), true); + if (!state.client && state.controller) { + // The first-frame promise can be fulfilled before its continuation adopts + // the renderer. Let that queued continuation run before cleanup can abort it. + queueMicrotask(fail); + } else { + fail(); + } return; } state.ended = true; diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index b9704e0d78b..35f26bc09cd 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -2090,35 +2090,38 @@ test("authoritative close keeps the existing presentation read-only without remo }); for (const reconnect of [false, true]) { - test(`completion before the ${reconnect ? "replacement" : "initial"} mount continuation retains only the latest frame`, async () => { - const { id, element } = mount({ options: { autoFit: true } }); - const original = attempts[0]; - if (reconnect) { - original.client.screenText = "Old screen"; - original.resolve(); + for (const code of [4000, 1000, 1001, 1006]) { + test(`close ${code} before the ${reconnect ? "replacement" : "initial"} mount continuation retains only the latest frame`, async () => { + const { id, element } = mount({ options: { autoFit: true } }); + const original = attempts[0]; + if (reconnect) { + original.client.screenText = "Old screen"; + original.resolve(); + await settle(); + original.close(1006); + retry(); + assert.equal(original.client.disposed, false); + } + const replacement = attempts.at(-1); + replacement.client.screenText = "Newest final frame"; + replacement.resolve(); + replacement.close(code); await settle(); - original.close(1006); - retry(); - assert.equal(original.client.disposed, false); - } - const replacement = attempts.at(-1); - replacement.client.screenText = "Newest final frame"; - replacement.resolve(); - replacement.close(4000); - await settle(); - assert.equal(terminal.getTerminalSnapshot(element).ended, true); - assert.equal(terminal.getTerminalSnapshot(element).screenText, "Newest final frame"); - assert.equal(terminal.getToolbarState(id).connected, false); - assert.equal(terminal.getToolbarState(id).error, null); - assert.equal(replacement.client.readOnly, true); - assert.equal(replacement.client.primaryRequests, 0); - assert.equal(replacement.client.disposed, false); - if (reconnect) { - assert.equal(original.client.disposed, true); - assert.equal(original.options.signal.aborted, true); - } - assert.equal(timers.size, 0); - }); + assert.equal(terminal.getTerminalSnapshot(element).ended, code === 4000); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Newest final frame"); + assert.equal(terminal.getToolbarState(id).connected, false); + assert.equal(terminal.getToolbarState(id).error, null); + assert.equal(replacement.client.readOnly, code === 4000); + assert.equal(replacement.client.primaryRequests, 0); + assert.equal(replacement.client.disposed, false); + assert.equal(replacement.options.signal.aborted, false); + if (reconnect) { + assert.equal(original.client.disposed, true); + assert.equal(original.options.signal.aborted, true); + } + assert.equal(timers.size, code === 4000 ? 0 : 1); + }); + } } for (const code of [1000, 1001, 1006]) { @@ -2134,6 +2137,8 @@ for (const code of [1000, 1001, 1006]) { await settle(); assert.equal(terminal.getTerminalSnapshot(element).ended, false); assert.equal(terminal.getToolbarState(id).connected, false); + assert.equal(attempts[0].options.signal.aborted, !mounted); + assert.equal(attempts[0].client.disposed, false); assert.equal(timers.size, 1); assert.equal(retry(), 500); attempts[1].resolve(); @@ -2143,6 +2148,30 @@ for (const code of [1000, 1001, 1006]) { } } +for (const action of ["dispose", "rebind"]) { + test(`${action} releases a pending frame before deferred transport cleanup`, async () => { + const { id } = mount(); + const original = attempts[0]; + original.resolve(); + original.close(1006); + if (action === "dispose") { + terminal.disposeTerminal(id); + } else { + terminal.reconnectTerminal(id, "wss://dashboard/api/terminal?resource=next"); + attempts[1].resolve(); + } + await settle(); + assert.equal(original.client.disposed, true); + assert.equal(original.options.signal.aborted, true); + assert.equal(timers.size, 0); + if (action === "rebind") { + assert.equal(attempts[1].client.disposed, false); + assert.equal(terminal.getToolbarState(id).connected, true); + assert.equal(terminal.getToolbarState(id).error, null); + } + }); +} + test("rebind ignores authoritative close from the old connection", async () => { const { id, element } = mount(); attempts[0].resolve(); From d6a503d72271ed599b6032c780fec38ef7ad199f Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 13:35:46 +1100 Subject: [PATCH 11/21] Match terminal recovery button hover to standard controls Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Controls/TerminalDisconnectedBanner.razor | 10 +++++----- .../Controls/TerminalDisconnectedBanner.razor.css | 3 ++- .../Components/Controls/TerminalView.razor | 4 ++-- .../Components/Controls/TerminalView.razor.css | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor index b4a03e2ddd8..014ce771750 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor @@ -4,7 +4,7 @@ {
@Message @if (OnReconnect.HasDelegate) { - @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] @@ -30,7 +30,7 @@ else } @if (RestartCommand is not null) { - @@ -39,7 +39,7 @@ else } @if (OnClose.HasDelegate) { - @@ -47,7 +47,7 @@ else } diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css index 25daa4210a4..7215704c222 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.css @@ -17,6 +17,7 @@ } .terminal-disconnected-banner ::deep fluent-button { + --colorNeutralBackground1: var(--colorNeutralBackground2); flex: 0 0 auto; white-space: nowrap; } @@ -41,5 +42,5 @@ width: 36px; height: 36px; border-radius: var(--aspire-control-radius); - background: var(--colorNeutralBackground2); + --colorNeutralBackground1: var(--colorNeutralBackground2); } diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index 45210c90a74..fd478ebdcf7 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -45,11 +45,11 @@ @GetErrorMessage() @if (_terminalError is "input-failed" or "sizing-failed" or "palette-failed") { - @Loc[nameof(Resources.TerminalStrings.TerminalDismissError)] + @Loc[nameof(Resources.TerminalStrings.TerminalDismissError)] } else { - @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] + @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] }
} diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css index ef64b8d8590..198f449fa8f 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.css @@ -293,6 +293,7 @@ } .terminal-error ::deep fluent-button { + --colorNeutralBackground1: var(--colorNeutralBackground2); flex: 0 0 auto; white-space: nowrap; } From 0cdfe072856ca10b3d04557a3ff9f248b06a2ba2 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 14:39:09 +1100 Subject: [PATCH 12/21] Keep terminal recovery state independent of action and renderer errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specs/with-terminal.md | 3 + .../Components/Controls/TerminalView.razor | 2 +- .../Components/Controls/TerminalView.razor.cs | 2 + .../Components/Controls/TerminalView.razor.js | 15 ++++- .../Controls/TerminalViewTests.cs | 17 ++++- .../JavaScript/TerminalView.test.mjs | 66 ++++++++++++++++++- 6 files changed, 99 insertions(+), 6 deletions(-) diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index 790f67bcba7..1be91571214 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -355,6 +355,7 @@ recovery banner replaces the redundant transport error when output is retained; unrelated action failures remain visible alongside recovery. After automatic transport retries are exhausted, recovery offers **Reconnect terminal** without restarting the resource, including in read-only and docked views. +This action remains available while unrelated action errors are displayed or dismissed. All banners offer **Show terminal**, which hides the banner behind a floating disconnected icon at the top right. The icon reopens the recovery actions without reconnecting, restarting, or replacing the @@ -371,6 +372,8 @@ a connected frame arrives; successful reconnect removes the treatment and releases the old view. Producer completion keeps the screen until the view is closed. Rebinding to another endpoint discards the previous screen. Retention does not imply producer completion or guarantee output that was never received. +A fatal error in a retained renderer releases that unusable view and surfaces an +error without canceling a pending replacement connection. The terminal host defers viewer handshakes until its producer has connected, including after a recycle. Its listening socket alone is not proof of a live terminal: reconnecting to an empty relay while a shell is stopped must not diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index fd478ebdcf7..ba9a8b8ee67 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -36,7 +36,7 @@ { } @if (_terminalError is not null && !(ShowDisconnectedRecovery && _terminalError is "disconnected")) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs index dafff103ccf..83ad5429d4c 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs @@ -564,6 +564,8 @@ public sealed record TerminalToolbarState public bool Connected { get; init; } /// Whether the transport closed, including while reconnecting or after producer completion. public bool Disconnected { get; init; } + /// Whether automatic transport retries stopped without reconnecting. + public bool RetriesExhausted { get; init; } /// The workload-reported title, or empty when unset. public string Title { get; init; } = string.Empty; /// The decoded working directory reported by the shell, or null when unset. diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index 2ddb271073c..d8831c6705a 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -334,6 +334,7 @@ function scheduleReconnect(state, generation) { return; } if (state.attempts >= MAX_RECONNECT_ATTEMPTS) { + state.retriesExhausted = true; state.error ??= "disconnected"; notifyToolbar(state); return; @@ -381,6 +382,7 @@ function connectionClosed(state, generation, details) { return; } state.ended = true; + state.retriesExhausted = false; state.connected = false; state.pendingSizing = null; state.autoFitPending = false; @@ -595,6 +597,7 @@ function connectClient(state) { } cancelReconnect(state); state.failurePending = false; + state.retriesExhausted = false; const generation = ++state.generation; releaseClient(state); state.peer = { id: null, primaryId: null, isPrimary: false }; @@ -688,6 +691,15 @@ async function mountClient(state, generation, controller) { // https://github.com/mitchdenny/hex1b/pull/491 renderer: /\bFirefox\//.test(navigator.userAgent) ? "webgl2" : "auto", onStatus(message, level) { + if (!state.disposed && level === "error" && state.retainedClient?.controller === controller) { + // Retained renderers outlive their connection generation. A fatal + // error must release that renderer, not cancel its pending replacement. + console.warn("Dashboard retained terminal renderer failed.", message); + releaseRetainedClient(state); + state.error = "mount-failed"; + notifyToolbar(state); + return; + } if (!current() || state.ended || level !== "error") { return; } @@ -827,6 +839,7 @@ export function initTerminal(element, wsUrl, dotNetRef, options, selectionTempla ended: false, connected: false, disconnected: false, + retriesExhausted: false, title: "", workingDirectory: null, workingDirectoryUri: null, @@ -1072,7 +1085,7 @@ export function getToolbarState(id) { terminalId: id, generation: state.generation, status: !connected ? "connecting" : isPrimary ? "primary" : state.peer.primaryId === null ? "no-primary" : "viewer", - connected, disconnected: state.disconnected, isPrimary, canTakeControl, + connected, disconnected: state.disconnected, retriesExhausted: state.retriesExhausted, isPrimary, canTakeControl, title: state.title, workingDirectory: state.workingDirectory, workingDirectoryUri: state.workingDirectoryUri, diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs index 9dfc3fa332c..1484b11708a 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs @@ -479,15 +479,28 @@ public async Task Disconnect_ExhaustedRetriesOfferReconnectWithoutRestartingReso }); await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState { - TerminalId = 1, Generation = 31, Disconnected = true, Error = "disconnected" + TerminalId = 1, Generation = 31, Disconnected = true, RetriesExhausted = true, Error = "disconnected" })); Assert.Empty(cut.FindAll(".terminal-error")); Assert.Equal(Resources.TerminalStrings.TerminalRetry, cut.Find(".terminal-reconnect").TextContent.Trim()); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 31, Disconnected = true, RetriesExhausted = true, Error = "palette-failed" + })); + Assert.Equal(Resources.TerminalStrings.TerminalPaletteSaveFailed, cut.Find("[role=alert]").TextContent); + Assert.Single(cut.FindAll(".terminal-reconnect")); + await cut.Find(".terminal-error fluent-button").ClickAsync(new()); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 31, Disconnected = true, RetriesExhausted = true + })); + Assert.Empty(cut.FindAll(".terminal-error")); + Assert.Single(cut.FindAll(".terminal-reconnect")); await cut.Find(".terminal-reconnect").ClickAsync(new()); Assert.Single(module.Invocations, i => i.Identifier == "reconnectTerminal"); Assert.Single(module.Invocations, i => i.Identifier == "initTerminal"); - Assert.Equal(["initTerminal", "getSizePresets", "reconnectTerminal"], module.Invocations.Select(i => i.Identifier)); + Assert.Equal(["initTerminal", "getSizePresets", "dismissError", "reconnectTerminal"], module.Invocations.Select(i => i.Identifier)); Assert.Single(cut.FindAll(".terminal-container")); await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index 35f26bc09cd..1c9cb77e236 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -1048,7 +1048,7 @@ test("init returns an id while mount waits for its first connected frame", async attempts[0].resolve(); await settle(); assert.deepEqual(snapshots.at(-1), { - terminalId: id, generation: 1, status: "viewer", connected: true, disconnected: false, + terminalId: id, generation: 1, status: "viewer", connected: true, disconnected: false, retriesExhausted: false, title: "", workingDirectory: null, workingDirectoryUri: null, progressState: "none", progressPercentage: null, isPrimary: false, canTakeControl: true, sizeMode: "font", sizeKey: "100x30", @@ -1316,7 +1316,7 @@ test("a disconnect schedules only one retry and restores focus only if still app document.activeElement = attempts[0].client.element; attempts[0].client.connected = false; attempts[0].close(1006); - attempts[0].options.onStatus("closed again", "error"); + attempts[0].close(1006); await settle(); assert.equal(timers.size, 1); assert.equal(attempts[0].client.disposed, false); @@ -2252,6 +2252,7 @@ test("exhausted retries keep the final screen until the view is disposed", async } assert.equal(timers.size, 0); assert.equal(terminal.getToolbarState(id).error, "disconnected"); + assert.equal(terminal.getToolbarState(id).retriesExhausted, true); assert.equal(terminal.getToolbarState(id).disconnected, true); assert.equal(terminal.getTerminalSnapshot(element).screenText, "Final output"); assert.equal(attempts[0].client.disposed, false); @@ -2259,6 +2260,31 @@ test("exhausted retries keep the final screen until the view is disposed", async assert.equal(attempts[0].client.disposed, true); }); +test("palette failures and dismissal preserve retry exhaustion until an explicit reconnect", async () => { + const { id } = mount({ options: { readOnly: true } }); + attempts[0].resolve(); + await settle(); + attempts[0].close(1006); + for (let i = 0; i < 30; i++) { + retry(); + attempts.at(-1).reject(); + await settle(); + } + mock.method(localStorage, "setItem", () => { throw new Error("Storage disabled"); }); + terminal.setPaletteFromHost(id, "light"); + assert.equal(terminal.getToolbarState(id).error, "palette-failed"); + assert.equal(terminal.getToolbarState(id).retriesExhausted, true); + terminal.dismissError(id); + assert.equal(terminal.getToolbarState(id).error, null); + assert.equal(terminal.getToolbarState(id).retriesExhausted, true); + assert.equal(timers.size, 0); + terminal.reconnectTerminal(id, attempts[0].options.url); + assert.equal(terminal.getToolbarState(id).retriesExhausted, false); + attempts.at(-1).resolve(); + await settle(); + assert.equal(terminal.getToolbarState(id).connected, true); +}); + test("successful resource restart resets exhausted retries and preserves the screen until a new frame", async () => { const { id, element } = mount(); const original = attempts[0]; @@ -2324,6 +2350,42 @@ test("renderer failures surface an error rather than retaining an unusable view" assert.equal(timers.size, 1); }); +for (const pendingReplacement of [false, true]) { + test(`fatal retained renderer errors release the view ${pendingReplacement ? "during" : "before"} replacement mounting`, async () => { + const { id, element } = mount(); + const original = attempts[0]; + original.client.screenText = "Retained output"; + original.resolve(); + await settle(); + original.close(1006); + if (pendingReplacement) { + retry(); + } + original.options.onStatus("WebGL2 context lost", "error"); + await settle(); + assert.equal(original.client.disposed, true); + assert.equal(original.options.signal.aborted, true); + assert.equal(terminal.getTerminalSnapshot(element).screenText, ""); + assert.equal(terminal.getToolbarState(id).error, "mount-failed"); + if (!pendingReplacement) { + assert.equal(timers.size, 1); + retry(); + } + const replacement = attempts[1]; + assert.equal(replacement.options.signal.aborted, false); + assert.equal(replacement.client.disposed, false); + replacement.client.screenText = "New output"; + replacement.resolve(); + await settle(); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "New output"); + assert.equal(terminal.getToolbarState(id).connected, true); + assert.equal(terminal.getToolbarState(id).error, null); + original.options.onStatus("Late renderer error", "error"); + assert.equal(terminal.getToolbarState(id).error, null); + assert.equal(replacement.client.disposed, false); + }); +} + test("disposing a view ignores later native close callbacks", async () => { const { id } = mount(); terminal.disposeTerminal(id); From 9c50792adb86e2726741628f239e2965e9f65ef8 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 14:49:48 +1100 Subject: [PATCH 13/21] Preserve disconnected terminal output on nonfatal inspection errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specs/with-terminal.md | 1 + .../Components/Controls/TerminalView.razor.js | 27 ++++++++------ .../JavaScript/TerminalView.test.mjs | 37 +++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index 1be91571214..3fa0ec909d6 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -374,6 +374,7 @@ closed. Rebinding to another endpoint discards the previous screen. Retention does not imply producer completion or guarantee output that was never received. A fatal error in a retained renderer releases that unusable view and surfaces an error without canceling a pending replacement connection. +Nonfatal scrollbar, selection UI, and link-detection errors do not discard retained output. The terminal host defers viewer handshakes until its producer has connected, including after a recycle. Its listening socket alone is not proof of a live terminal: reconnecting to an empty relay while a shell is stopped must not diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index d8831c6705a..582d8ec5a08 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -691,7 +691,20 @@ async function mountClient(state, generation, controller) { // https://github.com/mitchdenny/hex1b/pull/491 renderer: /\bFirefox\//.test(navigator.userAgent) ? "webgl2" : "auto", onStatus(message, level) { - if (!state.disposed && level === "error" && state.retainedClient?.controller === controller) { + const retained = !state.disposed && state.retainedClient?.controller === controller; + if (level !== "error" || (!retained && (!current() || state.ended))) { + return; + } + // Hex1b 0.173.0 prefixes nonfatal inspection/action errors, e.g. + // "Scrollbar: Terminal disconnected before the marker operation completed." + // These can arrive after onClose; only unprefixed renderer/worker errors + // imply that a disconnected projection is unusable. + if (/^(?:Scrollbar(?: tooltip)?|Link detection|Selection UI failed): /.test(message) || + (!retained && state.client?.connected)) { + console.log("Dashboard terminal status error.", message); + return; + } + if (retained) { // Retained renderers outlive their connection generation. A fatal // error must release that renderer, not cancel its pending replacement. console.warn("Dashboard retained terminal renderer failed.", message); @@ -700,16 +713,8 @@ async function mountClient(state, generation, controller) { notifyToolbar(state); return; } - if (!current() || state.ended || level !== "error") { - return; - } - if (state.client?.connected) { - // Local action/inspection failures do not break the connection. - // Fatal renderer and transport failures disconnect before reporting status. - console.log("Dashboard terminal status error.", message); - } else { - connectionFailed(state, generation, message, false); - } + // Fatal renderer and transport failures disconnect before reporting status. + connectionFailed(state, generation, message, false); }, onGeometry(geometry) { if (current()) { diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index 1c9cb77e236..c6b90652e55 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -2350,6 +2350,43 @@ test("renderer failures surface an error rather than retaining an unusable view" assert.equal(timers.size, 1); }); +for (const prefix of ["Scrollbar", "Scrollbar tooltip", "Link detection", "Selection UI failed"]) { + for (const pendingReplacement of [false, true]) { + test(`${prefix} errors after disconnect preserve the renderer ${pendingReplacement ? "during" : "before"} replacement mounting`, async () => { + const { id, element } = mount(); + const original = attempts[0]; + original.client.screenText = "Retained output with graphics"; + original.resolve(); + await settle(); + const markerOperation = Promise.withResolvers(); + const reported = markerOperation.promise.catch(error => + original.options.onStatus(`${prefix}: ${error.message}`, "error")); + original.close(1006); + if (pendingReplacement) { + retry(); + } + markerOperation.reject(new Error("Terminal disconnected before the marker operation completed.")); + await reported; + await settle(); + assert.equal(original.client.disposed, false); + assert.equal(original.options.signal.aborted, false); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Retained output with graphics"); + assert.equal(terminal.getToolbarState(id).error, null); + assert.equal(terminal.getToolbarState(id).disconnected, true); + if (!pendingReplacement) { + assert.equal(timers.size, 1); + retry(); + } + attempts[1].client.screenText = "Reconnected output"; + attempts[1].resolve(); + await settle(); + assert.equal(original.client.disposed, true); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Reconnected output"); + assert.equal(terminal.getToolbarState(id).connected, true); + }); + } +} + for (const pendingReplacement of [false, true]) { test(`fatal retained renderer errors release the view ${pendingReplacement ? "during" : "before"} replacement mounting`, async () => { const { id, element } = mount(); From fc368bafab0aa7ff1970d73fe79604b233161fda Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 15:13:04 +1100 Subject: [PATCH 14/21] Restore dock focus after closing a terminal from its recovery banner Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specs/with-terminal.md | 4 +- .../Components/Layout/TerminalDock.razor.js | 15 +++--- .../JavaScript/TerminalDock.test.mjs | 46 +++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index 3fa0ec909d6..16529313f16 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -347,7 +347,9 @@ reporting. A successful resource restart explicitly reconnects the view and rese its retry budget, including after producer completion or exhausted automatic retries. The retained screen remains visible until the replacement receives a frame. Docked terminals offer **Close tab** through the existing dock close -action. Other surfaces show the disconnected message without a close-tab action. +action. Closing from the banner restores keyboard focus after the pane is removed, +unless the user has moved focus elsewhere. Other surfaces show the disconnected +message without a close-tab action. Connection or renderer initialization failures show a single error with **Reconnect terminal**, rather than also claiming that output was preserved. The failure stays visible after automatic retries are exhausted. A disconnected diff --git a/src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.js b/src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.js index 1c6b5267fed..26d3c74f30d 100644 --- a/src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.js +++ b/src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.js @@ -198,12 +198,13 @@ const tabNavigationRegistrations = new WeakMap(); export function registerTabNavigation(dockElement, dotNetRef) { unregisterTabNavigation(dockElement); - let focusedTabGroup = null; + let focusedCloseTarget = null; const disposeTabStrip = registerTabStrip(dockElement, dotNetRef); const onFocusIn = (event) => { - const group = event.target.closest?.('.terminal-dock-tab'); - focusedTabGroup = group && dockElement.contains(group) ? group : null; + const group = event.target.closest?.('.terminal-dock-tab') + ?? (event.target.closest?.('.terminal-close-tab') ? event.target.closest('.terminal-dock-pane') : null); + focusedCloseTarget = group && dockElement.contains(group) ? group : null; }; // Automatic activation follows https://www.w3.org/WAI/ARIA/apg/patterns/tabs/. @@ -250,14 +251,14 @@ export function registerTabNavigation(dockElement, dotNetRef) { }; // Removal is confirmed by the watch stream, not by the close RPC finishing. A focused node's removal leaves - // focus on the document body, so remember the group until the DOM update arrives. Moving elsewhere while a - // close is pending clears it; unrelated metadata updates must not steal focus from a terminal or the page. + // focus on the document body, so remember the tab group or banner's pane until the DOM update arrives. + // Moving elsewhere while a close is pending clears it; unrelated metadata updates must not steal focus. const observer = new MutationObserver(() => { - if (!focusedTabGroup || focusedTabGroup.isConnected) { + if (!focusedCloseTarget || focusedCloseTarget.isConnected) { return; } - focusedTabGroup = null; + focusedCloseTarget = null; if (!dockElement.isConnected || dockElement.inert) { return; } diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalDock.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalDock.test.mjs index 8e6c6f8bdf7..12a3a4d5493 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalDock.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalDock.test.mjs @@ -160,6 +160,52 @@ function createTabRegistration() { return { calls, scrolls, controls, wrapper, list, tabs, emit, observers, buttonByDirection }; } +for (const lastTab of [false, true]) { + for (const moveFocus of [false, true]) { + test(`banner close restores focus after removal with ${lastTab ? "no" : "a"} remaining tab unless focus moves: ${moveFocus}`, () => { + const { tabs, observers } = createTabRegistration(); + const pane = { isConnected: true }; + const close = { + isConnected: true, + closest: selector => selector === ".terminal-dock-pane" ? pane + : selector === ".terminal-close-tab" ? close : null, + }; + const collapse = { + focus() { document.activeElement = collapse; }, + closest() { return null; }, + scrollIntoView() {}, + }; + const contains = dockElement.contains; + dockElement.contains = element => element === pane || contains(element); + const querySelector = dockElement.querySelector; + dockElement.querySelector = selector => selector === '.terminal-dock-tab-select[aria-selected="true"]' + ? lastTab ? null : tabs[1].tab + : selector === ".terminal-dock-collapse" ? collapse : querySelector(selector); + const focus = target => { + document.activeElement = target; + const event = new Event("focusin"); + Object.defineProperty(event, "target", { value: target }); + document.dispatchEvent(event); + }; + focus(close); + const removalObserver = observers.at(-1); + removalObserver.callback(); + assert.equal(document.activeElement, close, "Focus stays put until watch-driven removal"); + const elsewhere = { closest: () => null }; + if (moveFocus) { + focus(elsewhere); + } + close.isConnected = false; + pane.isConnected = false; + if (!moveFocus) { + document.activeElement = document.body; + } + removalObserver.callback(); + assert.equal(document.activeElement, moveFocus ? elsewhere : lastTab ? collapse : tabs[1].tab); + }); + } +} + test("overflow arrows remain visible and disable when tabs fit or the list is removed", () => { const { controls, buttonByDirection, emit, scrolls, list, observers } = createTabRegistration(); assert.equal(controls.hidden, false); From b4e36a175e933b5b764cf83cbce16521b4a62b40 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 17:39:42 +1100 Subject: [PATCH 15/21] Suppress unavailable terminal text history popup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Components/Controls/TerminalView.razor.js | 5 +++++ .../JavaScript/TerminalView.test.mjs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index 582d8ec5a08..081ccb55329 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -286,6 +286,7 @@ function configureTerminalChrome(client) { style.textContent = ` .return-live { display: none !important; } .inspection-message[data-aspire-history-position] { display: none !important; } + .inspection-message[data-aspire-history-unavailable] { display: none !important; } .inspection-message[data-aspire-disconnected-status] { display: none !important; } :host([data-aspire-pointer-input="true"]) .scrollbar-accessibility:focus-visible { outline: none; } .viewport { @@ -317,6 +318,10 @@ function configureTerminalChrome(client) { // feedback and navigation errors and must not be suppressed. status.toggleAttribute("data-aspire-history-position", status.dataset.level !== "error" && /^\d+ rows above live$/.test(status.textContent)); + // Unavailable history is expected for terminal views without text history, + // not an actionable error. Preserve unrelated navigation and clipboard errors. + status.toggleAttribute("data-aspire-history-unavailable", + status.textContent === "Text history is not available for this terminal view"); // Hex1b can report "Terminal view is not connected" directly or with an // action prefix, e.g. "Scrollbar: Terminal view is not connected". // Aspire's recovery banner owns this state; keep unrelated errors visible. diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index c6b90652e55..83912fed8e2 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -977,18 +977,23 @@ test("history chrome suppression preserves errors and selection feedback and dis const observer = themeObservers.find(observer => observer.element === status); assert.ok(observer); assert.match(shadow.styles[0], /\.return-live \{ display: none !important; \}/); + assert.match(shadow.styles[0], /\.inspection-message\[data-aspire-history-unavailable\] \{ display: none !important; \}/); for (const [text, level, hidden] of [ ["42 rows above live", "info", true], ["0 rows above live", "info", true], ["Selection copied", "info", false], ["Navigation rejected", "error", false], ["42 rows above live", "error", false], + ["Text history is not available for this terminal view", "error", false], + ["Text history is not available for this terminal view", "info", false], ["", "info", false], ]) { status.textContent = text; status.dataset.level = level; observer.callback(); assert.equal(status.attributes.has("data-aspire-history-position"), hidden); + assert.equal(status.attributes.has("data-aspire-history-unavailable"), + text === "Text history is not available for this terminal view"); } terminal.reconnectTerminal(id, attempts[0].options.url); assert.equal(observer.disconnected, true); From 5d663d3810e7665ae992dac97fbf8eac4642bffe Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 17:47:33 +1100 Subject: [PATCH 16/21] Keep retained terminal dimensions visible during reconnect Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Components/Controls/TerminalView.razor.js | 3 ++- .../Controls/TerminalViewTests.cs | 18 ++++++++++++++++++ .../JavaScript/TerminalView.test.mjs | 6 ++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index 081ccb55329..f91f392d0ea 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -606,7 +606,8 @@ function connectClient(state) { const generation = ++state.generation; releaseClient(state); state.peer = { id: null, primaryId: null, isPrimary: false }; - state.geometry = null; + // The disabled size picker describes the retained screen until a new frame arrives. + state.geometry = state.retainedClient?.client.geometry ?? null; state.connected = false; state.pendingSizing = null; state.autoFitPending = state.autoFit; diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs index 1484b11708a..06bd7823e16 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs @@ -334,6 +334,24 @@ await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalTool Assert.True(cut.Find(".terminal-fit").HasAttribute("disabled")); } + [Fact] + public async Task DisconnectedSizePicker_DisplaysRetainedDimensionsWhileDisabled() + { + var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); + module.Setup("initTerminal", _ => true).SetResult(1); + var cut = Render(builder => builder.Add(p => p.ResourceName, "shell")); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 2, Disconnected = true, + Cols = 97, Rows = 38, SizeKey = "97x38" + })); + var picker = cut.FindComponent>().Instance; + Assert.True(picker.Disabled); + Assert.Equal("97x38", picker.Value); + Assert.NotNull(picker.Items); + Assert.Equal([new("97x38", "97\u00d738", 97, 38), new TerminalSizePreset("80x24", "80\u00d724", 80, 24)], picker.Items); + } + [Fact] public void AutoFit_ChangesDuringInitializationApplyWithoutReconnecting() { diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index 83912fed8e2..aaf183fa9f5 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -2202,23 +2202,29 @@ test("transport retries retain the last screen until a replacement frame arrives assert.equal(original.options.signal.aborted, false); assert.equal(terminal.getToolbarState(id).disconnected, true); assert.equal(terminal.getTerminalSnapshot(element).screenText, "Final output with graphics"); + assert.equal(terminal.getToolbarState(id).sizeKey, "100x30"); retry(); + assert.equal(terminal.getToolbarState(id).sizeKey, "100x30"); + assert.equal(terminal.getToolbarState(id).sizeSelectEnabled, false); attempts[1].reject(); await settle(); assert.equal(original.client.disposed, false); assert.equal(terminal.getTerminalSnapshot(element).screenText, "Final output with graphics"); assert.equal(terminal.getToolbarState(id).disconnected, true); + assert.equal(terminal.getToolbarState(id).sizeKey, "100x30"); terminal.setTerminalPalette("light"); assert.equal(original.client.colorMode, "light"); retry(); const replacement = attempts[2]; + replacement.client.geometry = { columns: 97, rows: 38 }; replacement.client.screenText = "Reconnected output"; replacement.resolve(); await settle(); assert.equal(original.client.disposed, true); assert.equal(original.options.signal.aborted, true); assert.equal(terminal.getToolbarState(id).disconnected, false); + assert.equal(terminal.getToolbarState(id).sizeKey, "97x38"); assert.equal(terminal.getTerminalSnapshot(element).screenText, "Reconnected output"); }); From 006c11a2da041c110a294390cf978a2bca129227 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 17:52:07 +1100 Subject: [PATCH 17/21] Leave terminal size picker empty when dimensions are unavailable Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Dashboard/Components/Controls/TerminalView.razor | 1 - .../Controls/TerminalViewTests.cs | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index ba9a8b8ee67..ff98097bad0 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -82,7 +82,6 @@
>(); - Assert.Equal(loc[nameof(Resources.TerminalStrings.TerminalToolbarGridSize)].Value, dimensionsSelect.Instance.Placeholder); + Assert.Null(dimensionsSelect.Instance.Placeholder); Assert.Equal(loc[nameof(Resources.TerminalStrings.TerminalToolbarGridSize)].Value, dimensionsSelect.Instance.AriaLabel); } @@ -340,6 +340,7 @@ public async Task DisconnectedSizePicker_DisplaysRetainedDimensionsWhileDisabled var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); module.Setup("initTerminal", _ => true).SetResult(1); var cut = Render(builder => builder.Add(p => p.ResourceName, "shell")); + Assert.Null(cut.FindComponent>().Instance.Placeholder); await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState { TerminalId = 1, Generation = 2, Disconnected = true, @@ -347,6 +348,7 @@ await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalTool })); var picker = cut.FindComponent>().Instance; Assert.True(picker.Disabled); + Assert.Null(picker.Placeholder); Assert.Equal("97x38", picker.Value); Assert.NotNull(picker.Items); Assert.Equal([new("97x38", "97\u00d738", 97, 38), new TerminalSizePreset("80x24", "80\u00d724", 80, 24)], picker.Items); From 98cde65328cad1ba79cf1758c316b0f0a5a1b31c Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 18:14:43 +1100 Subject: [PATCH 18/21] Preserve terminal renderer after Hex1b post-close status Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Components/Controls/TerminalView.razor.js | 6 ++++++ .../JavaScript/TerminalView.test.mjs | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index f91f392d0ea..69bada37cf3 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -701,6 +701,12 @@ async function mountClient(state, generation, controller) { if (level !== "error" || (!retained && (!current() || state.ended))) { return; } + // The worker emits this status after onClose, including for preserved + // views: "View disconnected (1001: Going away). Attach another view to reconnect." + // It describes the closed transport, not a failed renderer. + if (state.disconnected && /^View disconnected \([\s\S]*\)\. Attach another view to reconnect\.$/.test(message)) { + return; + } // Hex1b 0.173.0 prefixes nonfatal inspection/action errors, e.g. // "Scrollbar: Terminal disconnected before the marker operation completed." // These can arrive after onClose; only unprefixed renderer/worker errors diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index aaf183fa9f5..cedeebd6d0c 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -174,6 +174,8 @@ beforeEach(() => { close(code, reason = "", wasClean = true) { client.connected = false; options.onClose({ code, reason, wasClean }); + // Match the worker's closed notification followed by its error-level status. + options.onStatus(`View disconnected (${code}${reason ? `: ${reason}` : ""}). Attach another view to reconnect.`, "error"); }, role(primary) { client.peer = { ...client.peer, primaryId: primary ? client.peer.id : "cli-1", isPrimary: primary }; From ecf71e9348272ebff714c8539214308dab3408c1 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 23:33:08 +1100 Subject: [PATCH 19/21] Handle completed terminal renderer failures and preserve action errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specs/with-terminal.md | 4 ++ .../Controls/TerminalDisconnectedBanner.razor | 21 ++++---- .../TerminalDisconnectedBanner.razor.cs | 8 ++- .../Components/Controls/TerminalView.razor | 5 +- .../Components/Controls/TerminalView.razor.js | 14 ++++- .../Controls/TerminalViewTests.cs | 40 +++++++++++++++ .../JavaScript/TerminalView.test.mjs | 51 ++++++++++++++++++- 7 files changed, 128 insertions(+), 15 deletions(-) diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index 16529313f16..518f161465a 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -376,6 +376,10 @@ closed. Rebinding to another endpoint discards the previous screen. Retention does not imply producer completion or guarantee output that was never received. A fatal error in a retained renderer releases that unusable view and surfaces an error without canceling a pending replacement connection. +If a completed terminal's renderer fails, the recovery banner reports unavailable +output instead of claiming it is preserved, retains applicable restart and close +actions, and does not reopen the completed endpoint. Failed automatic retries +do not dismiss unrelated local-action errors such as a failed palette save. Nonfatal scrollbar, selection UI, and link-detection errors do not discard retained output. The terminal host defers viewer handshakes until its producer has connected, including after a recycle. Its listening socket alone is not proof of a live diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor index 014ce771750..d738e02ef4f 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor @@ -1,6 +1,6 @@ @namespace Aspire.Dashboard.Components.Controls -@if (_collapsed) +@if (_collapsed && !OutputUnavailable) {
- @Message + @Message @if (OnReconnect.HasDelegate) { } - - @Loc[nameof(Resources.TerminalStrings.TerminalShowOutput)] - + @if (!OutputUnavailable) + { + + @Loc[nameof(Resources.TerminalStrings.TerminalShowOutput)] + + }
} diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs index 86ffe3671ea..0c56a1a9543 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/TerminalDisconnectedBanner.razor.cs @@ -30,6 +30,10 @@ public sealed partial class TerminalDisconnectedBanner : ComponentBase, IAsyncDi [Parameter] public bool ReadOnly { get; set; } + /// Gets or sets whether the disconnected renderer can no longer display its output. + [Parameter] + public bool OutputUnavailable { get; set; } + /// Raised when the user closes the owning dock tab. [Parameter] public EventCallback OnClose { get; set; } @@ -65,7 +69,9 @@ public sealed partial class TerminalDisconnectedBanner : ComponentBase, IAsyncDi private int? ExitCode => Resource is { } resource && resource.TryGetExitCode(out var exitCode) ? exitCode : null; - private string Message => ExitCode is { } exitCode + private string Message => OutputUnavailable + ? Loc[nameof(Resources.TerminalStrings.TerminalMountFailed)].Value + : ExitCode is { } exitCode ? Loc[nameof(Resources.TerminalStrings.TerminalDisconnectedWithExitCode), exitCode].Value : Loc[nameof(Resources.TerminalStrings.TerminalDisconnectedMessage)].Value; diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index ff98097bad0..7224361ae06 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -31,15 +31,16 @@
@if (_state.Disconnected || _terminalError is not null) { -
+
@if (ShowDisconnectedRecovery) { } - @if (_terminalError is not null && !(ShowDisconnectedRecovery && _terminalError is "disconnected")) + @if (_terminalError is not null && !(ShowDisconnectedRecovery && _terminalError is "disconnected" or "renderer-failed")) {
@GetErrorMessage() diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index 69bada37cf3..f1cee235144 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -363,7 +363,9 @@ function connectionFailed(state, generation, error, preserve) { state.pendingSizing = null; console.warn("Dashboard terminal connection failed.", error); releaseClient(state, preserve); - state.error = state.retainedClient ? null : "mount-failed"; + if (!state.retainedClient) { + state.error = "mount-failed"; + } scheduleReconnect(state, generation); notifyToolbar(state); } @@ -698,7 +700,8 @@ async function mountClient(state, generation, controller) { renderer: /\bFirefox\//.test(navigator.userAgent) ? "webgl2" : "auto", onStatus(message, level) { const retained = !state.disposed && state.retainedClient?.controller === controller; - if (level !== "error" || (!retained && (!current() || state.ended))) { + const completed = !state.disposed && state.ended && state.controller === controller && state.client !== null; + if (level !== "error" || (!retained && !completed && (!current() || state.ended))) { return; } // The worker emits this status after onClose, including for preserved @@ -725,6 +728,13 @@ async function mountClient(state, generation, controller) { notifyToolbar(state); return; } + if (completed) { + console.warn("Dashboard completed terminal renderer failed.", message); + releaseClient(state); + state.error = "renderer-failed"; + notifyToolbar(state); + return; + } // Fatal renderer and transport failures disconnect before reporting status. connectionFailed(state, generation, message, false); }, diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs index 05abe1239bb..82fc0095e0a 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs @@ -1,9 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading.Channels; using Aspire.Dashboard.Components.Controls; using Aspire.Dashboard.Components.Tests.Shared; +using Aspire.Dashboard.Model; using Aspire.Dashboard.Terminal; +using Aspire.Dashboard.Tests.Shared; +using Aspire.Tests.Shared.DashboardModel; using Bunit; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -566,6 +570,42 @@ await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalTool Assert.Single(module.Invocations, i => i.Identifier == "reconnectTerminal"); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Disconnect_CompletedRendererFailureKeepsRecoveryActionsWithoutClaimingPreservedOutput(bool collapsed) + { + var resource = ModelTestHelpers.CreateResource(resourceName: "shell", state: KnownResourceState.Finished, + commands: [new CommandViewModel(CommandViewModel.StartCommand, CommandViewModelState.Enabled, + "Start", string.Empty, string.Empty, [], false, "ArrowClockwise", IconVariant.Regular)]); + var client = new TestDashboardClient(isEnabled: true, initialResources: [resource], + resourceChannelProvider: () => Channel.CreateUnbounded>()); + Services.AddSingleton(client); + var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); + module.Setup("initTerminal", _ => true).SetResult(1); + var closed = false; + var cut = Render(builder => builder + .Add(p => p.ResourceName, "shell") + .Add(p => p.OnClose, () => closed = true)); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 1, Disconnected = true + })); + if (collapsed) + { + await cut.Find(".terminal-show-output").ClickAsync(new()); + } + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalToolbarState + { + TerminalId = 1, Generation = 1, Disconnected = true, Error = "renderer-failed" + })); + cut.WaitForAssertion(() => Assert.Single(cut.FindAll(".terminal-restart-resource"))); + Assert.Equal(Resources.TerminalStrings.TerminalMountFailed, cut.Find("[role=alert]").TextContent); + Assert.Empty(cut.FindAll(".terminal-disconnected-banner [role=status], .terminal-show-output, .terminal-show-banner, .terminal-error, .terminal-overlays-collapsed")); + await cut.Find(".terminal-close-tab").ClickAsync(new()); + Assert.True(closed); + } + [Fact] public async Task Disconnect_SuccessfulResourceRestartReconnectsExhaustedViewWithoutReplacingContainer() { diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index cedeebd6d0c..d47bc5a4eb9 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -2080,7 +2080,7 @@ test("authoritative close keeps the existing presentation read-only without remo attempts[0].resolve(); await settle(); attempts[0].close(4000); - attempts[0].options.onStatus("Late transport error", "error"); + attempts[0].options.onStatus("View disconnected (4000). Attach another view to reconnect.", "error"); await settle(); assert.equal(terminal.getTerminalSnapshot(element).ended, true); assert.equal(terminal.getToolbarState(id).disconnected, true); @@ -2273,6 +2273,26 @@ test("exhausted retries keep the final screen until the view is disposed", async assert.equal(attempts[0].client.disposed, true); }); +test("failed automatic retries preserve palette errors until dismissal", async () => { + const { id, element } = mount(); + const original = attempts[0]; + original.client.screenText = "Retained output"; + original.resolve(); + await settle(); + original.close(1006); + retry(); + mock.method(localStorage, "setItem", () => { throw new Error("Storage disabled"); }); + terminal.setPaletteFromHost(id, "light"); + attempts[1].reject(); + await settle(); + assert.equal(terminal.getToolbarState(id).error, "palette-failed"); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Retained output"); + assert.equal(original.client.disposed, false); + terminal.dismissError(id); + assert.equal(terminal.getToolbarState(id).error, null); + assert.equal(timers.size, 1); +}); + test("palette failures and dismissal preserve retry exhaustion until an explicit reconnect", async () => { const { id } = mount({ options: { readOnly: true } }); attempts[0].resolve(); @@ -2436,6 +2456,35 @@ for (const pendingReplacement of [false, true]) { }); } +test("fatal completed renderer errors release output without reopening the ended endpoint", async () => { + const { id, element } = mount(); + const original = attempts[0]; + original.client.screenText = "Completed output"; + original.resolve(); + await settle(); + original.close(4000); + original.options.onStatus("Scrollbar: Terminal disconnected before the marker operation completed.", "error"); + assert.equal(original.client.disposed, false); + assert.equal(terminal.getToolbarState(id).error, null); + original.options.onStatus("WebGL2 context lost", "error"); + assert.equal(original.client.disposed, true); + assert.equal(original.options.signal.aborted, true); + assert.equal(terminal.getTerminalSnapshot(element).screenText, ""); + assert.equal(terminal.getTerminalSnapshot(element).ended, true); + assert.equal(terminal.getToolbarState(id).error, "renderer-failed"); + assert.equal(timers.size, 0); + terminal.reconnectTerminal(id, original.options.url); + assert.equal(attempts.length, 1); + original.options.onStatus("Late transport failure", "error"); + assert.equal(terminal.getToolbarState(id).error, "renderer-failed"); + terminal.restartTerminal(id); + assert.equal(attempts.length, 2); + attempts[1].resolve(); + await settle(); + assert.equal(terminal.getToolbarState(id).connected, true); + assert.equal(terminal.getToolbarState(id).error, null); +}); + test("disposing a view ignores later native close callbacks", async () => { const { id } = mount(); terminal.disposeTerminal(id); From c907a71463340c514e249e45a40e34e08326eae3 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 23:47:46 +1100 Subject: [PATCH 20/21] Separate terminal projection failures from dismissible action errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specs/with-terminal.md | 3 ++ .../Components/Controls/TerminalView.razor | 13 ++++-- .../Components/Controls/TerminalView.razor.cs | 9 +++- .../Components/Controls/TerminalView.razor.js | 20 ++++++--- .../Controls/TerminalViewTests.cs | 33 ++++++++++++++ .../JavaScript/TerminalView.test.mjs | 44 ++++++++++++++++++- 6 files changed, 108 insertions(+), 14 deletions(-) diff --git a/docs/specs/with-terminal.md b/docs/specs/with-terminal.md index 518f161465a..065c577c9be 100644 --- a/docs/specs/with-terminal.md +++ b/docs/specs/with-terminal.md @@ -380,6 +380,9 @@ If a completed terminal's renderer fails, the recovery banner reports unavailabl output instead of claiming it is preserved, retains applicable restart and close actions, and does not reopen the completed endpoint. Failed automatic retries do not dismiss unrelated local-action errors such as a failed palette save. +Connection and renderer failures remain independent of dismissible action errors; +failed palette saves and their dismissal cannot restore an output-preserved claim +or remove the connection failure's retry action when no usable projection remains. Nonfatal scrollbar, selection UI, and link-detection errors do not discard retained output. The terminal host defers viewer handshakes until its producer has connected, including after a recycle. Its listening socket alone is not proof of a live diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor index 7224361ae06..5184dad67ee 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor @@ -31,19 +31,26 @@
@if (_state.Disconnected || _terminalError is not null) { -
+
@if (ShowDisconnectedRecovery) { } + @if (ConnectionError is "mount-failed" && _terminalError is not "mount-failed") + { +
+ @GetErrorMessage(ConnectionError) + @Loc[nameof(Resources.TerminalStrings.TerminalRetry)] +
+ } @if (_terminalError is not null && !(ShowDisconnectedRecovery && _terminalError is "disconnected" or "renderer-failed")) {
- @GetErrorMessage() + @GetErrorMessage(_terminalError) @if (_terminalError is "input-failed" or "sizing-failed" or "palette-failed") { @Loc[nameof(Resources.TerminalStrings.TerminalDismissError)] diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs index 83ad5429d4c..a09ede3fd3b 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.cs @@ -440,9 +440,12 @@ private string BuildWebSocketUrl(string pathAndQuery) // A failed mount may follow a transport close before any frame was received. // Show its connection/renderer error, not a claim that output was preserved. - private bool ShowDisconnectedRecovery => _state.Disconnected && _terminalError is not "mount-failed"; + private string? ConnectionError => _state.ConnectionError ?? + (_terminalError is "mount-failed" or "renderer-failed" or "disconnected" ? _terminalError : null); - private string GetErrorMessage() => Loc[_terminalError switch + private bool ShowDisconnectedRecovery => _state.Disconnected && ConnectionError is not "mount-failed"; + + private string GetErrorMessage(string? error) => Loc[error switch { "disconnected" => nameof(Resources.TerminalStrings.TerminalDisconnected), "input-failed" => nameof(Resources.TerminalStrings.TerminalInputFailed), @@ -604,6 +607,8 @@ public sealed record TerminalToolbarState public int Rows { get; init; } /// The localized error category, or null when healthy. public string? Error { get; init; } + /// The persistent connection or renderer failure, independent of dismissible action errors. + public string? ConnectionError { get; init; } } /// A named grid preset exposed by the JS terminal. diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index f1cee235144..2218c0d4867 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -340,7 +340,7 @@ function scheduleReconnect(state, generation) { } if (state.attempts >= MAX_RECONNECT_ATTEMPTS) { state.retriesExhausted = true; - state.error ??= "disconnected"; + state.connectionError ??= "disconnected"; notifyToolbar(state); return; } @@ -364,7 +364,7 @@ function connectionFailed(state, generation, error, preserve) { console.warn("Dashboard terminal connection failed.", error); releaseClient(state, preserve); if (!state.retainedClient) { - state.error = "mount-failed"; + state.connectionError = "mount-failed"; } scheduleReconnect(state, generation); notifyToolbar(state); @@ -394,7 +394,7 @@ function connectionClosed(state, generation, details) { state.pendingSizing = null; state.autoFitPending = false; state.waitingForVisibility = false; - state.error = null; + state.connectionError = null; cancelReconnect(state); state.client?.setReadOnly(true); // Keep an already mounted projection until the user dismisses the view. @@ -724,14 +724,14 @@ async function mountClient(state, generation, controller) { // error must release that renderer, not cancel its pending replacement. console.warn("Dashboard retained terminal renderer failed.", message); releaseRetainedClient(state); - state.error = "mount-failed"; + state.connectionError = "mount-failed"; notifyToolbar(state); return; } if (completed) { console.warn("Dashboard completed terminal renderer failed.", message); releaseClient(state); - state.error = "renderer-failed"; + state.connectionError = "renderer-failed"; notifyToolbar(state); return; } @@ -781,6 +781,7 @@ async function mountClient(state, generation, controller) { // Mount resolves after a replacement frame arrives, even if completion was // reported in the same turn. The old projection must not obscure that frame. releaseRetainedClient(state); + state.connectionError = null; if (state.ended) { notifyToolbar(state); return; @@ -790,7 +791,6 @@ async function mountClient(state, generation, controller) { state.peer = client.peer; state.geometry = client.geometry; state.sizing = client.sizing; - state.error = null; state.attempts = 0; applyPendingFocus(state); applyAutoFit(state); @@ -877,6 +877,7 @@ export function initTerminal(element, wsUrl, dotNetRef, options, selectionTempla pendingSizing: null, autoFitPending: false, error: null, + connectionError: null, generation: 0, attempts: 0, reconnectTimer: null, @@ -969,6 +970,7 @@ export function reconnectTerminal(id, wsUrl) { } if (state.wsUrl !== wsUrl) { releaseRetainedClient(state); + state.connectionError = null; state.disconnected = false; state.title = ""; state.workingDirectory = null; @@ -979,6 +981,9 @@ export function reconnectTerminal(id, wsUrl) { state.ended = false; state.attempts = 0; state.error = null; + if (state.connectionError === "renderer-failed") { + state.connectionError = "mount-failed"; + } requestFocus(state); connectClient(state); return state.generation; @@ -1131,7 +1136,8 @@ export function getToolbarState(id) { fitEnabled: !state.readOnly && (isPrimary || canTakeControl) && !(isPrimary && state.sizing.mode === "auto"), cols: state.geometry?.columns ?? 0, rows: state.geometry?.rows ?? 0, - error: state.error, + error: state.error ?? state.connectionError, + connectionError: state.connectionError, }; } diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs index 82fc0095e0a..ddc081ac5f9 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/TerminalViewTests.cs @@ -606,6 +606,39 @@ await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(new TerminalTool Assert.True(closed); } + [Theory] + [InlineData("mount-failed")] + [InlineData("renderer-failed")] + public async Task Disconnect_PaletteFailureAndDismissalDoNotChangeProjectionFailure(string connectionError) + { + var module = TerminalSetupHelpers.SetupTerminalViewModule(this, "/Components/Controls/TerminalView.razor.js"); + module.Setup("initTerminal", _ => true).SetResult(1); + var cut = Render(builder => builder + .Add(p => p.ResourceName, "shell") + .Add(p => p.OnClose, () => { })); + var state = new TerminalToolbarState + { + TerminalId = 1, Generation = 1, Disconnected = true, + ConnectionError = connectionError, Error = connectionError + }; + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state)); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state with { Error = "palette-failed" })); + Assert.Equal( + new[] { Resources.TerminalStrings.TerminalMountFailed, Resources.TerminalStrings.TerminalPaletteSaveFailed }, + cut.FindAll("[role=alert]").Select(e => e.TextContent)); + Assert.Empty(cut.FindAll(".terminal-show-output, .terminal-show-banner")); + Assert.Equal(connectionError == "renderer-failed" ? 1 : 0, cut.FindAll(".terminal-close-tab").Count); + if (connectionError == "mount-failed") + { + Assert.Equal(2, cut.FindAll(".terminal-error fluent-button").Count); + } + await cut.FindAll(".terminal-error fluent-button")[^1].ClickAsync(new()); + Assert.Single(module.Invocations, invocation => invocation.Identifier == "dismissError"); + await cut.InvokeAsync(() => cut.Instance.OnTerminalStateChanged(state)); + Assert.Equal(Resources.TerminalStrings.TerminalMountFailed, cut.Find("[role=alert]").TextContent); + Assert.Empty(cut.FindAll(".terminal-show-output, .terminal-show-banner")); + } + [Fact] public async Task Disconnect_SuccessfulResourceRestartReconnectsExhaustedViewWithoutReplacingContainer() { diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index d47bc5a4eb9..b1b51b82ca3 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -1063,7 +1063,7 @@ test("init returns an id while mount waits for its first connected frame", async fontPx: 13, fontControlsEnabled: true, sizeSelectEnabled: true, fitEnabled: true, canDecreaseFontSize: true, canIncreaseFontSize: true, - cols: 100, rows: 30, error: null, + cols: 100, rows: 30, error: null, connectionError: null, }); assert.equal(attempts[0].client.primaryRequests, 0); assert.equal(typeof attempts[0].options.onInput, "function"); @@ -2308,7 +2308,7 @@ test("palette failures and dismissal preserve retry exhaustion until an explicit assert.equal(terminal.getToolbarState(id).error, "palette-failed"); assert.equal(terminal.getToolbarState(id).retriesExhausted, true); terminal.dismissError(id); - assert.equal(terminal.getToolbarState(id).error, null); + assert.equal(terminal.getToolbarState(id).error, "disconnected"); assert.equal(terminal.getToolbarState(id).retriesExhausted, true); assert.equal(timers.size, 0); terminal.reconnectTerminal(id, attempts[0].options.url); @@ -2456,6 +2456,46 @@ for (const pendingReplacement of [false, true]) { }); } +for (const failure of ["mount", "retained", "completed"]) { + test(`palette failure and dismissal preserve ${failure} projection failure`, async () => { + const { id, element } = mount(); + const original = attempts[0]; + if (failure !== "mount") { + original.resolve(); + await settle(); + original.close(failure === "completed" ? 4000 : 1006); + original.options.onStatus("WebGL2 context lost", "error"); + } else { + original.close(1006); + original.reject(); + await settle(); + } + const connectionError = failure === "completed" ? "renderer-failed" : "mount-failed"; + assert.equal(terminal.getToolbarState(id).connectionError, connectionError); + mock.method(localStorage, "setItem", () => { throw new Error("Storage disabled"); }); + terminal.setPaletteFromHost(id, "light"); + assert.equal(terminal.getToolbarState(id).error, "palette-failed"); + assert.equal(terminal.getToolbarState(id).connectionError, connectionError); + terminal.dismissError(id); + assert.equal(terminal.getToolbarState(id).error, connectionError); + assert.equal(terminal.getToolbarState(id).connectionError, connectionError); + assert.equal(terminal.getTerminalSnapshot(element).screenText, ""); + assert.equal(terminal.getTerminalSnapshot(element).ended, failure === "completed"); + if (failure === "completed") { + terminal.restartTerminal(id); + } else { + retry(); + } + assert.equal(terminal.getToolbarState(id).connectionError, "mount-failed"); + attempts.at(-1).client.screenText = "Recovered output"; + attempts.at(-1).resolve(); + await settle(); + assert.equal(terminal.getToolbarState(id).connectionError, null); + assert.equal(terminal.getToolbarState(id).error, null); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Recovered output"); + }); +} + test("fatal completed renderer errors release output without reopening the ended endpoint", async () => { const { id, element } = mount(); const original = attempts[0]; From bd193f584b1a221e32397889c719f6501c1e3e0f Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 16 Oct 2026 23:51:28 +1100 Subject: [PATCH 21/21] Keep recovery actions after an ended retry loses its retained renderer Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Components/Controls/TerminalView.razor.js | 2 +- .../JavaScript/TerminalView.test.mjs | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js index 2218c0d4867..6906f4db026 100644 --- a/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js +++ b/src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js @@ -724,7 +724,7 @@ async function mountClient(state, generation, controller) { // error must release that renderer, not cancel its pending replacement. console.warn("Dashboard retained terminal renderer failed.", message); releaseRetainedClient(state); - state.connectionError = "mount-failed"; + state.connectionError = state.ended ? "renderer-failed" : "mount-failed"; notifyToolbar(state); return; } diff --git a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs index b1b51b82ca3..49f7715620a 100644 --- a/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs +++ b/tests/Aspire.Dashboard.Components.Tests/JavaScript/TerminalView.test.mjs @@ -2496,6 +2496,38 @@ for (const failure of ["mount", "retained", "completed"]) { }); } +test("fatal retained renderer failure after a frameless ended retry preserves completed recovery", async () => { + const { id, element } = mount(); + const original = attempts[0]; + original.client.screenText = "Retained final output"; + original.resolve(); + await settle(); + original.close(1006); + retry(); + const replacement = attempts[1]; + replacement.close(4000); + replacement.reject(); + await settle(); + assert.equal(terminal.getTerminalSnapshot(element).ended, true); + assert.equal(terminal.getTerminalSnapshot(element).screenText, "Retained final output"); + original.options.onStatus("WebGL2 context lost", "error"); + assert.equal(original.client.disposed, true); + assert.equal(original.options.signal.aborted, true); + assert.equal(terminal.getTerminalSnapshot(element).screenText, ""); + assert.equal(terminal.getTerminalSnapshot(element).ended, true); + assert.equal(terminal.getToolbarState(id).connectionError, "renderer-failed"); + assert.equal(terminal.getToolbarState(id).disconnected, true); + assert.equal(timers.size, 0); + terminal.reconnectTerminal(id, original.options.url); + assert.equal(attempts.length, 2); + terminal.restartTerminal(id); + assert.equal(attempts.length, 3); + attempts[2].resolve(); + await settle(); + assert.equal(terminal.getToolbarState(id).connected, true); + assert.equal(terminal.getToolbarState(id).connectionError, null); +}); + test("fatal completed renderer errors release output without reopening the ended endpoint", async () => { const { id, element } = mount(); const original = attempts[0];