feat(ai): add Jalapeno Cloud partner preset
This commit is contained in:
16 files changed
+253
-47
No files matched your search
@@ -55,7 +55,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import CodeSnapshotDialog from "@/components/codeSnapshot/CodeSnapshotDialog.vue";
|
||||
import type { CodeSnapshotSource } from "@/lib/codeSnapshot/codeSnapshot";
|
||||
import { useSettingsStore, AI_PROVIDER_PRESETS, aiProviderLabel, normalizeAiConfig } from "@/stores/settingsStore";
|
||||
import { useSettingsStore, AI_PROVIDER_PRESETS, aiProviderLabel, getAiProviderPreset, normalizeAiConfig } from "@/stores/settingsStore";
|
||||
import AiProviderLogo from "@/components/icons/AiProviderLogo.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
@@ -715,7 +715,7 @@ const activeFullConfig = computed(() => {
|
||||
});
|
||||
|
||||
function isModelCandidate(config: AiConfigItem): boolean {
|
||||
return isAiConfigModelCandidate(config, AI_PROVIDER_PRESETS[config.provider].requiresApiKey, supportsCliProviders);
|
||||
return isAiConfigModelCandidate(config, getAiProviderPreset(config.provider, config.endpoint).requiresApiKey, supportsCliProviders);
|
||||
}
|
||||
|
||||
function getModelsForConfig(configId: string) {
|
||||
@@ -724,8 +724,14 @@ function getModelsForConfig(configId: string) {
|
||||
return aiModelOptions(config, modelCatalogs.get(configId)?.models ?? []);
|
||||
}
|
||||
|
||||
function aiConfigProviderLabel(config: { provider: AiConfigItem["provider"]; endpoint: string } | null | undefined): string {
|
||||
if (!config) return aiProviderLabel("claude", t);
|
||||
const preset = getAiProviderPreset(config.provider, config.endpoint);
|
||||
return preset.provider === "custom" ? aiProviderLabel(config.provider, t) : preset.label;
|
||||
}
|
||||
|
||||
function configMatchesModelQuery(config: AiConfigItem, query: string): boolean {
|
||||
return config.name.toLowerCase().includes(query) || config.provider.toLowerCase().includes(query) || aiProviderLabel(config.provider, t).toLowerCase().includes(query) || AI_PROVIDER_PRESETS[config.provider].label.toLowerCase().includes(query);
|
||||
return config.name.toLowerCase().includes(query) || config.provider.toLowerCase().includes(query) || aiConfigProviderLabel(config).toLowerCase().includes(query);
|
||||
}
|
||||
|
||||
function getConfigModelOptions(config: AiConfigItem) {
|
||||
@@ -5001,7 +5007,13 @@ async function openExternalUrl(url: string) {
|
||||
<Popover v-model:open="providerSelectorOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<button type="button" class="min-w-0 flex shrink items-center gap-1.5 max-w-[220px] rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground">
|
||||
<AiProviderLogo :provider="activeFullConfig?.provider ?? 'claude'" :label="aiProviderLabel(activeFullConfig?.provider ?? 'claude', t)" :icon-slug="AI_PROVIDER_PRESETS[activeFullConfig?.provider ?? 'claude']?.iconSlug" class="h-3 w-3 shrink-0" />
|
||||
<AiProviderLogo
|
||||
:provider="activeFullConfig?.provider ?? 'claude'"
|
||||
:label="aiConfigProviderLabel(activeFullConfig)"
|
||||
:icon-slug="activeFullConfig ? getAiProviderPreset(activeFullConfig.provider, activeFullConfig.endpoint).iconSlug : AI_PROVIDER_PRESETS.claude.iconSlug"
|
||||
:icon-path="activeFullConfig ? getAiProviderPreset(activeFullConfig.provider, activeFullConfig.endpoint).iconPath : undefined"
|
||||
class="h-3 w-3 shrink-0"
|
||||
/>
|
||||
<span class="min-w-0 truncate">{{ activeFullConfig?.model || t("ai.selectModel") }}</span>
|
||||
<svg class="h-3 w-3 shrink-0 opacity-60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</button>
|
||||
@@ -5015,7 +5027,13 @@ async function openExternalUrl(url: string) {
|
||||
<div v-for="(config, configIndex) in configuredProviders" :key="config.id" class="relative">
|
||||
<button type="button" class="sticky top-0 z-10 flex w-full items-center gap-2 rounded-sm bg-popover px-2 py-1.5 text-left text-xs text-foreground hover:bg-muted" :aria-expanded="!isModelConfigCollapsed(config.id)" @click="toggleModelConfig(config.id)">
|
||||
<ChevronRight class="h-3.5 w-3.5 shrink-0 transition-transform" :class="{ 'rotate-90': !isModelConfigCollapsed(config.id) }" />
|
||||
<AiProviderLogo :provider="config.provider" :label="AI_PROVIDER_PRESETS[config.provider]?.label ?? config.provider" :icon-slug="AI_PROVIDER_PRESETS[config.provider]?.iconSlug" class="h-3.5 w-3.5 shrink-0" />
|
||||
<AiProviderLogo
|
||||
:provider="config.provider"
|
||||
:label="getAiProviderPreset(config.provider, config.endpoint).label"
|
||||
:icon-slug="getAiProviderPreset(config.provider, config.endpoint).iconSlug"
|
||||
:icon-path="getAiProviderPreset(config.provider, config.endpoint).iconPath"
|
||||
class="h-3.5 w-3.5 shrink-0"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{{ config.name }}</span>
|
||||
<Loader2 v-if="getModelCatalog(config.id).status === 'loading'" class="h-3 w-3 shrink-0 animate-spin text-muted-foreground" />
|
||||
<span v-if="config.isDefault" class="ml-auto text-[10px] text-muted-foreground">{{ t("ai.default") }}</span>
|
||||
|
||||
@@ -45,7 +45,7 @@ import { Input } from "@/components/ui/input";
|
||||
import PasswordInput from "@/components/ui/PasswordInput.vue";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
@@ -53,7 +53,7 @@ import { HelpTooltip, Tooltip, TooltipContent, TooltipTrigger, TooltipProvider }
|
||||
import {
|
||||
useSettingsStore,
|
||||
AI_PROVIDER_PRESETS,
|
||||
aiProviderLabel,
|
||||
AI_PROVIDER_PARTNER_PRESETS,
|
||||
EDITOR_THEMES,
|
||||
DEFAULT_EDITOR_SETTINGS,
|
||||
DEFAULT_DESKTOP_SETTINGS,
|
||||
@@ -63,6 +63,10 @@ import {
|
||||
normalizeDuckDbWorkerMaxProcesses,
|
||||
normalizeAiEnv,
|
||||
normalizeAiHeaders,
|
||||
getAiProviderPreset,
|
||||
getAiProviderPresetId,
|
||||
getAiProviderPresetOption,
|
||||
isAiPartnerProviderPreset,
|
||||
type AiProvider,
|
||||
type AiApiStyle,
|
||||
type AiAuthMethod,
|
||||
@@ -3381,10 +3385,11 @@ const aiDeleteConfigId = ref<string | null>(null);
|
||||
const CLI_AI_PROVIDERS = new Set<AiProvider>(["claude-code-cli", "codex-cli", "opencode-cli", "pi-agent-cli", "cursor-cli", "grok-cli", "codebuddy-cli", "qoder-cli"]);
|
||||
const OPENCODE_CONTROL_ENV = new Set(["OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_CONFIG_DIR", "OPENCODE_DB", "OPENCODE_PERMISSION", "OPENCODE_DISABLE_PROJECT_CONFIG"]);
|
||||
const CURSOR_CONTROL_ENV = new Set(["CURSOR_CONFIG_DIR", "CURSOR_DATA_DIR"]);
|
||||
const aiProviderOptions = computed(() => Object.values(AI_PROVIDER_PRESETS).filter((provider) => !isWeb || !CLI_AI_PROVIDERS.has(provider.provider)));
|
||||
const selectedAiProviderPreset = computed(() => AI_PROVIDER_PRESETS[aiEditProvider.value]);
|
||||
const builtinAiProviderOptions = computed(() => Object.values(AI_PROVIDER_PRESETS).filter((provider) => !isWeb || !CLI_AI_PROVIDERS.has(provider.provider)));
|
||||
const partnerAiProviderOptions = computed(() => AI_PROVIDER_PARTNER_PRESETS.filter((provider) => !isWeb || !CLI_AI_PROVIDERS.has(provider.provider)));
|
||||
|
||||
const aiEditProvider = ref<AiProvider>("claude");
|
||||
const aiEditProviderPresetId = ref("claude");
|
||||
const aiEditApiKey = ref("");
|
||||
const aiEditAuthMethod = ref<AiAuthMethod>("api-key");
|
||||
const aiEditEndpoint = ref("");
|
||||
@@ -3415,6 +3420,8 @@ const aiEditQoderCliPath = ref("");
|
||||
const aiEditQoderCliEnvRows = ref<AiEnvRow[]>([]);
|
||||
|
||||
const aiAnthropicMessagesMode = computed(() => aiEditApiStyle.value === "anthropic-messages");
|
||||
const selectedAiProviderPreset = computed(() => getAiProviderPresetOption(aiEditProviderPresetId.value));
|
||||
const selectedAiPartnerPreset = computed(() => (isAiPartnerProviderPreset(selectedAiProviderPreset.value) ? selectedAiProviderPreset.value : null));
|
||||
|
||||
const aiTesting = ref(false);
|
||||
const aiTestResult = ref<"" | "success" | "error">("");
|
||||
@@ -3515,7 +3522,7 @@ const aiEditCliEnvRows = computed(() => {
|
||||
watch(aiIsCliProvider, (isCliProvider) => {
|
||||
if (isCliProvider) void ensureCliMcpStatus();
|
||||
});
|
||||
const aiRequiresApiKey = computed(() => AI_PROVIDER_PRESETS[aiEditProvider.value].requiresApiKey);
|
||||
const aiRequiresApiKey = computed(() => selectedAiProviderPreset.value.requiresApiKey);
|
||||
const aiUsesConfigurableAnthropicAuth = computed(() => aiEditProvider.value === "claude" || aiEditProvider.value === "anthropic-compatible" || (aiEditProvider.value === "custom" && aiAnthropicMessagesMode.value));
|
||||
const aiUsesCompatibleAnthropicApi = computed(() => aiEditProvider.value === "anthropic-compatible" || (aiEditProvider.value === "custom" && aiAnthropicMessagesMode.value));
|
||||
const aiSupportsAuthMethod = computed(() => aiUsesConfigurableAnthropicAuth.value);
|
||||
@@ -3707,18 +3714,20 @@ function syncAiEditState() {
|
||||
aiTestErrorCopied.value = false;
|
||||
}
|
||||
|
||||
function aiSelectProvider(provider: AiProvider) {
|
||||
function aiSelectProvider(presetId: string) {
|
||||
const preset = getAiProviderPresetOption(presetId);
|
||||
const provider = preset.provider;
|
||||
if (isWeb && CLI_AI_PROVIDERS.has(provider)) return;
|
||||
if (provider === aiEditProvider.value) return;
|
||||
if (presetId === aiEditProviderPresetId.value) return;
|
||||
|
||||
// Apply new provider's preset defaults to edit state
|
||||
const preset = AI_PROVIDER_PRESETS[provider];
|
||||
aiEditProviderPresetId.value = presetId;
|
||||
aiEditProvider.value = provider;
|
||||
aiEditApiKey.value = "";
|
||||
aiEditAuthMethod.value = preset.authMethod;
|
||||
aiEditEndpoint.value = preset.endpoint;
|
||||
aiEditModel.value = "";
|
||||
aiEditLegacyModels.value = [];
|
||||
aiEditModel.value = preset.group === "partner" ? preset.model : "";
|
||||
aiEditLegacyModels.value = preset.group === "partner" ? [...(preset.models ?? [])] : [];
|
||||
aiEditApiStyle.value = preset.apiStyle;
|
||||
aiEditCustomHeaderRows.value = [];
|
||||
aiEditEnableThinking.value = true;
|
||||
@@ -3747,6 +3756,7 @@ function aiEnterEditMode(configId?: string) {
|
||||
if (config) {
|
||||
aiEditConfigName.value = config.name;
|
||||
aiEditProvider.value = config.provider;
|
||||
aiEditProviderPresetId.value = getAiProviderPresetId(config.provider, config.endpoint);
|
||||
aiEditApiKey.value = config.apiKey;
|
||||
aiEditAuthMethod.value = config.authMethod;
|
||||
aiEditEndpoint.value = config.endpoint;
|
||||
@@ -3782,6 +3792,7 @@ function aiEnterEditMode(configId?: string) {
|
||||
} else {
|
||||
aiEditConfigName.value = "";
|
||||
aiEditProvider.value = "claude";
|
||||
aiEditProviderPresetId.value = "claude";
|
||||
aiEditApiKey.value = "";
|
||||
aiEditAuthMethod.value = AI_PROVIDER_PRESETS["claude"].authMethod;
|
||||
aiEditEndpoint.value = AI_PROVIDER_PRESETS["claude"].endpoint;
|
||||
@@ -3823,6 +3834,7 @@ async function applyPendingAiConfigDeepLinkDraft() {
|
||||
aiEnterEditMode();
|
||||
aiEditConfigName.value = draft.name;
|
||||
aiEditProvider.value = draft.provider;
|
||||
aiEditProviderPresetId.value = getAiProviderPresetId(draft.provider, draft.endpoint);
|
||||
aiEditApiKey.value = "";
|
||||
aiEditAuthMethod.value = draft.authMethod;
|
||||
aiEditEndpoint.value = draft.endpoint;
|
||||
@@ -7057,7 +7069,7 @@ onUnmounted(() => {
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="config in displayedAiConfigs" :key="config.id" class="flex items-center justify-between rounded-md border p-3" :class="{ 'border-primary bg-primary/5': config.isDefault }">
|
||||
<div class="flex items-center gap-3">
|
||||
<AiProviderLogo :provider="config.provider" :label="config.provider" :icon-slug="AI_PROVIDER_PRESETS[config.provider].iconSlug" />
|
||||
<AiProviderLogo :provider="config.provider" :label="getAiProviderPreset(config.provider, config.endpoint).label" :icon-slug="getAiProviderPreset(config.provider, config.endpoint).iconSlug" :icon-path="getAiProviderPreset(config.provider, config.endpoint).iconPath" />
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">{{ config.name }}</span>
|
||||
@@ -7066,7 +7078,7 @@ onUnmounted(() => {
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ aiProviderLabel(config.provider, t) }}
|
||||
{{ getAiProviderPreset(config.provider, config.endpoint).label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7309,28 +7321,51 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Provider Selection -->
|
||||
<div class="grid grid-cols-3 items-center gap-3">
|
||||
<div class="grid grid-cols-3 items-start gap-3">
|
||||
<Label class="text-right text-xs">{{ t("ai.provider") }}</Label>
|
||||
<Select :model-value="aiEditProvider" @update:model-value="(v: any) => aiSelectProvider(v)">
|
||||
<SelectTrigger class="col-span-2" inputClass="h-8 text-xs">
|
||||
<SelectValue>
|
||||
<span class="flex items-center gap-2">
|
||||
<AiProviderLogo :provider="selectedAiProviderPreset.provider" :label="aiProviderLabel(selectedAiProviderPreset.provider, t)" :icon-slug="selectedAiProviderPreset.iconSlug" />
|
||||
<span>{{ aiProviderLabel(selectedAiProviderPreset.provider, t) }}</span>
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="provider in aiProviderOptions" :key="provider.provider" :value="provider.provider">
|
||||
<span class="flex w-full items-center justify-between gap-4">
|
||||
<div class="col-span-2 space-y-2">
|
||||
<Select :model-value="aiEditProviderPresetId" @update:model-value="(v: any) => aiSelectProvider(String(v))">
|
||||
<SelectTrigger inputClass="h-8 text-xs">
|
||||
<SelectValue>
|
||||
<span class="flex items-center gap-2">
|
||||
<AiProviderLogo :provider="provider.provider" :label="aiProviderLabel(provider.provider, t)" :icon-slug="provider.iconSlug" />
|
||||
<span>{{ aiProviderLabel(provider.provider, t) }}</span>
|
||||
<AiProviderLogo :provider="selectedAiProviderPreset.provider" :label="selectedAiProviderPreset.label" :icon-slug="selectedAiProviderPreset.iconSlug" :icon-path="selectedAiProviderPreset.iconPath" />
|
||||
<span>{{ selectedAiProviderPreset.label }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent class="w-[32rem] max-w-[calc(100vw-2rem)]">
|
||||
<div class="grid grid-cols-1 gap-1 sm:grid-cols-2">
|
||||
<SelectGroup class="min-w-0">
|
||||
<SelectLabel>{{ t("ai.builtinProviders") }}</SelectLabel>
|
||||
<SelectItem v-for="provider in builtinAiProviderOptions" :key="provider.provider" :value="provider.provider">
|
||||
<span class="flex w-full min-w-0 items-center gap-2">
|
||||
<AiProviderLogo :provider="provider.provider" :label="provider.label" :icon-slug="provider.iconSlug" :icon-path="provider.iconPath" />
|
||||
<span class="truncate">{{ provider.label }}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup v-if="partnerAiProviderOptions.length" class="min-w-0 border-border/60 sm:border-l">
|
||||
<SelectLabel>{{ t("ai.partnerProviders") }}</SelectLabel>
|
||||
<SelectItem v-for="provider in partnerAiProviderOptions" :key="provider.id" :value="provider.id">
|
||||
<span class="flex w-full min-w-0 items-center gap-2">
|
||||
<AiProviderLogo :provider="provider.provider" :label="provider.label" :icon-slug="provider.iconSlug" :icon-path="provider.iconPath" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ provider.label }}</span>
|
||||
<Badge variant="outline" class="h-5 shrink-0 px-1.5 text-[10px] font-normal">{{ t("ai.jalapenoSponsored") }}</Badge>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div v-if="selectedAiPartnerPreset" class="flex items-center gap-3 rounded-md border border-primary/20 bg-primary/5 px-3 py-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[11px] leading-4 text-muted-foreground">{{ t(selectedAiPartnerPreset.descriptionKey) }}</p>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="icon" class="h-7 w-7 shrink-0" :title="t('ai.visitPartner')" :aria-label="t('ai.visitPartner')" @click="openExternalUrl(selectedAiPartnerPreset.websiteUrl)">
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CLI MCP Status -->
|
||||
@@ -7388,7 +7423,13 @@ onUnmounted(() => {
|
||||
<!-- API Key -->
|
||||
<div v-if="!aiIsCliProvider" class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">{{ aiCredentialLabel }}</Label>
|
||||
<PasswordInput v-model="aiEditApiKey" autocomplete="off" class="col-span-2" inputClass="h-8 text-xs" :placeholder="aiCredentialPlaceholder" />
|
||||
<div class="col-span-2 flex min-w-0 items-center gap-2">
|
||||
<PasswordInput v-model="aiEditApiKey" autocomplete="off" class="min-w-0 flex-1" inputClass="h-8 text-xs" :placeholder="aiCredentialPlaceholder" />
|
||||
<Button v-if="selectedAiPartnerPreset" type="button" variant="outline" size="sm" class="h-8 shrink-0 gap-1.5 px-3 text-xs" :title="t('ai.getApiKey')" :aria-label="t('ai.getApiKey')" @click="openExternalUrl(selectedAiPartnerPreset.apiKeyUrl)">
|
||||
{{ t("ai.getApiKey") }}
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Endpoint -->
|
||||
|
||||
@@ -9,22 +9,23 @@ const props = defineProps<{
|
||||
provider: AiProvider;
|
||||
label: string;
|
||||
iconSlug?: string;
|
||||
iconPath?: string;
|
||||
}>();
|
||||
|
||||
const failed = ref(false);
|
||||
const { isDark } = useTheme();
|
||||
|
||||
watch(
|
||||
() => props.iconSlug,
|
||||
() => {
|
||||
failed.value = false;
|
||||
},
|
||||
);
|
||||
watch([() => props.iconSlug, () => props.iconPath], () => {
|
||||
failed.value = false;
|
||||
});
|
||||
|
||||
const usesWhiteDarkIcon = computed(
|
||||
() => props.provider === "claude" || props.provider === "anthropic-compatible" || props.provider === "ollama" || props.provider === "openai" || props.provider === "openai-compatible" || props.provider === "opencode-cli" || props.provider === "cursor-cli" || props.provider === "grok-cli",
|
||||
() =>
|
||||
!props.iconPath &&
|
||||
(props.provider === "claude" || props.provider === "anthropic-compatible" || props.provider === "ollama" || props.provider === "openai" || props.provider === "openai-compatible" || props.provider === "opencode-cli" || props.provider === "cursor-cli" || props.provider === "grok-cli"),
|
||||
);
|
||||
const localIconUrl = computed(() => {
|
||||
if (props.iconPath) return webPath(props.iconPath);
|
||||
if (props.provider === "openai-compatible") return webPath("/icons/ai/openai.svg");
|
||||
return props.iconSlug ? webPath(`/icons/ai/${props.iconSlug}.svg`) : "";
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user