Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
55bf479
Spike: AppHost-owned terminal as an interaction service input
mitchdenny Sep 3, 2026
ac6e6dc
Spike: dashboard terminal dock and AppHost TerminalService
mitchdenny Sep 4, 2026
119d370
Render dock terminals chromeless at the dashboard font size
mitchdenny Sep 4, 2026
fa373e3
Add a Resource surface to TerminalSurface
mitchdenny Sep 4, 2026
116aed5
Detach terminals into their own resizable windows
mitchdenny Sep 4, 2026
2f1616b
Improve terminal sizing controls
mitchdenny Aug 31, 2026
629effa
Add terminal browser regression tests
mitchdenny Aug 31, 2026
f45f70b
Fix terminal listener and sizing lifecycle
mitchdenny Sep 4, 2026
53a9c75
Update dashboard test mocks for the terminal client surface
mitchdenny Sep 4, 2026
21b4158
Show the terminal footer in chromeless terminals
mitchdenny Sep 4, 2026
cde6698
Re-fit chromeless terminals on detach and return to the dock
mitchdenny Sep 4, 2026
1ec713d
Render the interaction terminal chromeless
mitchdenny Sep 4, 2026
f208cbe
Explain why the terminal playground needs InternalsVisibleTo
mitchdenny Sep 4, 2026
9c74e9d
Make the AppHost terminal API public and experimental
mitchdenny Sep 4, 2026
d6e9e7b
Add automated number-guess terminal demo to Terminals playground
mitchdenny Sep 4, 2026
f09fb43
Add diagnostic logging to silent terminal catch handlers
mitchdenny Sep 5, 2026
0ed98fd
Harden terminal validation, teardown, and diagnostics
mitchdenny Sep 5, 2026
527d20f
Explain why the dashboard client and repository factory use TryAdd
mitchdenny Sep 5, 2026
fb073ed
Replace the dock's REPL placeholder with an informational panel
mitchdenny Sep 5, 2026
c484fc1
Toggle the terminal dock with Shift+` instead of Ctrl+`
mitchdenny Sep 5, 2026
99e4da7
Fix unstyled terminal dock buttons in light theme
mitchdenny Sep 5, 2026
6821678
Authenticate detached terminal windows via the login endpoint
mitchdenny Sep 5, 2026
feb18b0
Revert "Authenticate detached terminal windows via the login endpoint"
mitchdenny Sep 5, 2026
363c8cf
Move interaction terminal lifetime to the caller
mitchdenny Sep 5, 2026
219dba2
Let the caller start an AppHost terminal
mitchdenny Sep 5, 2026
7b6fdb8
Separate terminal ownership from terminal placement
mitchdenny Sep 5, 2026
59f7183
Report every terminal in one listing
mitchdenny Sep 5, 2026
038e51a
Make AppHost automation of resource terminals work
mitchdenny Sep 5, 2026
7ec2929
Fix CI failures in terminal listing and dashboard tests
mitchdenny Sep 5, 2026
af05c2c
Fix terminal lifecycle races and dashboard recovery
mitchdenny Sep 6, 2026
f3693ff
Reject required terminal interaction inputs
mitchdenny Sep 6, 2026
17847d8
Respect disabled and loading terminal interaction inputs
mitchdenny Sep 6, 2026
4ef444c
Preserve caller-owned terminals when cloning command inputs
mitchdenny Sep 6, 2026
41bd676
Validate terminal interaction registration
mitchdenny Sep 6, 2026
2ed8657
Reject unsupported AppHost terminal placements
mitchdenny Sep 6, 2026
48f14c4
Bound terminal disposal and notify on close timeout
mitchdenny Sep 6, 2026
395cbc1
Handle ended AppHost terminals without removing their tabs
mitchdenny Sep 7, 2026
814825f
Rebind detached terminal watchers on route changes
mitchdenny Sep 7, 2026
47b1b0a
Keep collapsed terminal dock out of keyboard navigation
mitchdenny Sep 7, 2026
a5594d1
Add accessible keyboard navigation to terminal dock
mitchdenny Sep 7, 2026
f954f16
Add keyboard resizing to the terminal dock
mitchdenny Sep 7, 2026
1db8479
Bound terminal watch queues with configurable snapshot recovery
mitchdenny Sep 7, 2026
521f0db
Expose sealed AspireTerminal handles with internal backends
mitchdenny Sep 7, 2026
f7cf6d2
Await number-guess automation after dialog cancellation
mitchdenny Sep 7, 2026
288d184
Fix terminal key validation and dashboard shortcut handling
mitchdenny Sep 7, 2026
9022836
Migrate dashboard terminals to Hex1b web terminal
mitchdenny Sep 7, 2026
13644d2
Upgrade Hex1b terminals with WebGL2 and hyperlink replay
mitchdenny Sep 8, 2026
c9f8901
Upgrade Hex1b terminal replay to published build 1522
mitchdenny Sep 8, 2026
d087247
Restore dashboard terminal window chrome
mitchdenny Sep 8, 2026
6933127
Add Notcurses shell to Terminals playground
mitchdenny Sep 8, 2026
606a70e
Add BB ASCII demo container to Terminals playground
mitchdenny Sep 8, 2026
3f03c7e
Remove BB from Terminals playground
mitchdenny Sep 8, 2026
6f1e200
Anchor terminal copy controls to selected text
mitchdenny Sep 8, 2026
fcb572b
Return terminal focus after copying selected text
mitchdenny Sep 8, 2026
94cc39b
Update paired Hex1b terminal packages to alpha.1547
mitchdenny Sep 10, 2026
a8c9a11
Merge origin/main into Hex1b dashboard terminal migration
mitchdenny Sep 10, 2026
654b016
Merge origin/main into terminal interaction spike
mitchdenny Sep 10, 2026
a7c727c
Merge Hex1b HWT web terminal integration
mitchdenny Sep 10, 2026
dcd1348
Complete terminal rendering, fitting, and tape automation
mitchdenny Sep 10, 2026
f666df1
Update Hex1b Windows PTY package
mitchdenny Sep 10, 2026
3b0cfec
Clarify Hex1b package compatibility
mitchdenny Sep 10, 2026
9dfc7af
Merge origin/main for terminal PR validation
mitchdenny Sep 11, 2026
8f20a04
Adopt Hex1b 0.166.0 and update terminal CI expectations
mitchdenny Sep 11, 2026
1c4f18c
Fix Windows terminal payload publishing and template restores
mitchdenny Sep 11, 2026
e545140
Fix VS Code fixture restores and terminal component tests
mitchdenny Sep 11, 2026
a74bb5d
Merge origin/main to unblock terminal PR CI
mitchdenny Sep 11, 2026
5ec29a2
Update Hex1b and enable terminal reflow
mitchdenny Sep 13, 2026
0856913
Merge origin/main into terminal interaction branch
mitchdenny Sep 13, 2026
879d9b1
Update Hex1b to 0.167.0
mitchdenny Sep 13, 2026
f1117a5
Address terminal review feedback and update Hex1b
mitchdenny Sep 14, 2026
9fed032
Keep detached terminal window names collision-free
mitchdenny Sep 14, 2026
76def42
Preserve dashboard base paths for AppHost terminal sockets
mitchdenny Sep 14, 2026
49efdee
Merge origin/main into terminal interaction branch
mitchdenny Sep 14, 2026
0c940c6
Configure bundled ConPTY for DCP terminals
mitchdenny Sep 14, 2026
3f063cd
Flatten terminal launch options and update Hex1b alpha
mitchdenny Sep 15, 2026
666536b
Add Command Prompt dock terminal
mitchdenny Sep 15, 2026
06dd324
Hide terminal dock in standalone dashboards
mitchdenny Sep 15, 2026
eb78e05
Remove duplicate terminal title-bar dimensions
mitchdenny Sep 15, 2026
fd92505
Launch terminal windows directly from browser clicks
mitchdenny Sep 15, 2026
6d500a7
Align terminal footer typography with dashboard message bars
mitchdenny Sep 15, 2026
5d1138d
Track terminal dock usage and opening trigger
mitchdenny Sep 15, 2026
bbc62f1
Link the empty terminal dock to terminal documentation
mitchdenny Sep 15, 2026
556f6d6
Introduce dedicated terminal interactions and dialogs
mitchdenny Sep 15, 2026
5fcc4f3
Align terminal window button with footer controls
mitchdenny Sep 15, 2026
87285ac
Merge main into terminal feature branch
mitchdenny Sep 16, 2026
997dba0
Unify terminal experimental APIs under ASPIRETERMINAL001
mitchdenny Sep 16, 2026
7ef2f8b
Suppress compatibility warning for terminal interactions
mitchdenny Sep 16, 2026
b1fa987
Update paired Hex1b terminal packages to 0.168.0
mitchdenny Sep 17, 2026
5012192
Merge main into terminal feature branch
mitchdenny Sep 17, 2026
a62b5fc
Merge remote-tracking branch 'origin/main' into mitchdenny-studious-d…
mitchdenny Sep 17, 2026
953a73f
Improve terminal focus and diagnostics with bundled ConPTY
mitchdenny Sep 17, 2026
56da25d
Honor terminal dimensions and address review feedback
mitchdenny Sep 17, 2026
3e16358
Design improvements
JamesNK Sep 17, 2026
80628bc
Improve empty terminal dock guidance and resource links
mitchdenny Sep 17, 2026
18de5f8
Design improvements
JamesNK Sep 17, 2026
eee199a
Design improvement
JamesNK Sep 17, 2026
e2704bc
Fix stale terminal test selectors
mitchdenny Sep 17, 2026
00ab632
Refine terminal backchannel and protobuf contracts
mitchdenny Sep 17, 2026
ac83d01
Select Windows PTY helper for pre-bundle signing
mitchdenny Sep 17, 2026
fe9ec79
Add mobile terminal dock access and dedicated terminal resources
mitchdenny Sep 17, 2026
ecab4be
Fix AppHost terminal shutdown ownership
mitchdenny Sep 17, 2026
59482a4
Defer terminal dock setup and serialize JavaScript initialization
mitchdenny Sep 17, 2026
4952a9d
Remove temporary Hex1b feeds and audit suppressions
mitchdenny Sep 17, 2026
b64a9b4
Expand terminal keys with composable modifiers
mitchdenny Sep 18, 2026
b2f7bb3
Consolidate remaining dashboard terminal resources
mitchdenny Sep 18, 2026
5b8461a
Clarify AppHost terminal ownership documentation
mitchdenny Sep 18, 2026
21c95e6
Harden terminal startup and attachment lifecycles
mitchdenny Sep 18, 2026
612c371
Restrict dashboard terminal close to AppHost ownership
mitchdenny Sep 18, 2026
883a677
Preserve detached terminals across dashboard reloads
mitchdenny Sep 18, 2026
7956cdd
Align terminal handshake tests with completion close status
mitchdenny Sep 18, 2026
d57892a
Preserve terminal input cancellation and validate bundle RIDs
mitchdenny Sep 18, 2026
e16d077
Make detached terminal recovery failure-safe
mitchdenny Sep 18, 2026
8bf9d91
Fix terminal component test setup and async synchronization
mitchdenny Sep 18, 2026
7adbc35
Polish dashboard terminal layout and selection UX
mitchdenny Sep 18, 2026
37f829c
InteractionsProvider refactor
JamesNK Sep 18, 2026
ef7d4e6
Address terminal review feedback and focus hints
mitchdenny Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</ItemGroup>

<Import Project="$(RepositoryEngineeringDir)/NullablePolyfill.targets" />
<Import Project="$(MSBuildThisFileDirectory)src/Aspire.Hosting/buildTransitive/Aspire.Hosting.Hex1b.targets" />

<!-- HACK: This resolves issues in CI builds where the extensions.json file is mapped to the incorrect directory. We only want
to copy the updated extensions.json file once the `_FunctionsExtensionUpdateMetadata` target has completed. -->
Expand Down
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@
<PackageVersion Include="Grpc.AspNetCore" Version="2.83.0" />
<PackageVersion Include="Grpc.Net.ClientFactory" Version="2.83.0" />
<PackageVersion Include="Grpc.Tools" Version="2.83.0" />
<PackageVersion Include="Hex1b" Version="0.165.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.168.0" />
<PackageVersion Include="Hex1b.McpServer" Version="0.165.0" />
<PackageVersion Include="Hex1b.Tool" Version="0.165.0" />
<PackageVersion Include="Humanizer.Core" Version="3.0.10" />
Expand Down
2 changes: 2 additions & 0 deletions docs/specs/bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ aspire-{version}-{platform}/

**Key change from previous layout**: The separate `.NET Runtime` (~106 MB), `dashboard/` (~42 MB), `aspire-server/` (~19 MB), `tools/aspire-nuget/` (~5 MB), and `tools/dev-certs/` directories have been consolidated into a single `managed/aspire-managed` self-contained binary. Certificate management has been moved natively into the CLI itself, eliminating the need for a separate dev-certs tool.

Windows bundles also include `managed/hex1bpty.exe`, `managed/conpty.dll`, and `managed/arm64/OpenConsole.exe`; `win-x64` additionally includes `managed/x64/OpenConsole.exe`. These PTY sidecars stay outside the managed single-file executable because Hex1b locates its helper beside the application. ConPTY selects `OpenConsole.exe` relative to its DLL using the **OS architecture**, so the x64 bundle must retain the ARM64 helper for execution under emulation. `CreateLayout` preserves this layout and fails if a required sidecar is missing.

**Total Bundle Size:**
- **Unzipped:** ~220 MB (down from ~323 MB — eliminated separate runtime)
- **Zipped:** ~80 MB
Expand Down
500 changes: 468 additions & 32 deletions docs/specs/with-terminal.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions eng/Signing.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<FileSignInfo Include="Dapper.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="Fractions.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="Hex1b.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="hex1bpty.exe" CertificateName="3PartySHA2" />
Comment thread
mitchdenny marked this conversation as resolved.
<FileSignInfo Include="IdentityModel.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="IdentityModel.OidcClient.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="KubernetesClient.Basic.dll" CertificateName="3PartySHA2" />
Expand Down Expand Up @@ -114,6 +115,8 @@
<ItemsToSign Condition="$([System.OperatingSystem]::IsWindows())" Include="$(ArtifactsBinDir)Aspire.Managed\**\publish\aspire-managed.exe" />
<ItemsToSign Condition="$([System.OperatingSystem]::IsLinux())" Include="$(ArtifactsBinDir)Aspire.Managed/**/publish/aspire-managed" />
<ItemsToSign Condition="$([System.OperatingSystem]::IsMacOS())" Include="$(ArtifactsBinDir)Aspire.Managed/**/publish/aspire-managed" />
<!-- Select only Hex1b's unsigned helper, preserving the upstream signatures on ConPTY and OpenConsole. -->
<ItemsToSign Condition="$([System.OperatingSystem]::IsWindows())" Include="$(ArtifactsBinDir)Aspire.Managed\**\publish\hex1bpty.exe" />
<!--
Note: VS Code extension (.vsix) is NOT included here.
Signing the VSIX changes its hash, breaking manifest integrity verification.
Expand Down
11 changes: 11 additions & 0 deletions eng/github-ci/test-trigger-map.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ ignore:
# Path rules: a glob set -> a target set. One mechanism; the comment headers below are organization
# only. targets may be test: / job: / a GROUP name / ALL.
path_rules:
- paths:
- src/Aspire.Dashboard/Components/Controls/TerminalView.razor.js
- src/Aspire.Dashboard/Components/Layout/TerminalDock.razor.js
- src/Aspire.Dashboard/wwwroot/js/app.js
- src/Aspire.Dashboard/wwwroot/js/app-terminalwindow.js
- src/Aspire.Dashboard/package.json
Comment thread
mitchdenny marked this conversation as resolved.
- src/Aspire.Dashboard/package-lock.json
- src/Aspire.Dashboard/wwwroot/js/hex1b-web-terminal/**
- tests/Aspire.Dashboard.Components.Tests/JavaScript/**
targets: [test:Infrastructure.Tests]
reason: DashboardTerminalScriptTests executes the terminal lifecycle JavaScript and reads the paired package assets outside its MSBuild graph.

# --- catch-all: build infrastructure & broadly shared code -> ALL --------------------------
- paths:
Expand Down
10 changes: 9 additions & 1 deletion extension/scripts/run-e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -2132,9 +2132,16 @@ function writeNuGetConfigIfLocalPackageSourcesExist() {
const sourceEntries = packageSources
.map((source, index) => ` <add key="e2e-source-${index}" value="${escapeXml(source)}" />`)
.join('\n');
const fallbackSourceEntries = getApprovedFallbackPackageSources()
const fallbackSources = getApprovedFallbackPackageSources();
const fallbackSourceEntries = fallbackSources
.map(source => ` <add key="${escapeXml(source.key)}" value="${escapeXml(source.value)}" />`)
.join('\n');
const sourceMappingEntries = [
...packageSources.map((_, index) => `e2e-source-${index}`),
...fallbackSources.map(source => source.key),
].map(key => ` <packageSource key="${escapeXml(key)}">
<package pattern="*" />
</packageSource>`).join('\n');
const nugetConfig = `<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
Expand All @@ -2144,6 +2151,7 @@ ${fallbackSourceEntries}
</packageSources>
<packageSourceMapping>
<clear />
${sourceMappingEntries}
</packageSourceMapping>
</configuration>
`;
Expand Down
76 changes: 75 additions & 1 deletion extension/src/test/e2eLaunchProfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'child_process';
import { runInNewContext } from 'vm';
import * as ts from 'typescript';
import * as vm from 'vm';
import { blazorWasmDebugProofResponseAllowanceMs, blazorWasmDebugProofTimeoutMs, getBlazorWasmDebugProofCleanupTimeoutMs, getBlazorWasmDebugProofControlTimeoutMs } from '../testing/blazorWasmDebugProofTimeouts';
Expand Down Expand Up @@ -87,6 +88,27 @@ function runE2eRunnerAsPlatform(extensionRoot: string, platform: 'darwin' | 'lin
});
}

function getFixtureNuGetConfigurations(packageSources: readonly string[]): Map<string, string> {
const extensionRoot = path.resolve(__dirname, '..', '..');
const runner = fs.readFileSync(path.join(extensionRoot, 'scripts', 'run-e2e.js'), 'utf8');
const sourceFile = ts.createSourceFile('run-e2e.js', runner, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
const functionNames = new Set(['writeNuGetConfigIfLocalPackageSourcesExist', 'getApprovedFallbackPackageSources', 'escapeXml']);
const declarations = sourceFile.statements.filter((statement): statement is ts.FunctionDeclaration =>
ts.isFunctionDeclaration(statement) && statement.name !== undefined && functionNames.has(statement.name.text));
assert.strictEqual(declarations.length, functionNames.size);

// Execute the actual config writer without the runner's CLI downloads, workspace cleanup, or VS Code launch.
const configurations = new Map<string, string>();
runInNewContext(`${declarations.map(declaration => declaration.getText(sourceFile)).join('\n')}
writeNuGetConfigIfLocalPackageSourcesExist();`, {
fs: { writeFileSync: (file: string, content: string) => configurations.set(file, content) },
getLocalPackageSourceDirectories: () => packageSources,
runRootNuGetConfigPath: 'run-root/NuGet.config',
workspaceNuGetConfigPath: 'workspace/NuGet.config',
});
return configurations;
}

function createE2eSpecFixtures(extensionRoot: string, fileNames: readonly string[]): string {
const testArtifactsRoot = path.join(extensionRoot, '.test-artifacts', 'unit');
fs.mkdirSync(testArtifactsRoot, { recursive: true });
Expand Down Expand Up @@ -916,7 +938,9 @@ builder.Build().Run();

assert.ok(runner.includes("['Directory.Build.props', 'Directory.Build.targets', 'Directory.Packages.props']"));
assert.ok(runner.includes("fs.writeFileSync(path.join(shortRunRoot, fileName), '<Project />\\n');"));
assert.ok(runner.replace(/\r\n/g, '\n').includes('<packageSourceMapping>\n <clear />\n </packageSourceMapping>'));
for (const config of getFixtureNuGetConfigurations(['/packages/local']).values()) {
assert.ok(config.includes('<packageSourceMapping>\n <clear />\n'));
}
});

test('requires only the debugger VSIX dependencies shared by browser and Azure Functions shards', () => {
Expand Down Expand Up @@ -1125,6 +1149,56 @@ builder.Build().Run();
assert.ok(writeConfig.includes('fs.writeFileSync(workspaceNuGetConfigPath, nugetConfig);'));
});

test('uses only local and approved package feeds in both fixture restore scopes', () => {
const configurations = getFixtureNuGetConfigurations(['/packages/local', '/packages/<preview> & "daily"']);
const expectedConfig = `<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="e2e-source-0" value="/packages/local" />
<add key="e2e-source-1" value="/packages/&lt;preview&gt; &amp; &quot;daily&quot;" />
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
<add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
<add key="dotnet10" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json" />
<add key="dotnet-libraries" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" />
</packageSources>
<packageSourceMapping>
<clear />
<packageSource key="e2e-source-0">
<package pattern="*" />
</packageSource>
<packageSource key="e2e-source-1">
<package pattern="*" />
</packageSource>
<packageSource key="dotnet-public">
<package pattern="*" />
</packageSource>
<packageSource key="dotnet-eng">
<package pattern="*" />
</packageSource>
<packageSource key="dotnet9">
<package pattern="*" />
</packageSource>
<packageSource key="dotnet10">
<package pattern="*" />
</packageSource>
<packageSource key="dotnet-libraries">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
`;
assert.deepStrictEqual([...configurations], [
['run-root/NuGet.config', expectedConfig],
['workspace/NuGet.config', expectedConfig],
]);
});

test('does not write fixture NuGet configurations without local package sources', () => {
assert.deepStrictEqual([...getFixtureNuGetConfigurations([])], []);
});

test('suppresses evaluation diagnostics for intentional E2E AppHost interaction APIs', () => {
const extensionRoot = path.resolve(__dirname, '..', '..');
const runner = fs.readFileSync(path.join(extensionRoot, 'scripts', 'run-e2e.js'), 'utf8');
Expand Down
47 changes: 45 additions & 2 deletions playground/Terminals/Terminals.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// because this playground project intentionally exercises the experimental API.
#pragma warning disable ASPIRETERMINAL001

using Terminals.AppHost;

var builder = DistributedApplication.CreateBuilder(args);

// A multi-replica project that calls `WithTerminal()` so each replica gets its
Expand All @@ -19,10 +21,42 @@
options.Columns = 120;
options.Rows = 32;
options.ShowTerminalHost = true;
});
})
// Opens a shell owned by the AppHost rather than orchestrated by Aspire. It has nothing to do with `repl`;
// commands just need a host resource to hang off.
.WithAppHostShellCommand()
// Drives an interactive console program from AppHost code: the terminal is shown in a dialog, but the guesses
// are typed by the AppHost, which reads each reply back off the screen and bisects until it wins.
.WithNumberGuessCommand()
// Automates a terminal the AppHost does NOT own: it joins `shell`'s existing terminal as an extra viewer and
// types into it, which is what shelling into a resource from AppHost code looks like.
.WithAutomateResourceTerminalCommand("shell");

// Long-running container that the "Shell into container" interaction command execs into. Aspire is not orchestrating
// the exec — the AppHost shells out to `docker exec` — so the container needs a stable, predictable name.
var shellbox = builder.AddContainer("shellbox", "alpine")
.WithContainerName("terminals-playground-shellbox")
.WithArgs("sleep", "infinity")
.WithContainerShellCommand()
// Same shell, but delivered as a tab in the dashboard's terminal dock (backtick shortcut) rather than a modal dialog.
.WithDockShellCommand();

// Latest Node.js image, kept alive so the "Node REPL" interaction command can exec into it. The Node REPL is a
// readline app, so it exercises cursor addressing, history, and tab completion across the tunnel in a way a plain
// shell prompt does not. `sleep infinity` replaces the image's default CMD ("node") — an interactive REPL with no TTY
// attached would exit immediately. The entrypoint (docker-entrypoint.sh) is left alone so PATH is set up normally.
builder.AddContainer("noderepl", "node", "latest")
.WithContainerName("terminals-playground-noderepl")
.WithArgs("sleep", "infinity")
.WithNodeReplCommand();

if (OperatingSystem.IsWindows())
{
// Local PowerShell launched directly by Hex1b, providing a Docker- and DCP-independent PTY debugging path.
shellbox
.WithPowerShellDockCommand()
.WithCommandPromptDockCommand();

// Single-replica executable wrapping cmd.exe to demonstrate that
// WithTerminal() also works for arbitrary executables, not just projects.
builder.AddExecutable("shell", "cmd.exe", ".")
Expand Down Expand Up @@ -59,6 +93,16 @@
.WithTerminal(options => options.ShowTerminalHost = true);
}

builder.AddDockerfile("notcurses", "../Terminals.Notcurses")
.WithTerminal(options =>
{
// The demo recommends at least 80x45 for its graphics and Unicode workloads:
// https://manpages.ubuntu.com/manpages/noble/man1/notcurses-demo.1.html
options.Columns = 120;
options.Rows = 45;
options.ShowTerminalHost = true;
});

#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
// of the dashboard. It is not required in end developer code. Comment out this code
Expand All @@ -70,4 +114,3 @@
#endif

builder.Build().Run();

67 changes: 67 additions & 0 deletions playground/Terminals/Terminals.AppHost/Scripts/numberguess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// A deliberately old-fashioned interactive console game, run as a .NET file-based app
// (`dotnet run --file numberguess.cs -- <limit>`).
//
// It exists to demonstrate driving an interactive process from AppHost code: the AppHost shows this program in an
// PromptTerminalAsync interaction, then plays it by typing guesses and reading the replies back off the terminal
// screen. Nothing here knows it is being automated - it is an ordinary Console.ReadLine app.
//
// The reply format is the contract the automation relies on, so it is deliberately unambiguous:
//
// Guess #1: 50 <- the prompt, plus the tty's echo of what was typed
// >> #1: 50 is too high <- the reply, tagged with the attempt number
//
// Tagging each reply with its attempt number means the automation can wait for ">> #3:" and be certain it is
// reading the response to its third guess rather than a stale line still on screen from an earlier one.

var limit = args.Length > 0 && int.TryParse(args[0], out var parsed) && parsed > 1 ? parsed : 100;
var secret = Random.Shared.Next(1, limit + 1);

Console.WriteLine();
Console.WriteLine("+------------------------------------------+");
Console.WriteLine("| N U M B E R G U E S S |");
Console.WriteLine("+------------------------------------------+");
Console.WriteLine();
Console.WriteLine($"I'm thinking of a number between 1 and {limit}.");
Console.WriteLine("Type a guess and press Enter. I'll tell you if it's too high or too low.");
Console.WriteLine();

for (var attempt = 1; ; attempt++)
{
Console.Write($"Guess #{attempt}: ");

var line = Console.ReadLine();
if (line is null)
{
// stdin closed - the terminal went away.
return 1;
}

if (!int.TryParse(line.Trim(), out var guess))
{
Console.WriteLine($" >> #{attempt}: '{line.Trim()}' is not a number");
continue;
}

if (guess < secret)
{
Console.WriteLine($" >> #{attempt}: {guess} is too low");
}
else if (guess > secret)
{
Console.WriteLine($" >> #{attempt}: {guess} is too high");
}
else
{
Console.WriteLine($" >> #{attempt}: {guess} is correct");
Console.WriteLine();
Console.WriteLine($"Got it in {attempt} {(attempt == 1 ? "guess" : "guesses")}. The number was {secret}.");

// Block rather than exit so the final screen stays live until whoever owns the terminal tears it down.
// Exiting immediately would race the automation's "let the human read the result" pause.
Console.ReadLine();
return 0;
}
}
Loading
Loading