Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ You can configure the extension under **Settings → Aspire**, or jump there wit
|---------|---------|-------------|
| `aspire.aspireCliExecutablePath` | `""` | Path to the Aspire CLI. Leave empty to use `aspire` from PATH. |
| `aspire.dashboardBrowser` | `openExternalBrowser` | Which browser to open the dashboard in — system default, or Chrome/Edge/Firefox as a debug session |
| `aspire.enableAspireDashboardAutoLaunch` | `true` | Open the dashboard automatically when you start debugging |
| `aspire.enableAspireDashboardAutoLaunch` | `launch` | Controls what happens with the dashboard when debugging starts: `launch` (auto-open), `notification` (show link), or `off` |
| `aspire.registerMcpServerInWorkspace` | `false` | Register the Aspire MCP server (see [below](#mcp-server-integration)) |

There are more settings for things like verbose logging, startup prompts, and polling intervals — run **Aspire: Extension settings** from the Command Palette to see them all.
Expand Down
25 changes: 20 additions & 5 deletions extension/loc/xlf/aspire-vscode.xlf

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

10 changes: 8 additions & 2 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,9 +470,15 @@
"scope": "window"
},
"aspire.enableAspireDashboardAutoLaunch": {
"type": "boolean",
"default": true,
"type": "string",
"enum": ["launch", "notification", "off"],
"default": "launch",
"description": "%configuration.aspire.enableAspireDashboardAutoLaunch%",
"enumDescriptions": [
"%configuration.aspire.enableAspireDashboardAutoLaunch.launch%",
"%configuration.aspire.enableAspireDashboardAutoLaunch.notification%",
"%configuration.aspire.enableAspireDashboardAutoLaunch.off%"
],
"scope": "window"
},
"aspire.dashboardBrowser": {
Expand Down
5 changes: 4 additions & 1 deletion extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
"configuration.aspire.aspireCliExecutablePath": "The path to the Aspire CLI executable. If not set, the extension will attempt to use 'aspire' from the system PATH.",
"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.enableAspireDashboardAutoLaunch": "Controls what happens with the Aspire Dashboard when debugging starts.",
"configuration.aspire.enableAspireDashboardAutoLaunch.launch": "Automatically open the dashboard in the browser.",
"configuration.aspire.enableAspireDashboardAutoLaunch.notification": "Show a notification with a link to open the dashboard.",
"configuration.aspire.enableAspireDashboardAutoLaunch.off": "Do nothing — the dashboard URL is still printed in the terminal.",
"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).",
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 settingsLabel = vscode.l10n.t('Settings');
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.');
Expand Down
35 changes: 30 additions & 5 deletions extension/src/server/interactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { MessageConnection } from 'vscode-jsonrpc';
import * as vscode from 'vscode';
import * as fs from 'fs/promises';
import { getRelativePathToWorkspace, isFolderOpenInWorkspace } from '../utils/workspace';
import { yesLabel, noLabel, directLink, codespacesLink, openAspireDashboard, failedToShowPromptEmpty, incompatibleAppHostError, aspireHostingSdkVersion, aspireCliVersion, requiredCapability, fieldRequired, aspireDebugSessionNotInitialized, errorMessage, failedToStartDebugSession, dashboard, codespaces, selectDirectoryTitle, selectFileTitle } from '../loc/strings';
import { yesLabel, noLabel, directLink, codespacesLink, openAspireDashboard, settingsLabel, failedToShowPromptEmpty, incompatibleAppHostError, aspireHostingSdkVersion, aspireCliVersion, requiredCapability, fieldRequired, aspireDebugSessionNotInitialized, errorMessage, failedToStartDebugSession, dashboard, codespaces, selectDirectoryTitle, selectFileTitle } from '../loc/strings';
import { ICliRpcClient } from './rpcClient';
import { ProgressNotifier } from './progressNotifier';
import { applyTextStyle, formatText } from '../utils/strings';
Expand Down Expand Up @@ -334,11 +334,27 @@ export class InteractionService implements IInteractionService {
this.writeDebugSessionMessage(codespacesUrl, true, AnsiColors.Blue);
}

// If aspire.enableAspireDashboardAutoLaunch is true, the dashboard will be launched automatically and we do not need
// to show an information message.
// Refresh the Aspire panel so it picks up dashboard URLs for the running app host
vscode.commands.executeCommand('aspire-vscode.refreshRunningAppHosts');

// If aspire.enableAspireDashboardAutoLaunch is 'launch', the dashboard will be launched automatically.
// If 'notification', a notification is shown with a link. If 'off', do nothing.
const aspireConfig = vscode.workspace.getConfiguration('aspire');
const enableDashboardAutoLaunch = aspireConfig.get<boolean>('enableAspireDashboardAutoLaunch', true);
if (enableDashboardAutoLaunch) {
const rawDashboardAutoLaunch = aspireConfig.get<unknown>('enableAspireDashboardAutoLaunch', 'launch');

// Handle legacy boolean values from before this setting was changed to an enum
let dashboardAutoLaunch: 'launch' | 'notification' | 'off';
if (rawDashboardAutoLaunch === true) {
dashboardAutoLaunch = 'launch';
} else if (rawDashboardAutoLaunch === false) {
dashboardAutoLaunch = 'notification';
} else if (rawDashboardAutoLaunch === 'launch' || rawDashboardAutoLaunch === 'notification' || rawDashboardAutoLaunch === 'off') {
dashboardAutoLaunch = rawDashboardAutoLaunch;
} else {
dashboardAutoLaunch = 'launch';
}

if (dashboardAutoLaunch === 'launch') {
// Open the dashboard URL in the configured browser. Prefer codespaces URL if available.
const urlToOpen = codespacesUrl || baseUrl;
const debugSession = this._getAspireDebugSession();
Expand All @@ -349,6 +365,10 @@ export class InteractionService implements IInteractionService {
return;
}

if (dashboardAutoLaunch === 'off') {
return;
}

const actions: vscode.MessageItem[] = [
{ title: directLink }
];
Expand All @@ -357,6 +377,8 @@ export class InteractionService implements IInteractionService {
actions.push({ title: codespacesLink });
}

actions.push({ title: settingsLabel });

// Delay 1 second to allow a slight pause between progress notification and message
setTimeout(() => {
// Don't await - fire and forget to avoid blocking
Expand All @@ -376,6 +398,9 @@ export class InteractionService implements IInteractionService {
else if (selected.title === codespacesLink && codespacesUrl) {
vscode.env.openExternal(vscode.Uri.parse(codespacesUrl));
}
else if (selected.title === settingsLabel) {
vscode.commands.executeCommand('workbench.action.openSettings', 'aspire.enableAspireDashboardAutoLaunch');
}
});
}, 1000);
}
Expand Down
6 changes: 4 additions & 2 deletions extension/src/test/appHostTreeView.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getResourceContextValue, getResourceIcon } from '../views/AspireAppHost
import type { ResourceJson } from '../views/AppHostDataRepository';

function makeResource(overrides: Partial<ResourceJson> = {}): ResourceJson {
return {
const base: ResourceJson = {
name: 'my-service',
displayName: null,
resourceType: 'Project',
Expand All @@ -14,8 +14,9 @@ function makeResource(overrides: Partial<ResourceJson> = {}): ResourceJson {
dashboardUrl: null,
urls: null,
commands: null,
...overrides,
properties: null,
};
return { ...base, ...overrides } as ResourceJson;
}

suite('shortenPath', () => {
Expand Down Expand Up @@ -97,6 +98,7 @@ suite('getResourceContextValue', () => {
}));
assert.strictEqual(result, 'resource:canRestart');
});

});

suite('getResourceIcon', () => {
Expand Down
12 changes: 6 additions & 6 deletions extension/src/test/rpc/interactionServiceTests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,11 @@ suite('InteractionService endpoints', () => {
stub.restore();
});

test("displayDashboardUrls writes URLs to output channel and shows info message when autoLaunch disabled", async () => {
test("displayDashboardUrls writes URLs to output channel and shows info message when autoLaunch is notification", async () => {
const stub = sinon.stub(extensionLogOutputChannel, 'info');
const showInformationMessageStub = sinon.stub(vscode.window, 'showInformationMessage').resolves();
const getConfigurationStub = sinon.stub(vscode.workspace, 'getConfiguration').returns({
get: (key: string, defaultValue?: any) => key === 'enableAspireDashboardAutoLaunch' ? false : defaultValue
get: (key: string, defaultValue?: any) => key === 'enableAspireDashboardAutoLaunch' ? 'notification' : defaultValue
} as any);
const testInfo = await createTestRpcServer();

Expand All @@ -212,17 +212,17 @@ suite('InteractionService endpoints', () => {

assert.ok(outputLines.some(line => line.includes(baseUrl)), 'Output should contain base URL');
assert.ok(outputLines.some(line => line.includes(codespacesUrl)), 'Output should contain codespaces URL');
assert.equal(showInformationMessageStub.callCount, 1, 'Should show info message when autoLaunch is disabled');
assert.equal(showInformationMessageStub.callCount, 1, 'Should show info message when autoLaunch is notification');
stub.restore();
showInformationMessageStub.restore();
getConfigurationStub.restore();
});

test("displayDashboardUrls writes URLs but does not show info message when autoLaunch enabled", async () => {
test("displayDashboardUrls writes URLs but does not show info message when autoLaunch is launch", async () => {
const stub = sinon.stub(extensionLogOutputChannel, 'info');
const showInformationMessageStub = sinon.stub(vscode.window, 'showInformationMessage').resolves();
const getConfigurationStub = sinon.stub(vscode.workspace, 'getConfiguration').returns({
get: (key: string, defaultValue?: any) => key === 'enableAspireDashboardAutoLaunch' ? true : defaultValue
get: (key: string, defaultValue?: any) => key === 'enableAspireDashboardAutoLaunch' ? 'launch' : defaultValue
} as any);
const testInfo = await createTestRpcServer();

Expand All @@ -239,7 +239,7 @@ suite('InteractionService endpoints', () => {
// No need to wait since no setTimeout should be called when autoLaunch is enabled
assert.ok(outputLines.some(line => line.includes(baseUrl)), 'Output should contain base URL');
assert.ok(outputLines.some(line => line.includes(codespacesUrl)), 'Output should contain codespaces URL');
assert.equal(showInformationMessageStub.callCount, 0, 'Should not show info message when autoLaunch is enabled');
assert.equal(showInformationMessageStub.callCount, 0, 'Should not show info message when autoLaunch is launch');
stub.restore();
showInformationMessageStub.restore();
getConfigurationStub.restore();
Expand Down
4 changes: 2 additions & 2 deletions extension/src/views/AppHostDataRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface ResourceJson {
dashboardUrl: string | null;
urls: ResourceUrlJson[] | null;
commands: Record<string, ResourceCommandJson> | null;
properties: Record<string, string | null> | null;
}

export interface AppHostDisplayInfo {
Expand Down Expand Up @@ -64,7 +65,6 @@ export class AppHostDataRepository {
private _describeRestarting = false;
private _describeRestartDelay = 5000;
private _describeRestartTimer: ReturnType<typeof setTimeout> | undefined;
private static readonly _maxDescribeRestartDelay = 60000;

// ── Global mode state (ps polling) ──
private _appHosts: AppHostDisplayInfo[] = [];
Expand Down Expand Up @@ -273,7 +273,7 @@ export class AppHostDataRepository {

// Auto-restart with exponential backoff
const delay = this._describeRestartDelay;
this._describeRestartDelay = Math.min(this._describeRestartDelay * 2, AppHostDataRepository._maxDescribeRestartDelay);
this._describeRestartDelay = Math.min(this._describeRestartDelay * 2, this._getPollingIntervalMs());
extensionLogOutputChannel.info(`Restarting describe --follow in ${delay}ms`);
this._describeRestartTimer = setTimeout(() => {
this._describeRestartTimer = undefined;
Expand Down
Loading
Loading