Proposal: Add live interactive terminal support to Aspire (.WithTerminal())
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 201
Description
## Summary
Add `.WithTerminal()` to the Aspire app model, enabling full interactive terminal sessions for resources. This unlocks scenarios like coding agent CLIs, interactive REPLs, and TUI applications that can be accessed through the Aspire Dashboard and the Aspire CLI.
## Motivation
Rich terminal-based experiences are an important development scenario that Aspire does not optimize for today:
- **Coding agent CLIs** — You can't interact with an agent CLI running as an Aspire resource via the dashboard
- **Interactive REPLs** — Python, Node, database shells, etc. have no interactive access
- **TUI applications** — Full-screen terminal UIs (e.g., `htop`, custom TUIs) can't be viewed or controlled
- **Aspire CLI** — No way to attach to a running resource's terminal session
Currently, all process I/O is captured as non-interactive log streams (stdout/stderr to temp files). There is no PTY allocation, no interactive input, and no terminal emulation.
## Architecture
```
┌─────────────┐ PTY I/O ┌─────────────────┐ UDS ┌──────────────────┐
│ DCP │ ───────────────► │ Terminal Host │ ◄────────► │ Dashboard/CLI │
│ (launches │ (forwards │ (Hex1b-based │ (client │ (xterm.js or │
│ process │ PTY bytes │ headless term) │ connects) │ raw terminal) │
│ with PTY) │ over UDS) │ │ │ │
└─────────────┘ └──────────────────┘ └──────────────────┘
```
### Key Components
1. **App Model API** — `.WithTerminal()` extension method, `TerminalAnnotation`, hidden terminal-host resource
2. **Terminal Host Process** — Hex1b-based .NET executable (one per terminal-enabled resource) that holds terminal state and supports reconnection
3. **DCP Changes** — PTY allocation for executables/containers, UDS forwarding to terminal host
4. **Dashboard Integration** — New "Terminal" tab with xterm.js for interactive terminal
5. **CLI Integration** — `aspire terminal ` command for interactive terminal access
6. **Backchannel Extension** — New RPC methods for terminal session management
### Design Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Terminal Host | Separate executable, one per resource | Isolation; crash doesn't affect AppHost |
| Transport | UDS (Unix Domain Socket) everywhere | Already used by backchannel; low-latency, secure |
| State Management | Hex1b headless terminal | Holds terminal state; supports reconnection with state replay |
| Concurrency | Single active client at a time | Simplifies state; new connection replaces old |
| Dashboard UX | Separate "Terminal" tab | Doesn't disrupt existing Console Logs |
| Resource types | Any (containers, executables, projects, custom) | Custom callback for non-DCP resources |
## Implementation Phases
### Phase 1: Aspire.Hosting App Model API (microsoft/aspire)
Define the public API surface:
- **`TerminalAnnotation`** — `IResourceAnnotation` with `Func> GetSocketPathAsync` callback and `TerminalOptions` (columns, rows, shell override)
- **`WithTerminal()`** — Extension method that adds `TerminalAnnotation`, creates a hidden `TerminalHostResource`, and adds a `WaitAnnotation` so the resource waits for the terminal host to be healthy
- **`TerminalHostResource`** — Hidden `ExecutableResource` with `IResourceWithParent`, launched by DCP as a regular executable
### Phase 2: Terminal Host Process (microsoft/aspire)
Build the terminal host executable:
- New project: `src/Aspire.Hosting.TerminalHost/`
- .NET console app with Hex1b dependency
- Listens on UDS for DCP PTY data (input side) and client connections (output side)
- Uses Hex1b's headless terminal to maintain terminal state
- On client connect: replays current terminal state, then streams live updates
- Forwards client input back to DCP UDS → PTY stdin
- Handles resize events
- Simple binary protocol: `[type:1byte][length:4bytes][data:N bytes]` with message types: `DATA`, `INPUT`, `RESIZE`, `STATE_SNAPSHOT`
### Phase 3: DCP PTY Support (microsoft/dcp)
Enable DCP to allocate PTYs and forward I/O:
- Extend Executable CRD spec with `terminal.enabled`, `terminal.socketPath`, `terminal.columns`, `terminal.rows`
- When `terminal.enabled = true`: allocate PTY, connect PTY master to UDS, support `SIGWINCH`
- Container support: Docker/Podman exec with TTY allocation, forward exec PTY over UDS
- Mirror spec changes in Aspire's `ExecutableSpec.cs` and `ContainerSpec.cs`
### Phase 4: Backchannel Extension (microsoft/aspire)
Expose terminal sessions through existing RPC:
- `GetTerminalInfoAsync` — returns terminal host UDS path and status
- `WatchTerminalAsync` — streams `IAsyncEnumerable`
- `SendTerminalInputAsync` — sends input bytes to terminal
### Phase 5: Dashboard Integration (microsoft/aspire)
Add interactive terminal to the Dashboard:
- xterm.js integration via JS interop
- New `Terminal.razor` page with "Terminal" tab (alongside Console Logs)
- Only visible for resources with `TerminalAnnotation`
- SignalR/WebSocket bridge: xterm.js ↔ AppHost backchannel ↔ terminal host UDS
- Resize handling from browser to terminal host
### Phase 6: CLI Terminal Command (microsoft/aspire)
Add `aspire terminal `:
- Puts local terminal in raw mode
- Forwards local stdin → terminal host, terminal host → local stdout
- Handles resize (SIGWINCH on Unix, console buffer change on Windows)
- Detach key: `Ctrl+]` (consistent with Hex1b)
### Phase 7: Non-DCP Resource Support (microsoft/aspire)
Support resources not launched by DCP:
- `WithTerminal(Func connectCallback)` overload
- Custom resources (remote SSH, cloud resources) provide their own PTY bridge
## Dependency Graph
```
Phase 1 (App Model API) ─── Can start immediately
│
├── Phase 2 (Terminal Host) ─── Depends on Phase 1
│ │
│ └── Phase 3 (DCP) ─── Depends on Phase 2 (protocol def)
│ │
│ └── Phase 4 ─── Depends on Phase 3
│
├── Phase 5 (Dashboard) ─── After Phase 4
│
├── Phase 6 (CLI) ─── After Phase 4
│
└── Phase 7 (Non-DCP) ─── After Phase 1
```
**Critical path:** Phase 1 → 2 → 3 → 4 → 5/6
## Open Questions
1. **Hex1b packaging** — Is Hex1b available as a NuGet package for the terminal host, or does it need to be vendored?
2. **Windows ConPTY** — Does DCP need separate PTY codepaths for Windows vs Unix?
3. **Terminal host lifecycle** — Should it restart on crash? Persist history across restarts?
4. **Security** — UDS file permissions restrict access, but should we add authentication tokens?
5. **Multiple terminals** — Should a resource support multiple concurrent terminal sessions?
6. **xterm.js sizing** — Full xterm.js or a lighter alternative? Fit addon for auto-resize?
## Existing Infrastructure Leveraged
- ✅ Annotation + extension method pattern (standard app model extension)
- ✅ Hidden resources (`IsHidden = true` in `CustomResourceSnapshot`)
- ✅ Wait annotations (`WaitAnnotation` with `WaitUntilHealthy`)
- ✅ UDS transport (already used by backchannel)
- ✅ Backchannel RPC (V2 request/response streaming)
- ✅ DCP Kubernetes CRD API (new fields are just JSON properties)
- ✅ Hex1b (headless terminal automation with state management)
/cc @davidfowl @danegsta
Contributor guide
Research direction
Start by reading the existing app-model annotation and extension-method patterns, then review the proposed Aspire.Hosting.TerminalHost project and the referenced ExecutableSpec.cs and ContainerSpec.cs files. Trace the dependency graph from Phase 1 through DCP, backchannel, dashboard, and CLI integration; done requires the terminal API and all listed interactive clients to work together, with the open design questions resolved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, javascript, typescript
- Domain
- api, backend, cli, devtools, frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100