fix(opencode): pass project path to provider management

This commit is contained in:
777genius 2026-04-25 18:27:08 +03:00
parent 351ae4f4ed
commit 2eba377be2
8 changed files with 166 additions and 64 deletions

View file

@ -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;
}

View file

@ -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<T extends { env: NodeJS.ProcessEnv }>(
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<RuntimeProviderManagementViewResponse>(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<RuntimeProviderManagementProviderResponse>(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<RuntimeProviderManagementModelsResponse>(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<RuntimeProviderManagementModelTestResponse>(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<RuntimeProviderManagementViewResponse>(stdout);
} catch (error) {

View file

@ -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> | 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 <RuntimeProviderManagementPanelView state={state} actions={actions} disabled={disabled} />;
return (
<RuntimeProviderManagementPanelView
state={state}
actions={actions}
disabled={disabled}
projectPath={projectPath}
/>
);
}

View file

@ -19,6 +19,7 @@ import type {
interface UseRuntimeProviderManagementOptions {
runtimeId: RuntimeProviderManagementRuntimeId;
enabled: boolean;
projectPath?: string | null;
onProviderChanged?: () => Promise<void> | 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

View file

@ -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<RuntimeProviderManagementPanelViewProps, 'state' | 'disabled'> & {
projectPath,
}: Pick<RuntimeProviderManagementPanelViewProps, 'state' | 'disabled' | 'projectPath'> & {
onRefresh: () => void;
}): JSX.Element {
const runtime = state.view?.runtime;
@ -136,6 +138,15 @@ function RuntimeSummary({
</span>
) : null}
</div>
<div
className="mt-1 truncate text-[11px]"
style={{ color: 'var(--color-text-muted)' }}
title={projectPath ?? undefined}
>
{projectPath
? `Managing selected project profile: ${projectPath}`
: 'Managing fallback OpenCode profile. Select a project to manage launch credentials for that project.'}
</div>
{state.loading ? (
<div
className="mt-2 flex items-center gap-2 text-xs"
@ -784,6 +795,7 @@ export function RuntimeProviderManagementPanelView({
state,
actions,
disabled,
projectPath = null,
}: RuntimeProviderManagementPanelViewProps): JSX.Element {
const providerQuery = state.providerQuery.trim().toLowerCase();
const filteredProviders = providerQuery
@ -809,7 +821,12 @@ export function RuntimeProviderManagementPanelView({
return (
<div className="space-y-3">
<RuntimeSummary state={state} disabled={disabled} onRefresh={() => void actions.refresh()} />
<RuntimeSummary
state={state}
disabled={disabled}
projectPath={projectPath}
onRefresh={() => void actions.refresh()}
/>
{state.error ? (
<div

View file

@ -44,6 +44,7 @@ import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInst
import { formatBytes } from '@renderer/utils/formatters';
import { filterMainScreenCliProviders } from '@renderer/utils/geminiUiFreeze';
import { isMultimodelRuntimeStatus } from '@renderer/utils/multimodelProviderVisibility';
import { resolveProjectPathById } from '@renderer/utils/projectLookup';
import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus';
import { getRuntimeDisplayName as getHumanRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName';
import {
@ -1013,6 +1014,9 @@ const InstalledBanner = ({
export const CliStatusBanner = (): 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 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

View file

@ -72,6 +72,7 @@ interface Props {
readonly onOpenChange: (open: boolean) => void;
readonly providers: CliProviderStatus[];
readonly initialProviderId: CliProviderId;
readonly projectPath?: string | null;
readonly providerStatusLoading?: Partial<Record<CliProviderId, boolean>>;
readonly disabled?: boolean;
readonly onSelectBackend: (providerId: CliProviderId, backendId: string) => Promise<void> | void;
@ -543,6 +544,7 @@ export const ProviderRuntimeSettingsDialog = ({
onOpenChange,
providers,
initialProviderId,
projectPath = null,
providerStatusLoading = {},
disabled = false,
onSelectBackend,
@ -1168,6 +1170,7 @@ export const ProviderRuntimeSettingsDialog = ({
<RuntimeProviderManagementPanel
runtimeId="opencode"
open={open}
projectPath={projectPath}
disabled={disabled || selectedProviderLoading}
onProviderChanged={() => onRefreshProvider?.('opencode')}
/>

View file

@ -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<CliProviderId>('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}