Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
37b85f6
Preserve disconnected terminal output and add recovery actions
mitchdenny Oct 4, 2026
b4d7d37
Show reported resource exit codes in terminal recovery banners
mitchdenny Oct 4, 2026
16c20fb
Repoint Hex1b dependencies to stable 0.173.0
mitchdenny Oct 8, 2026
2a65836
Remove temporary Hex1b feed now stable packages are mirrored
mitchdenny Oct 15, 2026
6dfdbdb
Address terminal recovery and CI routing review feedback
mitchdenny Oct 15, 2026
85399e1
Merge main and preserve terminal disconnect recovery
mitchdenny Oct 15, 2026
d892cef
Keep terminal action errors visible when recovery is collapsed
mitchdenny Oct 15, 2026
46873c9
Avoid duplicate terminal recovery banners after refresh
mitchdenny Oct 16, 2026
738a258
Offer non-mutating terminal reconnect after retry exhaustion
mitchdenny Oct 16, 2026
156f1e7
Soften terminal recovery banner styling
mitchdenny Oct 16, 2026
d92be3e
Preserve first frame when transport closes before mount continuation
mitchdenny Oct 16, 2026
d6a503d
Match terminal recovery button hover to standard controls
mitchdenny Oct 16, 2026
0cdfe07
Keep terminal recovery state independent of action and renderer errors
mitchdenny Oct 16, 2026
9c50792
Preserve disconnected terminal output on nonfatal inspection errors
mitchdenny Oct 16, 2026
fc368ba
Restore dock focus after closing a terminal from its recovery banner
mitchdenny Oct 16, 2026
b4e36a1
Suppress unavailable terminal text history popup
mitchdenny Oct 16, 2026
5d663d3
Keep retained terminal dimensions visible during reconnect
mitchdenny Oct 16, 2026
006c11a
Leave terminal size picker empty when dimensions are unavailable
mitchdenny Oct 16, 2026
98cde65
Preserve terminal renderer after Hex1b post-close status
mitchdenny Oct 16, 2026
5a63dcb
Merge main into terminal disconnect treatment
mitchdenny Oct 16, 2026
ecf71e9
Handle completed terminal renderer failures and preserve action errors
mitchdenny Oct 16, 2026
c907a71
Separate terminal projection failures from dismissible action errors
mitchdenny Oct 16, 2026
bd193f5
Keep recovery actions after an ended retry loses its retained renderer
mitchdenny Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
]
},
"hex1b.tool": {
"version": "0.172.0",
"version": "0.173.0",
"commands": [
"hex1b"
]
Expand Down
2 changes: 1 addition & 1 deletion .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "stdio",
"command": "dnx",
"args": [
"Hex1b.McpServer@0.172.0",
"Hex1b.McpServer@0.173.0",
"--yes"
]
},
Expand Down
6 changes: 3 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@
<PackageVersion Include="Grpc.Tools" Version="2.83.0" />
<!-- Keep compatible with @hex1b/web-terminal in src/Aspire.Dashboard/package.json.
Native-only Hex1b builds can differ without changing the web terminal package. -->
<PackageVersion Include="Hex1b" Version="0.172.0" />
<PackageVersion Include="Hex1b.McpServer" Version="0.172.0" />
<PackageVersion Include="Hex1b.Tool" Version="0.172.0" />
<PackageVersion Include="Hex1b" Version="0.173.0" />
<PackageVersion Include="Hex1b.McpServer" Version="0.173.0" />
<PackageVersion Include="Hex1b.Tool" Version="0.173.0" />
<PackageVersion Include="Humanizer.Core" Version="3.0.10" />
<PackageVersion Include="KubernetesClient" Version="19.0.2" />
<PackageVersion Include="Markdig" Version="1.3.2" />
Expand Down
64 changes: 60 additions & 4 deletions docs/specs/with-terminal.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,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
Expand Down Expand Up @@ -333,6 +335,60 @@ 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
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. 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. 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
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
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.
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
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.
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.
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
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
Expand All @@ -348,7 +404,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.172.0`. 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.
Expand Down
1 change: 0 additions & 1 deletion eng/github-ci/ci-skip-entirely-patterns.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/preload-azure-cli-requests/**

Expand Down
2 changes: 2 additions & 0 deletions eng/github-ci/test-trigger-map.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@namespace Aspire.Dashboard.Components.Controls

@if (_collapsed && !OutputUnavailable)
{
<div class="terminal-disconnected-indicator">
<FluentButton @ref="_toggleButton"
Appearance="ButtonAppearance.Default"
IconOnly="true"
Class="terminal-show-banner"
Title="@ShowBannerLabel"
aria-label="@ShowBannerLabel"
aria-expanded="false"
OnClick="@(() => SetCollapsedAsync(false))">
<FluentIcon Value="@(new Icons.Regular.Size20.PlugDisconnected())" Color="Color.Default" />
</FluentButton>
</div>
}
else
{
<div class="terminal-disconnected-banner">
<FluentIcon Value="@(new Icons.Regular.Size20.PlugDisconnected())" Color="Color.Default" />
<span role="@(OutputUnavailable ? "alert" : "status")">@Message</span>
@if (OnReconnect.HasDelegate)
{
<FluentButton Appearance="ButtonAppearance.Default"
Class="terminal-reconnect"
OnClick="@OnReconnect">
@Loc[nameof(Resources.TerminalStrings.TerminalRetry)]
</FluentButton>
}
@if (RestartCommand is not null)
{
<FluentButton Appearance="ButtonAppearance.Default"
Class="terminal-restart-resource"
Disabled="@RestartDisabled"
OnClick="@RestartResourceAsync">
@Loc[nameof(Resources.TerminalStrings.TerminalRestartResource)]
</FluentButton>
}
@if (OnClose.HasDelegate)
{
<FluentButton Appearance="ButtonAppearance.Default"
Class="terminal-close-tab"
Disabled="@_executing"
OnClick="@CloseAsync">
@Loc[nameof(Resources.TerminalStrings.TerminalCloseTab)]
</FluentButton>
}
@if (!OutputUnavailable)
{
<FluentButton @ref="_toggleButton"
Appearance="ButtonAppearance.Default"
Class="terminal-show-output"
aria-expanded="true"
OnClick="@(() => SetCollapsedAsync(true))">
@Loc[nameof(Resources.TerminalStrings.TerminalShowOutput)]
</FluentButton>
}
</div>
}
Loading
Loading