From 64e43f5ebd55af0c4703c344060b35d006015e12 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Thu, 15 Jan 2026 07:59:52 -0600 Subject: [PATCH 01/12] Refactor serverReadyAction handling --- extension/src/dcp/types.ts | 17 +++++ extension/src/debugger/languages/dotnet.ts | 2 +- extension/src/debugger/launchProfiles.ts | 39 ++++++++---- extension/src/test/dotnetDebugger.test.ts | 6 +- extension/src/test/launchProfiles.test.ts | 72 ++++++++++++++-------- 5 files changed, 98 insertions(+), 38 deletions(-) diff --git a/extension/src/dcp/types.ts b/extension/src/dcp/types.ts index e76e7f3bc21..8283260b6da 100644 --- a/extension/src/dcp/types.ts +++ b/extension/src/dcp/types.ts @@ -49,6 +49,21 @@ export interface EnvVar { value: string; } +export type ServerReadyActionAction = 'openExternally' | 'debugWithChrome' | 'debugWithEdge'; + +export interface ServerReadyAction { + action: ServerReadyActionAction; + /** + * Regex that matches a URL. Prefer a capture group so VS Code can substitute it into uriFormat. + * Example match: "Now listening on: https://localhost:5001" + */ + pattern: string; + /** + * URI format string used with the first capture group (commonly "%s"). + */ + uriFormat?: string; +} + export interface RunSessionPayload { launch_configurations: ExecutableLaunchConfiguration[]; env?: EnvVar[]; @@ -98,6 +113,7 @@ export interface LaunchOptions { debugSessionId: string; isApphost: boolean; debugSession: AspireDebugSession; + parentDebugConfiguration?: AspireExtendedDebugConfiguration; }; export interface AspireResourceDebugSession { @@ -115,6 +131,7 @@ export interface AspireResourceExtendedDebugConfiguration extends vscode.DebugCo export interface AspireExtendedDebugConfiguration extends vscode.DebugConfiguration { program: string; debuggers?: AspireDebuggersConfiguration; + serverReadyAction?: ServerReadyAction; } interface AspireDebuggersConfiguration { diff --git a/extension/src/debugger/languages/dotnet.ts b/extension/src/debugger/languages/dotnet.ts index d7f50e9c076..21f6ffa6b68 100644 --- a/extension/src/debugger/languages/dotnet.ts +++ b/extension/src/debugger/languages/dotnet.ts @@ -291,7 +291,7 @@ export function createProjectDebuggerExtension(dotNetServiceProducer: (debugSess // The apphost's application URL is the Aspire dashboard URL. We already get the dashboard login URL later on, // so we should just avoid setting up serverReadyAction and manually open the browser ourselves. if (!launchOptions.isApphost) { - debugConfiguration.serverReadyAction = determineServerReadyAction(baseProfile?.launchBrowser, baseProfile?.applicationUrl); + debugConfiguration.serverReadyAction = determineServerReadyAction({ launchBrowser: baseProfile?.launchBrowser, parentDebugConfiguration: launchOptions.parentDebugConfiguration }); } // Temporarily disable GH Copilot on the dashboard before the extension implementation is approved diff --git a/extension/src/debugger/launchProfiles.ts b/extension/src/debugger/launchProfiles.ts index d08cfe6f0c8..d9e41cdf1b3 100644 --- a/extension/src/debugger/launchProfiles.ts +++ b/extension/src/debugger/launchProfiles.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as fs from 'fs'; -import { ExecutableLaunchConfiguration, EnvVar, ProjectLaunchConfiguration } from '../dcp/types'; +import { ExecutableLaunchConfiguration, EnvVar, ProjectLaunchConfiguration, ServerReadyAction, ServerReadyActionAction } from '../dcp/types'; import { extensionLogOutputChannel } from '../utils/logging'; import { isSingleFileApp } from './languages/dotnet'; import { stripComments } from 'jsonc-parser'; @@ -50,6 +50,8 @@ export async function readLaunchSettings(projectPath: string): Promise { assert.strictEqual(debugConfig.executablePath, 'exePath'); assert.strictEqual(debugConfig.checkForDevCert, true); - // serverReadyAction should be present with the applicationUrl + // serverReadyAction should be present when launchBrowser is true assert.notStrictEqual(debugConfig.serverReadyAction, undefined); - assert.strictEqual(debugConfig.serverReadyAction.uriFormat, 'https://localhost:5001'); + assert.strictEqual(debugConfig.serverReadyAction.uriFormat, '%s'); + assert.strictEqual(debugConfig.serverReadyAction.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); // cleanup fs.rmSync(tempDir, { recursive: true, force: true }); }); + }); diff --git a/extension/src/test/launchProfiles.test.ts b/extension/src/test/launchProfiles.test.ts index 2a04c153294..9f41b998030 100644 --- a/extension/src/test/launchProfiles.test.ts +++ b/extension/src/test/launchProfiles.test.ts @@ -86,8 +86,8 @@ suite('Launch Profile Tests', () => { const result = determineBaseLaunchProfile(launchConfig, sampleLaunchSettings); - assert.strictEqual(result.profile, null); - assert.strictEqual(result.profileName, null); + assert.strictEqual(result.profile, null); + assert.strictEqual(result.profileName, null); }); test('returns first profile with commandName=Project when no explicit profile specified', () => { @@ -335,17 +335,25 @@ suite('Launch Profile Tests', () => { }); suite('determineWorkingDirectory', () => { - const projectPath = path.join('C:', 'project', 'MyApp.csproj'); + const isWin = process.platform === 'win32'; + const pathImpl = isWin ? path.win32 : path.posix; + const projectPath = isWin + ? pathImpl.join('C:', 'project', 'MyApp.csproj') + : pathImpl.join('/', 'project', 'MyApp.csproj'); test('uses absolute working directory from launch profile', () => { const baseProfile: LaunchProfile = { commandName: 'Project', - workingDirectory: path.join('C:', 'custom', 'working', 'dir') + workingDirectory: isWin + ? pathImpl.join('C:', 'custom', 'working', 'dir') + : pathImpl.join('/', 'custom', 'working', 'dir') }; const result = determineWorkingDirectory(projectPath, baseProfile); - assert.strictEqual(result, path.join('C:', 'custom', 'working', 'dir')); + assert.strictEqual(result, isWin + ? pathImpl.join('C:', 'custom', 'working', 'dir') + : pathImpl.join('/', 'custom', 'working', 'dir')); }); test('resolves relative working directory from launch profile', () => { @@ -356,7 +364,9 @@ suite('Launch Profile Tests', () => { const result = determineWorkingDirectory(projectPath, baseProfile); - assert.strictEqual(result, path.join('C:', 'project', 'custom')); + assert.strictEqual(result, isWin + ? pathImpl.join('C:', 'project', 'custom') + : pathImpl.join('/', 'project', 'custom')); }); test('uses project directory when no working directory specified', () => { @@ -366,45 +376,59 @@ suite('Launch Profile Tests', () => { const result = determineWorkingDirectory(projectPath, baseProfile); - assert.strictEqual(result, path.join('C:', 'project')); + assert.strictEqual(result, isWin + ? pathImpl.join('C:', 'project') + : pathImpl.join('/', 'project')); }); test('uses project directory when base profile is null', () => { const result = determineWorkingDirectory(projectPath, null); - assert.strictEqual(result, path.join('C:', 'project')); + assert.strictEqual(result, isWin + ? pathImpl.join('C:', 'project') + : pathImpl.join('/', 'project')); }); }); suite('determineServerReadyAction', () => { test('returns undefined when launchBrowser is false', () => { - const result = determineServerReadyAction(false, 'https://localhost:5001'); + const result = determineServerReadyAction({ launchBrowser: false }); assert.strictEqual(result, undefined); }); - test('returns undefined when applicationUrl is undefined', () => { - const result = determineServerReadyAction(true, undefined); - assert.strictEqual(result, undefined); + test('returns serverReadyAction when launchBrowser true', () => { + const result = determineServerReadyAction({ launchBrowser: true }); + + assert.notStrictEqual(result, undefined); + assert.strictEqual(result?.action, 'openExternally'); + assert.strictEqual(result?.uriFormat, '%s'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); }); - test('returns serverReadyAction when launchBrowser true and applicationUrl provided', () => { - const applicationUrl = 'https://localhost:5001'; - const result = determineServerReadyAction(true, applicationUrl); + test('uses provided action when specified', () => { + const result = determineServerReadyAction({ launchBrowser: true, action: 'debugWithEdge' }); assert.notStrictEqual(result, undefined); - assert.strictEqual(result?.action, 'openExternally'); - assert.strictEqual(result?.uriFormat, applicationUrl); - assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); + assert.strictEqual(result?.action, 'debugWithEdge'); + assert.strictEqual(result?.uriFormat, '%s'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); }); - test('returns serverReadyAction with first URL when multiple URLs separated by semicolon', () => { - const applicationUrl = 'https://localhost:5001;http://localhost:5000'; - const result = determineServerReadyAction(true, applicationUrl); + test('returns parent serverReadyAction when provided', () => { + const parent = { + serverReadyAction: { + action: 'debugWithChrome' as const, + pattern: '\\bNow listening on:\\s+(https?://\\S+)', + uriFormat: '%s' + } + }; + + const result = determineServerReadyAction({ launchBrowser: true, parentDebugConfiguration: parent }); assert.notStrictEqual(result, undefined); - assert.strictEqual(result?.action, 'openExternally'); - assert.strictEqual(result?.uriFormat, 'https://localhost:5001'); - assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); + assert.strictEqual(result?.action, 'debugWithChrome'); + assert.strictEqual(result?.uriFormat, '%s'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); }); }); From 5d62f62c9c9fff436ad9fcb5c22204e1f6c2333b Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Thu, 15 Jan 2026 08:29:02 -0600 Subject: [PATCH 02/12] Align ServerReadyAction with VS Code --- extension/src/dcp/types.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/extension/src/dcp/types.ts b/extension/src/dcp/types.ts index 8283260b6da..f85398bba12 100644 --- a/extension/src/dcp/types.ts +++ b/extension/src/dcp/types.ts @@ -49,10 +49,10 @@ export interface EnvVar { value: string; } -export type ServerReadyActionAction = 'openExternally' | 'debugWithChrome' | 'debugWithEdge'; +export type ServerReadyActionAction = 'openExternally' | 'debugWithChrome' | 'debugWithEdge' | 'startDebugging'; export interface ServerReadyAction { - action: ServerReadyActionAction; + action?: ServerReadyActionAction; /** * Regex that matches a URL. Prefer a capture group so VS Code can substitute it into uriFormat. * Example match: "Now listening on: https://localhost:5001" @@ -62,6 +62,22 @@ export interface ServerReadyAction { * URI format string used with the first capture group (commonly "%s"). */ uriFormat?: string; + /** + * Web root for browser debugging (used by VS Code debug-server-ready). + */ + webRoot?: string; + /** + * Optional name for startDebugging. + */ + name?: string; + /** + * Optional debug configuration to start (used with startDebugging). + */ + config?: vscode.DebugConfiguration; + /** + * Whether to stop the browser debug session when the server stops. + */ + killOnServerStop?: boolean; } export interface RunSessionPayload { From 402c00e3387c846e94f857fae288276693d271d6 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Thu, 15 Jan 2026 08:31:35 -0600 Subject: [PATCH 03/12] Document ServerReadyAction source --- extension/src/dcp/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extension/src/dcp/types.ts b/extension/src/dcp/types.ts index f85398bba12..40206864db1 100644 --- a/extension/src/dcp/types.ts +++ b/extension/src/dcp/types.ts @@ -49,6 +49,8 @@ export interface EnvVar { value: string; } +// VS Code does not export a ServerReadyAction type; mirror it here: +// https://github.com/microsoft/vscode/blob/c36e2b89d9171f212b34491dd9b61eb72abbfb04/extensions/debug-server-ready/src/extension.ts#L15C1-L23C2 export type ServerReadyActionAction = 'openExternally' | 'debugWithChrome' | 'debugWithEdge' | 'startDebugging'; export interface ServerReadyAction { From cb74424aa3bb1dc3fcc5b32eb3b5f848cb58eb65 Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Thu, 15 Jan 2026 08:37:08 -0600 Subject: [PATCH 04/12] Simplify determineServerReadyAction signature --- extension/src/debugger/launchProfiles.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/extension/src/debugger/launchProfiles.ts b/extension/src/debugger/launchProfiles.ts index d9e41cdf1b3..0c5fda8965b 100644 --- a/extension/src/debugger/launchProfiles.ts +++ b/extension/src/debugger/launchProfiles.ts @@ -209,13 +209,8 @@ export interface ServerReadyActionOptions { } export function determineServerReadyAction( - launchBrowserOrOptions?: boolean | ServerReadyActionOptions, - action?: ServerReadyActionAction + options: ServerReadyActionOptions = {} ): ServerReadyAction | undefined { - const options: ServerReadyActionOptions = typeof launchBrowserOrOptions === 'object' - ? launchBrowserOrOptions - : { launchBrowser: launchBrowserOrOptions, action }; - if (!options.launchBrowser) { return undefined; } From 7a26f87cd69f8d81ea39b813d3020bff016b57ef Mon Sep 17 00:00:00 2001 From: Darren Kattan Date: Thu, 15 Jan 2026 09:12:54 -0600 Subject: [PATCH 05/12] tests: make determineWorkingDirectory cross-platform --- extension/src/test/launchProfiles.test.ts | 31 ++++++++--------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/extension/src/test/launchProfiles.test.ts b/extension/src/test/launchProfiles.test.ts index 9f41b998030..95e706af245 100644 --- a/extension/src/test/launchProfiles.test.ts +++ b/extension/src/test/launchProfiles.test.ts @@ -335,25 +335,22 @@ suite('Launch Profile Tests', () => { }); suite('determineWorkingDirectory', () => { - const isWin = process.platform === 'win32'; - const pathImpl = isWin ? path.win32 : path.posix; - const projectPath = isWin - ? pathImpl.join('C:', 'project', 'MyApp.csproj') - : pathImpl.join('/', 'project', 'MyApp.csproj'); + // Keep these tests cross-platform by deriving the platform's root from the current process. + // On Windows this is typically something like "C:\\" (drive-dependent), and on POSIX it's "/". + const systemRoot = path.parse(process.cwd()).root; + const projectPath = path.join(systemRoot, 'project', 'MyApp.csproj'); + const projectDir = path.dirname(projectPath); + const absoluteWorkingDir = path.join(systemRoot, 'custom', 'working', 'dir'); test('uses absolute working directory from launch profile', () => { const baseProfile: LaunchProfile = { commandName: 'Project', - workingDirectory: isWin - ? pathImpl.join('C:', 'custom', 'working', 'dir') - : pathImpl.join('/', 'custom', 'working', 'dir') + workingDirectory: absoluteWorkingDir }; const result = determineWorkingDirectory(projectPath, baseProfile); - assert.strictEqual(result, isWin - ? pathImpl.join('C:', 'custom', 'working', 'dir') - : pathImpl.join('/', 'custom', 'working', 'dir')); + assert.strictEqual(result, absoluteWorkingDir); }); test('resolves relative working directory from launch profile', () => { @@ -364,9 +361,7 @@ suite('Launch Profile Tests', () => { const result = determineWorkingDirectory(projectPath, baseProfile); - assert.strictEqual(result, isWin - ? pathImpl.join('C:', 'project', 'custom') - : pathImpl.join('/', 'project', 'custom')); + assert.strictEqual(result, path.join(projectDir, 'custom')); }); test('uses project directory when no working directory specified', () => { @@ -376,17 +371,13 @@ suite('Launch Profile Tests', () => { const result = determineWorkingDirectory(projectPath, baseProfile); - assert.strictEqual(result, isWin - ? pathImpl.join('C:', 'project') - : pathImpl.join('/', 'project')); + assert.strictEqual(result, projectDir); }); test('uses project directory when base profile is null', () => { const result = determineWorkingDirectory(projectPath, null); - assert.strictEqual(result, isWin - ? pathImpl.join('C:', 'project') - : pathImpl.join('/', 'project')); + assert.strictEqual(result, projectDir); }); }); From a3db71b765d243c07734ced6307df065834532cb Mon Sep 17 00:00:00 2001 From: Darren Kattan <1424395+dkattan@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:22:37 -0600 Subject: [PATCH 06/12] Update extension/src/debugger/launchProfiles.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extension/src/debugger/launchProfiles.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/extension/src/debugger/launchProfiles.ts b/extension/src/debugger/launchProfiles.ts index 0c5fda8965b..7f30087aa28 100644 --- a/extension/src/debugger/launchProfiles.ts +++ b/extension/src/debugger/launchProfiles.ts @@ -50,9 +50,14 @@ export async function readLaunchSettings(projectPath: string): Promise Date: Thu, 15 Jan 2026 09:22:56 -0600 Subject: [PATCH 07/12] Update extension/src/debugger/launchProfiles.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extension/src/debugger/launchProfiles.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/extension/src/debugger/launchProfiles.ts b/extension/src/debugger/launchProfiles.ts index 7f30087aa28..857d99514a7 100644 --- a/extension/src/debugger/launchProfiles.ts +++ b/extension/src/debugger/launchProfiles.ts @@ -85,7 +85,13 @@ export function determineBaseLaunchProfile( launchConfig: ProjectLaunchConfiguration, launchSettings: LaunchSettings | null ): LaunchProfileResult { - extensionLogOutputChannel.debug(`[launchProfile] determineBaseLaunchProfile: disable_launch_profile=${launchConfig.disable_launch_profile === true} launch_profile='${launchConfig.launch_profile ?? ''}' hasLaunchSettings=${!!launchSettings} profileCount=${launchSettings?.profiles ? Object.keys(launchSettings.profiles).length : 0}`); + const debugMessage = + `[launchProfile] determineBaseLaunchProfile: + disable_launch_profile=${launchConfig.disable_launch_profile === true} + launch_profile='${launchConfig.launch_profile ?? ''}' + hasLaunchSettings=${!!launchSettings} + profileCount=${launchSettings?.profiles ? Object.keys(launchSettings.profiles).length : 0}`; + extensionLogOutputChannel.debug(debugMessage); // If disable_launch_profile property is set to true in project launch configuration, there is no base profile, regardless of the value of launch_profile property. if (launchConfig.disable_launch_profile === true) { From 1dc1de654ef7ce021755e45716545fd7a54191da Mon Sep 17 00:00:00 2001 From: Darren Kattan <1424395+dkattan@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:23:20 -0600 Subject: [PATCH 08/12] Update extension/src/dcp/types.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extension/src/dcp/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/extension/src/dcp/types.ts b/extension/src/dcp/types.ts index 40206864db1..9f33417d746 100644 --- a/extension/src/dcp/types.ts +++ b/extension/src/dcp/types.ts @@ -58,6 +58,7 @@ export interface ServerReadyAction { /** * Regex that matches a URL. Prefer a capture group so VS Code can substitute it into uriFormat. * Example match: "Now listening on: https://localhost:5001" + * Example pattern: '\\bNow listening on:\\s+(https?://\\S+)' */ pattern: string; /** From d9d219f46423f2e764aac5161b6d44c0f5a9ae95 Mon Sep 17 00:00:00 2001 From: Darren Kattan <1424395+dkattan@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:23:37 -0600 Subject: [PATCH 09/12] Update extension/src/debugger/launchProfiles.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extension/src/debugger/launchProfiles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/debugger/launchProfiles.ts b/extension/src/debugger/launchProfiles.ts index 857d99514a7..4a1d60fe01b 100644 --- a/extension/src/debugger/launchProfiles.ts +++ b/extension/src/debugger/launchProfiles.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as fs from 'fs'; -import { ExecutableLaunchConfiguration, EnvVar, ProjectLaunchConfiguration, ServerReadyAction, ServerReadyActionAction } from '../dcp/types'; +import { EnvVar, ProjectLaunchConfiguration, ServerReadyAction, ServerReadyActionAction } from '../dcp/types'; import { extensionLogOutputChannel } from '../utils/logging'; import { isSingleFileApp } from './languages/dotnet'; import { stripComments } from 'jsonc-parser'; From 04bdeea31756f86a7f19b1ecfcd6421e8e6a6a5a Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Fri, 16 Jan 2026 12:35:33 -0500 Subject: [PATCH 10/12] nits --- extension/src/debugger/launchProfiles.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extension/src/debugger/launchProfiles.ts b/extension/src/debugger/launchProfiles.ts index 4a1d60fe01b..001db669588 100644 --- a/extension/src/debugger/launchProfiles.ts +++ b/extension/src/debugger/launchProfiles.ts @@ -87,7 +87,7 @@ export function determineBaseLaunchProfile( ): LaunchProfileResult { const debugMessage = `[launchProfile] determineBaseLaunchProfile: - disable_launch_profile=${launchConfig.disable_launch_profile === true} + disable_launch_profile=${!!launchConfig.disable_launch_profile} launch_profile='${launchConfig.launch_profile ?? ''}' hasLaunchSettings=${!!launchSettings} profileCount=${launchSettings?.profiles ? Object.keys(launchSettings.profiles).length : 0}`; @@ -226,9 +226,9 @@ export function determineServerReadyAction( return undefined; } - const parentSra = options.parentDebugConfiguration?.serverReadyAction; - if (parentSra) { - return parentSra; + const parentServerReadyAction = options.parentDebugConfiguration?.serverReadyAction; + if (parentServerReadyAction) { + return parentServerReadyAction; } return { From b9154823583a3d310677557df82c0d5978d1164f Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Fri, 16 Jan 2026 13:22:42 -0500 Subject: [PATCH 11/12] Use aspire debug configuration's serverReadyAction on a resource, if present. --- extension/src/dcp/types.ts | 38 +------------ extension/src/debugger/languages/dotnet.ts | 2 +- extension/src/debugger/launchProfiles.ts | 34 ++++++------ extension/src/test/dotnetDebugger.test.ts | 4 +- extension/src/test/launchProfiles.test.ts | 62 ++++++++++++++-------- 5 files changed, 63 insertions(+), 77 deletions(-) diff --git a/extension/src/dcp/types.ts b/extension/src/dcp/types.ts index 9f33417d746..7c93ec2b6be 100644 --- a/extension/src/dcp/types.ts +++ b/extension/src/dcp/types.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { AspireDebugSession } from '../debugger/AspireDebugSession'; +import { ServerReadyAction } from '../debugger/launchProfiles'; export interface ErrorResponse { error: ErrorDetails; @@ -49,40 +50,6 @@ export interface EnvVar { value: string; } -// VS Code does not export a ServerReadyAction type; mirror it here: -// https://github.com/microsoft/vscode/blob/c36e2b89d9171f212b34491dd9b61eb72abbfb04/extensions/debug-server-ready/src/extension.ts#L15C1-L23C2 -export type ServerReadyActionAction = 'openExternally' | 'debugWithChrome' | 'debugWithEdge' | 'startDebugging'; - -export interface ServerReadyAction { - action?: ServerReadyActionAction; - /** - * Regex that matches a URL. Prefer a capture group so VS Code can substitute it into uriFormat. - * Example match: "Now listening on: https://localhost:5001" - * Example pattern: '\\bNow listening on:\\s+(https?://\\S+)' - */ - pattern: string; - /** - * URI format string used with the first capture group (commonly "%s"). - */ - uriFormat?: string; - /** - * Web root for browser debugging (used by VS Code debug-server-ready). - */ - webRoot?: string; - /** - * Optional name for startDebugging. - */ - name?: string; - /** - * Optional debug configuration to start (used with startDebugging). - */ - config?: vscode.DebugConfiguration; - /** - * Whether to stop the browser debug session when the server stops. - */ - killOnServerStop?: boolean; -} - export interface RunSessionPayload { launch_configurations: ExecutableLaunchConfiguration[]; env?: EnvVar[]; @@ -132,7 +99,6 @@ export interface LaunchOptions { debugSessionId: string; isApphost: boolean; debugSession: AspireDebugSession; - parentDebugConfiguration?: AspireExtendedDebugConfiguration; }; export interface AspireResourceDebugSession { @@ -145,12 +111,12 @@ export interface AspireResourceExtendedDebugConfiguration extends vscode.DebugCo runId: string; debugSessionId: string | null; projectFile?: string; + serverReadyAction?: ServerReadyAction; } export interface AspireExtendedDebugConfiguration extends vscode.DebugConfiguration { program: string; debuggers?: AspireDebuggersConfiguration; - serverReadyAction?: ServerReadyAction; } interface AspireDebuggersConfiguration { diff --git a/extension/src/debugger/languages/dotnet.ts b/extension/src/debugger/languages/dotnet.ts index 21f6ffa6b68..3650cb7781d 100644 --- a/extension/src/debugger/languages/dotnet.ts +++ b/extension/src/debugger/languages/dotnet.ts @@ -291,7 +291,7 @@ export function createProjectDebuggerExtension(dotNetServiceProducer: (debugSess // The apphost's application URL is the Aspire dashboard URL. We already get the dashboard login URL later on, // so we should just avoid setting up serverReadyAction and manually open the browser ourselves. if (!launchOptions.isApphost) { - debugConfiguration.serverReadyAction = determineServerReadyAction({ launchBrowser: baseProfile?.launchBrowser, parentDebugConfiguration: launchOptions.parentDebugConfiguration }); + debugConfiguration.serverReadyAction = determineServerReadyAction(baseProfile?.launchBrowser, baseProfile?.applicationUrl, debugConfiguration.serverReadyAction); } // Temporarily disable GH Copilot on the dashboard before the extension implementation is approved diff --git a/extension/src/debugger/launchProfiles.ts b/extension/src/debugger/launchProfiles.ts index 001db669588..04827ce9614 100644 --- a/extension/src/debugger/launchProfiles.ts +++ b/extension/src/debugger/launchProfiles.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as fs from 'fs'; -import { EnvVar, ProjectLaunchConfiguration, ServerReadyAction, ServerReadyActionAction } from '../dcp/types'; +import { ExecutableLaunchConfiguration, EnvVar, ProjectLaunchConfiguration } from '../dcp/types'; import { extensionLogOutputChannel } from '../utils/logging'; import { isSingleFileApp } from './languages/dotnet'; import { stripComments } from 'jsonc-parser'; @@ -213,27 +213,31 @@ export function determineWorkingDirectory( return projectDir; } -export interface ServerReadyActionOptions { - launchBrowser?: boolean; - action?: ServerReadyActionAction; - parentDebugConfiguration?: { serverReadyAction?: ServerReadyAction } | null; +export interface ServerReadyAction { + action: "openExternally"; + pattern: string; + uriFormat: string; } -export function determineServerReadyAction( - options: ServerReadyActionOptions = {} -): ServerReadyAction | undefined { - if (!options.launchBrowser) { +export function determineServerReadyAction(launchBrowser?: boolean, applicationUrl?: string, debugConfigurationServerReadyAction?: ServerReadyAction): ServerReadyAction | undefined { + if (launchBrowser === false) { return undefined; } + + // A serverReadyAction may already have been defined in the aspire launch configuration. In that case, it applies to all resources unless launchBrowser is false (resource explicitly has opted out of browser launch). + if (debugConfigurationServerReadyAction) { + return debugConfigurationServerReadyAction; + } - const parentServerReadyAction = options.parentDebugConfiguration?.serverReadyAction; - if (parentServerReadyAction) { - return parentServerReadyAction; + if (launchBrowser === undefined || !applicationUrl) { + return undefined; } + let uriFormat = applicationUrl.includes(';') ? applicationUrl.split(';')[0] : applicationUrl; + return { - action: options.action ?? 'openExternally', - pattern: "\\bNow listening on:\\s+(https?://\\S+)", - uriFormat: "%s" + action: "openExternally", + pattern: "\\bNow listening on:\\s+https?://\\S+", + uriFormat: uriFormat }; } diff --git a/extension/src/test/dotnetDebugger.test.ts b/extension/src/test/dotnetDebugger.test.ts index f9895885eb6..5c5f73c018a 100644 --- a/extension/src/test/dotnetDebugger.test.ts +++ b/extension/src/test/dotnetDebugger.test.ts @@ -180,8 +180,8 @@ suite('Dotnet Debugger Extension Tests', () => { // serverReadyAction should be present when launchBrowser is true assert.notStrictEqual(debugConfig.serverReadyAction, undefined); - assert.strictEqual(debugConfig.serverReadyAction.uriFormat, '%s'); - assert.strictEqual(debugConfig.serverReadyAction.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); + assert.strictEqual(debugConfig.serverReadyAction?.uriFormat, '%s'); + assert.strictEqual(debugConfig.serverReadyAction?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); // cleanup fs.rmSync(tempDir, { recursive: true, force: true }); diff --git a/extension/src/test/launchProfiles.test.ts b/extension/src/test/launchProfiles.test.ts index 95e706af245..a843e343a90 100644 --- a/extension/src/test/launchProfiles.test.ts +++ b/extension/src/test/launchProfiles.test.ts @@ -383,43 +383,59 @@ suite('Launch Profile Tests', () => { suite('determineServerReadyAction', () => { test('returns undefined when launchBrowser is false', () => { - const result = determineServerReadyAction({ launchBrowser: false }); + const result = determineServerReadyAction(false, 'https://localhost:5001'); assert.strictEqual(result, undefined); }); - test('returns serverReadyAction when launchBrowser true', () => { - const result = determineServerReadyAction({ launchBrowser: true }); + test('returns undefined when applicationUrl is undefined and no launch config serverReadyAction', () => { + const result = determineServerReadyAction(true, undefined, undefined); + assert.strictEqual(result, undefined); + }); + test('returns existing when launchBrowser is undefined, applicationUrl is undefined and existing launch config serverReadyAction', () => { + const result = determineServerReadyAction(undefined, undefined, { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+https?://\\S+' }); assert.notStrictEqual(result, undefined); assert.strictEqual(result?.action, 'openExternally'); - assert.strictEqual(result?.uriFormat, '%s'); - assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); + assert.strictEqual(result?.uriFormat, 'https://localhost:5001'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); }); - test('uses provided action when specified', () => { - const result = determineServerReadyAction({ launchBrowser: true, action: 'debugWithEdge' }); - + test('returns existing when launchBrowser is true, applicationUrl is undefined and existing launch config serverReadyAction', () => { + const result = determineServerReadyAction(true, undefined, { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+https?://\\S+' }); assert.notStrictEqual(result, undefined); - assert.strictEqual(result?.action, 'debugWithEdge'); - assert.strictEqual(result?.uriFormat, '%s'); - assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); + assert.strictEqual(result?.action, 'openExternally'); + assert.strictEqual(result?.uriFormat, 'https://localhost:5001'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); }); - test('returns parent serverReadyAction when provided', () => { - const parent = { - serverReadyAction: { - action: 'debugWithChrome' as const, - pattern: '\\bNow listening on:\\s+(https?://\\S+)', - uriFormat: '%s' - } - }; + test('returns undefined when launchBrowser is false, applicationUrl is undefined and existing launch config serverReadyAction', () => { + const result = determineServerReadyAction(false, undefined, { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+https?://\\S+' }); + assert.strictEqual(result, undefined); + }); - const result = determineServerReadyAction({ launchBrowser: true, parentDebugConfiguration: parent }); + test('returns undefined when launchBrowser is false, applicationUrl is not undefined and existing launch config serverReadyAction', () => { + const result = determineServerReadyAction(false, 'https://localhost:5001', { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+https?://\\S+' }); + assert.strictEqual(result, undefined); + }); + + test('returns serverReadyAction when launchBrowser true and applicationUrl provided', () => { + const applicationUrl = 'https://localhost:5001'; + const result = determineServerReadyAction(true, applicationUrl); + + assert.notStrictEqual(result, undefined); + assert.strictEqual(result?.action, 'openExternally'); + assert.strictEqual(result?.uriFormat, applicationUrl); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); + }); + + test('returns serverReadyAction with first URL when multiple URLs separated by semicolon', () => { + const applicationUrl = 'https://localhost:5001;http://localhost:5000'; + const result = determineServerReadyAction(true, applicationUrl); assert.notStrictEqual(result, undefined); - assert.strictEqual(result?.action, 'debugWithChrome'); - assert.strictEqual(result?.uriFormat, '%s'); - assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); + assert.strictEqual(result?.action, 'openExternally'); + assert.strictEqual(result?.uriFormat, 'https://localhost:5001'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); }); }); From d92e76578cc45c1ec9dd980d2fe1a90a5dfbd9cd Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Fri, 16 Jan 2026 13:38:49 -0500 Subject: [PATCH 12/12] update test expectations --- extension/src/debugger/launchProfiles.ts | 2 +- extension/src/test/dotnetDebugger.test.ts | 2 +- extension/src/test/launchProfiles.test.ts | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extension/src/debugger/launchProfiles.ts b/extension/src/debugger/launchProfiles.ts index 04827ce9614..c80b5f33fe2 100644 --- a/extension/src/debugger/launchProfiles.ts +++ b/extension/src/debugger/launchProfiles.ts @@ -237,7 +237,7 @@ export function determineServerReadyAction(launchBrowser?: boolean, applicationU return { action: "openExternally", - pattern: "\\bNow listening on:\\s+https?://\\S+", + pattern: "\\bNow listening on:\\s+(https?://\\S+)", uriFormat: uriFormat }; } diff --git a/extension/src/test/dotnetDebugger.test.ts b/extension/src/test/dotnetDebugger.test.ts index 5c5f73c018a..9dda8982823 100644 --- a/extension/src/test/dotnetDebugger.test.ts +++ b/extension/src/test/dotnetDebugger.test.ts @@ -180,7 +180,7 @@ suite('Dotnet Debugger Extension Tests', () => { // serverReadyAction should be present when launchBrowser is true assert.notStrictEqual(debugConfig.serverReadyAction, undefined); - assert.strictEqual(debugConfig.serverReadyAction?.uriFormat, '%s'); + assert.strictEqual(debugConfig.serverReadyAction?.uriFormat, 'https://localhost:5001'); assert.strictEqual(debugConfig.serverReadyAction?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); // cleanup diff --git a/extension/src/test/launchProfiles.test.ts b/extension/src/test/launchProfiles.test.ts index a843e343a90..a63f3f1459a 100644 --- a/extension/src/test/launchProfiles.test.ts +++ b/extension/src/test/launchProfiles.test.ts @@ -393,28 +393,28 @@ suite('Launch Profile Tests', () => { }); test('returns existing when launchBrowser is undefined, applicationUrl is undefined and existing launch config serverReadyAction', () => { - const result = determineServerReadyAction(undefined, undefined, { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+https?://\\S+' }); + const result = determineServerReadyAction(undefined, undefined, { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+(https?://\\S+)' }); assert.notStrictEqual(result, undefined); assert.strictEqual(result?.action, 'openExternally'); assert.strictEqual(result?.uriFormat, 'https://localhost:5001'); - assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); }); test('returns existing when launchBrowser is true, applicationUrl is undefined and existing launch config serverReadyAction', () => { - const result = determineServerReadyAction(true, undefined, { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+https?://\\S+' }); + const result = determineServerReadyAction(true, undefined, { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+(https?://\\S+)' }); assert.notStrictEqual(result, undefined); assert.strictEqual(result?.action, 'openExternally'); assert.strictEqual(result?.uriFormat, 'https://localhost:5001'); - assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); }); test('returns undefined when launchBrowser is false, applicationUrl is undefined and existing launch config serverReadyAction', () => { - const result = determineServerReadyAction(false, undefined, { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+https?://\\S+' }); + const result = determineServerReadyAction(false, undefined, { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+(https?://\\S+)' }); assert.strictEqual(result, undefined); }); test('returns undefined when launchBrowser is false, applicationUrl is not undefined and existing launch config serverReadyAction', () => { - const result = determineServerReadyAction(false, 'https://localhost:5001', { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+https?://\\S+' }); + const result = determineServerReadyAction(false, 'https://localhost:5001', { action: 'openExternally', uriFormat: 'https://localhost:5001', pattern: '\\bNow listening on:\\s+(https?://\\S+)' }); assert.strictEqual(result, undefined); }); @@ -425,7 +425,7 @@ suite('Launch Profile Tests', () => { assert.notStrictEqual(result, undefined); assert.strictEqual(result?.action, 'openExternally'); assert.strictEqual(result?.uriFormat, applicationUrl); - assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); }); test('returns serverReadyAction with first URL when multiple URLs separated by semicolon', () => { @@ -435,7 +435,7 @@ suite('Launch Profile Tests', () => { assert.notStrictEqual(result, undefined); assert.strictEqual(result?.action, 'openExternally'); assert.strictEqual(result?.uriFormat, 'https://localhost:5001'); - assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+https?://\\S+'); + assert.strictEqual(result?.pattern, '\\bNow listening on:\\s+(https?://\\S+)'); }); });