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
9 changes: 9 additions & 0 deletions extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@
"aspire-vscode.strings.dismissLabel": "Dismiss",
"aspire-vscode.strings.selectDirectoryTitle": "Select directory",
"aspire-vscode.strings.selectFileTitle": "Select file",
"aspire-vscode.strings.statusBarStopped": "Aspire: Stopped",
"aspire-vscode.strings.statusBarError": "Aspire: Error",
"aspire-vscode.strings.statusBarRunning": "Aspire: {0}/{1} running",
"aspire-vscode.strings.statusBarRunningNoResourcesSingular": "Aspire: {0} apphost",
"aspire-vscode.strings.statusBarRunningNoResourcesPlural": "Aspire: {0} apphosts",
"aspire-vscode.strings.statusBarTooltipStopped": "No Aspire apphosts running. Click to open the Aspire panel.",
"aspire-vscode.strings.statusBarTooltipError": "Error fetching Aspire apphost status. Click to open the Aspire panel.",
"aspire-vscode.strings.statusBarTooltipRunningSingular": "{0} Aspire apphost running. Click to open the Aspire panel.",
"aspire-vscode.strings.statusBarTooltipRunningPlural": "{0} Aspire apphosts running. Click to open the Aspire panel.",
"viewsContainers.aspirePanel.title": "Aspire",
"views.runningAppHosts.name": "Running apphosts",
"views.runningAppHosts.welcome": "No running Aspire apphosts found.\n[Run an apphost](command:aspire-vscode.runAppHost)",
Expand Down
20 changes: 8 additions & 12 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { openLocalSettingsCommand, openGlobalSettingsCommand } from './commands/
import { checkCliAvailableOrRedirect, checkForExistingAppHostPathInWorkspace } from './utils/workspace';
import { AspireEditorCommandProvider } from './editor/AspireEditorCommandProvider';
import { AspireAppHostTreeProvider } from './views/AspireAppHostTreeProvider';
import { AspireStatusBarProvider } from './views/AspireStatusBarProvider';

let aspireExtensionContext = new AspireExtensionContext();

Expand Down Expand Up @@ -84,21 +85,16 @@ export async function activate(context: vscode.ExtensionContext) {
// Set initial context for welcome view
vscode.commands.executeCommand('setContext', 'aspire.noRunningAppHosts', true);

// Start polling when the tree view becomes visible, stop when hidden
if (appHostTreeView.visible) {
appHostTreeProvider.startPolling();
}

appHostTreeView.onDidChangeVisibility(e => {
if (e.visible) {
appHostTreeProvider.startPolling();
} else {
appHostTreeProvider.stopPolling();
}
});
// Always poll for app host status — the status bar needs up-to-date data even
// when the tree view panel is hidden.
appHostTreeProvider.startPolling();

Comment on lines +88 to 91
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Polling is now started unconditionally on activation. Since the extension activates on many workspaces (e.g., workspaceContains:**/*.csproj), this will spawn aspire ps every ~3s even when the Aspire view isn’t used, which can add background CPU/process overhead. Consider introducing a slower interval/backoff when there are no running app hosts (or when the CLI is missing), or re-introducing a visibility-based gate with a separate lightweight refresh strategy for the status bar.

Suggested change
// Always poll for app host status — the status bar needs up-to-date data even
// when the tree view panel is hidden.
appHostTreeProvider.startPolling();
// Lazily start polling for app host status to avoid background overhead in
// workspaces where the Aspire view is never used.
let pollingStarted = false;
const ensurePollingStarted = () => {
if (!pollingStarted) {
pollingStarted = true;
appHostTreeProvider.startPolling();
}
};
// If the Aspire tree view is already visible, start polling immediately.
if (appHostTreeView.visible) {
ensurePollingStarted();
} else {
// Otherwise, wait until the view is first shown before starting polling.
const visibilityDisposable = appHostTreeView.onDidChangeVisibility(e => {
if (e.visible) {
ensurePollingStarted();
visibilityDisposable.dispose();
}
});
context.subscriptions.push(visibilityDisposable);
}

Copilot uses AI. Check for mistakes.
context.subscriptions.push(appHostTreeView, refreshRunningAppHostsRegistration, openDashboardRegistration, stopAppHostRegistration, stopResourceRegistration, startResourceRegistration, restartResourceRegistration, viewResourceLogsRegistration, executeResourceCommandRegistration, { dispose: () => appHostTreeProvider.dispose() });

// Status bar
const statusBarProvider = new AspireStatusBarProvider(appHostTreeProvider);
context.subscriptions.push(statusBarProvider);

context.subscriptions.push(cliAddCommandRegistration, cliNewCommandRegistration, cliInitCommandRegistration, cliDeployCommandRegistration, cliPublishCommandRegistration, openTerminalCommandRegistration, configureLaunchJsonCommandRegistration);
context.subscriptions.push(cliUpdateCommandRegistration, cliUpdateSelfCommandRegistration, settingsCommandRegistration, openLocalSettingsCommandRegistration, openGlobalSettingsCommandRegistration, runAppHostCommandRegistration, debugAppHostCommandRegistration);

Expand Down
18 changes: 18 additions & 0 deletions extension/src/loc/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,21 @@ export const cliNotAvailable = vscode.l10n.t('Aspire CLI is not available on PAT
export const cliFoundAtDefaultPath = (path: string) => vscode.l10n.t('Aspire CLI found at {0}. The extension will use this path.', path);
export const selectDirectoryTitle = vscode.l10n.t('Select directory');
export const selectFileTitle = vscode.l10n.t('Select file');

// Status bar strings
export const statusBarStopped = vscode.l10n.t('Aspire: Stopped');
export const statusBarError = vscode.l10n.t('Aspire: Error');
export function statusBarRunning(appHostCount: number, runningResources: number, totalResources: number): string {
if (totalResources === 0) {
return appHostCount === 1
? vscode.l10n.t('Aspire: {0} apphost', appHostCount)
: vscode.l10n.t('Aspire: {0} apphosts', appHostCount);
}
return vscode.l10n.t('Aspire: {0}/{1} running', runningResources, totalResources);
}
Comment on lines +92 to +99
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The status bar running text currently resolves to only a resource count ("Aspire: {running}/{total} running") and omits the app host count. This doesn’t match the PR description/table, which says the running text should include both the number of app hosts and the resource counts (e.g., "Aspire: N app host, M/T resources"). Either update the implementation/strings to match the described UX, or adjust the PR description to reflect the actual text.

Copilot uses AI. Check for mistakes.
export const statusBarTooltipStopped = vscode.l10n.t('No Aspire apphosts running. Click to open the Aspire panel.');
export const statusBarTooltipError = vscode.l10n.t('Error fetching Aspire apphost status. Click to open the Aspire panel.');
export const statusBarTooltipRunning = (appHostCount: number) =>
appHostCount === 1
? vscode.l10n.t('{0} Aspire apphost running. Click to open the Aspire panel.', appHostCount)
: vscode.l10n.t('{0} Aspire apphosts running. Click to open the Aspire panel.', appHostCount);
16 changes: 12 additions & 4 deletions extension/src/views/AspireAppHostTreeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ import {
selectCommandPlaceholder,
} from '../loc/strings';

interface ResourceUrlJson {
export interface ResourceUrlJson {
name: string | null;
displayName: string | null;
url: string;
isInternal: boolean;
}

interface ResourceCommandJson {
export interface ResourceCommandJson {
description: string | null;
}

interface ResourceJson {
export interface ResourceJson {
name: string;
displayName: string | null;
resourceType: string;
Expand All @@ -36,7 +36,7 @@ interface ResourceJson {
commands: Record<string, ResourceCommandJson> | null;
}

interface AppHostDisplayInfo {
export interface AppHostDisplayInfo {
appHostPath: string;
appHostPid: number;
cliPid: number | null;
Expand Down Expand Up @@ -153,6 +153,14 @@ export class AspireAppHostTreeProvider implements vscode.TreeDataProvider<TreeEl

constructor(private readonly _terminalProvider: AspireTerminalProvider) {}

get appHosts(): readonly AppHostDisplayInfo[] {
return this._appHosts;
}

get hasError(): boolean {
return this._errorMessage !== undefined;
}

refresh(): void {
this._fetchAppHosts();
}
Expand Down
105 changes: 105 additions & 0 deletions extension/src/views/AspireStatusBarProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import * as vscode from 'vscode';
import { AspireAppHostTreeProvider, type AppHostDisplayInfo } from './AspireAppHostTreeProvider';
import {
statusBarRunning,
statusBarStopped,
statusBarError,
statusBarTooltipRunning,
statusBarTooltipStopped,
statusBarTooltipError,
} from '../loc/strings';

const runningStates = new Set(['Running', 'Active']);

function countResources(appHosts: readonly AppHostDisplayInfo[]): { total: number; running: number } {
let total = 0;
let running = 0;
for (const appHost of appHosts) {
if (appHost.resources) {
for (const resource of appHost.resources) {
total++;
if (resource.state !== null && runningStates.has(resource.state)) {
running++;
}
}
}
}
return { total, running };
}

function hasUnhealthyResource(appHosts: readonly AppHostDisplayInfo[]): boolean {
for (const appHost of appHosts) {
if (appHost.resources) {
for (const resource of appHost.resources) {
if (resource.stateStyle === 'error' || resource.state === 'FailedToStart' || resource.state === 'RuntimeUnhealthy') {
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

hasUnhealthyResource treats only stateStyle === 'error' (or specific states) as unhealthy. The tree view already uses stateStyle === 'warning' to render a warning icon for Running/Active resources; those cases currently won’t trigger the status bar’s warning icon/background. Consider including stateStyle === 'warning' in the unhealthy check so the status bar reflects degraded resources consistently.

Suggested change
if (resource.stateStyle === 'error' || resource.state === 'FailedToStart' || resource.state === 'RuntimeUnhealthy') {
if (
resource.stateStyle === 'error' ||
resource.stateStyle === 'warning' ||
resource.state === 'FailedToStart' ||
resource.state === 'RuntimeUnhealthy'
) {

Copilot uses AI. Check for mistakes.
return true;
}
}
}
}
return false;
}

export class AspireStatusBarProvider implements vscode.Disposable {
private readonly _statusBarItem: vscode.StatusBarItem;
private readonly _disposables: vscode.Disposable[] = [];

constructor(treeProvider: AspireAppHostTreeProvider) {
this._statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50);
this._statusBarItem.name = 'Aspire';
this._statusBarItem.command = 'aspire-vscode.runningAppHosts.focus';

this._disposables.push(
treeProvider.onDidChangeTreeData(() => this._update(treeProvider))
);

this._update(treeProvider);
}

private _update(treeProvider: AspireAppHostTreeProvider): void {
const appHosts = treeProvider.appHosts;

if (treeProvider.hasError) {
this._statusBarItem.text = `$(error) ${statusBarError}`;
this._statusBarItem.tooltip = statusBarTooltipError;
this._statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
this._statusBarItem.show();
return;
}

if (appHosts.length === 0) {
this._statusBarItem.text = `$(circle-outline) ${statusBarStopped}`;
this._statusBarItem.tooltip = statusBarTooltipStopped;
this._statusBarItem.backgroundColor = undefined;
this._statusBarItem.show();
return;
}

const { total, running } = countResources(appHosts);
const unhealthy = hasUnhealthyResource(appHosts);

if (total === 0) {
// App host running but no resource info (older CLI without --resources)
this._statusBarItem.text = `$(radio-tower) ${statusBarRunning(appHosts.length, 0, 0)}`;
this._statusBarItem.tooltip = statusBarTooltipRunning(appHosts.length);
this._statusBarItem.backgroundColor = undefined;
this._statusBarItem.show();
return;
}

const icon = unhealthy ? '$(warning)' : '$(radio-tower)';
this._statusBarItem.text = `${icon} ${statusBarRunning(appHosts.length, running, total)}`;
this._statusBarItem.tooltip = statusBarTooltipRunning(appHosts.length);
this._statusBarItem.backgroundColor = unhealthy
? new vscode.ThemeColor('statusBarItem.warningBackground')
: undefined;
this._statusBarItem.show();
}

dispose(): void {
this._statusBarItem.dispose();
for (const d of this._disposables) {
d.dispose();
}
}
}
Loading