perf(runtime): speed up dashboard provider status loading

This commit is contained in:
777genius 2026-04-28 20:36:17 +03:00
parent 8da5b7f25d
commit 8ac0b43a2a
5 changed files with 290 additions and 27 deletions

View file

@ -353,6 +353,26 @@ function createDefaultProviderStatus(providerId: CliProviderId): CliProviderStat
};
}
function createPendingProviderStatus(providerId: CliProviderId): CliProviderStatus {
return {
...createDefaultProviderStatus(providerId),
statusMessage: 'Checking...',
};
}
function createRuntimeStatusErrorProviderStatus(
providerId: CliProviderId,
error: unknown
): CliProviderStatus {
const message = error instanceof Error ? error.message : String(error);
return {
...createDefaultProviderStatus(providerId),
verificationState: 'error',
statusMessage: 'Provider status unavailable',
detailMessage: message,
};
}
function mapRuntimeExtensionCapabilities(
providerId: CliProviderId,
capabilities?: RuntimeExtensionCapabilitiesResponse
@ -668,6 +688,97 @@ export class ClaudeMultimodelBridgeService {
return providers.map((provider) => this.applyConnectionIssue(provider, connectionIssues));
}
private buildProviderStatusesSnapshot(
providers: Map<CliProviderId, CliProviderStatus>
): CliProviderStatus[] {
return ORDERED_PROVIDER_IDS.map(
(providerId) => providers.get(providerId) ?? createPendingProviderStatus(providerId)
);
}
private async getProviderStatusFromRuntimeStatusCommand(
binaryPath: string,
providerId: CliProviderId,
env: NodeJS.ProcessEnv,
connectionIssues: Partial<Record<CliProviderId, string>>
): Promise<CliProviderStatus> {
const { stdout } = await execCli(
binaryPath,
['runtime', 'status', '--json', '--provider', providerId],
{
timeout: PROVIDER_STATUS_TIMEOUT_MS,
env,
}
);
const parsed = extractJsonObject<UnifiedRuntimeStatusResponse>(stdout);
return providerConnectionService.enrichProviderStatus(
this.applyConnectionIssue(
this.mapRuntimeProviderStatus(providerId, parsed.providers?.[providerId]),
connectionIssues
)
);
}
private async getProviderStatusFromScopedRuntimeStatus(
binaryPath: string,
providerId: CliProviderId
): Promise<CliProviderStatus> {
const { env, connectionIssues } = await this.buildProviderCliEnv(binaryPath, providerId);
return this.getProviderStatusFromRuntimeStatusCommand(
binaryPath,
providerId,
env,
connectionIssues
);
}
private async getProviderStatusesFromScopedRuntimeStatus(
binaryPath: string,
onUpdate?: (providers: CliProviderStatus[]) => void
): Promise<CliProviderStatus[] | null> {
const providers = new Map<CliProviderId, CliProviderStatus>(
ORDERED_PROVIDER_IDS.map((providerId) => [
providerId,
createPendingProviderStatus(providerId),
])
);
const failures: { providerId: CliProviderId; error: unknown }[] = [];
await Promise.all(
ORDERED_PROVIDER_IDS.map(async (providerId) => {
try {
providers.set(
providerId,
await this.getProviderStatusFromScopedRuntimeStatus(binaryPath, providerId)
);
onUpdate?.(this.buildProviderStatusesSnapshot(providers));
} catch (error) {
failures.push({ providerId, error });
}
})
);
if (failures.length === 0) {
return this.buildProviderStatusesSnapshot(providers);
}
if (failures.length === ORDERED_PROVIDER_IDS.length) {
return null;
}
logger.warn(
`Provider-scoped runtime status failed for ${failures
.map(({ providerId }) => providerId)
.join(', ')}; using partial provider statuses`
);
for (const { providerId, error } of failures) {
providers.set(providerId, createRuntimeStatusErrorProviderStatus(providerId, error));
}
onUpdate?.(this.buildProviderStatusesSnapshot(providers));
return this.buildProviderStatusesSnapshot(providers);
}
private async getOpenCodeVerifySnapshot(
binaryPath: string
): Promise<OpenCodeRuntimeVerifyResponse['snapshot'] | null> {
@ -761,24 +872,9 @@ export class ClaudeMultimodelBridgeService {
providerId: CliProviderId
): Promise<CliProviderStatus> {
await resolveInteractiveShellEnv();
const { env, connectionIssues } = await this.buildCliEnv(binaryPath);
try {
const { stdout } = await execCli(
binaryPath,
['runtime', 'status', '--json', '--provider', providerId],
{
timeout: PROVIDER_STATUS_TIMEOUT_MS,
env,
}
);
const parsed = extractJsonObject<UnifiedRuntimeStatusResponse>(stdout);
return providerConnectionService.enrichProviderStatus(
this.applyConnectionIssue(
this.mapRuntimeProviderStatus(providerId, parsed.providers?.[providerId]),
connectionIssues
)
);
return await this.getProviderStatusFromScopedRuntimeStatus(binaryPath, providerId);
} catch (error) {
if (!this.isUnifiedRuntimeUnsupported(error)) {
logger.warn(
@ -937,6 +1033,20 @@ export class ClaudeMultimodelBridgeService {
onUpdate?: (providers: CliProviderStatus[]) => void
): Promise<CliProviderStatus[]> {
await resolveInteractiveShellEnv();
try {
const providers = await this.getProviderStatusesFromScopedRuntimeStatus(binaryPath, onUpdate);
if (providers) {
return providers;
}
} catch (error) {
logger.warn(
`Provider-scoped runtime status unavailable, falling back to full probe: ${
error instanceof Error ? error.message : String(error)
}`
);
}
const { env, connectionIssues } = await this.buildCliEnv(binaryPath);
try {

View file

@ -19,9 +19,9 @@ import { create } from 'zustand';
import { createChangeReviewSlice } from './slices/changeReviewSlice';
import {
createCliInstallerSlice,
getIncompleteMultimodelProviderIds,
getModelOnlyFallbackProviderIds,
mergeCliStatusPreservingHydratedProviders,
reconcileMultimodelProviderLoading,
} from './slices/cliInstallerSlice';
import { createConfigSlice } from './slices/configSlice';
import { createConnectionSlice } from './slices/connectionSlice';
@ -1485,20 +1485,14 @@ export function initializeNotificationListeners(): () => void {
state.cliStatus,
progress.status!
);
const incompleteProviderIds = getIncompleteMultimodelProviderIds(nextStatus);
modelOnlyFallbackProviderIds = getModelOnlyFallbackProviderIds(nextStatus);
return {
cliStatus: nextStatus,
cliProviderStatusLoading:
incompleteProviderIds.length > 0
? {
...state.cliProviderStatusLoading,
...Object.fromEntries(
incompleteProviderIds.map((providerId) => [providerId, true])
),
}
: state.cliProviderStatusLoading,
cliProviderStatusLoading: reconcileMultimodelProviderLoading(
nextStatus,
state.cliProviderStatusLoading
),
};
});
for (const providerId of modelOnlyFallbackProviderIds) {

View file

@ -125,6 +125,24 @@ export function getModelOnlyFallbackProviderIds(
.map((provider) => provider.providerId);
}
export function reconcileMultimodelProviderLoading(
status: CliInstallationStatus | null,
currentLoading: Partial<Record<CliProviderId, boolean>>
): Partial<Record<CliProviderId, boolean>> {
if (status?.flavor !== 'agent_teams_orchestrator' || !status.installed) {
return {};
}
const incompleteProviderIds = new Set(getIncompleteMultimodelProviderIds(status));
return status.providers.reduce<Partial<Record<CliProviderId, boolean>>>(
(nextLoading, provider) => ({
...nextLoading,
[provider.providerId]: incompleteProviderIds.has(provider.providerId),
}),
{ ...currentLoading }
);
}
export function mergeCliStatusPreservingHydratedProviders(
current: CliInstallationStatus | null,
incoming: CliInstallationStatus

View file

@ -233,6 +233,108 @@ describe('ClaudeMultimodelBridgeService', () => {
});
});
it('loads all providers with parallel provider-scoped runtime status probes', async () => {
const providerPayloads = {
anthropic: {
supported: true,
authenticated: true,
authMethod: 'oauth_token',
verificationState: 'verified',
canLoginFromUi: true,
models: ['claude-sonnet-4-5'],
capabilities: { teamLaunch: true, oneShot: true },
backend: { kind: 'anthropic', label: 'Anthropic' },
},
codex: {
supported: true,
authenticated: true,
authMethod: 'api_key',
verificationState: 'verified',
canLoginFromUi: false,
models: ['gpt-5-codex'],
capabilities: { teamLaunch: true, oneShot: true },
backend: { kind: 'codex-native', label: 'Codex native' },
},
gemini: {
supported: true,
authenticated: false,
verificationState: 'unknown',
canLoginFromUi: true,
statusMessage: 'No Gemini runtime backend is ready',
models: ['gemini-2.5-pro'],
capabilities: { teamLaunch: true, oneShot: true },
},
opencode: {
supported: true,
authenticated: true,
authMethod: 'opencode_managed',
verificationState: 'verified',
canLoginFromUi: false,
models: ['openai/gpt-5.4-mini'],
capabilities: { teamLaunch: true, oneShot: false },
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
},
} as const;
execCliMock.mockImplementation((_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
const providerArgIndex = Array.isArray(args) ? args.indexOf('--provider') : -1;
const providerId =
providerArgIndex >= 0 && Array.isArray(args)
? (args[providerArgIndex + 1] as keyof typeof providerPayloads)
: null;
if (
normalizedArgs.startsWith('runtime status --json --provider ') &&
providerId &&
providerPayloads[providerId]
) {
return Promise.resolve({
stdout: JSON.stringify({
schemaVersion: 2,
providers: {
[providerId]: providerPayloads[providerId],
},
}),
stderr: '',
exitCode: 0,
});
}
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
});
const { ClaudeMultimodelBridgeService } =
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
const service = new ClaudeMultimodelBridgeService();
const onUpdate = vi.fn();
const providers = await service.getProviderStatuses('/mock/agent_teams_orchestrator', onUpdate);
expect(execCliMock).toHaveBeenCalledTimes(4);
expect(execCliMock.mock.calls.map((call) => call[1].join(' '))).toEqual(
expect.arrayContaining([
'runtime status --json --provider anthropic',
'runtime status --json --provider codex',
'runtime status --json --provider gemini',
'runtime status --json --provider opencode',
])
);
expect(providers.map((provider) => provider.providerId)).toEqual([
'anthropic',
'codex',
'gemini',
'opencode',
]);
expect(providers.find((provider) => provider.providerId === 'codex')).toMatchObject({
authenticated: true,
models: ['gpt-5-codex'],
backend: { kind: 'codex-native' },
});
expect(onUpdate).toHaveBeenCalled();
expect(onUpdate.mock.calls.at(-1)?.[0]).toEqual(providers);
});
it('overrides provider auth status when provider-aware env reports a missing API key', async () => {
buildProviderAwareCliEnvMock.mockResolvedValue({
env: { HOME: '/Users/tester' },

View file

@ -55,6 +55,7 @@ import {
getIncompleteMultimodelProviderIds,
getModelOnlyFallbackProviderIds,
mergeCliStatusPreservingHydratedProviders,
reconcileMultimodelProviderLoading,
} from '@renderer/store/slices/cliInstallerSlice';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
@ -251,6 +252,44 @@ describe('cliInstallerSlice', () => {
expect(getModelOnlyFallbackProviderIds(status)).toEqual([]);
});
it('clears loading for hydrated providers while keeping pending providers marked', () => {
const status = createMultimodelStatus([
createMultimodelProvider({
providerId: 'anthropic',
displayName: 'Anthropic',
authenticated: true,
authMethod: 'oauth_token',
statusMessage: null,
models: ['claude-sonnet-4-5'],
backend: { kind: 'anthropic', label: 'Anthropic' },
}),
createMultimodelProvider({
providerId: 'codex',
displayName: 'Codex',
supported: false,
authenticated: false,
authMethod: null,
verificationState: 'unknown',
statusMessage: 'Checking...',
models: [],
backend: null,
availableBackends: [],
}),
]);
expect(
reconcileMultimodelProviderLoading(status, {
anthropic: true,
codex: true,
opencode: true,
})
).toEqual({
anthropic: false,
codex: true,
opencode: true,
});
});
it('still allows real OpenCode runtime errors to replace previous ready status', () => {
const current = createMultimodelStatus([
createMultimodelProvider({