fix(runtime-provider-management): unify opencode provider list
This commit is contained in:
parent
2518445b3b
commit
c2e14ea9df
4 changed files with 263 additions and 106 deletions
|
|
@ -356,7 +356,7 @@ export function useRuntimeProviderManagement(
|
||||||
}, [options.enabled, refresh]);
|
}, [options.enabled, refresh]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!options.enabled || !directoryOpen || !directorySupported) {
|
if (!options.enabled || !directorySupported) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -376,7 +376,6 @@ export function useRuntimeProviderManagement(
|
||||||
}, [
|
}, [
|
||||||
directoryFilter,
|
directoryFilter,
|
||||||
directoryLoaded,
|
directoryLoaded,
|
||||||
directoryOpen,
|
|
||||||
directoryQuery,
|
directoryQuery,
|
||||||
directorySupported,
|
directorySupported,
|
||||||
loadDirectoryPage,
|
loadDirectoryPage,
|
||||||
|
|
@ -572,6 +571,18 @@ export function useRuntimeProviderManagement(
|
||||||
setSuccessMessage(null);
|
setSuccessMessage(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const updateProviderQuery = useCallback(
|
||||||
|
(value: string): void => {
|
||||||
|
setProviderQuery(value);
|
||||||
|
if (!directorySupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDirectoryQuery(value);
|
||||||
|
setDirectoryNextCursor(null);
|
||||||
|
},
|
||||||
|
[directorySupported]
|
||||||
|
);
|
||||||
|
|
||||||
const cancelConnect = useCallback((): void => {
|
const cancelConnect = useCallback((): void => {
|
||||||
setActiveFormProviderId(null);
|
setActiveFormProviderId(null);
|
||||||
setApiKeyValue('');
|
setApiKeyValue('');
|
||||||
|
|
@ -612,12 +623,7 @@ export function useRuntimeProviderManagement(
|
||||||
setApiKeyValue('');
|
setApiKeyValue('');
|
||||||
void Promise.resolve(options.onProviderChanged?.())
|
void Promise.resolve(options.onProviderChanged?.())
|
||||||
.then(() => refresh())
|
.then(() => refresh())
|
||||||
.then(() => {
|
.then(() => loadDirectoryPage({ refresh: true, cursor: null }))
|
||||||
if (directoryOpen) {
|
|
||||||
return loadDirectoryPage({ refresh: true, cursor: null });
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
.catch((refreshError) => {
|
.catch((refreshError) => {
|
||||||
setError(
|
setError(
|
||||||
refreshError instanceof Error ? refreshError.message : 'Failed to refresh providers'
|
refreshError instanceof Error ? refreshError.message : 'Failed to refresh providers'
|
||||||
|
|
@ -631,7 +637,7 @@ export function useRuntimeProviderManagement(
|
||||||
setSavingProviderId(null);
|
setSavingProviderId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[apiKeyValue, directoryOpen, loadDirectoryPage, options, refresh]
|
[apiKeyValue, loadDirectoryPage, options, refresh]
|
||||||
);
|
);
|
||||||
|
|
||||||
const forgetProvider = useCallback(
|
const forgetProvider = useCallback(
|
||||||
|
|
@ -659,12 +665,7 @@ export function useRuntimeProviderManagement(
|
||||||
setSavingProviderId(null);
|
setSavingProviderId(null);
|
||||||
void Promise.resolve(options.onProviderChanged?.())
|
void Promise.resolve(options.onProviderChanged?.())
|
||||||
.then(() => refresh())
|
.then(() => refresh())
|
||||||
.then(() => {
|
.then(() => loadDirectoryPage({ refresh: true, cursor: null }))
|
||||||
if (directoryOpen) {
|
|
||||||
return loadDirectoryPage({ refresh: true, cursor: null });
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
.catch((refreshError) => {
|
.catch((refreshError) => {
|
||||||
setError(
|
setError(
|
||||||
refreshError instanceof Error ? refreshError.message : 'Failed to refresh providers'
|
refreshError instanceof Error ? refreshError.message : 'Failed to refresh providers'
|
||||||
|
|
@ -678,7 +679,7 @@ export function useRuntimeProviderManagement(
|
||||||
setSavingProviderId(null);
|
setSavingProviderId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[directoryOpen, loadDirectoryPage, options, refresh]
|
[loadDirectoryPage, options, refresh]
|
||||||
);
|
);
|
||||||
|
|
||||||
const openModelPicker = useCallback(
|
const openModelPicker = useCallback(
|
||||||
|
|
@ -882,7 +883,7 @@ export function useRuntimeProviderManagement(
|
||||||
() => ({
|
() => ({
|
||||||
refresh,
|
refresh,
|
||||||
selectProvider,
|
selectProvider,
|
||||||
setProviderQuery,
|
setProviderQuery: updateProviderQuery,
|
||||||
openDirectory,
|
openDirectory,
|
||||||
closeDirectory,
|
closeDirectory,
|
||||||
setDirectoryQuery: updateDirectoryQuery,
|
setDirectoryQuery: updateDirectoryQuery,
|
||||||
|
|
@ -923,6 +924,7 @@ export function useRuntimeProviderManagement(
|
||||||
submitConnect,
|
submitConnect,
|
||||||
testModel,
|
testModel,
|
||||||
updateDirectoryQuery,
|
updateDirectoryQuery,
|
||||||
|
updateProviderQuery,
|
||||||
useModelForNewTeams,
|
useModelForNewTeams,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,44 @@ function getDirectoryModelsLabel(provider: RuntimeProviderDirectoryEntryDto): st
|
||||||
return `${provider.modelCount} model${provider.modelCount === 1 ? '' : 's'}`;
|
return `${provider.modelCount} model${provider.modelCount === 1 ? '' : 's'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function directoryEntryMatchesQuery(
|
||||||
|
provider: RuntimeProviderDirectoryEntryDto,
|
||||||
|
query: string
|
||||||
|
): boolean {
|
||||||
|
if (!query) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
provider.providerId,
|
||||||
|
provider.displayName,
|
||||||
|
provider.detail ?? '',
|
||||||
|
provider.defaultModelId ?? '',
|
||||||
|
provider.sourceLabel ?? '',
|
||||||
|
provider.providerSource ?? '',
|
||||||
|
getDirectoryModelsLabel(provider),
|
||||||
|
formatDirectorySetupKind(provider),
|
||||||
|
...provider.authMethods,
|
||||||
|
]
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function directorySetupKindClassName(provider: RuntimeProviderDirectoryEntryDto): string {
|
||||||
|
switch (provider.setupKind) {
|
||||||
|
case 'connected':
|
||||||
|
return 'border-emerald-400/35 bg-emerald-400/10 text-emerald-200';
|
||||||
|
case 'connect-api-key':
|
||||||
|
case 'available-readonly':
|
||||||
|
return 'border-sky-400/30 bg-sky-400/10 text-sky-200';
|
||||||
|
case 'configure-manually':
|
||||||
|
case 'requires-environment':
|
||||||
|
return 'border-white/10 bg-white/[0.04] text-[var(--color-text-muted)]';
|
||||||
|
case 'unsupported':
|
||||||
|
return 'border-red-400/25 bg-red-400/10 text-red-200';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function directoryEntryToProviderConnection(
|
function directoryEntryToProviderConnection(
|
||||||
provider: RuntimeProviderDirectoryEntryDto
|
provider: RuntimeProviderDirectoryEntryDto
|
||||||
): RuntimeProviderConnectionDto {
|
): RuntimeProviderConnectionDto {
|
||||||
|
|
@ -593,13 +631,19 @@ function ProviderRow({
|
||||||
|
|
||||||
function DirectoryProviderRow({
|
function DirectoryProviderRow({
|
||||||
provider,
|
provider,
|
||||||
|
state,
|
||||||
active,
|
active,
|
||||||
|
formOpen,
|
||||||
|
apiKeyValue,
|
||||||
disabled,
|
disabled,
|
||||||
busy,
|
busy,
|
||||||
actions,
|
actions,
|
||||||
}: {
|
}: {
|
||||||
readonly provider: RuntimeProviderDirectoryEntryDto;
|
readonly provider: RuntimeProviderDirectoryEntryDto;
|
||||||
|
readonly state: RuntimeProviderManagementState;
|
||||||
readonly active: boolean;
|
readonly active: boolean;
|
||||||
|
readonly formOpen: boolean;
|
||||||
|
readonly apiKeyValue: string;
|
||||||
readonly disabled: boolean;
|
readonly disabled: boolean;
|
||||||
readonly busy: boolean;
|
readonly busy: boolean;
|
||||||
readonly actions: RuntimeProviderManagementActions;
|
readonly actions: RuntimeProviderManagementActions;
|
||||||
|
|
@ -636,11 +680,7 @@ function DirectoryProviderRow({
|
||||||
</span>
|
</span>
|
||||||
{provider.recommended ? <Badge variant="secondary">Recommended</Badge> : null}
|
{provider.recommended ? <Badge variant="secondary">Recommended</Badge> : null}
|
||||||
<span
|
<span
|
||||||
className={`rounded-md border px-2 py-0.5 text-[11px] ${
|
className={`rounded-md border px-2 py-0.5 text-[11px] ${directorySetupKindClassName(provider)}`}
|
||||||
provider.state === 'connected'
|
|
||||||
? 'border-emerald-400/35 bg-emerald-400/10 text-emerald-200'
|
|
||||||
: 'border-white/10 bg-white/[0.04] text-[var(--color-text-muted)]'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{formatDirectorySetupKind(provider)}
|
{formatDirectorySetupKind(provider)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -718,6 +758,59 @@ function DirectoryProviderRow({
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{formOpen ? (
|
||||||
|
<div
|
||||||
|
className="mt-3 rounded-md border p-3"
|
||||||
|
style={{ borderColor: 'var(--color-border-subtle)' }}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor={`runtime-provider-key-${provider.providerId}`} className="text-xs">
|
||||||
|
{provider.displayName} API key
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`runtime-provider-key-${provider.providerId}`}
|
||||||
|
type="password"
|
||||||
|
value={apiKeyValue}
|
||||||
|
disabled={disabled || busy}
|
||||||
|
onChange={(event) => actions.setApiKeyValue(event.target.value)}
|
||||||
|
placeholder="Paste API key"
|
||||||
|
className="h-9 text-sm"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={actions.cancelConnect}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
disabled={disabled || busy || !apiKeyValue.trim()}
|
||||||
|
onClick={() => void actions.submitConnect(provider.providerId)}
|
||||||
|
>
|
||||||
|
{busy ? <Loader2 className="mr-1 size-3.5 animate-spin" /> : null}
|
||||||
|
Save key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{active && provider.state === 'connected' && provider.modelCount !== 0 ? (
|
||||||
|
<ProviderModelList
|
||||||
|
state={state}
|
||||||
|
actions={actions}
|
||||||
|
provider={directoryEntryToProviderConnection(provider)}
|
||||||
|
disabled={disabled || busy}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -813,21 +906,14 @@ function ProviderDirectoryPanel({
|
||||||
<div key={provider.providerId}>
|
<div key={provider.providerId}>
|
||||||
<DirectoryProviderRow
|
<DirectoryProviderRow
|
||||||
provider={provider}
|
provider={provider}
|
||||||
|
state={state}
|
||||||
active={active}
|
active={active}
|
||||||
|
formOpen={state.activeFormProviderId === provider.providerId}
|
||||||
|
apiKeyValue={state.apiKeyValue}
|
||||||
disabled={disabled || state.directoryLoading}
|
disabled={disabled || state.directoryLoading}
|
||||||
busy={state.savingProviderId === provider.providerId}
|
busy={state.savingProviderId === provider.providerId}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
{active && provider.state === 'connected' && provider.modelCount !== 0 ? (
|
|
||||||
<div className="ml-3 border-l border-sky-300/25 pl-3">
|
|
||||||
<ProviderModelList
|
|
||||||
state={state}
|
|
||||||
actions={actions}
|
|
||||||
provider={directoryEntryToProviderConnection(provider)}
|
|
||||||
disabled={disabled || state.directoryLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -1142,8 +1228,17 @@ export function RuntimeProviderManagementPanelView({
|
||||||
.includes(providerQuery)
|
.includes(providerQuery)
|
||||||
)
|
)
|
||||||
: state.providers;
|
: state.providers;
|
||||||
const canSearchDirectory =
|
const useDirectoryRows =
|
||||||
state.directorySupported && providerQuery.length >= 2 && filteredProviders.length === 0;
|
state.directorySupported &&
|
||||||
|
(state.directoryLoaded || state.directoryLoading || state.directoryEntries.length > 0);
|
||||||
|
const visibleDirectoryRows = state.directoryEntries.filter((provider) =>
|
||||||
|
directoryEntryMatchesQuery(provider, providerQuery)
|
||||||
|
);
|
||||||
|
const providerCountLabel = state.directoryTotalCount
|
||||||
|
? `${state.directoryTotalCount} OpenCode providers`
|
||||||
|
: state.directorySupported
|
||||||
|
? 'OpenCode provider catalog'
|
||||||
|
: 'OpenCode providers';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -1182,32 +1277,32 @@ export function RuntimeProviderManagementPanelView({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{state.directoryOpen ? (
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<ProviderDirectoryPanel state={state} actions={actions} disabled={disabled} />
|
<div className="min-w-0">
|
||||||
) : null}
|
<div className="text-sm font-medium text-[var(--color-text)]">Providers</div>
|
||||||
|
<div className="text-xs text-[var(--color-text-muted)]">
|
||||||
{!state.directoryOpen && state.directorySupported ? (
|
{providerCountLabel}. Connected and recommended providers are shown first.
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-full rounded-lg border px-3 py-2.5 text-left transition-colors hover:border-sky-300/55 hover:bg-sky-400/[0.07]"
|
|
||||||
style={{
|
|
||||||
borderColor: 'var(--color-border-subtle)',
|
|
||||||
color: 'var(--color-text-secondary)',
|
|
||||||
}}
|
|
||||||
onClick={actions.openDirectory}
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium text-[var(--color-text)]">
|
|
||||||
Browse all OpenCode providers
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 text-xs">
|
</div>
|
||||||
{state.directoryTotalCount === null
|
{state.directorySupported ? (
|
||||||
? 'Load the dynamic provider catalog from OpenCode'
|
<Button
|
||||||
: `${state.directoryTotalCount} providers available from OpenCode`}
|
type="button"
|
||||||
</div>
|
size="sm"
|
||||||
</button>
|
variant="ghost"
|
||||||
) : null}
|
disabled={disabled || state.directoryLoading || state.directoryRefreshing}
|
||||||
|
onClick={() => void actions.refreshDirectory()}
|
||||||
|
>
|
||||||
|
{state.directoryRefreshing ? (
|
||||||
|
<Loader2 className="mr-1 size-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RefreshCcw className="mr-1 size-3.5" />
|
||||||
|
)}
|
||||||
|
Refresh catalog
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
{!state.directoryOpen && state.providers.length > 0 ? (
|
{state.providers.length > 0 || state.directorySupported ? (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[var(--color-text-muted)]" />
|
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[var(--color-text-muted)]" />
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1227,42 +1322,86 @@ export function RuntimeProviderManagementPanelView({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!state.directoryOpen && canSearchDirectory ? (
|
{state.directoryError ? (
|
||||||
<button
|
<div className="rounded-md border border-red-400/25 bg-red-400/10 px-3 py-2 text-xs text-red-200">
|
||||||
type="button"
|
{state.directoryError}
|
||||||
className="w-full rounded-md border px-3 py-2 text-left text-sm transition-colors hover:border-sky-300/55 hover:bg-sky-400/[0.07]"
|
|
||||||
style={{
|
|
||||||
borderColor: 'var(--color-border-subtle)',
|
|
||||||
color: 'var(--color-text)',
|
|
||||||
}}
|
|
||||||
onClick={() => actions.searchAllProviders(state.providerQuery.trim())}
|
|
||||||
>
|
|
||||||
Search all OpenCode providers for "{state.providerQuery.trim()}"
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!state.directoryOpen ? (
|
|
||||||
<div className="max-h-[62vh] space-y-2 overflow-y-auto pr-1">
|
|
||||||
{state.loading && state.providers.length === 0 ? (
|
|
||||||
<RuntimeProviderLoadingPlaceholder />
|
|
||||||
) : null}
|
|
||||||
{filteredProviders.map((provider) => (
|
|
||||||
<ProviderRow
|
|
||||||
key={provider.providerId}
|
|
||||||
provider={provider}
|
|
||||||
state={state}
|
|
||||||
active={provider.providerId === state.selectedProviderId}
|
|
||||||
formOpen={state.activeFormProviderId === provider.providerId}
|
|
||||||
apiKeyValue={state.apiKeyValue}
|
|
||||||
busy={state.savingProviderId === provider.providerId}
|
|
||||||
disabled={disabled || state.loading}
|
|
||||||
actions={actions}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!state.directoryOpen &&
|
<div className="max-h-[min(52vh,640px)] space-y-2 overflow-y-auto pr-1">
|
||||||
|
{useDirectoryRows ? (
|
||||||
|
<>
|
||||||
|
{state.directoryLoading && state.directoryEntries.length === 0 ? (
|
||||||
|
<RuntimeProviderLoadingPlaceholder />
|
||||||
|
) : null}
|
||||||
|
{visibleDirectoryRows.map((provider) => (
|
||||||
|
<DirectoryProviderRow
|
||||||
|
key={provider.providerId}
|
||||||
|
provider={provider}
|
||||||
|
state={state}
|
||||||
|
active={provider.providerId === state.selectedProviderId}
|
||||||
|
formOpen={state.activeFormProviderId === provider.providerId}
|
||||||
|
apiKeyValue={state.apiKeyValue}
|
||||||
|
busy={state.savingProviderId === provider.providerId}
|
||||||
|
disabled={disabled || state.directoryLoading}
|
||||||
|
actions={actions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{state.directoryNextCursor ? (
|
||||||
|
<div className="flex justify-center py-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={disabled || state.directoryRefreshing}
|
||||||
|
onClick={() => void actions.loadMoreDirectory()}
|
||||||
|
>
|
||||||
|
{state.directoryRefreshing ? (
|
||||||
|
<Loader2 className="mr-1 size-3.5 animate-spin" />
|
||||||
|
) : null}
|
||||||
|
Load more providers
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{state.loading && state.providers.length === 0 ? (
|
||||||
|
<RuntimeProviderLoadingPlaceholder />
|
||||||
|
) : null}
|
||||||
|
{filteredProviders.map((provider) => (
|
||||||
|
<ProviderRow
|
||||||
|
key={provider.providerId}
|
||||||
|
provider={provider}
|
||||||
|
state={state}
|
||||||
|
active={provider.providerId === state.selectedProviderId}
|
||||||
|
formOpen={state.activeFormProviderId === provider.providerId}
|
||||||
|
apiKeyValue={state.apiKeyValue}
|
||||||
|
busy={state.savingProviderId === provider.providerId}
|
||||||
|
disabled={disabled || state.loading}
|
||||||
|
actions={actions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{useDirectoryRows &&
|
||||||
|
!state.directoryLoading &&
|
||||||
|
visibleDirectoryRows.length === 0 &&
|
||||||
|
!state.directoryError ? (
|
||||||
|
<div
|
||||||
|
className="rounded-lg border p-3 text-sm"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--color-border-subtle)',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No providers match that search.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!useDirectoryRows &&
|
||||||
!state.loading &&
|
!state.loading &&
|
||||||
state.providers.length > 0 &&
|
state.providers.length > 0 &&
|
||||||
filteredProviders.length === 0 ? (
|
filteredProviders.length === 0 ? (
|
||||||
|
|
@ -1277,7 +1416,7 @@ export function RuntimeProviderManagementPanelView({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!state.directoryOpen && !state.loading && state.providers.length === 0 ? (
|
{!useDirectoryRows && !state.loading && state.providers.length === 0 ? (
|
||||||
<div
|
<div
|
||||||
className="rounded-lg border p-3 text-sm"
|
className="rounded-lg border p-3 text-sm"
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -1070,7 +1070,7 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="w-[min(96vw,980px)] max-w-[min(96vw,980px)]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Provider Settings</DialogTitle>
|
<DialogTitle>Provider Settings</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,7 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
expect(host.textContent).toContain('DeepSeek');
|
expect(host.textContent).toContain('DeepSeek');
|
||||||
expect(host.textContent).toContain('62 models');
|
expect(host.textContent).toContain('62 models');
|
||||||
expect(host.textContent).toContain('OpenCode catalog');
|
expect(host.textContent).toContain('OpenCode catalog');
|
||||||
expect(host.querySelector('[data-testid="runtime-provider-directory-search"]')).not.toBeNull();
|
expect(host.querySelector('[data-testid="runtime-provider-search"]')).not.toBeNull();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
host
|
host
|
||||||
|
|
@ -384,7 +384,7 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
expect(actions.selectDirectoryProvider).toHaveBeenCalledWith('deepseek');
|
expect(actions.selectDirectoryProvider).toHaveBeenCalledWith('deepseek');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('offers global provider search when compact search has no matches', async () => {
|
it('uses the unified provider search when compact search has no matches', async () => {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
const root = createRoot(host);
|
const root = createRoot(host);
|
||||||
|
|
@ -398,6 +398,31 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
...state,
|
...state,
|
||||||
providers: state.view?.providers ?? [],
|
providers: state.view?.providers ?? [],
|
||||||
providerQuery: 'deep',
|
providerQuery: 'deep',
|
||||||
|
directoryLoaded: true,
|
||||||
|
directoryTotalCount: 1,
|
||||||
|
directoryEntries: [
|
||||||
|
{
|
||||||
|
providerId: 'deepseek',
|
||||||
|
displayName: 'DeepSeek',
|
||||||
|
state: 'available',
|
||||||
|
setupKind: 'available-readonly',
|
||||||
|
ownership: [],
|
||||||
|
recommended: false,
|
||||||
|
modelCount: 62,
|
||||||
|
defaultModelId: null,
|
||||||
|
authMethods: [],
|
||||||
|
actions: [],
|
||||||
|
sources: ['opencode-provider'],
|
||||||
|
sourceLabel: 'OpenCode catalog',
|
||||||
|
providerSource: 'models.dev',
|
||||||
|
detail: 'Models are visible, but no connected credential was reported',
|
||||||
|
metadata: {
|
||||||
|
hasKnownModels: true,
|
||||||
|
requiresManualConfig: false,
|
||||||
|
supportedInlineAuth: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
actions,
|
actions,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
@ -406,17 +431,8 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchAll = Array.from(host.querySelectorAll('button')).find((button) =>
|
expect(host.textContent).toContain('DeepSeek');
|
||||||
button.textContent?.includes('Search all OpenCode providers for "deep"')
|
expect(host.textContent).not.toContain('Search all OpenCode providers');
|
||||||
);
|
|
||||||
expect(searchAll).not.toBeNull();
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
searchAll?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(actions.searchAllProviders).toHaveBeenCalledWith('deep');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders connected provider model picker actions', async () => {
|
it('renders connected provider model picker actions', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue