diff --git a/extension/package.nls.json b/extension/package.nls.json index 293799ef081..e5eb3ee8891 100644 --- a/extension/package.nls.json +++ b/extension/package.nls.json @@ -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)", diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 1878b2930a5..2e159fcc92b 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -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(); @@ -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(); 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); diff --git a/extension/src/loc/strings.ts b/extension/src/loc/strings.ts index 03821ecdaca..44d743af0d9 100644 --- a/extension/src/loc/strings.ts +++ b/extension/src/loc/strings.ts @@ -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); +} +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); diff --git a/extension/src/views/AspireAppHostTreeProvider.ts b/extension/src/views/AspireAppHostTreeProvider.ts index 863656293d4..5178b99e7bc 100644 --- a/extension/src/views/AspireAppHostTreeProvider.ts +++ b/extension/src/views/AspireAppHostTreeProvider.ts @@ -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; @@ -36,7 +36,7 @@ interface ResourceJson { commands: Record | null; } -interface AppHostDisplayInfo { +export interface AppHostDisplayInfo { appHostPath: string; appHostPid: number; cliPid: number | null; @@ -153,6 +153,14 @@ export class AspireAppHostTreeProvider implements vscode.TreeDataProvider 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(); + } + } +}