Add a global native Aspire tray companion for macOS and Windows
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 196
Description
More information on our issue management policies can be found here: https://aka.ms/aspnet/issue-policies
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Is your feature request related to a problem? Please describe the problem.
> Want to add an aspire tray icon? I deal with enough app hosts running at once that 'aspire ps' is not ergonomic enough
Developers running multiple AppHosts need an always-available way to identify applications, open the right dashboard, and stop a particular instance. Similar project names across repositories and worktrees make this harder.
### Describe the solution you'd like
Add **Aspire Tray**, a native Windows notification-area app and macOS menu-bar app shipped inside the Aspire CLI bundle and backed by the CLI's structured commands.
**One global tray per signed-in user shows all discoverable local AppHosts across repositories and worktrees.** Discovery is independent of the directory from which the tray was started. Individual AppHosts are selected only when performing an action on them.
The tray runs as a separate native process. Its files arrive with the Aspire bundle, and using it requires no separate tray installation.
#### Commands
Add these CLI commands:
```sh
aspire tray start
aspire tray stop
```
`start` ensures the bundle is extracted, resolves the platform's tray payload, and launches or activates the single global tray instance. It supplies the absolute path of the launching CLI so the tray uses the intended Aspire installation. The command returns after successful activation; the tray remains running when the launching CLI or terminal exits.
Repeated or concurrent starts, including from different repositories or Aspire installations, must activate the existing instance rather than create duplicates. They must not silently switch the active installation/channel.
`stop` gracefully quits the global tray and its owned CLI subprocesses. It leaves AppHosts, bundle files, and launch-at-login preferences unchanged. Launch, activation, and shutdown failures must be reported explicitly.
#### User experience
Clicking the icon opens a compact, keyboard- and screen-reader-accessible native panel:
```text
Aspire 3 running
-----------------------------------------------------
Search apps...
TestShop ...
main | ~/src/testshop
Open dashboard
TestShop ...
checkout-fix | ~/worktrees/testshop-checkout
Open dashboard
Playground ...
main | ~/src/aspire/playground
Open dashboard
-----------------------------------------------------
Stop all 3 AppHosts...
Settings...
Quit Aspire Tray
```
- Show friendly names with repository/worktree/path context; expose the full AppHost path and PID in details. Branch names are optional enrichment, not instance identity.
- Search by application name, path, or worktree, and keep ordering stable during updates. Search filters the displayed list, not discovery scope.
- Make opening the selected dashboard in the default browser the primary action. Secondary actions open the containing folder or available AppHost log, or gracefully stop the selected AppHost.
- Target the exact live instance, including when the same project runs multiple times. Keep destructive actions separate from navigation and confirm bulk stop with the affected applications and count. Execute bulk stop against the confirmed instances, not applications that appear afterward.
- Surface startup, shutdown, unavailable capabilities, and connection failures. Mark stale information and disable unsafe actions; connection loss must not appear as an empty application list. AppHost presence must not be labeled as resource health.
- Start without opening the panel. Quitting or restarting the tray leaves AppHosts and persistent resources untouched.
#### Architecture
| Platform | Native UI | Backend |
|---|---|---|
| Windows | C# GUI executable with self-contained dependencies | Aspire CLI subprocesses using structured output |
| macOS | Swift/AppKit `.app` | Aspire CLI subprocesses using structured output |
```text
Native tray UI
|-- discovery <- aspire ps --follow --format json
|-- dashboard -> default browser
`-- actions -> targeted, non-interactive Aspire CLI commands
```
Use one long-lived `aspire ps --follow --format json` subprocess to maintain the global AppHost list. Consume its NDJSON updates rather than repeatedly launching polling commands. The stream must include all discoverable AppHosts for the user, without an AppHost/project selector or current-directory filtering.
Execute lifecycle actions through non-interactive CLI commands with explicit instance targeting. The CLI owns discovery, AppHost protocol compatibility, capability checks, and action semantics; the tray owns presentation and user interaction. Support mixed AppHost/SDK versions through the CLI's compatibility handling.
Launch subprocesses using the supplied absolute executable path and argument lists, without a shell or `PATH` lookup. Read stdout and stderr concurrently, keeping machine-readable output separate from diagnostics. The tray owns its discovery/action subprocesses and closes only those processes on shutdown. Native UI dependencies stay outside the CLI executable.
Use current-user local coordination for global activation/shutdown. Keep writable state outside signed payload directories and avoid logging or persisting dashboard authentication tokens. The Aspire distribution supplies the required runtime components; users do not need to install an additional .NET runtime, SDK, or Docker merely to run the tray.
#### CLI contract requirements
Treat structured commands as a supported automation interface:
- Provide an opt-in initial snapshot-complete/readiness signal for discovery, including when zero AppHosts are running. The UI must distinguish an empty completed scan from startup or connection failure. Preserve existing `ps` JSON consumers when extending the stream.
- Expose a reliable live instance identity and support exact-instance targeting for actions. Project path alone is insufficient when multiple instances run the same project; validate that the selected instance is still the intended target before stopping it.
- Surface discovery and action failures through actionable diagnostics and appropriate process results. Unexpected discovery-process termination leaves the UI unavailable/stale, not empty or healthy.
- Reconnect with bounded backoff and reconcile a fresh complete snapshot so instances that disappeared during a disconnect do not remain indefinitely. Handle malformed output and bound buffered data.
- Coordinate the native UI and CLI backend versions during upgrade so discovery and action commands remain compatible throughout a tray session.
#### Bundle and release integration
Include a platform-specific `tray/` payload alongside the existing `managed/` and `dcp/` directories:
| Bundle target | Tray payload |
|---|---|
| Windows x64 and ARM64 | `tray/aspire-tray.exe` and required self-contained dependencies |
| macOS Intel and Apple Silicon | `tray/Aspire Tray.app` |
Launch the Windows UI and its CLI subprocesses without console windows, and launch the macOS app directly from the extracted bundle. An Applications-directory copy is not required. Linux bundles do not include a tray payload initially. Shipping or extracting the files never enables the tray automatically.
Build and sign the desktop payload before embedding it. Windows executables require signing; macOS requires signing the app and nested code, hardened-runtime entitlements as appropriate, notarization, and stapling. Preserve signatures, permissions, bundle structure, and tickets through packaging and extraction, and validate the final extracted payloads under OS trust checks.
Extend bundle assembly, layout discovery, and validation for supported architectures. Publish verified payloads through Aspire's existing CLI bundle artifacts and signed release flow. The tray and CLI backend share the bundle's release/update lifecycle; observed AppHosts need not match that version.
#### Lifetime, upgrades, and desktop integration
- Protect the resident tray payload with a bundle-version lease after the launching CLI exits. Also account for the CLI backend executable and any bundle resources held by its subprocesses during replacement and cleanup.
- Coordinate handoff of the UI and backend to newer Aspire versions. Preserve settings and startup preferences, handle Windows executable locks and macOS app identity, and retain a usable prior payload if extraction or upgrade fails. Downloading a new bundle does not itself replace the running processes.
- Offer launch at login as an opt-in setting. Desktop shortcuts are optional conveniences, not prerequisites for starting the tray.
- Persistent registration must use an upgrade-safe activation path tied to the selected Aspire installation, providing the correct CLI path on every launch. Disabling integration removes its registration; the owning Aspire removal path must account for registrations it created.
- Activation, updates, and integration use current-user permissions and respect OS/enterprise policies. Tray shutdown, upgrades, or disabling integration must never stop AppHosts or delete their persistent resources.
#### Initial scope
Deliver global AppHost discovery, worktree-aware identification, dashboard navigation, graceful AppHost stop, tray start/stop, reliable CLI automation contracts, signed bundle distribution, and optional login integration.
AppHost launching/restarting, remote/WSL/container-host discovery bridges, per-resource controls, resource-health aggregation, and health/exit notifications are outside the initial scope. The dashboard remains the detailed application-management surface.
#### Acceptance criteria
- [ ] Signed native tray payloads ship and launch from Windows x64/ARM64 and macOS Intel/Apple Silicon bundles without a separate tray installation or additional runtime prerequisites.
- [ ] `aspire tray start` activates one global instance per user regardless of working directory. It shows all discoverable local AppHosts across repositories/worktrees and survives launching CLI/terminal exit.
- [ ] The tray uses the launcher's resolved CLI path for one global discovery stream and targeted actions, with safe argument passing and no interactive prompts.
- [ ] `aspire tray stop` quits only the tray and its owned CLI subprocesses, preserving AppHosts and startup preferences.
- [ ] The CLI contract distinguishes initial empty state from startup/failure, supports exact-instance actions, and allows recovery from disconnected/malformed streams without stale instances or unbounded buffering.
- [ ] The panel distinguishes worktrees/concurrent instances, opens the correct dashboard, and targets AppHost shutdown precisely, with bulk-stop confirmation.
- [ ] Mixed AppHost versions, unsupported capabilities, and failed commands produce explicit, safe UI states.
- [ ] Concurrent activation, bundle leases, and UI/backend upgrade handoff preserve usable payloads, settings, and startup preferences without duplicate instances or unintended channel changes.
- [ ] Login integration is opt-in and upgrade-safe. Stopping or updating the tray leaves AppHosts and persistent resources untouched.
- [ ] Final extracted payloads pass platform signing/trust checks and ship through existing Aspire release routes.
### Additional context
Implementation foundations:
- [`PsCommand`](https://github.com/microsoft/aspire/blob/main/src/Aspire.Cli/Commands/PsCommand.cs) and [CLI output formats](https://github.com/microsoft/aspire/blob/main/docs/specs/cli-output-formats.md): global AppHost discovery and NDJSON follow output.
- [`StopCommand`](https://github.com/microsoft/aspire/blob/main/src/Aspire.Cli/Commands/StopCommand.cs): graceful AppHost shutdown and the command surface to extend for exact-instance targeting.
- [`eng/Bundle.proj`](https://github.com/microsoft/aspire/blob/main/eng/Bundle.proj) and [`tools/CreateLayout`](https://github.com/microsoft/aspire/tree/main/tools/CreateLayout): bundle build and layout assembly.
- [`BundleService`](https://github.com/microsoft/aspire/blob/main/src/Aspire.Cli/Bundles/BundleService.cs) and [`BundleVersionLease`](https://github.com/microsoft/aspire/blob/main/src/Shared/BundleVersionLease.cs): versioned extraction and running-payload protection.
- [Native CLI packaging](https://github.com/microsoft/aspire/blob/main/docs/ci/native-cli-packaging.md), [`Signing.props`](https://github.com/microsoft/aspire/blob/main/eng/Signing.props), and [release process](https://github.com/microsoft/aspire/blob/main/docs/release-process.md): signed artifact production and publication.
Contributor guide
Research direction
Start with src/Aspire.Cli/Commands/PsCommand.cs and docs/specs/cli-output-formats.md to understand global discovery and NDJSON output, then inspect the StopCommand entry point mentioned in the issue. The acceptance criteria define completion: native signed tray payloads for Windows and macOS, global discovery and exact-instance actions, CLI start/stop commands, bundle integration, recovery, and optional login integration.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, swift
- Domain
- cli, desktop, devtools, release
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100