Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions extension/loc/xlf/aspire-vscode.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,25 @@
"description": "%configuration.aspire.enableAspireDashboardAutoLaunch%",
"scope": "window"
},
"aspire.dashboardBrowser": {
"type": "string",
"enum": ["openExternalBrowser", "debugChrome", "debugEdge", "debugFirefox"],
"default": "openExternalBrowser",
"description": "%configuration.aspire.dashboardBrowser%",
"enumDescriptions": [
"%configuration.aspire.dashboardBrowser.openExternalBrowser%",
"%configuration.aspire.dashboardBrowser.debugChrome%",
"%configuration.aspire.dashboardBrowser.debugEdge%",
"%configuration.aspire.dashboardBrowser.debugFirefox%"
],
"scope": "window"
},
"aspire.closeDashboardOnDebugEnd": {
"type": "boolean",
"default": true,
"description": "%configuration.aspire.closeDashboardOnDebugEnd%",
"scope": "window"
},
"aspire.enableDebugConfigEnvironmentLogging": {
"type": "boolean",
"default": true,
Expand Down
6 changes: 6 additions & 0 deletions extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
"configuration.aspire.enableAspireCliDebugLogging": "Enable console debug logging for Aspire CLI commands executed by the extension.",
"configuration.aspire.enableAspireDcpDebugLogging": "Enable Developer Control Plane (DCP) debug logging for Aspire applications. Logs will be stored in the workspace's .aspire/dcp/logs-{debugSessionId} folder.",
"configuration.aspire.enableAspireDashboardAutoLaunch": "Enable automatic launch of the Aspire Dashboard when using the Aspire debugger.",
"configuration.aspire.dashboardBrowser": "The browser to use when auto-launching the Aspire Dashboard.",
"configuration.aspire.dashboardBrowser.openExternalBrowser": "Use the system default browser (cannot auto-close).",
"configuration.aspire.dashboardBrowser.debugChrome": "Launch Chrome as a debug session (auto-closes when debugging ends).",
"configuration.aspire.dashboardBrowser.debugEdge": "Launch Microsoft Edge as a debug session (auto-closes when debugging ends).",
"configuration.aspire.dashboardBrowser.debugFirefox": "Launch Firefox as a debug session (requires Firefox Debugger extension).",
"configuration.aspire.closeDashboardOnDebugEnd": "Close the dashboard browser when the debug session ends. Works with debug browser options (Chrome, Edge, Firefox).",
"configuration.aspire.enableDebugConfigEnvironmentLogging": "Include environment variables when logging debug session configurations. This can help diagnose environment-related issues but may expose sensitive information in logs.",
"command.runAppHost": "Run Aspire apphost",
"command.debugAppHost": "Debug Aspire apphost",
Expand Down
109 changes: 108 additions & 1 deletion extension/src/debugger/AspireDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import { AspireResourceExtendedDebugConfiguration, AspireResourceDebugSession, E
import { extensionLogOutputChannel } from "../utils/logging";
import AspireDcpServer, { generateDcpIdPrefix } from "../dcp/AspireDcpServer";
import { spawnCliProcess } from "./languages/cli";
import { disconnectingFromSession, launchingWithAppHost, launchingWithDirectory, processExceptionOccurred, processExitedWithCode } from "../loc/strings";
import { disconnectingFromSession, launchingWithAppHost, launchingWithDirectory, processExceptionOccurred, processExitedWithCode, aspireDashboard } from "../loc/strings";
import { projectDebuggerExtension } from "./languages/dotnet";
import AspireRpcServer from "../server/AspireRpcServer";
import { createDebugSessionConfiguration } from "./debuggerExtensions";
import { AspireTerminalProvider } from "../utils/AspireTerminalProvider";
import { ICliRpcClient } from "../server/rpcClient";
import path from "path";
import os from "os";
import { EnvironmentVariables } from "../utils/environment";

export type DashboardBrowserType = 'openExternalBrowser' | 'debugChrome' | 'debugEdge' | 'debugFirefox';

export class AspireDebugSession implements vscode.DebugAdapter {
private readonly _onDidSendMessage = new EventEmitter<any>();
private _messageSeq = 1;
Expand All @@ -28,6 +31,7 @@ export class AspireDebugSession implements vscode.DebugAdapter {
private _resourceDebugSessions: AspireResourceDebugSession[] = [];
private _trackedDebugAdapters: string[] = [];
private _rpcClient?: ICliRpcClient;
private _dashboardDebugSession: vscode.DebugSession | null = null;
private readonly _disposables: vscode.Disposable[] = [];

public readonly onDidSendMessage = this._onDidSendMessage.event;
Expand Down Expand Up @@ -280,6 +284,79 @@ export class AspireDebugSession implements vscode.DebugAdapter {
});
}

/**
* Opens the dashboard URL in the specified browser.
* For debugChrome/debugEdge/debugFirefox, launches as a child debug session that auto-closes with the Aspire debug session.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is probably a dumb question, but why would a user want to debug the dashboard?

@adamint adamint Mar 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It ties the lifetime of the dashboard window to the Aspire debug session by opening a controlled browser window. It's what VS does with the aspire dashboard

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does the user have to configure something themselves to get this to work? In VS it just happens AFAICT

@adamint adamint Mar 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Currently yes because the default is to open the external browser. I could change the default to debugChrome? But we don't know that Chrome is installed. What do you think

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What does VS do?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Actually, dev kit has the same behavior we do (launching the external browser). I think we should keep that approach

*/
async openDashboard(url: string, browserType: DashboardBrowserType): Promise<void> {
extensionLogOutputChannel.info(`Opening dashboard in browser: ${browserType}, URL: ${url}`);

switch (browserType) {
case 'debugChrome':
await this.launchDebugBrowser(url, 'pwa-chrome');
break;

case 'debugEdge':
await this.launchDebugBrowser(url, 'pwa-msedge');
break;

case 'debugFirefox':
await this.launchDebugBrowser(url, 'firefox');
break;

case 'openExternalBrowser':
default:
// Use VS Code's default external browser handling
await vscode.env.openExternal(vscode.Uri.parse(url));
break;
}
}

/**
* Launches a browser as a child debug session.
* The browser will automatically close when the parent Aspire debug session ends.
*/
private async launchDebugBrowser(url: string, debugType: 'pwa-chrome' | 'pwa-msedge' | 'firefox'): Promise<void> {
const debugConfig: vscode.DebugConfiguration = {
type: debugType,
name: aspireDashboard,
request: 'launch',
url: url,
};

// Add type-specific options
if (debugType === 'pwa-chrome' || debugType === 'pwa-msedge') {
// Don't pause on entry for Chrome/Edge
debugConfig.pauseForSourceMap = false;
}
else if (debugType === 'firefox') {
// Firefox debugger requires webRoot; resolve to actual workspace path
debugConfig.webRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? os.tmpdir();
debugConfig.pathMappings = [];
}

// Register listener before starting so we don't miss the event
const disposable = vscode.debug.onDidStartDebugSession((session) => {
if (session.configuration.name === aspireDashboard && session.type === debugType) {
this._dashboardDebugSession = session;
disposable.dispose();
}
});

// Start as a child debug session - it will close when parent closes
const didStart = await vscode.debug.startDebugging(
undefined,
debugConfig,
this._session
);

if (!didStart) {
disposable.dispose();
extensionLogOutputChannel.warn(`Failed to start debug browser (${debugType}), falling back to default browser`);
await vscode.env.openExternal(vscode.Uri.parse(url));
}
Comment thread
adamint marked this conversation as resolved.
}

dispose(): void {
extensionLogOutputChannel.info('Stopping the Aspire debug session');
vscode.debug.stopDebugging(this._session);
Expand All @@ -288,6 +365,36 @@ export class AspireDebugSession implements vscode.DebugAdapter {
this._rpcClient?.stopCli();
}

/**
* Closes the dashboard browser if closeDashboardOnDebugEnd is enabled.
* Handles closing debug browser sessions.
*/
private closeDashboard(): void {
const aspireConfig = vscode.workspace.getConfiguration('aspire');
const shouldClose = aspireConfig.get<boolean>('closeDashboardOnDebugEnd', true);
Comment on lines +387 to +388

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In VS Code extensions I've written, I always end up creating a config class that reads the config and exposes property values the class as a service. Then you simply import the config class (and it implicitly reads from keys) - eliminating duplicated hardcoded strings throughout the code. This is an abstraction I'd strongly recommend.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's a good idea! I will follow this up with a clean up


if (!shouldClose) {
this._dashboardDebugSession = null;
return;
}

extensionLogOutputChannel.info('Closing dashboard browser...');

// For debug browsers, stop the debug session
if (this._dashboardDebugSession) {
vscode.debug.stopDebugging(this._dashboardDebugSession).then(
() => extensionLogOutputChannel.info('Dashboard debug session stopped.'),
(err) => extensionLogOutputChannel.warn(`Failed to stop dashboard debug session: ${err}`)
);
this._dashboardDebugSession = null;
return;
}
// At this point there is no tracked dashboard debug session to stop.
// Any debug browser child sessions (debugChrome, debugEdge, debugFirefox) will
// automatically close when the parent Aspire session is stopped, so no further
// cleanup is required here.
}

private sendResponse(request: any, body: any = {}) {
this._onDidSendMessage.fire({
type: 'response',
Expand Down
1 change: 1 addition & 0 deletions extension/src/loc/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const codespacesUrl = (url: string) => vscode.l10n.t('Codespaces: {0}', u
export const directLink = vscode.l10n.t('Open local URL');
export const codespacesLink = vscode.l10n.t('Open codespaces URL');
export const openAspireDashboard = vscode.l10n.t('Launch Aspire Dashboard');
export const aspireDashboard = vscode.l10n.t('Aspire Dashboard');
export const noWorkspaceOpen = vscode.l10n.t('No workspace is open. Please open a folder or workspace before running this command.');
export const failedToShowPromptEmpty = vscode.l10n.t('Failed to show prompt, text was empty.');
export const rpcServerAddressError = vscode.l10n.t('Failed to get RPC server address. The extension may not function correctly.');
Expand Down
21 changes: 17 additions & 4 deletions extension/src/server/interactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ProgressNotifier } from './progressNotifier';
import { applyTextStyle, formatText } from '../utils/strings';
import { extensionLogOutputChannel } from '../utils/logging';
import { AspireExtendedDebugConfiguration, EnvVar } from '../dcp/types';
import { AspireDebugSession } from '../debugger/AspireDebugSession';
import { AspireDebugSession, DashboardBrowserType } from '../debugger/AspireDebugSession';
import { AnsiColors } from '../utils/AspireTerminalProvider';
import { isDirectory } from '../utils/io';

Expand All @@ -33,6 +33,7 @@ export interface IInteractionService {
logMessage: (logLevel: CSLogLevel, message: string) => void;
launchAppHost(projectFile: string, args: string[], environment: EnvVar[], debug: boolean): Promise<void>;
stopDebugging: () => void;
closeDashboard: () => void;
notifyAppHostStartupCompleted: () => void;
startDebugSession: (workingDirectory: string, projectFile: string | null, debug: boolean) => Promise<void>;
writeDebugSessionMessage: (message: string, stdout: boolean, textStyle?: string) => void;
Expand Down Expand Up @@ -305,11 +306,16 @@ export class InteractionService implements IInteractionService {

// If aspire.enableAspireDashboardAutoLaunch is true, the dashboard will be launched automatically and we do not need
// to show an information message.
const enableDashboardAutoLaunch = vscode.workspace.getConfiguration('aspire').get<boolean>('enableAspireDashboardAutoLaunch', true);
const aspireConfig = vscode.workspace.getConfiguration('aspire');
const enableDashboardAutoLaunch = aspireConfig.get<boolean>('enableAspireDashboardAutoLaunch', true);
if (enableDashboardAutoLaunch) {
// Open the dashboard URL in an external browser. Prefer codespaces URL if available.
// Open the dashboard URL in the configured browser. Prefer codespaces URL if available.
const urlToOpen = codespacesUrl || baseUrl;
vscode.env.openExternal(vscode.Uri.parse(urlToOpen));
const debugSession = this._getAspireDebugSession();
if (debugSession) {
const browserType = aspireConfig.get<DashboardBrowserType>('dashboardBrowser', 'openExternalBrowser');
await debugSession.openDashboard(urlToOpen, browserType);
}
Comment thread
adamint marked this conversation as resolved.
return;
}

Expand Down Expand Up @@ -458,6 +464,13 @@ export class InteractionService implements IInteractionService {
clearProgressNotification() {
this._progressNotifier.clear();
}

/**
* Closes the dashboard browser. Delegates to the current AspireDebugSession.
*/
closeDashboard(): void {
// No-op when called from InteractionService - the debug session handles closing in dispose()
}
}

function tryExecuteEndpoint(interactionService: IInteractionService, withAuthentication: (callback: (...params: any[]) => any) => (...params: any[]) => any) {
Expand Down