fix(runtime-provider): clarify opencode model routes ux
This commit is contained in:
parent
2b3a184bef
commit
c04871747c
5 changed files with 249 additions and 88 deletions
|
|
@ -55,16 +55,24 @@
|
||||||
"emptyRecommended": "No recommended models found.",
|
"emptyRecommended": "No recommended models found.",
|
||||||
"emptyRecommendedFree": "No recommended free models found.",
|
"emptyRecommendedFree": "No recommended free models found.",
|
||||||
"freeOnly": "Free only",
|
"freeOnly": "Free only",
|
||||||
"launchableDescription": "Routes you can test or use in the team picker: local config, free built-in models, and current default.",
|
"launchableDescription": "Known routes from OpenCode config, free built-in models, and the current default. Local routes need a successful test before they are ready for team launches.",
|
||||||
"launchableTitle": "Launchable OpenCode models",
|
"launchableTitle": "OpenCode model routes",
|
||||||
"loadingRoutes": "Loading OpenCode model routes...",
|
"loadingRoutes": "Loading OpenCode model routes...",
|
||||||
"noRoutesMatch": "No OpenCode model routes match \"{{query}}\".",
|
"noRoutesMatch": "No OpenCode model routes match \"{{query}}\".",
|
||||||
"noneReported": "No launchable OpenCode model routes were reported yet. Configure a local route in OpenCode or use the Providers tab to inspect catalog providers.",
|
"noneReported": "No OpenCode model routes were reported yet. Configure a local route in OpenCode or use the Providers tab to inspect catalog providers.",
|
||||||
"recommendedOnly": "Recommended only",
|
"recommendedOnly": "Recommended only",
|
||||||
"searchPlaceholder": "Search models",
|
"searchPlaceholder": "Search models",
|
||||||
"selectProjectBeforeTesting": "Select a project context before testing models.",
|
"selectProjectBeforeTesting": "Select a project context before testing models.",
|
||||||
"selectProjectBeforeTestingDefaults": "Select a project context before testing or saving OpenCode defaults.",
|
"selectProjectBeforeTestingDefaults": "Select a project context before testing or saving OpenCode defaults.",
|
||||||
"useInTeamPicker": "Use in team picker"
|
"testInProgress": "Model test is already running.",
|
||||||
|
"useInTeamPicker": "Save for team picker",
|
||||||
|
"validationContextRequired": "Select a validation context above to enable Test and Set default. Saving for team picker only stores the route for new teams.",
|
||||||
|
"actionsUnavailable": "Actions are temporarily unavailable.",
|
||||||
|
"defaultSaveInProgress": "OpenCode default is being saved.",
|
||||||
|
"routeUnavailableAuth": "This provider requires authentication before this model can be used.",
|
||||||
|
"routeUnavailableFailed": "This model route failed its last execution test.",
|
||||||
|
"routeUnavailableGeneric": "This model route cannot be used right now.",
|
||||||
|
"routeUnavailableUnknown": "This model is the current OpenCode default, but it is not available in the live catalog yet."
|
||||||
},
|
},
|
||||||
"providers": {
|
"providers": {
|
||||||
"catalog": "OpenCode provider catalog",
|
"catalog": "OpenCode provider catalog",
|
||||||
|
|
@ -99,10 +107,11 @@
|
||||||
"searchPlaceholder": "Search model routes"
|
"searchPlaceholder": "Search model routes"
|
||||||
},
|
},
|
||||||
"badges": {
|
"badges": {
|
||||||
"usedInTeamPicker": "Used in team picker",
|
"usedInTeamPicker": "Saved for team picker",
|
||||||
"free": "free",
|
"free": "free",
|
||||||
"local": "local",
|
"local": "local",
|
||||||
"configured": "configured",
|
"configured": "configured",
|
||||||
|
"knownRoute": "known route",
|
||||||
"connected": "connected",
|
"connected": "connected",
|
||||||
"verified": "verified",
|
"verified": "verified",
|
||||||
"needsTest": "needs test",
|
"needsTest": "needs test",
|
||||||
|
|
|
||||||
|
|
@ -55,16 +55,24 @@
|
||||||
"emptyRecommended": "Recommended models не найдены.",
|
"emptyRecommended": "Recommended models не найдены.",
|
||||||
"emptyRecommendedFree": "Recommended free models не найдены.",
|
"emptyRecommendedFree": "Recommended free models не найдены.",
|
||||||
"freeOnly": "Только free",
|
"freeOnly": "Только free",
|
||||||
"launchableDescription": "Routes, которые можно тестировать или использовать в team picker: local config, free built-in models и текущий default.",
|
"launchableDescription": "Известные routes из OpenCode config, free built-in models и текущий default. Local routes нужно проверить тестом перед запуском команд.",
|
||||||
"launchableTitle": "Launchable OpenCode models",
|
"launchableTitle": "Маршруты моделей OpenCode",
|
||||||
"loadingRoutes": "Загрузка OpenCode model routes...",
|
"loadingRoutes": "Загрузка OpenCode model routes...",
|
||||||
"noRoutesMatch": "OpenCode model routes не найдены по запросу \"{{query}}\".",
|
"noRoutesMatch": "OpenCode model routes не найдены по запросу \"{{query}}\".",
|
||||||
"noneReported": "Launchable OpenCode model routes пока не получены. Настройте local route в OpenCode или используйте вкладку Providers для просмотра catalog providers.",
|
"noneReported": "OpenCode model routes пока не получены. Настройте local route в OpenCode или используйте вкладку Providers для просмотра catalog providers.",
|
||||||
"recommendedOnly": "Только recommended",
|
"recommendedOnly": "Только recommended",
|
||||||
"searchPlaceholder": "Поиск моделей",
|
"searchPlaceholder": "Поиск моделей",
|
||||||
"selectProjectBeforeTesting": "Выберите project context перед тестированием моделей.",
|
"selectProjectBeforeTesting": "Выберите project context перед тестированием моделей.",
|
||||||
"selectProjectBeforeTestingDefaults": "Выберите project context перед тестированием или сохранением OpenCode defaults.",
|
"selectProjectBeforeTestingDefaults": "Выберите project context перед тестированием или сохранением OpenCode defaults.",
|
||||||
"useInTeamPicker": "Использовать в team picker"
|
"testInProgress": "Тест модели уже выполняется.",
|
||||||
|
"useInTeamPicker": "Сохранить для team picker",
|
||||||
|
"validationContextRequired": "Выберите validation context выше, чтобы включить Test и Set default. Сохранение для team picker только запоминает route для новых команд.",
|
||||||
|
"actionsUnavailable": "Действия временно недоступны.",
|
||||||
|
"defaultSaveInProgress": "OpenCode default сохраняется.",
|
||||||
|
"routeUnavailableAuth": "Этому provider нужна авторизация перед использованием модели.",
|
||||||
|
"routeUnavailableFailed": "Этот model route не прошёл последний execution test.",
|
||||||
|
"routeUnavailableGeneric": "Этот model route сейчас нельзя использовать.",
|
||||||
|
"routeUnavailableUnknown": "Эта модель выбрана текущим OpenCode default, но её пока нет в live catalog."
|
||||||
},
|
},
|
||||||
"providers": {
|
"providers": {
|
||||||
"catalog": "OpenCode provider catalog",
|
"catalog": "OpenCode provider catalog",
|
||||||
|
|
@ -99,10 +107,11 @@
|
||||||
"searchPlaceholder": "Поиск маршрутов моделей"
|
"searchPlaceholder": "Поиск маршрутов моделей"
|
||||||
},
|
},
|
||||||
"badges": {
|
"badges": {
|
||||||
"usedInTeamPicker": "Используется в выборе команды",
|
"usedInTeamPicker": "Сохранено для team picker",
|
||||||
"free": "free",
|
"free": "free",
|
||||||
"local": "local",
|
"local": "local",
|
||||||
"configured": "настроено",
|
"configured": "настроено",
|
||||||
|
"knownRoute": "известный route",
|
||||||
"connected": "подключено",
|
"connected": "подключено",
|
||||||
"verified": "проверено",
|
"verified": "проверено",
|
||||||
"needsTest": "нужен тест",
|
"needsTest": "нужен тест",
|
||||||
|
|
|
||||||
|
|
@ -2852,10 +2852,11 @@ export default interface Resources {
|
||||||
default: 'default';
|
default: 'default';
|
||||||
failed: 'failed';
|
failed: 'failed';
|
||||||
free: 'free';
|
free: 'free';
|
||||||
|
knownRoute: 'known route';
|
||||||
local: 'local';
|
local: 'local';
|
||||||
needsTest: 'needs test';
|
needsTest: 'needs test';
|
||||||
unknown: 'unknown';
|
unknown: 'unknown';
|
||||||
usedInTeamPicker: 'Used in team picker';
|
usedInTeamPicker: 'Saved for team picker';
|
||||||
verified: 'verified';
|
verified: 'verified';
|
||||||
};
|
};
|
||||||
compatibleEndpoint: {
|
compatibleEndpoint: {
|
||||||
|
|
@ -2889,22 +2890,30 @@ export default interface Resources {
|
||||||
searchPlaceholder: 'Search model routes';
|
searchPlaceholder: 'Search model routes';
|
||||||
};
|
};
|
||||||
models: {
|
models: {
|
||||||
|
actionsUnavailable: 'Actions are temporarily unavailable.';
|
||||||
alreadyDefault: 'This is already the selected OpenCode default.';
|
alreadyDefault: 'This is already the selected OpenCode default.';
|
||||||
|
defaultSaveInProgress: 'OpenCode default is being saved.';
|
||||||
empty: 'No models found.';
|
empty: 'No models found.';
|
||||||
emptyFree: 'No free models found.';
|
emptyFree: 'No free models found.';
|
||||||
emptyRecommended: 'No recommended models found.';
|
emptyRecommended: 'No recommended models found.';
|
||||||
emptyRecommendedFree: 'No recommended free models found.';
|
emptyRecommendedFree: 'No recommended free models found.';
|
||||||
freeOnly: 'Free only';
|
freeOnly: 'Free only';
|
||||||
launchableDescription: 'Routes you can test or use in the team picker: local config, free built-in models, and current default.';
|
launchableDescription: 'Known routes from OpenCode config, free built-in models, and the current default. Local routes need a successful test before they are ready for team launches.';
|
||||||
launchableTitle: 'Launchable OpenCode models';
|
launchableTitle: 'OpenCode model routes';
|
||||||
loadingRoutes: 'Loading OpenCode model routes...';
|
loadingRoutes: 'Loading OpenCode model routes...';
|
||||||
noRoutesMatch: 'No OpenCode model routes match "{{query}}".';
|
noRoutesMatch: 'No OpenCode model routes match "{{query}}".';
|
||||||
noneReported: 'No launchable OpenCode model routes were reported yet. Configure a local route in OpenCode or use the Providers tab to inspect catalog providers.';
|
noneReported: 'No OpenCode model routes were reported yet. Configure a local route in OpenCode or use the Providers tab to inspect catalog providers.';
|
||||||
recommendedOnly: 'Recommended only';
|
recommendedOnly: 'Recommended only';
|
||||||
|
routeUnavailableAuth: 'This provider requires authentication before this model can be used.';
|
||||||
|
routeUnavailableFailed: 'This model route failed its last execution test.';
|
||||||
|
routeUnavailableGeneric: 'This model route cannot be used right now.';
|
||||||
|
routeUnavailableUnknown: 'This model is the current OpenCode default, but it is not available in the live catalog yet.';
|
||||||
searchPlaceholder: 'Search models';
|
searchPlaceholder: 'Search models';
|
||||||
selectProjectBeforeTesting: 'Select a project context before testing models.';
|
selectProjectBeforeTesting: 'Select a project context before testing models.';
|
||||||
selectProjectBeforeTestingDefaults: 'Select a project context before testing or saving OpenCode defaults.';
|
selectProjectBeforeTestingDefaults: 'Select a project context before testing or saving OpenCode defaults.';
|
||||||
useInTeamPicker: 'Use in team picker';
|
testInProgress: 'Model test is already running.';
|
||||||
|
useInTeamPicker: 'Save for team picker';
|
||||||
|
validationContextRequired: 'Select a validation context above to enable Test and Set default. Saving for team picker only stores the route for new teams.';
|
||||||
};
|
};
|
||||||
providers: {
|
providers: {
|
||||||
catalog: 'OpenCode provider catalog';
|
catalog: 'OpenCode provider catalog';
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@renderer/components/ui/select';
|
} from '@renderer/components/ui/select';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@renderer/components/ui/tooltip';
|
||||||
import { cn } from '@renderer/lib/utils';
|
import { cn } from '@renderer/lib/utils';
|
||||||
import {
|
import {
|
||||||
compareOpenCodeTeamModelRecommendations,
|
compareOpenCodeTeamModelRecommendations,
|
||||||
|
|
@ -212,6 +218,33 @@ function isDefaultForScope(
|
||||||
return scopedDefault === model.modelId;
|
return scopedDefault === model.modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DisabledActionTooltip = ({
|
||||||
|
reason,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
readonly reason: string | undefined;
|
||||||
|
readonly children: JSX.Element;
|
||||||
|
}): JSX.Element => {
|
||||||
|
if (!reason) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={150}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="inline-flex" title={reason} aria-label={reason}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-72 text-pretty text-xs leading-relaxed">
|
||||||
|
{reason}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function directoryEntryMatchesQuery(
|
function directoryEntryMatchesQuery(
|
||||||
provider: RuntimeProviderDirectoryEntryDto,
|
provider: RuntimeProviderDirectoryEntryDto,
|
||||||
query: string
|
query: string
|
||||||
|
|
@ -1449,7 +1482,7 @@ function ModelBadges({
|
||||||
{t('runtimeProvider.badges.local')}
|
{t('runtimeProvider.badges.local')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge className="bg-sky-400/15 px-1.5 py-0 text-[10px] text-sky-200">
|
<Badge className="bg-sky-400/15 px-1.5 py-0 text-[10px] text-sky-200">
|
||||||
{t('runtimeProvider.badges.configured')}
|
{t('runtimeProvider.badges.knownRoute')}
|
||||||
</Badge>
|
</Badge>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -1517,17 +1550,47 @@ function canUseOpenCodeModelRoute(model: RuntimeProviderModelDto): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOpenCodeRouteUnavailableTitle(model: RuntimeProviderModelDto): string | undefined {
|
function getOpenCodeRouteUnavailableTitle(
|
||||||
|
model: RuntimeProviderModelDto,
|
||||||
|
t: SettingsT
|
||||||
|
): string | undefined {
|
||||||
if (isUnknownOpenCodeModelRoute(model)) {
|
if (isUnknownOpenCodeModelRoute(model)) {
|
||||||
return 'This model is the current OpenCode default, but it is not available in the live catalog yet.';
|
return t('runtimeProvider.models.routeUnavailableUnknown');
|
||||||
}
|
}
|
||||||
if (model.accessKind === 'not_authenticated') {
|
if (model.accessKind === 'not_authenticated') {
|
||||||
return (
|
return model.accessReason ?? t('runtimeProvider.models.routeUnavailableAuth');
|
||||||
model.accessReason ?? 'This provider requires authentication before this model can be used.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (model.accessKind === 'execution_failed' || model.proofState === 'failed') {
|
if (model.accessKind === 'execution_failed' || model.proofState === 'failed') {
|
||||||
return model.accessReason ?? 'This model route failed its last execution test.';
|
return model.accessReason ?? t('runtimeProvider.models.routeUnavailableFailed');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisabledActionReason(input: {
|
||||||
|
readonly disabled: boolean;
|
||||||
|
readonly contextRequiredTitle?: string;
|
||||||
|
readonly unavailableTitle?: string;
|
||||||
|
readonly busy: boolean;
|
||||||
|
readonly busyTitle: string;
|
||||||
|
readonly alreadyDefault?: boolean;
|
||||||
|
readonly alreadyDefaultTitle?: string;
|
||||||
|
readonly capabilityAvailable: boolean;
|
||||||
|
readonly t: SettingsT;
|
||||||
|
}): string | undefined {
|
||||||
|
if (input.disabled) {
|
||||||
|
return input.t('runtimeProvider.models.actionsUnavailable');
|
||||||
|
}
|
||||||
|
if (input.contextRequiredTitle) {
|
||||||
|
return input.contextRequiredTitle;
|
||||||
|
}
|
||||||
|
if (input.busy) {
|
||||||
|
return input.busyTitle;
|
||||||
|
}
|
||||||
|
if (input.alreadyDefault) {
|
||||||
|
return input.alreadyDefaultTitle;
|
||||||
|
}
|
||||||
|
if (!input.capabilityAvailable) {
|
||||||
|
return input.unavailableTitle ?? input.t('runtimeProvider.models.routeUnavailableGeneric');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -1863,6 +1926,18 @@ function ConfiguredOpenCodeModelsPanel({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
|
{!hasProjectContext ? (
|
||||||
|
<div
|
||||||
|
className="rounded-md border px-3 py-2 text-xs leading-5"
|
||||||
|
style={{
|
||||||
|
borderColor: 'rgba(251, 191, 36, 0.28)',
|
||||||
|
backgroundColor: 'rgba(251, 191, 36, 0.08)',
|
||||||
|
color: '#fde68a',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('runtimeProvider.models.validationContextRequired')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{visibleModels.length === 0 ? (
|
{visibleModels.length === 0 ? (
|
||||||
<div className="rounded-md border border-dashed border-white/10 px-3 py-3 text-sm text-[var(--color-text-muted)]">
|
<div className="rounded-md border border-dashed border-white/10 px-3 py-3 text-sm text-[var(--color-text-muted)]">
|
||||||
{t('runtimeProvider.models.noRoutesMatch', { query: query.trim() })}
|
{t('runtimeProvider.models.noRoutesMatch', { query: query.trim() })}
|
||||||
|
|
@ -1873,20 +1948,55 @@ function ConfiguredOpenCodeModelsPanel({
|
||||||
const testing = state.testingModelIds.includes(model.modelId);
|
const testing = state.testingModelIds.includes(model.modelId);
|
||||||
const savingDefault = state.savingDefaultModelId === model.modelId;
|
const savingDefault = state.savingDefaultModelId === model.modelId;
|
||||||
const result = state.modelResults[model.modelId];
|
const result = state.modelResults[model.modelId];
|
||||||
const unavailableTitle = getOpenCodeRouteUnavailableTitle(model);
|
const unavailableTitle = getOpenCodeRouteUnavailableTitle(model, t);
|
||||||
const contextRequiredTitle = hasProjectContext
|
const contextRequiredTitle = hasProjectContext
|
||||||
? undefined
|
? undefined
|
||||||
: t('runtimeProvider.models.selectProjectBeforeTestingDefaults');
|
: t('runtimeProvider.models.selectProjectBeforeTestingDefaults');
|
||||||
const alreadyDefaultForScope = isDefaultForScope(model, state, defaultScope);
|
const alreadyDefaultForScope = isDefaultForScope(model, state, defaultScope);
|
||||||
const canTest =
|
const testCapabilityAvailable = canTestOpenCodeModelRoute(model);
|
||||||
!disabled && hasProjectContext && !testing && canTestOpenCodeModelRoute(model);
|
const useCapabilityAvailable = canUseOpenCodeModelRoute(model);
|
||||||
const canUse = !disabled && canUseOpenCodeModelRoute(model);
|
const canTest = !disabled && hasProjectContext && !testing && testCapabilityAvailable;
|
||||||
|
const canUse = !disabled && useCapabilityAvailable;
|
||||||
const canSetDefault =
|
const canSetDefault =
|
||||||
!disabled &&
|
!disabled &&
|
||||||
hasProjectContext &&
|
hasProjectContext &&
|
||||||
!savingDefault &&
|
!savingDefault &&
|
||||||
!alreadyDefaultForScope &&
|
!alreadyDefaultForScope &&
|
||||||
canUseOpenCodeModelRoute(model);
|
useCapabilityAvailable;
|
||||||
|
const testDisabledReason = canTest
|
||||||
|
? undefined
|
||||||
|
: getDisabledActionReason({
|
||||||
|
disabled,
|
||||||
|
contextRequiredTitle,
|
||||||
|
unavailableTitle,
|
||||||
|
busy: testing,
|
||||||
|
busyTitle: t('runtimeProvider.models.testInProgress'),
|
||||||
|
capabilityAvailable: testCapabilityAvailable,
|
||||||
|
t,
|
||||||
|
});
|
||||||
|
const useDisabledReason = canUse
|
||||||
|
? undefined
|
||||||
|
: getDisabledActionReason({
|
||||||
|
disabled,
|
||||||
|
unavailableTitle,
|
||||||
|
busy: false,
|
||||||
|
busyTitle: '',
|
||||||
|
capabilityAvailable: useCapabilityAvailable,
|
||||||
|
t,
|
||||||
|
});
|
||||||
|
const setDefaultDisabledReason = canSetDefault
|
||||||
|
? undefined
|
||||||
|
: getDisabledActionReason({
|
||||||
|
disabled,
|
||||||
|
contextRequiredTitle,
|
||||||
|
unavailableTitle,
|
||||||
|
busy: savingDefault,
|
||||||
|
busyTitle: t('runtimeProvider.models.defaultSaveInProgress'),
|
||||||
|
alreadyDefault: alreadyDefaultForScope,
|
||||||
|
alreadyDefaultTitle: t('runtimeProvider.models.alreadyDefault'),
|
||||||
|
capabilityAvailable: useCapabilityAvailable,
|
||||||
|
t,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={model.modelId}
|
key={model.modelId}
|
||||||
|
|
@ -1912,61 +2022,57 @@ function ConfiguredOpenCodeModelsPanel({
|
||||||
<ModelBadges model={model} usedForNewTeams={selected} />
|
<ModelBadges model={model} usedForNewTeams={selected} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 flex-wrap justify-end gap-1.5">
|
<div className="flex shrink-0 flex-wrap justify-end gap-1.5">
|
||||||
<Button
|
<DisabledActionTooltip reason={testDisabledReason}>
|
||||||
type="button"
|
<Button
|
||||||
size="sm"
|
type="button"
|
||||||
variant="outline"
|
size="sm"
|
||||||
className="h-8"
|
variant="outline"
|
||||||
disabled={!canTest}
|
className="h-8"
|
||||||
title={canTest ? undefined : (contextRequiredTitle ?? unavailableTitle)}
|
disabled={!canTest}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!canTest) return;
|
if (!canTest) return;
|
||||||
void actions.testModel(model.providerId, model.modelId);
|
void actions.testModel(model.providerId, model.modelId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{testing ? (
|
{testing ? (
|
||||||
<Loader2 className="mr-1 size-3.5 animate-spin" />
|
<Loader2 className="mr-1 size-3.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<CheckCircle2 className="mr-1 size-3.5" />
|
<CheckCircle2 className="mr-1 size-3.5" />
|
||||||
)}
|
)}
|
||||||
{t('runtimeProvider.actions.test')}
|
{t('runtimeProvider.actions.test')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</DisabledActionTooltip>
|
||||||
type="button"
|
<DisabledActionTooltip reason={useDisabledReason}>
|
||||||
size="sm"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
className="h-8"
|
size="sm"
|
||||||
disabled={!canUse}
|
variant="ghost"
|
||||||
title={canUse ? undefined : unavailableTitle}
|
className="h-8"
|
||||||
onClick={() => {
|
disabled={!canUse}
|
||||||
if (!canUse) return;
|
onClick={() => {
|
||||||
actions.useModelForNewTeams(model.modelId);
|
if (!canUse) return;
|
||||||
}}
|
actions.useModelForNewTeams(model.modelId);
|
||||||
>
|
}}
|
||||||
{t('runtimeProvider.models.useInTeamPicker')}
|
>
|
||||||
</Button>
|
{t('runtimeProvider.models.useInTeamPicker')}
|
||||||
<Button
|
</Button>
|
||||||
type="button"
|
</DisabledActionTooltip>
|
||||||
size="sm"
|
<DisabledActionTooltip reason={setDefaultDisabledReason}>
|
||||||
variant="ghost"
|
<Button
|
||||||
className="h-8"
|
type="button"
|
||||||
disabled={!canSetDefault}
|
size="sm"
|
||||||
title={
|
variant="ghost"
|
||||||
canSetDefault
|
className="h-8"
|
||||||
? undefined
|
disabled={!canSetDefault}
|
||||||
: (contextRequiredTitle ??
|
onClick={() => {
|
||||||
(alreadyDefaultForScope
|
if (!canSetDefault) return;
|
||||||
? t('runtimeProvider.models.alreadyDefault')
|
void actions.setDefaultModel(model.providerId, model.modelId, defaultScope);
|
||||||
: unavailableTitle))
|
}}
|
||||||
}
|
>
|
||||||
onClick={() => {
|
{savingDefault ? <Loader2 className="mr-1 size-3.5 animate-spin" /> : null}
|
||||||
if (!canSetDefault) return;
|
{getDefaultScopeButtonLabel(defaultScope, t)}
|
||||||
void actions.setDefaultModel(model.providerId, model.modelId, defaultScope);
|
</Button>
|
||||||
}}
|
</DisabledActionTooltip>
|
||||||
>
|
|
||||||
{savingDefault ? <Loader2 className="mr-1 size-3.5 animate-spin" /> : null}
|
|
||||||
{getDefaultScopeButtonLabel(defaultScope, t)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ModelResult result={result} />
|
<ModelResult result={result} />
|
||||||
|
|
|
||||||
|
|
@ -653,9 +653,10 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
const row = host.querySelector<HTMLElement>(
|
const row = host.querySelector<HTMLElement>(
|
||||||
'[data-testid="configured-opencode-model-row-llama.cpp/qwen-test:0.5b"]'
|
'[data-testid="configured-opencode-model-row-llama.cpp/qwen-test:0.5b"]'
|
||||||
);
|
);
|
||||||
expect(host.textContent).toContain('Launchable OpenCode models');
|
expect(host.textContent).toContain('OpenCode model routes');
|
||||||
|
expect(host.textContent).toContain('Known routes from OpenCode config');
|
||||||
expect(row?.textContent).toContain('local');
|
expect(row?.textContent).toContain('local');
|
||||||
expect(row?.textContent).toContain('configured');
|
expect(row?.textContent).toContain('known route');
|
||||||
expect(row?.textContent).toContain('needs test');
|
expect(row?.textContent).toContain('needs test');
|
||||||
|
|
||||||
const buttons = Array.from(row?.querySelectorAll('button') ?? []);
|
const buttons = Array.from(row?.querySelectorAll('button') ?? []);
|
||||||
|
|
@ -664,7 +665,7 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
buttons.find((button) => button.textContent?.includes('Use in team picker'))?.click();
|
buttons.find((button) => button.textContent?.includes('Save for team picker'))?.click();
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
@ -847,10 +848,30 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain('Launchable OpenCode models');
|
expect(host.textContent).toContain('OpenCode model routes');
|
||||||
expect(host.textContent).toContain('llama.cpp/qwen-test:0.5b');
|
expect(host.textContent).toContain('llama.cpp/qwen-test:0.5b');
|
||||||
|
expect(host.textContent).toContain(
|
||||||
|
'Select a validation context above to enable Test and Set default'
|
||||||
|
);
|
||||||
expect(host.textContent).toContain('Providers');
|
expect(host.textContent).toContain('Providers');
|
||||||
expect(host.querySelector('[data-testid="runtime-provider-row-openrouter"]')).toBeNull();
|
expect(host.querySelector('[data-testid="runtime-provider-row-openrouter"]')).toBeNull();
|
||||||
|
|
||||||
|
const row = host.querySelector<HTMLElement>(
|
||||||
|
'[data-testid="configured-opencode-model-row-llama.cpp/qwen-test:0.5b"]'
|
||||||
|
);
|
||||||
|
const buttons = Array.from(row?.querySelectorAll('button') ?? []);
|
||||||
|
expect(buttons.map((button) => [button.textContent?.trim(), button.disabled])).toEqual([
|
||||||
|
['Test', true],
|
||||||
|
['Save for team picker', false],
|
||||||
|
['Set all-projects default', true],
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
Array.from(row?.querySelectorAll('[title]') ?? []).some(
|
||||||
|
(element) =>
|
||||||
|
element.getAttribute('title') ===
|
||||||
|
'Select a project context before testing or saving OpenCode defaults.'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows unknown OpenCode defaults without enabling launch actions', async () => {
|
it('shows unknown OpenCode defaults without enabling launch actions', async () => {
|
||||||
|
|
@ -897,6 +918,13 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
|
|
||||||
const buttons = Array.from(row?.querySelectorAll('button') ?? []);
|
const buttons = Array.from(row?.querySelectorAll('button') ?? []);
|
||||||
expect(buttons.map((button) => button.disabled)).toEqual([true, true, true]);
|
expect(buttons.map((button) => button.disabled)).toEqual([true, true, true]);
|
||||||
|
expect(
|
||||||
|
Array.from(row?.querySelectorAll('[title]') ?? []).some(
|
||||||
|
(element) =>
|
||||||
|
element.getAttribute('title') ===
|
||||||
|
'This model is the current OpenCode default, but it is not available in the live catalog yet.'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
buttons.forEach((button) => button.click());
|
buttons.forEach((button) => button.click());
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
@ -1807,7 +1835,7 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain('openrouter/openai/gpt-oss-20b:free');
|
expect(host.textContent).toContain('openrouter/openai/gpt-oss-20b:free');
|
||||||
expect(host.textContent).toContain('Used in team picker');
|
expect(host.textContent).toContain('Saved for team picker');
|
||||||
expect(host.textContent).toContain('Model probe passed');
|
expect(host.textContent).toContain('Model probe passed');
|
||||||
expect(host.textContent).toContain('Recommended');
|
expect(host.textContent).toContain('Recommended');
|
||||||
expect(host.textContent).toContain('Not recommended');
|
expect(host.textContent).toContain('Not recommended');
|
||||||
|
|
@ -1818,7 +1846,7 @@ describe('RuntimeProviderManagementPanelView', () => {
|
||||||
expect(host.textContent).not.toContain('Set OpenCode default');
|
expect(host.textContent).not.toContain('Set OpenCode default');
|
||||||
expect(
|
expect(
|
||||||
Array.from(host.querySelectorAll('button')).some(
|
Array.from(host.querySelectorAll('button')).some(
|
||||||
(button) => button.textContent?.trim() === 'Use in team picker'
|
(button) => button.textContent?.trim() === 'Save for team picker'
|
||||||
)
|
)
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue