diff --git a/package.json b/package.json index 1bf48564..14744119 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65db8493..d82cfdc3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@radix-ui/react-label': specifier: ^2.1.7 version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.2.10 version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1048,6 +1051,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.8': resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} peerDependencies: @@ -3890,6 +3906,29 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index db229776..e7c21471 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -323,7 +323,7 @@ fn scan_cli_version(tool: &str) -> (Option, Option) { let mut search_paths: Vec = vec![ home.join(".local/bin"), // Native install (official recommended) home.join(".npm-global/bin"), - home.join("n/bin"), // n version manager + home.join("n/bin"), // n version manager home.join(".volta/bin"), // Volta package manager ]; diff --git a/src/components/providers/forms/OmoFormFields.tsx b/src/components/providers/forms/OmoFormFields.tsx index 3c5cb821..94f2359f 100644 --- a/src/components/providers/forms/OmoFormFields.tsx +++ b/src/components/providers/forms/OmoFormFields.tsx @@ -12,6 +12,19 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; import { Plus, Trash2, @@ -22,6 +35,9 @@ import { FolderInput, Loader2, HelpCircle, + Check, + ChevronsUpDown, + X, } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; @@ -61,12 +77,138 @@ type BuiltinModelDef = Pick< >; type ModelOption = { value: string; label: string }; +function DeferredKeyInput({ + value, + onCommit, + placeholder, + className, +}: { + value: string; + onCommit: (value: string) => void; + placeholder?: string; + className?: string; +}) { + const [draft, setDraft] = useState(value); + + useEffect(() => { + setDraft(value); + }, [value]); + + return ( + setDraft(e.target.value)} + onBlur={() => { + if (draft !== value) { + onCommit(draft); + } + }} + placeholder={placeholder} + className={className} + /> + ); +} + const BUILTIN_AGENT_KEYS = new Set(OMO_BUILTIN_AGENTS.map((a) => a.key)); const BUILTIN_CATEGORY_KEYS = new Set(OMO_BUILTIN_CATEGORIES.map((c) => c.key)); -const EMPTY_MODEL_VALUE = "__cc_switch_omo_model_empty__"; -const UNAVAILABLE_MODEL_VALUE = "__cc_switch_omo_model_unavailable__"; const EMPTY_VARIANT_VALUE = "__cc_switch_omo_variant_empty__"; -const UNAVAILABLE_VARIANT_VALUE = "__cc_switch_omo_variant_unavailable__"; + +function ModelCombobox({ + value, + options, + recommended, + onChange, +}: { + value: string; + options: ModelOption[]; + recommended?: string; + onChange: (value: string) => void; +}) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const selectedLabel = options.find((o) => o.value === value)?.label; + + const selectModelText = t("omo.selectModel", { + defaultValue: "Select configured model", + }); + const placeholderText = recommended + ? `${selectModelText} (${t("omo.recommendedHint", { model: recommended, defaultValue: "Recommended: {{model}}" })})` + : selectModelText; + + return ( + + + + + + + + + + {t("omo.noEnabledModels", { + defaultValue: "No configured models", + })} + + + {options.map((option) => ( + { + onChange(option.value); + setOpen(false); + }} + > + + {option.label} + + ))} + + + + + + ); +} function getAdvancedStr(config: Record | undefined): string { if (!config) return ""; @@ -97,6 +239,7 @@ function mergeCustomModelsIntoStore( store: Record>, builtinKeys: Set, customs: CustomModelItem[], + modelVariantsMap: Record, ): Record> { const updated = { ...store }; for (const key of Object.keys(updated)) { @@ -104,7 +247,27 @@ function mergeCustomModelsIntoStore( } for (const custom of customs) { if (custom.key.trim()) { - updated[custom.key] = { ...updated[custom.key], model: custom.model }; + const nextEntry = { ...(updated[custom.key] || {}) }; + if (custom.model.trim()) { + nextEntry.model = custom.model; + const currentVariant = + typeof nextEntry.variant === "string" ? nextEntry.variant : ""; + if (currentVariant) { + const validVariants = modelVariantsMap[custom.model] || []; + if (!validVariants.includes(currentVariant)) { + delete nextEntry.variant; + } + } + updated[custom.key] = nextEntry; + } else { + delete nextEntry.model; + delete nextEntry.variant; + if (Object.keys(nextEntry).length > 0) { + updated[custom.key] = nextEntry; + } else { + delete updated[custom.key]; + } + } } } return updated; @@ -159,19 +322,29 @@ export function OmoFormFields({ const syncCustomAgents = useCallback( (customs: CustomModelItem[]) => { onAgentsChange( - mergeCustomModelsIntoStore(agents, BUILTIN_AGENT_KEYS, customs), + mergeCustomModelsIntoStore( + agents, + BUILTIN_AGENT_KEYS, + customs, + modelVariantsMap, + ), ); }, - [agents, onAgentsChange], + [agents, onAgentsChange, modelVariantsMap], ); const syncCustomCategories = useCallback( (customs: CustomModelItem[]) => { onCategoriesChange( - mergeCustomModelsIntoStore(categories, BUILTIN_CATEGORY_KEYS, customs), + mergeCustomModelsIntoStore( + categories, + BUILTIN_CATEGORY_KEYS, + customs, + modelVariantsMap, + ), ); }, - [categories, onCategoriesChange], + [categories, onCategoriesChange, modelVariantsMap], ); const buildEffectiveModelOptions = useCallback( @@ -212,43 +385,16 @@ export function OmoFormFields({ const renderModelSelect = ( currentModel: string, onChange: (value: string) => void, - placeholder?: string, + recommended?: string, ) => { const options = buildEffectiveModelOptions(currentModel); return ( - + ); }; @@ -268,13 +414,21 @@ export function OmoFormFields({ currentVariant: string, onChange: (value: string) => void, ) => { + const hasModel = Boolean(currentModel); + const modelVariantKeys = hasModel + ? modelVariantsMap[currentModel] || [] + : []; + const hasVariants = modelVariantKeys.length > 0; + const shouldShow = hasModel && (hasVariants || Boolean(currentVariant)); + + if (!shouldShow) { + return null; + } + const variantOptions = buildEffectiveVariantOptions( currentModel, currentVariant, ); - const hasModel = Boolean(currentModel); - const hasVariants = - hasModel && (modelVariantsMap[currentModel] || []).length > 0; const firstIsUnavailable = Boolean(currentVariant) && !(modelVariantsMap[currentModel] || []).includes(currentVariant); @@ -285,9 +439,8 @@ export function OmoFormFields({ onValueChange={(value) => onChange(value === EMPTY_VARIANT_VALUE ? "" : value) } - disabled={!hasModel || !hasVariants} > - + {t("omo.defaultWrapped", { defaultValue: "(Default)" })} - {!hasModel ? ( - - {t("omo.selectModelFirst", { - defaultValue: "Select model first", - })} + {variantOptions.map((variant, index) => ( + + {firstIsUnavailable && index === 0 + ? t("omo.currentValueUnavailable", { + value: variant, + defaultValue: "{{value}} (current value, unavailable)", + }) + : variant} - ) : variantOptions.length === 0 ? ( - - {t("omo.noVariantsForModel", { - defaultValue: "No variants for model", - })} - - ) : ( - variantOptions.map((variant, index) => ( - - {firstIsUnavailable && index === 0 - ? t("omo.currentValueUnavailable", { - value: variant, - defaultValue: "{{value}} (current value, unavailable)", - }) - : variant} - - )) - )} + ))} ); @@ -538,7 +677,7 @@ export function OmoFormFields({ toast.warning( t("omo.noEnabledModelsWarning", { defaultValue: - "No enabled models available. Configure and enable OpenCode models first.", + "No configured models available. Configure OpenCode models first.", }), ); return; @@ -737,16 +876,14 @@ export function OmoFormFields({ className="border-b border-border/30 last:border-b-0" >
- updateCustom({ key: e.target.value })} + onCommit={(value) => updateCustom({ key: value })} placeholder={keyPlaceholder} className="w-32 shrink-0 h-8 text-sm text-primary" /> - {renderModelSelect( - item.model, - (value) => updateCustom({ model: value }), - t("omo.modelNamePlaceholder", { defaultValue: "model-name" }), + {renderModelSelect(item.model, (value) => + updateCustom({ model: value }), )} {renderVariantSelect(item.model, currentVariant, (value) => { if (!item.key) return; @@ -941,7 +1078,7 @@ export function OmoFormFields({ ·{" "} {t("omo.enabledModelsCount", { count: modelOptions.length, - defaultValue: "{{count}} enabled models available", + defaultValue: "{{count}} configured models available", })} {localFilePath && ( diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 1e2b7666..f3d4a01d 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -113,21 +113,25 @@ const isKnownOpencodeOptionKey = (key: string) => function parseOpencodeConfig( settingsConfig?: Record, ): OpenCodeProviderConfig { + const normalize = ( + parsed: Partial, + ): OpenCodeProviderConfig => ({ + npm: parsed.npm || OPENCODE_DEFAULT_NPM, + options: + parsed.options && typeof parsed.options === "object" + ? (parsed.options as OpenCodeProviderConfig["options"]) + : {}, + models: + parsed.models && typeof parsed.models === "object" + ? (parsed.models as Record) + : {}, + }); + try { const parsed = JSON.parse( settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG, ) as Partial; - return { - npm: parsed.npm || OPENCODE_DEFAULT_NPM, - options: - parsed.options && typeof parsed.options === "object" - ? (parsed.options as OpenCodeProviderConfig["options"]) - : {}, - models: - parsed.models && typeof parsed.models === "object" - ? (parsed.models as Record) - : {}, - }; + return normalize(parsed); } catch { return { npm: OPENCODE_DEFAULT_NPM, @@ -137,6 +141,25 @@ function parseOpencodeConfig( } } +function parseOpencodeConfigStrict( + settingsConfig?: Record, +): OpenCodeProviderConfig { + const parsed = JSON.parse( + settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG, + ) as Partial; + return { + npm: parsed.npm || OPENCODE_DEFAULT_NPM, + options: + parsed.options && typeof parsed.options === "object" + ? (parsed.options as OpenCodeProviderConfig["options"]) + : {}, + models: + parsed.models && typeof parsed.models === "object" + ? (parsed.models as Record) + : {}, + }; +} + function toOpencodeExtraOptions( options: OpenCodeProviderConfig["options"], ): Record { @@ -581,19 +604,24 @@ export function ProviderForm({ ); }, [opencodeProvidersData?.providers, providerId]); const [enabledOpencodeProviderIds, setEnabledOpencodeProviderIds] = useState< - string[] - >([]); + string[] | null + >(null); + const [omoLiveIdsLoadFailed, setOmoLiveIdsLoadFailed] = useState(false); + const lastOmoModelSourceWarningRef = useRef(""); useEffect(() => { let active = true; - if (!isOmoCategory) { - setEnabledOpencodeProviderIds([]); + setEnabledOpencodeProviderIds(null); + setOmoLiveIdsLoadFailed(false); return () => { active = false; }; } + setEnabledOpencodeProviderIds(null); + setOmoLiveIdsLoadFailed(false); + (async () => { try { const ids = await providersApi.getOpenCodeLiveProviderIds(); @@ -601,9 +629,13 @@ export function ProviderForm({ setEnabledOpencodeProviderIds(ids); } } catch (error) { - console.error("Failed to load OpenCode live provider ids:", error); + console.warn( + "[OMO_MODEL_SOURCE_LIVE_IDS_FAILED] failed to load live provider ids", + error, + ); if (active) { - setEnabledOpencodeProviderIds([]); + setOmoLiveIdsLoadFailed(true); + setEnabledOpencodeProviderIds(null); } } })(); @@ -613,23 +645,57 @@ export function ProviderForm({ }; }, [isOmoCategory]); - const omoModelOptions = useMemo(() => { - if (!isOmoCategory) return []; + const omoModelBuild = useMemo(() => { + const empty = { + options: [] as Array<{ value: string; label: string }>, + variantsMap: {} as Record, + parseFailedProviders: [] as string[], + usedFallbackSource: false, + }; + if (!isOmoCategory) { + return empty; + } const allProviders = opencodeProvidersData?.providers; - if (!allProviders) return []; + if (!allProviders) { + return empty; + } - const enabledSet = new Set(enabledOpencodeProviderIds); - if (enabledSet.size === 0) return []; + const shouldFilterByLive = !omoLiveIdsLoadFailed; + if (shouldFilterByLive && enabledOpencodeProviderIds === null) { + return empty; + } + const liveSet = + shouldFilterByLive && enabledOpencodeProviderIds + ? new Set(enabledOpencodeProviderIds) + : null; const dedupedOptions = new Map(); + const variantsMap: Record = {}; + const parseFailedProviders: string[] = []; for (const [providerKey, provider] of Object.entries(allProviders)) { - if (provider.category === "omo" || !enabledSet.has(providerKey)) { + if (provider.category === "omo") { + continue; + } + if (liveSet && !liveSet.has(providerKey)) { continue; } - const parsedConfig = parseOpencodeConfig(provider.settingsConfig); + let parsedConfig: OpenCodeProviderConfig; + try { + parsedConfig = parseOpencodeConfigStrict(provider.settingsConfig); + } catch (error) { + parseFailedProviders.push(providerKey); + console.warn( + "[OMO_MODEL_SOURCE_PARSE_FAILED] failed to parse provider settings", + { + providerKey, + error, + }, + ); + continue; + } for (const [modelId, model] of Object.entries( parsedConfig.models || {}, )) { @@ -646,55 +712,18 @@ export function ProviderForm({ if (!dedupedOptions.has(value)) { dedupedOptions.set(value, label); } - } - } - return Array.from(dedupedOptions.entries()) - .map(([value, label]) => ({ value, label })) - .sort((a, b) => a.label.localeCompare(b.label, "zh-CN")); - }, [ - isOmoCategory, - opencodeProvidersData?.providers, - enabledOpencodeProviderIds, - ]); - const omoModelVariantsMap = useMemo(() => { - const variantsMap: Record = {}; - if (!isOmoCategory) { - return variantsMap; - } - - const allProviders = opencodeProvidersData?.providers; - if (!allProviders) { - return variantsMap; - } - - const enabledSet = new Set(enabledOpencodeProviderIds); - if (enabledSet.size === 0) { - return variantsMap; - } - - for (const [providerKey, provider] of Object.entries(allProviders)) { - if (provider.category === "omo" || !enabledSet.has(providerKey)) { - continue; - } - - const parsedConfig = parseOpencodeConfig(provider.settingsConfig); - for (const [modelId, model] of Object.entries( - parsedConfig.models || {}, - )) { const rawVariants = model.variants; if ( - !rawVariants || - typeof rawVariants !== "object" || - Array.isArray(rawVariants) + rawVariants && + typeof rawVariants === "object" && + !Array.isArray(rawVariants) ) { - continue; + const variantKeys = Object.keys(rawVariants).filter(Boolean); + if (variantKeys.length > 0) { + variantsMap[value] = variantKeys; + } } - const variantKeys = Object.keys(rawVariants).filter(Boolean); - if (variantKeys.length === 0) { - continue; - } - variantsMap[`${providerKey}/${modelId}`] = variantKeys; } // Preset fallback: for models without config-defined variants, @@ -717,11 +746,58 @@ export function ProviderForm({ } } - return variantsMap; + return { + options: Array.from(dedupedOptions.entries()) + .map(([value, label]) => ({ value, label })) + .sort((a, b) => a.label.localeCompare(b.label, "zh-CN")), + variantsMap, + parseFailedProviders, + usedFallbackSource: omoLiveIdsLoadFailed, + }; }, [ isOmoCategory, opencodeProvidersData?.providers, enabledOpencodeProviderIds, + omoLiveIdsLoadFailed, + ]); + const omoModelOptions = omoModelBuild.options; + const omoModelVariantsMap = omoModelBuild.variantsMap; + + useEffect(() => { + if (!isOmoCategory) return; + const failed = omoModelBuild.parseFailedProviders; + const fallback = omoModelBuild.usedFallbackSource; + if (failed.length === 0 && !fallback) return; + + const signature = `${fallback ? "fallback:" : ""}${failed + .slice() + .sort() + .join(",")}`; + if (lastOmoModelSourceWarningRef.current === signature) return; + lastOmoModelSourceWarningRef.current = signature; + + if (failed.length > 0) { + toast.warning( + t("omo.modelSourcePartialWarning", { + count: failed.length, + defaultValue: + "Some provider model configs are invalid and were skipped.", + }), + ); + } + if (fallback) { + toast.warning( + t("omo.modelSourceFallbackWarning", { + defaultValue: + "Failed to load live provider state. Falling back to configured providers.", + }), + ); + } + }, [ + isOmoCategory, + omoModelBuild.parseFailedProviders, + omoModelBuild.usedFallbackSource, + t, ]); const initialOmoSettings = diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 00000000..34895b26 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,103 @@ +import * as React from "react"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const Command = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +const CommandInput = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandItem = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandItem.displayName = CommandPrimitive.Item.displayName; + +export { + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, +}; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 00000000..9f00cbca --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; +import { cn } from "@/lib/utils"; + +const Popover = PopoverPrimitive.Root; +const PopoverTrigger = PopoverPrimitive.Trigger; +const PopoverAnchor = PopoverPrimitive.Anchor; + +const PopoverContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, align = "start", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 979b4d7b..d5bd4632 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1572,16 +1572,21 @@ "clearWrapped": "(Clear)", "defaultWrapped": "(Default)", "variantPlaceholder": "variant", - "selectEnabledModel": "Select enabled model", + "selectEnabledModel": "Select configured model", + "selectModel": "Select configured model", + "recommendedHint": "Recommended: {{model}}", + "searchModel": "Search model...", "selectModelFirst": "Select model first", - "noEnabledModels": "No enabled models", + "noEnabledModels": "No configured models", "noVariantsForModel": "No variants for model", - "currentValueNotEnabled": "{{value}} (current value, not enabled)", + "currentValueNotEnabled": "{{value}} (current value, not configured)", "currentValueUnavailable": "{{value}} (current value, unavailable)", "advancedLabel": "Advanced", "advancedJsonInvalid": "Advanced JSON is invalid", "advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission, etc. Leave empty for defaults", - "noEnabledModelsWarning": "No enabled models available. Configure and enable OpenCode models first.", + "noEnabledModelsWarning": "No configured models available. Configure OpenCode models first.", + "modelSourcePartialWarning": "Some provider model configs are invalid and were skipped.", + "modelSourceFallbackWarning": "Failed to load live provider state. Falling back to configured providers.", "importLocalReplaceSuccess": "Imported local file and replaced Agents/Categories/Other Fields", "importLocalFailed": "Failed to read local file: {{error}}", "agentKeyPlaceholder": "agent key", @@ -1592,7 +1597,7 @@ "modelConfiguration": "Model Configuration", "fillRecommended": "Fill Recommended", "configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params", - "enabledModelsCount": "{{count}} enabled models available", + "enabledModelsCount": "{{count}} configured models available", "source": "from:", "otherFieldsJson": "Other Fields (JSON)", "searchOrType": "Search or type custom value...", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index c78e8b49..908d45f4 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1553,16 +1553,21 @@ "clearWrapped": "(クリア)", "defaultWrapped": "(デフォルト)", "variantPlaceholder": "variant", - "selectEnabledModel": "有効なモデルを選択", + "selectEnabledModel": "設定済みモデルを選択", + "selectModel": "設定済みモデルを選択", + "recommendedHint": "推奨: {{model}}", + "searchModel": "モデルを検索...", "selectModelFirst": "先にモデルを選択", - "noEnabledModels": "有効なモデルがありません", + "noEnabledModels": "設定済みモデルがありません", "noVariantsForModel": "このモデルには思考レベルがありません", - "currentValueNotEnabled": "{{value}} (現在値・未有効)", + "currentValueNotEnabled": "{{value}} (現在値・未設定)", "currentValueUnavailable": "{{value}} (現在値・利用不可)", "advancedLabel": "詳細", "advancedJsonInvalid": "詳細 JSON が不正です", "advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission など。空欄でデフォルトを使用します", - "noEnabledModelsWarning": "利用可能な有効モデルがありません。先に OpenCode モデルを有効化してください。", + "noEnabledModelsWarning": "利用可能な設定済みモデルがありません。先に OpenCode モデルを設定してください。", + "modelSourcePartialWarning": "一部プロバイダーのモデル設定が不正なため、候補から除外しました。", + "modelSourceFallbackWarning": "live プロバイダー状態の取得に失敗したため、設定済みプロバイダーへフォールバックしました。", "importLocalReplaceSuccess": "ローカルファイルから読み込み、Agents/Categories/Other Fields を置き換えました", "importLocalFailed": "ローカルファイルの読み込みに失敗しました: {{error}}", "agentKeyPlaceholder": "agent キー", @@ -1573,7 +1578,7 @@ "modelConfiguration": "モデル設定", "fillRecommended": "推奨を入力", "configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開", - "enabledModelsCount": "有効モデル {{count}} 件", + "enabledModelsCount": "設定済みモデル {{count}} 件", "source": "出典:", "otherFieldsJson": "その他のフィールド (JSON)", "searchOrType": "検索またはカスタム値を入力...", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 972d2a4e..e325e9bb 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1572,16 +1572,21 @@ "clearWrapped": "(清空)", "defaultWrapped": "(默认)", "variantPlaceholder": "思考等级", - "selectEnabledModel": "选择已启用模型", + "selectEnabledModel": "选择已配置模型", + "selectModel": "选择已配置模型", + "recommendedHint": "推荐: {{model}}", + "searchModel": "搜索模型...", "selectModelFirst": "先选择模型", - "noEnabledModels": "暂无已启用模型", + "noEnabledModels": "暂无已配置模型", "noVariantsForModel": "该模型无思考等级", - "currentValueNotEnabled": "{{value}}(当前值,未启用)", - "currentValueUnavailable": "{{value}}(当前值,未启用)", + "currentValueNotEnabled": "{{value}}(当前值,未配置)", + "currentValueUnavailable": "{{value}}(当前值,不可用)", "advancedLabel": "高级参数", "advancedJsonInvalid": "高级参数 JSON 无效", "advancedJsonHint": "temperature, top_p, budgetTokens, prompt_append, permission 等,留空使用默认值", - "noEnabledModelsWarning": "当前没有可用的已启用模型,请先启用并配置 OpenCode 模型", + "noEnabledModelsWarning": "当前没有可用的已配置模型,请先配置 OpenCode 模型", + "modelSourcePartialWarning": "部分供应商模型配置无效,已自动跳过。", + "modelSourceFallbackWarning": "读取 live 供应商状态失败,已回退到已配置供应商列表。", "importLocalReplaceSuccess": "已从本地文件导入并覆盖 Agent/Category/Other Fields", "importLocalFailed": "读取本地文件失败: {{error}}", "agentKeyPlaceholder": "agent 键名", @@ -1592,7 +1597,7 @@ "modelConfiguration": "模型配置", "fillRecommended": "填充推荐", "configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数", - "enabledModelsCount": "可选已启用模型 {{count}} 个", + "enabledModelsCount": "可选已配置模型 {{count}} 个", "source": "来源:", "otherFieldsJson": "其他字段 (JSON)", "searchOrType": "搜索或输入自定义值...",