Skip to content

Add AppHost terminals, dashboard docking, and terminal automation - #19887

Merged
Mitch Denny (mitchdenny) merged 116 commits into
mainfrom
mitchdenny-terminal-interaction-input-spike
Sep 18, 2026
Merged

Mitch Denny (mitchdenny) merged 116 commits into
mainfrom
mitchdenny-terminal-interaction-input-spike

Conversation

@mitchdenny

@mitchdenny Mitch Denny (mitchdenny) commented Sep 3, 2026 •

Copy link
Copy Markdown
Member

Features

Extends Aspire's existing WithTerminal() support to interactive processes owned by the AppHost, dashboard terminal dialogs and a persistent dock, detached windows, and C#/CLI automation. This enables workflows such as authenticating an external tool, running interactive setup, or opening a container shell from a resource command.

  • AppHost-owned terminals: experimental TerminalService, AspireTerminal, and TerminalLaunchOptions APIs create interactive processes independently of resources orchestrated by DCP. Placement can be Dock, Dialog, or None for headless automation; the public API does not expose Hex1b types.
  • Terminal dialogs: IInteractionService.PromptTerminalAsync follows the progress-dialog pattern, with a terminal instead of a spinner. An optional TerminalInteractionOptions.Work callback can drive the terminal while the user watches; successful completion closes the dialog, and Cancel signals the callback's cancellation token.
  • Persistent terminal dock: resizable, tabbed AppHost terminals with activation, switching, closing, and keyboard access. Sessions remain available while navigating elsewhere in the dashboard.
  • Detached windows: open AppHost-owned or resource terminals in a dedicated browser window and return them to the originating view without creating another process. The detached window takes primary sizing ownership and retains the selected font size.
  • State-based terminal rendering: replaces xterm.js with @hex1b/web-terminal, rendering authoritative Hex1b terminal state rather than interpreting terminal output again in the browser. Includes Kitty Graphics Protocol and Sixel support, copy/paste and scrollback improvements, a WebGPU renderer with WebGL2 fallback, and vendored browser modules, workers, fonts, and licenses.
  • Font and sizing controls: font-size adjustment, dimension presets, fit-to-container, and automatic fitting. Dock/window resizing preserves the selected font size, with grid resizing governed by primary-viewer ownership.
  • C# and CLI automation: inspect the screen, send text and keys, wait for output, and drive existing resource sessions. The feature-gated CLI adds a VHS-style tape subset with input/wait sequences, final screen output, and text recordings.

API examples

The examples below use one Alpine container and add commands to it. Place the command registrations between this setup and builder.Build().Run(). All terminal APIs share the existing WithTerminal() diagnostic, ASPIRETERMINAL001, so one suppression covers resource terminals, AppHost-owned terminals, interactions, and automation.

#pragma warning disable ASPIRETERMINAL001

using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Terminals;
using Microsoft.Extensions.DependencyInjection;

var builder = DistributedApplication.CreateBuilder(args);
const string containerName = "terminal-demo-shellbox";
var shellbox = builder.AddContainer("shellbox", "alpine")
    .WithContainerName(containerName)
    .WithArgs("sleep", "infinity");

A command that opens a terminal in the dock

shellbox.WithCommand("open-dock", "Open shell in terminal dock", context =>
{
    var terminals = context.Services.GetRequiredService<TerminalService>();
    var terminal = terminals.CreateTerminal(new TerminalLaunchOptions
    {
        Title = "Container shell",
        Executable = "docker",
        Arguments = ["exec", "-it", containerName, "/bin/sh"],
        Placement = TerminalPlacement.Dock
    });

    terminal.Start();
    terminal.Show();
    return Task.FromResult(CommandResults.Success());
});

Start() starts the process without waiting for it to exit; Show() reveals and activates its dock tab. This command intentionally does not use await using: the terminal must outlive the command. Closing its dock tab or shutting down the AppHost tears down the AppHost-owned session.

Playground: container shell in the dock, with the resource page still available above it.

Container shell in the dashboard terminal dock

A command that opens a terminal dialog

shellbox.WithCommand("open-dialog", "Open shell in terminal dialog", async context =>
{
    var terminals = context.Services.GetRequiredService<TerminalService>();
    var interactions = context.Services.GetRequiredService<IInteractionService>();
    await using var terminal = terminals.CreateTerminal(new TerminalLaunchOptions
    {
        Title = "Container shell",
        Executable = "docker",
        Arguments = ["exec", "-it", containerName, "/bin/sh"],
        Placement = TerminalPlacement.Dialog
    });

    terminal.Start();
    var result = await interactions.PromptTerminalAsync(
        "Interact with the container, then cancel when finished.",
        terminal,
        new TerminalInteractionOptions
        {
            Title = "Container shell",
            PrimaryButtonText = "Cancel"
        },
        context.CancellationToken);

    return result.Canceled
        ? CommandResults.Failure("Canceled")
        : CommandResults.Success();
});

The interaction borrows the terminal; it does not own or dispose it. Here, the command owns the terminal and disposes it when the prompt returns. Without a Work callback, this manual dialog stays open until explicitly completed or canceled; process exit alone does not close it. The same caller-owned terminal can also be reused across prompts when its lifetime is managed outside the command.

Playground: an interactive docker exec shell presented by PromptTerminalAsync.

Container shell in a terminal interaction dialog

Automating a terminal from C#

This command runs an AppHost-owned terminal without showing a dashboard view, sends a command and Enter, waits for its output, and reads the resulting screen.

shellbox.WithCommand("automate-terminal", "Automate a terminal", async context =>
{
    var terminals = context.Services.GetRequiredService<TerminalService>();
    await using var terminal = terminals.CreateTerminal(new TerminalLaunchOptions
    {
        Title = "Automated shell",
        Executable = "docker",
        Arguments = ["exec", "-it", containerName, "/bin/sh"],
        Placement = TerminalPlacement.None
    });

    terminal.Start();
    await terminal.SendTextAsync(
        "printf 'AUTOMATION_%s\\n' ready",
        context.CancellationToken);
    await terminal.SendKeyAsync(AspireTerminalKey.Enter, context.CancellationToken);
    await terminal.WaitForTextAsync(
        "AUTOMATION_ready",
        TimeSpan.FromSeconds(10),
        context.CancellationToken);

    Console.WriteLine(terminal.GetScreenText());
    return CommandResults.Success();
});

The expected output marker is deliberately not present as a contiguous string in the typed command, so shell input echo cannot satisfy the wait before the command runs.

For visible automation, use TerminalPlacement.Dialog and perform the same operations inside TerminalInteractionOptions.Work, using the supplied TerminalContext.CancellationToken. Successful callback completion closes the dialog. Cancellation signals the callback and the prompt waits for it to finish before returning; callback failures propagate after the dialog is removed.

The playground's Number guess command demonstrates this: AppHost code waits for prompts, types guesses, reads the game's responses, and narrows the range until it finds the number. The terminal below is being driven by the AppHost, not by manual typing.

AppHost automation driving the number-guess terminal dialog

Number-guess completion

When Work completes, the terminal dialog closes and the command displays the result. This captured run found 673 in 9 guesses.

Number-guess completion after terminal automation

Automating a resource terminal with a tape

The CLI can drive an existing WithTerminal() resource using a supported VHS-style tape subset. For example, playground/Terminals/repl-basics.tape runs commands in the playground REPL and waits for their output:

Output repl-basics.txt
Set TypingSpeed 50ms
Set WaitTimeout 10s

Wait+Line /repl#[0-9]+>$/
Type "help"
Enter
Wait+Screen /Available commands:/

Wait+Line /repl#[0-9]+>$/
Type "whoami"
Enter
Wait+Screen /repl pid [0-9]+/

Wait+Line /repl#[0-9]+>$/
Type "size"
Enter
Wait+Screen /[0-9]+ cols x [0-9]+ rows/

Wait+Line /repl#[0-9]+>$/
Type "time"
Enter
Wait+Screen /[0-9]+-[0-9]+-[0-9]+ [0-9]+:[0-9]+:[0-9]+/
Wait+Line /repl#[0-9]+>$/

With the playground running, use a CLI built from this PR:

cd playground/Terminals
aspire config set features.terminalCommandsEnabled true
aspire terminal tape play repl --replica 0 --tape-file repl-basics.tape

The playground has two REPL replicas, so the example selects replica 0 explicitly. Playback uses a secondary connection to that session: it does not launch another shell, take primary sizing ownership, resize the terminal, or stop the resource when finished. Input is shared with other viewers, so coordinate playback with anyone using that terminal.

The final screen is printed to stdout, diagnostics go to stderr, and Output records per-command screens to .txt/.ascii files. Output paths are relative to the root tape directory; existing files are not overwritten. This is not full VHS video rendering: unsupported media, clipboard, sizing, and environment commands are rejected during preflight before input is sent.

Playground: the resource terminal after playing the tape above.

Resource terminal showing the output of REPL tape playback

More dashboard UX

All screenshots in this description were captured from the repository-built dashboard running playground/Terminals.

The same session in a separate window

The window button detaches the view without starting another process. The session retains its existing shell output and can be returned to the panel.

AppHost-owned shell in a detached terminal window

Originating dock while its terminal is detached

The dock offers Focus window and Return to panel rather than creating a second terminal.

Originating dock showing detached-window controls

Empty dock and discoverability

The empty state includes the terminal documentation link and keyboard shortcut hint.

Empty terminal dock with documentation and shortcut guidance

Implementation and scope

AppHost-owned terminal sessions are tunneled over the existing dashboard gRPC connection. The browser connects to the dashboard over WebSocket using Hex1b's HWT1 presentation transport; resource terminals continue to use the terminal-host path. Releasing a handle acquired for an existing resource terminal does not stop the resource.

The new AppHost APIs remain experimental, and CLI terminal commands remain feature-gated. This PR does not add an ATS/polyglot terminal API.

Pre-merge checklist

  • Adopt the final stable Hex1b NuGet and @hex1b/web-terminal packages. Current paired version: 0.168.0.
  • Confirm Hex1b 0.168.0 is mirrored into the approved dotnet-public feed.
  • Remove temporary nuget.org sources/mappings from NuGet.config, tests/Shared/TemplatesTesting/data/nuget8.config, and the VS Code E2E config generator in extension/scripts/run-e2e.js; remove the repository/template-test NU1902/NU1903 suppressions at the same time.
  • Confirm Windows x64 CLI publishing and stabilization pass with the correct OpenConsole payload layout.
  • Confirm template package tests pass with the final feed configuration.
  • Confirm full VS Code E2E fixture restores pass with the final internal-only feed configuration.
  • Confirm targeted Dashboard component coverage passes after merging main, including terminal dock and detached-window behavior.
  • Complete review of initial terminal grid behavior and final API/implementation/CSS feedback.
  • Resolve outstanding feedback and obtain green required CI on the final commit.

Adds `InputType.Terminal`, an interaction input whose process is owned by
the AppHost itself rather than orchestrated by Aspire. The session is
tunneled to the browser over the dashboard's existing gRPC connection:

    xterm.js --ws--> dashboard --grpc--> AppHost --> Hex1b PTY

This is the counterpart to `WithTerminal()`. There the terminal attaches
to a resource DCP already runs and the PTY lives in a separate
Aspire.TerminalHost process. Here the AppHost spawns and owns the
process, which makes it possible to drive interactive flows Aspire does
not orchestrate — auth prompts, or `docker exec -it` into a container in
the app model.

The load-bearing seam is that `ITerminalConnectionResolver.ConnectAsync`
returns a plain `Stream` and `TerminalWebSocketProxy.BridgeAsync` is a
transport-agnostic byte pump. Surfacing the gRPC tunnel as a `Stream`
makes the entire existing dashboard -> browser stack (websocket proxy,
hmp1-client.js, xterm.js) reusable verbatim; the browser needs no
protocol changes.

AppHost:
- `AttachTerminal` bidi RPC plus `INPUT_TYPE_TERMINAL` in the dashboard
  service proto.
- `GrpcTerminalStream` adapts the bidi call to a duplex `Stream`.
- `InteractionTerminalSessionStore` owns session lifetime: lazy start on
  first attach, multiple viewers, teardown when the interaction
  completes or is cancelled.

Dashboard:
- `GrpcTerminalClientStream` + `IDashboardClient.AttachInteractionTerminalAsync`.
- New `/api/interaction-terminal` websocket endpoint.
- `TerminalView` is now keyed by resolved endpoint rather than
  resource/replica, so it can serve both endpoints.

Playground (Terminals): an AppHost-owned shell on `repl`, a
`docker exec` shell on `shellbox`, and a Node REPL on `noderepl`.

SPIKE CAVEAT: `InteractionInput.Terminal` takes a `Hex1bTerminalBuilder`,
leaking Hex1b into the Aspire.Hosting API surface. A builder rather than
a built terminal because the HMP1 server transport must be attached
before `Build()`. This shape is deliberately temporary and needs to be
reworked before this is anything but a spike.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3de8011a-08b4-48c9-aa13-f0feef9a378c
Copilot AI balanced review requested due to automatic review settings September 3, 2026 07:09
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19887

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19887"

@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

A critical teardown race and multiple unresolved lifecycle, validation, and WebSocket issues require fixes and human review.

Review tier: Balanced
Findings: 1 High severity · 6 Medium severity

New issues introduced by this change (7)
Severity Finding
High severity src/​Aspire.Hosting/​Dashboard/​InteractionTerminalSessionStore.cs — Canceling _stopCts completes every AttachAsync waiter immediately, before…
Medium severity src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor — A terminal input never supplies a Value, but both the dashboard's required-field check and…
Medium severity src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor — TerminalView is mounted even when InputDisabled is true. Its first render immediately connects…
Medium severity src/​Aspire.Dashboard/​Terminal/​TerminalWebSocketProxy.cs — This does not actually verify the session before accepting the WebSocket.…
Medium severity src/​Aspire.Hosting/​Dashboard/​DashboardService.cs — This streaming RPC bypasses ExecuteAsync and only observes the call token, despite the…
Medium severity src/​Aspire.Hosting/​IInteractionService.cs — InteractionInput is also the public type used by CommandOptions.Arguments, but…
Medium severity src/​Aspire.Hosting/​InteractionService.cs — Terminal inputs never produce a Value, but this validation permits Required = true. Both the…
What changed in this PR

This spike adds AppHost-owned interaction terminals tunneled through the dashboard’s existing gRPC and WebSocket infrastructure.

Changes:

  • Adds terminal interaction APIs, stream adapters, and session lifecycle management.
  • Adds dashboard rendering, proxying, and generalized terminal endpoints.
  • Adds tests, shared fakes, and playground terminal scenarios.
File Review summary
tests/​Shared/​TestInteractionTerminalSessionStore.cs Adds a terminal-session test fake.
tests/​Shared/​TestDashboardClient.cs Stubs terminal attachment.
tests/​Aspire.Hosting.Tests/​Publishing/​PipelineActivityReporterTests.cs Supplies the new dependency.
tests/​Aspire.Hosting.Tests/​Orchestrator/​ParameterProcessorTests.cs Supplies the new dependency.
tests/​Aspire.Hosting.Tests/​Orchestrator/​ApplicationOrchestratorTests.cs Supplies the new dependency.
tests/​Aspire.Hosting.Tests/​InteractionServiceTests.cs Updates service construction.
tests/​Aspire.Hosting.Tests/​Dashboard/​InteractionTerminalSessionStoreTests.cs Tests session lifecycle; teardown timeout handling needs tightening.
tests/​Aspire.Hosting.Tests/​Dashboard/​GrpcTerminalStreamTests.cs Tests server stream framing.
tests/​Aspire.Hosting.Tests/​Dashboard/​DashboardServiceTests.cs Updates service fixtures.
tests/​Aspire.Hosting.Tests/​Dashboard/​DashboardServiceDataTerminalTests.cs Updates terminal data fixtures.
tests/​Aspire.Hosting.Tests/​Aspire.Hosting.Tests.csproj Includes the shared terminal fake.
tests/​Aspire.Dashboard.Tests/​Terminal/​DefaultTerminalConnectionResolverTests.cs Updates the dashboard client fake.
tests/​Aspire.Dashboard.Tests/​ResourceOutgoingPeerResolverTests.cs Updates the dashboard client fake.
tests/​Aspire.Dashboard.Tests/​Integration/​Playwright/​Infrastructure/​MockDashboardClient.cs Updates the Playwright client fake.
src/​Aspire.Hosting/​InteractionService.cs Manages terminal sessions; required-input semantics and lifecycle coverage need correction.
src/​Aspire.Hosting/​IInteractionService.cs Adds the terminal API; cloned command inputs currently lose terminal configuration.
src/​Aspire.Hosting/​DistributedApplicationBuilder.cs Registers the session store.
src/​Aspire.Hosting/​Dashboard/​proto/​dashboard_service.proto Adds terminal frames and bidi RPC.
src/​Aspire.Hosting/​Dashboard/​InteractionTerminalSessionStore.cs Implements sessions; teardown has a critical transport-disposal race.
src/​Aspire.Hosting/​Dashboard/​IInteractionTerminalSessionStore.cs Defines the session-store contract.
src/​Aspire.Hosting/​Dashboard/​GrpcTerminalStream.cs Adapts server-side gRPC to Stream.
src/​Aspire.Hosting/​Dashboard/​DashboardServiceHost.cs Forwards the store into dashboard DI.
src/​Aspire.Hosting/​Dashboard/​DashboardService.cs Handles terminal attachment; host-shutdown cancellation is missing.
src/​Aspire.Hosting/​Aspire.Hosting.csproj Adds the spike Hex1b dependency.
src/​Aspire.Dashboard/​Terminal/​TerminalWebSocketProxy.cs Adds the WebSocket endpoint; pre-upgrade validation and Origin-gate coverage are incomplete.
src/​Aspire.Dashboard/​ServiceClient/​SelectedDashboardClient.cs Delegates terminal attachment.
src/​Aspire.Dashboard/​ServiceClient/​IDashboardClient.cs Adds the attachment contract.
src/​Aspire.Dashboard/​ServiceClient/​GrpcTerminalClientStream.cs Adapts client gRPC streams; focused framing and disposal tests are missing.
src/​Aspire.Dashboard/​ServiceClient/​DashboardClient.cs Opens terminal gRPC calls.
src/​Aspire.Dashboard/​Components/​Interactions/​InteractionsProvider.cs Widens terminal dialogs.
src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor.css Sizes terminal containers.
src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor.cs Builds terminal endpoint URLs.
src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor Renders terminals; disabled and required states are broken, and component coverage is missing.
src/​Aspire.Dashboard/​Components/​Controls/​TerminalView.razor.cs Generalizes terminal endpoint identity.
playground/​Terminals/​Terminals.AppHost/​TerminalInteractionCommands.cs Adds shell and container commands.
playground/​Terminals/​Terminals.AppHost/​AppHost.cs Wires playground scenarios.
Suppressed comments (5)

src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor:210

  • No component test renders this new terminal branch or verifies that the interaction endpoint reaches TerminalView/initTerminal. Existing InteractionsInputDialogTests cover other input types and existing terminal tests cover only resource/replica URLs. Add a bUnit test for a terminal input, including an escaped input name and the explicit endpoint.
                                <div class="interaction-terminal-container" id="@terminalId">
                                    <TerminalView EndpointPathAndQuery="@BuildInteractionTerminalEndpoint(input.ViewModel)" />

src/Aspire.Dashboard/ServiceClient/GrpcTerminalClientStream.cs:19

  • This client-side stream adapter has no focused tests, while the corresponding server adapter does. Add coverage for selector ordering, split/empty response frames, buffer-copy behavior, and disposal/call cancellation; otherwise the dashboard half of the tunnel can regress without the existing GrpcTerminalStreamTests detecting it.
internal sealed class GrpcTerminalClientStream : Stream

src/Aspire.Dashboard/Terminal/TerminalWebSocketProxy.cs:140

  • This new security-sensitive WebSocket route has no endpoint-level regression test for its Origin gate. The existing /api/terminal route has TerminalWebSocketProxyEndpointTests specifically to prove rejected origins never reach the resolver; add equivalent coverage here so route wiring cannot silently bypass the copied CSWSH defense.
        // Same Cross-Site WebSocket Hijacking defense as /api/terminal — see the commentary there. This endpoint is
        // arguably more sensitive because the terminal runs as the AppHost process itself.
        if (!WebSocketOriginValidator.IsSameOrigin(context, out var originLogValue))
        {

src/Aspire.Hosting/InteractionService.cs:218

  • The new fake records start/complete/cancel calls, but no test injects and asserts it; CreateInteractionService always constructs it internally. As a result, regressions in registration and both teardown branches remain undetected. Add lifecycle tests analogous to the existing file-upload-store tests, including cancellation-token cleanup.
                var terminalInputs = inputs
                    .Where(input => input.InputType == InputType.Terminal)
                    .Select(input => (input.Name, Builder: input.Terminal!))
                    .ToArray();
                _terminalSessionStore.StartInteraction(newState.InteractionId, terminalInputs);

tests/Aspire.Hosting.Tests/Dashboard/InteractionTerminalSessionStoreTests.cs:164

  • Catching every exception also swallows the cancellation caused by the two-minute test CTS, so a client that never notices tunnel shutdown waits two minutes and then makes the test pass. Bound this await independently and let a timeout fail the test while still ignoring the expected transport exception.
            await clientRunTask;
        }
        catch (Exception)
        {
            // The client terminal is torn down by the server closing the tunnel; how that surfaces is not under test.

Comment thread src/Aspire.Hosting/Dashboard/InteractionTerminalSessionStore.cs Outdated
Comment thread src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor Outdated
Comment thread src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor Outdated
Comment thread src/Aspire.Dashboard/Terminal/TerminalWebSocketProxy.cs Outdated
Comment thread src/Aspire.Hosting/Dashboard/DashboardService.cs Outdated
Comment thread src/Aspire.Hosting/IInteractionService.cs Outdated
Comment thread src/Aspire.Hosting/InteractionService.cs Outdated
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Generalises the spike-1 interaction terminal into an AppHost-owned terminal
service, then builds a dashboard-wide terminal dock on top of it.

Aspire.Hosting

- New Terminals/ namespace: IAspireTerminal (internal abstraction that keeps
  Hex1b out of the Aspire API surface), TerminalService (registry + change
  fan-out), Hex1bAspireTerminal, TerminalLaunchOptions, TerminalSurface,
  AspireTerminalKey, TerminalChange.
- Deletes InteractionTerminalSessionStore/IInteractionTerminalSessionStore; the
  interaction service is now just one consumer of TerminalService.
- Terminals are addressed by opaque ids instead of (interactionId, inputName),
  so dock tabs and interaction inputs share one tunnel and one websocket
  endpoint (/api/apphost-terminal?terminalId=...).
- Fixes the teardown race flagged in PR feedback by splitting workload
  cancellation from session completion, so AttachAsync waiters are not released
  before DisposeAsync finishes.
- WatchTerminals / CreateDockTerminal / CloseTerminal RPCs for dock discovery
  and lifecycle.
- Minimal automation API (SendTextAsync, SendKeyAsync, WaitForTextAsync,
  GetScreenText) deliberately excluding Hex1b's cell-pattern DSL.

Aspire.Dashboard

- TerminalDock component: tabbed, resizable, collapsible bottom panel. Collapse
  hides rather than unmounts so xterm cell metrics and websockets survive, and
  terminal state lives AppHost-side so it survives browser sessions entirely.
- Toggle via a terminal button in the header icon cluster, plus Ctrl+` (the
  shortcut check is hoisted above the input-focus guard and xterm's key handler
  so a focused terminal cannot swallow it).
- The watch stream starts eagerly so IAspireTerminal.Show() can reveal the dock
  in a browser that has never opened it.

Playground

- Terminals.AppHost gains a "Shell into container (terminal dock)" command on
  shellbox that creates a dock terminal from AppHost code, calls Show(), and
  exercises the automation API end to end.

TerminalService stays internal for this spike. The terminal host used by
DCP-owned processes is untouched.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3de8011a-08b4-48c9-aa13-f0feef9a378c
Dock terminals previously attached as HMP1 secondaries, which locks the
grid to the producer's 80x24 and shrinks the font until that grid fits.
In a 1600x276 dock pane that produced 8px text letterboxed into a quarter
of the available width, with a card border, titlebar and dims readout
wrapped around it.

Add an opt-in chromeless mode to TerminalView:

- buildChrome skips the titlebar entirely and the injected stylesheet
  zeroes the frame border, padding and radius so only the xterm grid
  shows. Frame/body inset constants become per-state helpers so the
  space calculations stay correct.
- applyRoleAwareLayout never takes the secondary branch when chromeless,
  and the terminal auto-promotes to HMP1 primary on hello, so the
  producer PTY resizes to the fitted grid instead of the reverse.
- The initial font comes from --type-ramp-base-font-size so dock
  terminals track dashboard text scaling.

Only TerminalDock opts in; the console logs view and the interaction
input dialog keep their chrome.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3de8011a-08b4-48c9-aa13-f0feef9a378c
Copilot AI review requested due to automatic review settings September 4, 2026 03:25
Captures the planned third surface for resource-attached terminals so the
intent lives in the code rather than in review discussion. Nothing produces
the value yet; the remarks explain that every surface check in
TerminalService is an explicit test for Dock, so a resource terminal
already behaves correctly by default (excluded from the dock tab list,
Show() is a no-op).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3de8011a-08b4-48c9-aa13-f0feef9a378c

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical concurrency, stream-lifetime, synchronization, and test compilation issues remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 7 High severity · 11 Medium severity · 3 Low severity

New issues introduced by this change (16)
Severity Finding
High severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor.cs — The watch loop mutates _terminals and _activeTerminalId on a thread-pool thread because both…
High severity src/​Aspire.Hosting/​Terminals/​Hex1bAspireTerminal.cs — When the attachment token is canceled, this returns immediately even though Hex1b may still be…
High severity src/​Aspire.Hosting/​Terminals/​TerminalService.cs — The snapshot subscription lock does not cover terminal insertion or publication. A creator can…
High severity tests/​Aspire.Dashboard.Tests/​Integration/​Playwright/​Infrastructure/​MockDashboardClient.cs — The Playwright fake no longer satisfies IDashboardClient: this method has the obsolete signature,…
High severity tests/​Aspire.Dashboard.Tests/​ResourceOutgoingPeerResolverTests.cs — This mock still implements the old interaction/name attach method and omits the other three new…
High severity tests/​Aspire.Dashboard.Tests/​Terminal/​DefaultTerminalConnectionResolverTests.cs — This class no longer implements IDashboardClient: the interface uses terminal IDs and also added…
High severity tests/​Shared/​TestDashboardClient.cs — IDashboardClient now requires AttachTerminalAsync, SubscribeTerminalsAsync,…
Medium severity src/​Aspire.Dashboard/​Components/​Layout/​MainLayout.razor — The terminal control is always enabled in historical/read-only mode, but SelectedDashboardClient…
Medium severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor — These dock tabs are clickable div elements with no tab semantics, focusability, or keyboard…
Medium severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor.cs — Once this server stream ends because of a transient AppHost/dashboard disconnect, the exception is…
Medium severity src/​Aspire.Dashboard/​ServiceClient/​DashboardClient.cs — This direct server stream has no recovery path. A transient AppHost/gRPC disconnect makes the…
Medium severity src/​Aspire.Hosting/​IInteractionService.cs — The documentation labels this property experimental, but there is no [Experimental] attribute, so…
Medium severity src/​Aspire.Hosting/​IInteractionService.cs — This enum member is emitted into the Go/Java/Python/Rust SDKs, but InteractionInput.Terminal is…
Low severity src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor.cs — The documentation still describes the abandoned interaction-ID/input-name addressing scheme, while…
Low severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor.cs — There is no component coverage for this new stateful dock even though it handles asynchronous…
Low severity src/​Aspire.Dashboard/​ServiceClient/​GrpcTerminalClientStream.cs — This comment is stale: payload frames no longer carry interaction ID or input name; only the…
Pre-existing issues (5)
Severity Finding
Medium severity src/​Aspire.Hosting/​InteractionService.cs — Terminal inputs never produce a Value, but this validation permits Required = true. Both the… View comment
Medium severity src/​Aspire.Hosting/​IInteractionService.cs — InteractionInput is also the public type used by CommandOptions.Arguments, but… View comment
Medium severity src/​Aspire.Dashboard/​Terminal/​TerminalWebSocketProxy.cs — This does not actually verify the session before accepting the WebSocket.… View comment
Medium severity src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor — TerminalView is mounted even when InputDisabled is true. Its first render immediately connects… View comment
Medium severity src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor — A terminal input never supplies a Value, but both the dashboard's required-field check and… View comment
Issues resolved since last review (2)
Severity Finding
Medium severity src/​Aspire.Hosting/​Dashboard/​DashboardService.cs — This streaming RPC bypasses ExecuteAsync and only observes the call token, despite the… View resolved comment
High severity src/​Aspire.Hosting/​Dashboard/​InteractionTerminalSessionStore.cs — Canceling _stopCts completes every AttachAsync waiter immediately, before… View resolved comment
Files not reviewed (1)
  • src/Aspire.Dashboard/Resources/Layout.Designer.cs: Generated file
Suppressed comments (3)

src/Aspire.Dashboard/Terminal/TerminalWebSocketProxy.cs:165

  • Sending the selector frame does not wait for the bidi RPC to validate the terminal ID, so this call can return a stream before the AppHost reports FailedPrecondition. The WebSocket is then accepted despite the comment's promise of a pre-upgrade HTTP error, and an unknown terminal appears as an immediately closing socket. Add an explicit server acknowledgement (or a separate validation RPC) before accepting the WebSocket.
    src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto:495
  • This comment and the reserved fields encode an abandoned intermediate shape from the branch. Because TerminalClientFrame is introduced by this PR, origin/main never shipped interaction_id or input_name; retaining this history permanently burns field numbers/names and violates the repository convention that committed comments describe the current state rather than within-branch evolution. Remove the reservations and the “Formerly” narrative.
    src/Aspire.Hosting/InteractionService.cs:174
  • A terminal does not submit a Value, but the existing server and dialog validation treat every required non-file input with an empty value as invalid. Therefore InputType.Terminal with Required = true can never complete. Either reject that combination here or explicitly exempt terminal inputs from required-value validation in both layers.

Comment thread src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.cs
Comment thread src/Aspire.Hosting/Terminals/Hex1bAspireTerminal.cs Outdated
Comment thread src/Aspire.Hosting/Terminals/TerminalService.cs Outdated
Comment thread tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs Outdated
Comment thread src/Aspire.Hosting/IInteractionService.cs Outdated
Comment thread src/Aspire.Hosting/IInteractionService.cs Outdated
Comment thread src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor.cs Outdated
Comment thread src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.cs
Comment thread src/Aspire.Dashboard/ServiceClient/GrpcTerminalClientStream.cs
@github-actions

This comment has been minimized.

Adds a shared popup-window mechanism so any terminal in the dashboard can be
opened in a dedicated browser window that is resizable independently of the
dashboard layout.

- app-terminalwindow.js: a popup registry keyed by an opaque string. Close is
  detected by polling win.closed rather than pagehide + postMessage, because
  pagehide never fires on a crash or a force-close and a terminal stuck on
  "running in a separate window" with no way back is far worse than a 400ms
  poll.
- TerminalWindowLauncher: the reusable C# wrapper over that module, shared by
  the dock and the resource console page.
- TerminalWindow page: a full-viewport chromeless TerminalView routed for both
  AppHost terminals (/terminal-window/apphost/{id}) and resource terminals
  (/terminal-window/resource/{name}[/{replica}]). It reaches the dashboard on
  its own, so it survives the opener being reloaded or closed.

The two callers deliberately differ in what happens to the in-page view:

- The dock swaps the pane for a placeholder while detached. It must not keep an
  attached TerminalView, because two attached viewers contend for the HMP1
  primary role and would fight over the PTY grid size. Reattaching remounts the
  view and HMP1 StateSync replays the screen, the same path that already
  survives a browser reload.
- Resource terminals keep the inline view rendering. They are multi-headed and
  the window is an additional viewer, not a relocation.

Verified live against the Terminals playground: dock detach swaps to the
placeholder and reattaches when the window closes; resource detach leaves the
inline view intact; both windows refit their grid on resize.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3de8011a-08b4-48c9-aa13-f0feef9a378c
Copilot AI review requested due to automatic review settings September 4, 2026 04:26
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 9 High severity · 11 Medium severity · 3 Low severity

New issues introduced by this change (2)
Severity Finding
High severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor.cs — The terminal dock represents live AppHost state, but this unkeyed injection resolves…
High severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor.cs — Starting this watcher with Task.Run and using ConfigureAwait(false) later causes _terminals,…
Pre-existing issues (21)
Severity Finding
High severity tests/​Shared/​TestDashboardClient.cs — IDashboardClient now requires AttachTerminalAsync, SubscribeTerminalsAsync,… View comment
High severity tests/​Aspire.Dashboard.Tests/​Terminal/​DefaultTerminalConnectionResolverTests.cs — This class no longer implements IDashboardClient: the interface uses terminal IDs and also added… View comment
High severity tests/​Aspire.Dashboard.Tests/​ResourceOutgoingPeerResolverTests.cs — This mock still implements the old interaction/name attach method and omits the other three new… View comment
High severity tests/​Aspire.Dashboard.Tests/​Integration/​Playwright/​Infrastructure/​MockDashboardClient.cs — The Playwright fake no longer satisfies IDashboardClient: this method has the obsolete signature,… View comment
High severity src/​Aspire.Hosting/​Terminals/​TerminalService.cs — The snapshot subscription lock does not cover terminal insertion or publication. A creator can… View comment
High severity src/​Aspire.Hosting/​Terminals/​Hex1bAspireTerminal.cs — When the attachment token is canceled, this returns immediately even though Hex1b may still be… View comment
High severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor.cs — The watch loop mutates _terminals and _activeTerminalId on a thread-pool thread because both… View comment
Medium severity src/​Aspire.Hosting/​IInteractionService.cs — This enum member is emitted into the Go/Java/Python/Rust SDKs, but InteractionInput.Terminal is… View comment
Medium severity src/​Aspire.Hosting/​IInteractionService.cs — The documentation labels this property experimental, but there is no [Experimental] attribute, so… View comment
Medium severity src/​Aspire.Dashboard/​ServiceClient/​DashboardClient.cs — This direct server stream has no recovery path. A transient AppHost/gRPC disconnect makes the… View comment
Medium severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor.cs — Once this server stream ends because of a transient AppHost/dashboard disconnect, the exception is… View comment
Medium severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor — These dock tabs are clickable div elements with no tab semantics, focusability, or keyboard… View comment
Medium severity src/​Aspire.Dashboard/​Components/​Layout/​MainLayout.razor — The terminal control is always enabled in historical/read-only mode, but SelectedDashboardClient… View comment
Medium severity src/​Aspire.Hosting/​InteractionService.cs — Terminal inputs never produce a Value, but this validation permits Required = true. Both the… View comment
Medium severity src/​Aspire.Hosting/​IInteractionService.cs — InteractionInput is also the public type used by CommandOptions.Arguments, but… View comment
Medium severity src/​Aspire.Dashboard/​Terminal/​TerminalWebSocketProxy.cs — This does not actually verify the session before accepting the WebSocket.… View comment
Medium severity src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor — TerminalView is mounted even when InputDisabled is true. Its first render immediately connects… View comment
Medium severity src/​Aspire.Dashboard/​Components/​Dialogs/​InteractionsInputDialog.razor — A terminal input never supplies a Value, but both the dashboard's required-field check and… View comment
Low severity src/​Aspire.Dashboard/​ServiceClient/​GrpcTerminalClientStream.cs — This comment is stale: payload frames no longer carry interaction ID or input name; only the… View comment
Low severity src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor.cs — There is no component coverage for this new stateful dock even though it handles asynchronous… View comment

And 1 more that still need to be addressed.

Files not reviewed (2)
  • src/Aspire.Dashboard/Resources/ConsoleLogs.Designer.cs: Generated file
  • src/Aspire.Dashboard/Resources/Layout.Designer.cs: Generated file
Suppressed comments (8)

src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor:200

  • A required terminal input can never be submitted. The existing dialog validation treats every required non-boolean input as missing when InputViewModel.Value is blank, but terminal inputs never produce a Value; they only carry TerminalId. Exclude terminal inputs from value validation (or reject Required = true for this type) and cover that behavior.
                        case InputType.Terminal:

src/Aspire.Dashboard/Components/Layout/TerminalDock.razor:16

  • These clickable tab <div> elements are neither focusable nor keyboard-operable, so keyboard-only users cannot select an inactive terminal tab. Use a tab/button control, or add the appropriate tab semantics, focus management, and Enter/Space/arrow-key handling.
                <div class="terminal-dock-tab @(terminal.TerminalId == _activeTerminalId ? "active" : string.Empty)"
                     @key="terminal.TerminalId"
                     @onclick="@(() => Activate(terminal.TerminalId))">

src/Aspire.Dashboard/ServiceClient/DashboardClient.cs:1161

  • This direct gRPC stream is not part of DashboardClient's WatchWithRecoveryAsync loop. When the AppHost connection drops and reconnects, this call ends and each dock/window watcher catches the exception and exits, leaving terminal state permanently stale for the circuit. Add recovery/fan-out like the resource and interaction watchers, or retry this subscription until cancellation.
    src/Aspire.Hosting/Terminals/TerminalService.cs:124
  • The lock does not make the snapshot and subscription atomic because terminal creation/removal and Publish never take _syncLock. A create can add the subscriber, insert the terminal before the snapshot, and then publish Added, delivering the same terminal in both the snapshot and change stream. Synchronize registry mutation plus publication with this lock (or add sequencing/deduplication) so the documented subscription contract holds.
    tests/Aspire.Dashboard.Tests/Integration/Playwright/Infrastructure/MockDashboardClient.cs:53
  • This mock still has the removed interaction-based attachment signature and omits the other new terminal members from IDashboardClient, so the Playwright test infrastructure no longer compiles. Replace it with implementations of the current attach, subscribe, create, and close methods.
    tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs:697
  • This stale method does not satisfy the new IDashboardClient.AttachTerminalAsync(string, ...) contract, and the test double also lacks the new subscribe/create/close terminal members. Consequently this test project will fail with CS0535; implement the current four-member terminal contract here.
    tests/Aspire.Dashboard.Tests/Terminal/DefaultTerminalConnectionResolverTests.cs:154
  • This class no longer implements IDashboardClient: the interface now exposes AttachTerminalAsync(string, ...) plus the terminal watch/create/close methods, while this stale interaction-based method matches none of them. The dashboard test project will fail to compile until all four current members are implemented.
    tests/Shared/TestDashboardClient.cs:91
  • This test double still implements the superseded AttachInteractionTerminalAsync signature and does not implement the new AttachTerminalAsync, SubscribeTerminalsAsync, CreateDockTerminalAsync, or CloseTerminalAsync members. Because it implements IDashboardClient, every test project linking this shared file will fail with CS0535. Update the fake to the current interface contract.

Comment thread src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.cs
Comment thread src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.cs Outdated
Move terminal sizing controls into the footer, preserve the active resolution across terminal peers, and default new terminals to 132x50. Ensure configured dimensions reach both DCP PTYs and HMP consumers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 395e0f6a-f4c1-4db8-a3dd-e0fbf5392773
Cover terminal focus navigation and producer-dimension preservation with Playwright using a lightweight HMP test peer.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 395e0f6a-f4c1-4db8-a3dd-e0fbf5392773
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 395e0f6a-f4c1-4db8-a3dd-e0fbf5392773
Spike 2 renamed IDashboardClient.AttachInteractionTerminalAsync to
AttachTerminalAsync and added SubscribeTerminalsAsync,
CreateDockTerminalAsync and CloseTerminalAsync, but the four test
doubles were never updated, so Aspire.Dashboard.Tests did not compile.

SubscribeTerminalsAsync is written as an async iterator rather than
returning AsyncEnumerable.Empty<T>() because these projects target
net8.0, which does not ship System.Linq.AsyncEnumerable.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3de8011a-08b4-48c9-aa13-f0feef9a378c
The footer carrying the font stepper, the fixed-resolution picker and
the F6 focus hint was hidden for every chromeless host, which meant the
terminal dock and detached terminal windows lost the sizing controls
that were just integrated from the font-controls work.

Show the footer everywhere and gate only the resolution picker, via a
new ShowDimensionsPicker parameter that maps to a `dimensions-hidden`
host class. A detached window is resizable and a resource terminal has
a fixed frame, so both keep the picker; a dock pane is sized by the
dock splitter and always fits its grid to the available space, so a
fixed resolution has nothing to act on there. The font stepper stays in
all three, so dock terminals can still be zoomed.

The footer is still always constructed regardless of visibility so the
control references on the JS state object stay non-null and the wiring
is identical in every mode.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3de8011a-08b4-48c9-aa13-f0feef9a378c
Copilot AI review requested due to automatic review settings September 4, 2026 05:13
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Share tab and splitter styling, improve responsive empty-state content, and paint terminal input focus above the canvas. Refine selection copying and expiration, and update localized navigation and shortcut help.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3de8011a-08b4-48c9-aa13-f0feef9a378c
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Four moderate compatibility, cleanup, and end-to-end coverage issues remain unresolved.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Always unregister detached pages after record cleanup fails

src/​Aspire.Dashboard/​wwwroot/​js/​app-terminalwindow.js:596

If removeRecord(page) throws while parsing corrupt or unavailable storage, unregisterDetachedTerminalWindow(id) is skipped. The detached page then keeps its storage listener and detachedPages entry, which can continue invoking the disposed .NET owner after navigation. Put the unregister call in a finally block while allowing the storage error to propagate.

Low severity Run tests that execute TerminalDock.razor.js

eng/​github-ci/​test-trigger-map.yml:105

This rule routes TerminalDock.razor.js changes to Infrastructure.Tests, but DashboardTerminalScriptTests only runs TerminalView.test.mjs, KeyboardShortcuts.test.mjs, and TerminalWindow.test.mjs. The bUnit TerminalDock tests mock the JS module and do not execute the shipped resize/tab-navigation code, so changes to this file can pass the selected target without coverage. Add a Node test for this module and invoke it from the mapped target, or route the input to a target that actually executes it.

Comment thread src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto Outdated
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The terminal backchannel breaks version compatibility, and a consumer client handle is not disposed, risking leaked session resources.

Review effort: Balanced
Findings: 1 High severity

Open (1)

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Renumber terminal client frame fields, restrict dashboard closure to AppHost-owned dock terminals, and remove obsolete migration guidance. Show the F6 hint only while terminal input is focused.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3de8011a-08b4-48c9-aa13-f0feef9a378c
@github-actions

Copy link
Copy Markdown
Contributor

Tests selector

Selects the full PR test matrix + all PR-gated jobs (ALL) — a rule matching 'Directory.Build.targets' selects ALL


Selection computed for commit ef7d4e6.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

One or more issues must be addressed before approval.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Low severity Update dock comments after the xterm migration

src/​Aspire.Dashboard/​Components/​Layout/​TerminalDock.razor.css:3

These comments still attribute the pane's sizing and refit behavior to xterm, but the dock now mounts Hex1b's WebTerminal. The stale implementation name makes the sizing invariant misleading; update the rationale to describe the mounted Hex1b renderer/state instead.

This issue also appears on line 112 of the same file.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ CI Failure Analysis: Possible Flaky Test(s)

The CI build failed due to test failure(s) that appear unrelated to the PR changes. These may be flaky tests.

Suspected flaky test(s):

  • Aspire.Hosting.Azure.Tests.AzureServiceBusExtensionsTests.AzureServiceBusEmulatorResourceGeneratesConfigJsonOnlyChangedProperties in job Tests / No-package tests (regular, Aspire.Hosting.Azure.Tests, Hosting.Azure, Hosting.Azure, tests/Aspire... / Hosting.Azure (ubuntu-latest)
    • Error: System.Threading.Tasks.TaskCanceledException : A task was canceled.
    • Stack Trace (first frames):
at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
   at Microsoft.Extensions.Hosting.Internal.Host.StopAsync(CancellationToken cancellationToken)
   at Aspire.Hosting.DistributedApplication.StopAsync(CancellationToken cancellationToken) in /_/src/Aspire.Hosting/DistributedApplication.cs:line 497
   at Aspire.Hosting.Azure.Tests.AzureServiceBusExtensionsTests.AzureServiceBusEmulatorResourceGeneratesConfigJsonOnlyChangedProperties() in /home/runner/work/aspire/aspire/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs:line 499
--- End of stack trace from previous location ---
  • Why likely flaky: Failure occurs during host teardown (StopAsync), not in test logic; PR does not modify this area of code.
  • Aspire.Templates.Tests.NUnit_NewUpAndBuildSupportProjectTemplatesTests.CanNewAndBuild(templateName: "aspire-nunit", extraTestCreationArgs: "", sdk: Net9, tfm: Net9, error: null) in job Tests / Package tests - macOS (class, Aspire.Templates.Tests, Templates-NUnit_NewUpAndBuildSupportProjectTemplatesTests... / Templates-NUnit_NewUpAndBuildSupportProjectTemplatesTests (macos-latest)
    • Error: Aspire.Templates.Tests.ToolCommandException : Expected 0 exit code but got 1: /Users/runner/work/aspire/aspire/artifacts/bin/dotnet-9/dotnet build "-bl:/Users/runner/work/aspire/aspire/artifacts/log/test-logs/new_build_aspire_nunit_2xksfaki_f44.AppHost/new_build_aspire_nunit_2xksfaki_f44.AppHost-build.binlog" /p:TreatWarningsAsErrors=true -c Debug /p:AspireUseCliBundle=false
      Standard Output:
      Determining projects to restore...
      All projects are up-to-date for restore.
      CSC : error CS1504: Source file '.../new_build_aspire_nunit_2xksfaki_f44.AppHost.AssemblyInfo.cs' could not be opened -- Method not found: 'PAL_HashAlgorithm System.Security.Cryptography.HashAlgorithmNames.HashAlgorithmToPal(System.String)'.
    • Stack Trace (first frames):
at Aspire.Templates.Tests.CommandResult.EnsureExitCode(Int32 expectedExitCode, String messagePrefix, Boolean suppressOutput) in /_/tests/Shared/TemplatesTesting/CommandResult.cs:line 36
   at Aspire.Templates.Tests.CommandResult.EnsureSuccessful(String messagePrefix, Boolean suppressOutput) in /_/tests/Shared/TemplatesTesting/CommandResult.cs:line 20
   at Aspire.Templates.Tests.AspireProject.BuildAsync(String[] extraBuildArgs, CancellationToken token, String workingDirectory) in /_/tests/Shared/TemplatesTesting/AspireProject.cs:line 347
   at Aspire.Templates.Tests.NewUpAndBuildSupportProjectTemplatesBase.CanNewAndBuildActual(String templateName, String extraTestCreationArgs, TestSdk sdk, TestTargetFramework tfm, String error) in /_/tests/Aspire.Templates.Tests/NewUpAndBuildSupportProjectTemplatesTests.cs:line 56
  • Why likely flaky: Roslyn compiler crashed with a 'Method not found' reflection error unrelated to any PR-generated code; indicates a corrupted macOS SDK/Roslyn toolset, matching a previously observed similar pattern.
  • Aspire AppHost tree E2E discovers the workspace AppHost and renders it in the Aspire view in job Tests / Run VS Code extension E2E tests / VS Code extension E2E (Windows, apphost-tree)
    • Error: Timed out waiting for child tree item 'Run AppHost' on 'Deploy AppHost'.
      Wait timed out after 30174ms
    • Stack Trace (first frames):
TimeoutError: Timed out waiting for child tree item 'Run AppHost' on 'Deploy AppHost'.
Wait timed out after 30174ms
    at D:\a\aspire\aspire\extension\node_modules\selenium-webdriver\lib\webdriver.js:929:22
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
  • Why likely flaky: UI timing timeout in a Selenium-based E2E test on Windows; the same shard has a documented history of similar flaky timeouts across unrelated PRs.
  • Aspire dynamic debug configuration E2E "after each" hook for "re-resolves the selected AppHost by dynamic configuration name in duplicate-alias workspaces" in job Tests / Run VS Code extension E2E tests / VS Code extension E2E (Windows, dynamic-debug-configuration)
    • Error: Dynamic debug configuration E2E teardown failed.
  1. Error: EBUSY: resource busy or locked, rmdir 'C:\Users\runneradmin\AppData\Local\Temp\aev-nhtS8o\workspace.e2e-dynamic-debug\first'
  • Stack Trace (first frames):
AggregateError: Dynamic debug configuration E2E teardown failed.
1. Error: EBUSY: resource busy or locked, rmdir 'C:\Users\runneradmin\AppData\Local\Temp\aev-nhtS8o\workspace\.e2e-dynamic-debug\first'
    at runE2eTeardown (out\test-e2e\test-e2e\helpers\fixtures.js:217:15)
    at async Context.<anonymous> (out\test-e2e\test-e2e\dynamicDebugConfiguration.e2e.test.js:62:9)
  • Why likely flaky: Windows file-locking race in the E2E teardown helper (EBUSY on rmdir), a previously observed recurring pattern unrelated to the PR's changes.

Suggested actions:

  • Re-run the failed CI jobs to confirm if the failure is intermittent
  • If the test continues to fail, consider quarantining it using /quarantine-test <test name> <issue URL>
  • Search existing issues to see if this test is already known to be flaky

You can re-run the failed jobs from the workflow run page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants