From 2eba377be24a7867b5fa7cfb530efa40d010e25f Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 25 Apr 2026 18:27:08 +0300 Subject: [PATCH] fix(opencode): pass project path to provider management --- .../contracts/types.ts | 6 + ...TeamsRuntimeProviderManagementCliClient.ts | 157 +++++++++++------- .../RuntimeProviderManagementPanel.tsx | 12 +- .../hooks/useRuntimeProviderManagement.ts | 13 +- .../ui/RuntimeProviderManagementPanelView.tsx | 21 ++- .../components/dashboard/CliStatusBanner.tsx | 9 + .../runtime/ProviderRuntimeSettingsDialog.tsx | 3 + .../settings/sections/CliStatusSection.tsx | 9 + 8 files changed, 166 insertions(+), 64 deletions(-) diff --git a/src/features/runtime-provider-management/contracts/types.ts b/src/features/runtime-provider-management/contracts/types.ts index 3c7dabe5..65996d5d 100644 --- a/src/features/runtime-provider-management/contracts/types.ts +++ b/src/features/runtime-provider-management/contracts/types.ts @@ -150,22 +150,26 @@ export interface RuntimeProviderManagementModelTestResponse { export interface RuntimeProviderManagementLoadViewInput { runtimeId: RuntimeProviderManagementRuntimeId; + projectPath?: string | null; } export interface RuntimeProviderManagementConnectApiKeyInput { runtimeId: RuntimeProviderManagementRuntimeId; providerId: string; apiKey: string; + projectPath?: string | null; } export interface RuntimeProviderManagementForgetInput { runtimeId: RuntimeProviderManagementRuntimeId; providerId: string; + projectPath?: string | null; } export interface RuntimeProviderManagementLoadModelsInput { runtimeId: RuntimeProviderManagementRuntimeId; providerId: string; + projectPath?: string | null; query?: string | null; limit?: number | null; } @@ -174,6 +178,7 @@ export interface RuntimeProviderManagementTestModelInput { runtimeId: RuntimeProviderManagementRuntimeId; providerId: string; modelId: string; + projectPath?: string | null; } export interface RuntimeProviderManagementSetDefaultModelInput { @@ -181,4 +186,5 @@ export interface RuntimeProviderManagementSetDefaultModelInput { providerId: string; modelId: string; probe?: boolean; + projectPath?: string | null; } diff --git a/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts b/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts index e70dedd3..b7066658 100644 --- a/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts +++ b/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts @@ -107,6 +107,22 @@ function normalizeCommandFailure(error: unknown): string { return 'Runtime provider management command failed'; } +function normalizeProjectPath(projectPath: string | null | undefined): string | null { + const normalized = projectPath?.trim(); + return normalized ? normalized : null; +} + +function appendProjectPathArgs(args: string[], projectPath: string | null): string[] { + return projectPath ? [...args, '--project-path', projectPath] : args; +} + +function runtimeProviderCommandOptions( + options: T, + projectPath: string | null +): T & { cwd?: string } { + return projectPath ? { ...options, cwd: projectPath } : options; +} + async function resolveCliEnv(): Promise<{ binaryPath: string | null; env: NodeJS.ProcessEnv; @@ -194,11 +210,15 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv ); } + const projectPath = normalizeProjectPath(input.projectPath); try { const { stdout } = await execCli( binaryPath, - ['runtime', 'providers', 'view', '--runtime', input.runtimeId, '--json', '--compact'], - { env, timeout: COMMAND_TIMEOUT_MS } + appendProjectPathArgs( + ['runtime', 'providers', 'view', '--runtime', input.runtimeId, '--json', '--compact'], + projectPath + ), + runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { @@ -225,24 +245,31 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv ); } + const projectPath = normalizeProjectPath(input.projectPath); try { const child = spawnCli( binaryPath, - [ - 'runtime', - 'providers', - 'connect-api-key', - '--runtime', - input.runtimeId, - '--provider', - input.providerId, - '--stdin-key', - '--json', - ], - { - env, - stdio: 'pipe', - } + appendProjectPathArgs( + [ + 'runtime', + 'providers', + 'connect-api-key', + '--runtime', + input.runtimeId, + '--provider', + input.providerId, + '--stdin-key', + '--json', + ], + projectPath + ), + runtimeProviderCommandOptions( + { + env, + stdio: 'pipe' as const, + }, + projectPath + ) ) as ChildProcessWithoutNullStreams; const result = await collectSpawnOutput(child, input.apiKey); if (result.code === 0) { @@ -281,20 +308,24 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv ); } + const projectPath = normalizeProjectPath(input.projectPath); try { const { stdout } = await execCli( binaryPath, - [ - 'runtime', - 'providers', - 'forget', - '--runtime', - input.runtimeId, - '--provider', - input.providerId, - '--json', - ], - { env, timeout: COMMAND_TIMEOUT_MS } + appendProjectPathArgs( + [ + 'runtime', + 'providers', + 'forget', + '--runtime', + input.runtimeId, + '--provider', + input.providerId, + '--json', + ], + projectPath + ), + runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { @@ -317,7 +348,8 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv ); } - const args = [ + const projectPath = normalizeProjectPath(input.projectPath); + let args = [ 'runtime', 'providers', 'models', @@ -333,10 +365,11 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv if (typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0) { args.push('--limit', String(Math.floor(input.limit))); } + args = appendProjectPathArgs(args, projectPath); try { const { stdout } = await execCli(binaryPath, args, { - env, + ...runtimeProviderCommandOptions({ env }, projectPath), timeout: COMMAND_TIMEOUT_MS, }); return extractJsonObject(stdout); @@ -364,22 +397,26 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv ); } + const projectPath = normalizeProjectPath(input.projectPath); try { const { stdout } = await execCli( binaryPath, - [ - 'runtime', - 'providers', - 'test-model', - '--runtime', - input.runtimeId, - '--provider', - input.providerId, - '--model', - input.modelId, - '--json', - ], - { env, timeout: PROBE_COMMAND_TIMEOUT_MS } + appendProjectPathArgs( + [ + 'runtime', + 'providers', + 'test-model', + '--runtime', + input.runtimeId, + '--provider', + input.providerId, + '--model', + input.modelId, + '--json', + ], + projectPath + ), + runtimeProviderCommandOptions({ env, timeout: PROBE_COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { @@ -408,24 +445,28 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv ); } + const projectPath = normalizeProjectPath(input.projectPath); try { const { stdout } = await execCli( binaryPath, - [ - 'runtime', - 'providers', - 'set-default', - '--runtime', - input.runtimeId, - '--provider', - input.providerId, - '--model', - input.modelId, - '--probe', - '--compact', - '--json', - ], - { env, timeout: PROBE_COMMAND_TIMEOUT_MS } + appendProjectPathArgs( + [ + 'runtime', + 'providers', + 'set-default', + '--runtime', + input.runtimeId, + '--provider', + input.providerId, + '--model', + input.modelId, + '--probe', + '--compact', + '--json', + ], + projectPath + ), + runtimeProviderCommandOptions({ env, timeout: PROBE_COMMAND_TIMEOUT_MS }, projectPath) ); return extractJsonObject(stdout); } catch (error) { diff --git a/src/features/runtime-provider-management/renderer/RuntimeProviderManagementPanel.tsx b/src/features/runtime-provider-management/renderer/RuntimeProviderManagementPanel.tsx index c86a9c3a..bf6a678c 100644 --- a/src/features/runtime-provider-management/renderer/RuntimeProviderManagementPanel.tsx +++ b/src/features/runtime-provider-management/renderer/RuntimeProviderManagementPanel.tsx @@ -7,6 +7,7 @@ import type { JSX } from 'react'; interface RuntimeProviderManagementPanelProps { readonly runtimeId: RuntimeProviderManagementRuntimeId; readonly open: boolean; + readonly projectPath?: string | null; readonly disabled?: boolean; readonly onProviderChanged?: () => Promise | void; } @@ -14,14 +15,23 @@ interface RuntimeProviderManagementPanelProps { export function RuntimeProviderManagementPanel({ runtimeId, open, + projectPath = null, disabled = false, onProviderChanged, }: RuntimeProviderManagementPanelProps): JSX.Element { const [state, actions] = useRuntimeProviderManagement({ runtimeId, enabled: open, + projectPath, onProviderChanged, }); - return ; + return ( + + ); } diff --git a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts index 0b0cd27a..50ba6d3a 100644 --- a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts +++ b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts @@ -19,6 +19,7 @@ import type { interface UseRuntimeProviderManagementOptions { runtimeId: RuntimeProviderManagementRuntimeId; enabled: boolean; + projectPath?: string | null; onProviderChanged?: () => Promise | void; } @@ -175,6 +176,7 @@ export function useRuntimeProviderManagement( try { const response = await api.runtimeProviderManagement.loadView({ runtimeId: options.runtimeId, + projectPath: options.projectPath ?? null, }); if (response.error) { setView(null); @@ -195,7 +197,7 @@ export function useRuntimeProviderManagement( } finally { setLoading(false); } - }, [options.enabled, options.runtimeId]); + }, [options.enabled, options.projectPath, options.runtimeId]); useEffect(() => { if (!options.enabled) { @@ -226,6 +228,7 @@ export function useRuntimeProviderManagement( api.runtimeProviderManagement.loadModels({ runtimeId: options.runtimeId, providerId: modelPickerProviderId, + projectPath: options.projectPath ?? null, query: modelQuery.trim() || null, limit: 250, }), @@ -268,7 +271,7 @@ export function useRuntimeProviderManagement( return () => { cancelled = true; }; - }, [modelPickerProviderId, modelQuery, options.enabled, options.runtimeId]); + }, [modelPickerProviderId, modelQuery, options.enabled, options.projectPath, options.runtimeId]); useEffect(() => { if (!options.enabled || activeFormProviderId) { @@ -338,6 +341,7 @@ export function useRuntimeProviderManagement( runtimeId: options.runtimeId, providerId, apiKey, + projectPath: options.projectPath ?? null, }), 'Provider connect timed out' ); @@ -381,6 +385,7 @@ export function useRuntimeProviderManagement( api.runtimeProviderManagement.forgetCredential({ runtimeId: options.runtimeId, providerId, + projectPath: options.projectPath ?? null, }), 'Provider forget timed out' ); @@ -456,6 +461,7 @@ export function useRuntimeProviderManagement( runtimeId: options.runtimeId, providerId, modelId, + projectPath: options.projectPath ?? null, }), 'Model test timed out', 100_000 @@ -486,7 +492,7 @@ export function useRuntimeProviderManagement( setTestingModelId(null); } }, - [options.runtimeId] + [options.projectPath, options.runtimeId] ); const setDefaultModel = useCallback( @@ -501,6 +507,7 @@ export function useRuntimeProviderManagement( providerId, modelId, probe: true, + projectPath: options.projectPath ?? null, }), 'Set default model timed out', 100_000 diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx index 8d09799c..f09116ac 100644 --- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx @@ -45,6 +45,7 @@ interface RuntimeProviderManagementPanelViewProps { readonly state: RuntimeProviderManagementState; readonly actions: RuntimeProviderManagementActions; readonly disabled: boolean; + readonly projectPath?: string | null; } interface ProviderActionsProps { @@ -97,7 +98,8 @@ function RuntimeSummary({ state, onRefresh, disabled, -}: Pick & { + projectPath, +}: Pick & { onRefresh: () => void; }): JSX.Element { const runtime = state.view?.runtime; @@ -136,6 +138,15 @@ function RuntimeSummary({ ) : null} +
+ {projectPath + ? `Managing selected project profile: ${projectPath}` + : 'Managing fallback OpenCode profile. Select a project to manage launch credentials for that project.'} +
{state.loading ? (
- void actions.refresh()} /> + void actions.refresh()} + /> {state.error ? (
{ const isElectron = useMemo(() => isElectronMode(), []); const appConfig = useStore((s) => s.appConfig); + const selectedProjectId = useStore((s) => s.selectedProjectId); + const projects = useStore((s) => s.projects); + const repositoryGroups = useStore((s) => s.repositoryGroups); const updateConfig = useStore((s) => s.updateConfig); const { cliStatus, @@ -1048,6 +1052,10 @@ export const CliStatusBanner = (): React.JSX.Element | null => { loadDashboardCliStatusBannerCollapsed() ); const multimodelEnabled = appConfig?.general?.multimodelEnabled ?? true; + const selectedProjectPath = useMemo( + () => resolveProjectPathById(selectedProjectId, projects, repositoryGroups)?.path ?? null, + [projects, repositoryGroups, selectedProjectId] + ); const loadingCliStatus = useMemo( () => !cliStatus && cliStatusLoading && multimodelEnabled @@ -1289,6 +1297,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => { open={manageDialogOpen} onOpenChange={setManageDialogOpen} providers={visibleCliProviders} + projectPath={selectedProjectPath} initialProviderId={ visibleCliProviders.some((provider) => provider.providerId === manageProviderId) ? manageProviderId diff --git a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx index c625526c..7279a59c 100644 --- a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +++ b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx @@ -72,6 +72,7 @@ interface Props { readonly onOpenChange: (open: boolean) => void; readonly providers: CliProviderStatus[]; readonly initialProviderId: CliProviderId; + readonly projectPath?: string | null; readonly providerStatusLoading?: Partial>; readonly disabled?: boolean; readonly onSelectBackend: (providerId: CliProviderId, backendId: string) => Promise | void; @@ -543,6 +544,7 @@ export const ProviderRuntimeSettingsDialog = ({ onOpenChange, providers, initialProviderId, + projectPath = null, providerStatusLoading = {}, disabled = false, onSelectBackend, @@ -1168,6 +1170,7 @@ export const ProviderRuntimeSettingsDialog = ({ onRefreshProvider?.('opencode')} /> diff --git a/src/renderer/components/settings/sections/CliStatusSection.tsx b/src/renderer/components/settings/sections/CliStatusSection.tsx index d86e8950..85a49f2d 100644 --- a/src/renderer/components/settings/sections/CliStatusSection.tsx +++ b/src/renderer/components/settings/sections/CliStatusSection.tsx @@ -32,6 +32,7 @@ import { useCliInstaller } from '@renderer/hooks/useCliInstaller'; import { useStore } from '@renderer/store'; import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice'; import { formatBytes } from '@renderer/utils/formatters'; +import { resolveProjectPathById } from '@renderer/utils/projectLookup'; import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus'; import { getRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName'; import { @@ -185,6 +186,9 @@ function getProviderTerminalLogoutCommand(provider: CliProviderStatus): { export const CliStatusSection = (): React.JSX.Element | null => { const isElectron = useMemo(() => isElectronMode(), []); const appConfig = useStore((s) => s.appConfig); + const selectedProjectId = useStore((s) => s.selectedProjectId); + const projects = useStore((s) => s.projects); + const repositoryGroups = useStore((s) => s.repositoryGroups); const openExtensionsTab = useStore((s) => s.openExtensionsTab); const updateConfig = useStore((s) => s.updateConfig); const { @@ -211,6 +215,10 @@ export const CliStatusSection = (): React.JSX.Element | null => { const [manageProviderId, setManageProviderId] = useState('gemini'); const [manageDialogOpen, setManageDialogOpen] = useState(false); const multimodelEnabled = appConfig?.general?.multimodelEnabled ?? true; + const selectedProjectPath = useMemo( + () => resolveProjectPathById(selectedProjectId, projects, repositoryGroups)?.path ?? null, + [projects, repositoryGroups, selectedProjectId] + ); const loadingCliStatus = !cliStatus && cliStatusLoading && multimodelEnabled ? createLoadingMultimodelCliStatus() @@ -645,6 +653,7 @@ export const CliStatusSection = (): React.JSX.Element | null => { open={manageDialogOpen} onOpenChange={setManageDialogOpen} providers={effectiveCliStatus.providers} + projectPath={selectedProjectPath} initialProviderId={manageProviderId} providerStatusLoading={cliProviderStatusLoading} disabled={!effectiveCliStatus.binaryPath || isBusy || cliStatusLoading}