feat(ai): add Codex CLI as a no-API-key AI provider for Desktop
* codex cli provider * fixes * mcp check * reasoning * simplify * minor fixes
This commit is contained in:
1 parent
93a6a65707
commit
4f5d09aaa8
22 files changed
+1401
-101
No files matched your search
@@ -405,7 +405,7 @@ function agentEventToStep(event: AgentEvent, index: number): AiAgentStepItem | u
|
||||
|
||||
if (event.type !== "tool_call_start" && event.type !== "tool_call_end") return undefined;
|
||||
|
||||
const isExecuteQuery = event.tool_name === "execute_query";
|
||||
const isExecuteQuery = event.tool_name === "execute_query" || event.tool_name === "dbx_execute_query";
|
||||
const labelKey = event.type === "tool_call_start" ? "ai.agentSteps.callingTool" : isExecuteQuery ? (event.is_error ? "ai.agentSteps.executeBlocked" : "ai.agentSteps.executeSafe") : event.is_error ? "ai.agentSteps.toolError" : "ai.agentSteps.toolDone";
|
||||
const tone = (event.type === "tool_call_start" ? "active" : event.is_error ? "danger" : "success") as AiAgentStepTone;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
type AiProvider,
|
||||
type AiApiStyle,
|
||||
type AiAuthMethod,
|
||||
type AiReasoningLevel,
|
||||
type EditorTheme,
|
||||
type DesktopIconTheme,
|
||||
type InterfaceLayout,
|
||||
@@ -1018,6 +1019,7 @@ watch(
|
||||
await refreshWebDavPasswordStatus();
|
||||
syncAiEditState();
|
||||
if (!isWeb && activeSettingsTab.value === "mcp") void refreshMcpStatus();
|
||||
if (!isWeb && activeSettingsTab.value === "ai" && aiIsCodexCli.value) void ensureCodexMcpStatus();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -1032,6 +1034,7 @@ watch(webdavRememberPassword, (val) => {
|
||||
|
||||
watch(activeSettingsTab, (tab) => {
|
||||
if (tab === "mcp" && !mcpStatus.value && !mcpStatusLoading.value) void refreshMcpStatus();
|
||||
if (tab === "ai" && aiIsCodexCli.value) void ensureCodexMcpStatus();
|
||||
if (tab === "appearance") {
|
||||
checkLayoutDescTruncation();
|
||||
checkIconThemeDescTruncation();
|
||||
@@ -1085,7 +1088,7 @@ async function changePassword() {
|
||||
}
|
||||
|
||||
// ---------- AI Settings ----------
|
||||
const aiProviderOptions = Object.values(AI_PROVIDER_PRESETS);
|
||||
const aiProviderOptions = computed(() => Object.values(AI_PROVIDER_PRESETS).filter((provider) => !isWeb || provider.provider !== "codex-cli"));
|
||||
const selectedAiProviderPreset = computed(() => AI_PROVIDER_PRESETS[aiEditProvider.value]);
|
||||
|
||||
const aiEditProvider = ref<AiProvider>(settingsStore.aiConfig.provider);
|
||||
@@ -1097,7 +1100,9 @@ const aiEditApiStyle = ref<AiApiStyle>(settingsStore.aiConfig.apiStyle || "compl
|
||||
const aiEditProxyEnabled = ref(!!settingsStore.aiConfig.proxyEnabled);
|
||||
const aiEditProxyUrl = ref(settingsStore.aiConfig.proxyUrl || "");
|
||||
const aiEditEnableThinking = ref(settingsStore.aiConfig.enableThinking ?? true);
|
||||
const aiEditReasoningLevel = ref<AiReasoningLevel>(settingsStore.aiConfig.reasoningLevel || "default");
|
||||
const aiEditContextWindow = ref<number | undefined>(settingsStore.aiConfig.contextWindow);
|
||||
const aiEditCodexCliPath = ref(settingsStore.aiConfig.codexCliPath || "");
|
||||
|
||||
const aiModelOptions = ref<AiModelInfo[]>([]);
|
||||
const aiModelLoading = ref(false);
|
||||
@@ -1106,11 +1111,23 @@ const aiModelLoadedSignature = ref("");
|
||||
let aiModelRequestToken = 0;
|
||||
|
||||
const aiCompletionsMode = computed(() => aiEditApiStyle.value === "completions");
|
||||
const aiReasoningLevelOptions: Array<{ value: AiReasoningLevel; labelKey: string }> = [
|
||||
{ value: "default", labelKey: "ai.reasoningLevelDefault" },
|
||||
{ value: "minimal", labelKey: "ai.reasoningLevelMinimal" },
|
||||
{ value: "low", labelKey: "ai.reasoningLevelLow" },
|
||||
{ value: "medium", labelKey: "ai.reasoningLevelMedium" },
|
||||
{ value: "high", labelKey: "ai.reasoningLevelHigh" },
|
||||
];
|
||||
|
||||
const aiTesting = ref(false);
|
||||
const aiTestResult = ref<"" | "success" | "error">("");
|
||||
const aiTestError = ref("");
|
||||
const aiTestLatency = ref<number | null>(null);
|
||||
const aiTestErrorCopied = ref(false);
|
||||
const aiIsCodexCli = computed(() => aiEditProvider.value === "codex-cli");
|
||||
watch(aiIsCodexCli, (isCodex) => {
|
||||
if (isCodex) void ensureCodexMcpStatus();
|
||||
});
|
||||
const aiRequiresApiKey = computed(() => AI_PROVIDER_PRESETS[aiEditProvider.value].requiresApiKey);
|
||||
const aiSupportsAuthMethod = computed(() => aiEditProvider.value === "claude");
|
||||
const aiCredentialLabel = computed(() => (aiSupportsAuthMethod.value && aiEditAuthMethod.value === "bearer" ? "Auth Token" : "API Key"));
|
||||
@@ -1131,9 +1148,19 @@ const aiEndpointHint = computed(() => {
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const aiSupportsApiStyle = computed(() => aiEditProvider.value === "openai" || aiEditProvider.value === "openai-compatible" || aiEditProvider.value === "custom");
|
||||
const aiSupportsApiStyle = computed(() => !aiIsCodexCli.value && (aiEditProvider.value === "openai" || aiEditProvider.value === "openai-compatible" || aiEditProvider.value === "custom"));
|
||||
const aiCodexMcpNeedsInstall = computed(() => aiIsCodexCli.value && (!mcpStatus.value || !mcpStatus.value.installed));
|
||||
const aiCodexMcpCanInstall = computed(() => {
|
||||
const status = mcpStatus.value;
|
||||
return !mcpInstalling.value && !!status?.npm_available && (!status.installed || status.update_available);
|
||||
});
|
||||
const aiCodexMcpActionLabel = computed(() => {
|
||||
if (!mcpStatus.value?.installed) return t("settings.mcpInstallButton");
|
||||
if (mcpStatus.value.update_available) return t("settings.mcpUpdateButton");
|
||||
return t("settings.mcpUpToDate");
|
||||
});
|
||||
const aiModelListSupported = computed(() => aiEditProvider.value !== "gemini");
|
||||
const aiCanListModels = computed(() => aiModelListSupported.value && !!aiEditEndpoint.value.trim() && (!aiRequiresApiKey.value || !!aiEditApiKey.value.trim()));
|
||||
const aiCanListModels = computed(() => aiModelListSupported.value && (aiIsCodexCli.value || !!aiEditEndpoint.value.trim()) && (!aiRequiresApiKey.value || !!aiEditApiKey.value.trim()));
|
||||
const aiModelOptionIds = computed(() => aiModelOptions.value.map((model) => model.id));
|
||||
const aiModelEmptyText = computed(() => {
|
||||
if (aiModelError.value) return aiModelError.value;
|
||||
@@ -1157,6 +1184,7 @@ function aiModelConfigSignature() {
|
||||
authMethod: aiEditAuthMethod.value,
|
||||
proxyEnabled: aiEditProxyEnabled.value,
|
||||
proxyUrl: aiEditProxyUrl.value.trim(),
|
||||
codexCliPath: aiEditCodexCliPath.value.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1171,7 +1199,9 @@ function currentAiEditConfig() {
|
||||
proxyEnabled: aiEditProxyEnabled.value,
|
||||
proxyUrl: aiEditProxyUrl.value,
|
||||
enableThinking: aiEditEnableThinking.value,
|
||||
reasoningLevel: aiEditReasoningLevel.value,
|
||||
contextWindow: aiEditContextWindow.value || undefined,
|
||||
codexCliPath: aiEditCodexCliPath.value.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1197,7 +1227,7 @@ async function aiRefreshModels() {
|
||||
aiModelError.value = t("ai.modelListUnsupported");
|
||||
return;
|
||||
}
|
||||
if (!aiEditEndpoint.value.trim()) {
|
||||
if (!aiIsCodexCli.value && !aiEditEndpoint.value.trim()) {
|
||||
aiModelError.value = t("ai.modelListEndpointRequired");
|
||||
return;
|
||||
}
|
||||
@@ -1236,33 +1266,42 @@ function aiSelectModel(modelId: string) {
|
||||
}
|
||||
|
||||
function syncAiEditState() {
|
||||
aiEditProvider.value = settingsStore.aiConfig.provider;
|
||||
const provider = isWeb && settingsStore.aiConfig.provider === "codex-cli" ? "claude" : settingsStore.aiConfig.provider;
|
||||
aiEditProvider.value = provider;
|
||||
aiEditApiKey.value = settingsStore.aiConfig.apiKey;
|
||||
aiEditAuthMethod.value = settingsStore.aiConfig.authMethod || AI_PROVIDER_PRESETS[settingsStore.aiConfig.provider].authMethod;
|
||||
aiEditEndpoint.value = settingsStore.aiConfig.endpoint;
|
||||
aiEditModel.value = settingsStore.aiConfig.model;
|
||||
aiEditApiStyle.value = settingsStore.aiConfig.apiStyle || "completions";
|
||||
aiEditAuthMethod.value = settingsStore.aiConfig.authMethod || AI_PROVIDER_PRESETS[provider].authMethod;
|
||||
aiEditEndpoint.value = provider === settingsStore.aiConfig.provider ? settingsStore.aiConfig.endpoint : AI_PROVIDER_PRESETS[provider].endpoint;
|
||||
aiEditModel.value = provider === settingsStore.aiConfig.provider ? settingsStore.aiConfig.model : AI_PROVIDER_PRESETS[provider].model;
|
||||
aiEditApiStyle.value = provider === settingsStore.aiConfig.provider ? settingsStore.aiConfig.apiStyle || "completions" : AI_PROVIDER_PRESETS[provider].apiStyle;
|
||||
aiEditProxyEnabled.value = !!settingsStore.aiConfig.proxyEnabled;
|
||||
aiEditProxyUrl.value = settingsStore.aiConfig.proxyUrl || "";
|
||||
aiEditEnableThinking.value = settingsStore.aiConfig.enableThinking ?? true;
|
||||
aiEditReasoningLevel.value = settingsStore.aiConfig.reasoningLevel || "default";
|
||||
aiEditContextWindow.value = settingsStore.aiConfig.contextWindow;
|
||||
aiEditCodexCliPath.value = settingsStore.aiConfig.codexCliPath || "";
|
||||
aiTestResult.value = "";
|
||||
aiTestError.value = "";
|
||||
aiTestLatency.value = null;
|
||||
aiTestErrorCopied.value = false;
|
||||
clearAiModelOptions();
|
||||
}
|
||||
|
||||
function aiSelectProvider(provider: AiProvider) {
|
||||
if (isWeb && provider === "codex-cli") return;
|
||||
aiEditProvider.value = provider;
|
||||
aiEditEndpoint.value = AI_PROVIDER_PRESETS[provider].endpoint;
|
||||
aiEditModel.value = AI_PROVIDER_PRESETS[provider].model;
|
||||
aiEditApiStyle.value = AI_PROVIDER_PRESETS[provider].apiStyle;
|
||||
aiEditAuthMethod.value = AI_PROVIDER_PRESETS[provider].authMethod;
|
||||
aiEditReasoningLevel.value = "default";
|
||||
if (!AI_PROVIDER_PRESETS[provider].requiresApiKey) aiEditApiKey.value = "";
|
||||
aiEditCodexCliPath.value = "";
|
||||
aiTestResult.value = "";
|
||||
aiTestError.value = "";
|
||||
aiTestLatency.value = null;
|
||||
aiTestErrorCopied.value = false;
|
||||
clearAiModelOptions();
|
||||
if (provider === "codex-cli") void ensureCodexMcpStatus();
|
||||
}
|
||||
|
||||
function aiHasChanges(): boolean {
|
||||
@@ -1276,7 +1315,9 @@ function aiHasChanges(): boolean {
|
||||
aiEditProxyEnabled.value !== !!settingsStore.aiConfig.proxyEnabled ||
|
||||
aiEditProxyUrl.value !== (settingsStore.aiConfig.proxyUrl || "") ||
|
||||
aiEditEnableThinking.value !== (settingsStore.aiConfig.enableThinking ?? true) ||
|
||||
aiEditContextWindow.value !== settingsStore.aiConfig.contextWindow
|
||||
aiEditReasoningLevel.value !== (settingsStore.aiConfig.reasoningLevel || "default") ||
|
||||
aiEditContextWindow.value !== settingsStore.aiConfig.contextWindow ||
|
||||
aiEditCodexCliPath.value !== (settingsStore.aiConfig.codexCliPath || "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1285,11 +1326,12 @@ function aiApplySettings() {
|
||||
}
|
||||
|
||||
async function aiTestConn() {
|
||||
if ((aiRequiresApiKey.value && !aiEditApiKey.value.trim()) || !aiEditEndpoint.value.trim() || !aiEditModel.value.trim()) return;
|
||||
if ((aiRequiresApiKey.value && !aiEditApiKey.value.trim()) || (!aiIsCodexCli.value && !aiEditEndpoint.value.trim()) || (!aiIsCodexCli.value && !aiEditModel.value.trim())) return;
|
||||
aiTesting.value = true;
|
||||
aiTestResult.value = "";
|
||||
aiTestError.value = "";
|
||||
aiTestLatency.value = null;
|
||||
aiTestErrorCopied.value = false;
|
||||
try {
|
||||
const result = await aiTestConnection(currentAiEditConfig());
|
||||
aiTestResult.value = "success";
|
||||
@@ -1302,6 +1344,20 @@ async function aiTestConn() {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyAiTestError() {
|
||||
if (!aiTestError.value) return;
|
||||
await copyToClipboard(aiTestError.value);
|
||||
aiTestErrorCopied.value = true;
|
||||
window.setTimeout(() => {
|
||||
aiTestErrorCopied.value = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
async function ensureCodexMcpStatus() {
|
||||
if (isWeb || activeSettingsTab.value !== "ai" || !aiIsCodexCli.value || mcpStatus.value || mcpStatusLoading.value) return;
|
||||
await refreshMcpStatus();
|
||||
}
|
||||
|
||||
// ---------- CodeMirror preview ----------
|
||||
const previewRef = ref<HTMLDivElement>();
|
||||
const previewView = shallowRef<EditorViewType | null>(null);
|
||||
@@ -2358,7 +2414,40 @@ watch(
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div v-if="aiSupportsAuthMethod" class="grid grid-cols-3 items-center gap-3">
|
||||
<div v-if="aiIsCodexCli && !isWeb" class="rounded-md border px-3 py-2.5 text-xs" :class="aiCodexMcpNeedsInstall ? 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300' : 'border-green-500/30 bg-green-500/10 text-green-700 dark:text-green-300'">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex min-w-0 items-center gap-2 font-medium">
|
||||
<Loader2 v-if="mcpStatusLoading" class="h-3.5 w-3.5 shrink-0 animate-spin" />
|
||||
<AlertTriangle v-else-if="aiCodexMcpNeedsInstall || mcpStatus?.error || mcpStatusError" class="h-3.5 w-3.5 shrink-0" />
|
||||
<CheckCircle2 v-else class="h-3.5 w-3.5 shrink-0" />
|
||||
<span>{{ t("ai.codexMcpRequiredTitle") }}</span>
|
||||
<Badge variant="outline" class="h-5 shrink-0 rounded-md border-current/30 px-1.5 text-[11px] font-normal">
|
||||
{{ mcpStatusLabel }}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="leading-relaxed">
|
||||
{{ t("ai.codexMcpRequiredDescription") }}
|
||||
</p>
|
||||
<p v-if="mcpStatus?.error || mcpStatusError" class="select-text leading-relaxed">
|
||||
{{ mcpStatusError || mcpStatus?.error }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Button type="button" size="sm" variant="outline" class="h-7 bg-background/80 px-2 text-xs" :disabled="mcpStatusLoading" @click="refreshMcpStatus">
|
||||
<Loader2 v-if="mcpStatusLoading" class="mr-1 h-3 w-3 animate-spin" />
|
||||
<RefreshCw v-else class="mr-1 h-3 w-3" />
|
||||
{{ t("settings.mcpRefresh") }}
|
||||
</Button>
|
||||
<Button v-if="aiCodexMcpNeedsInstall || mcpStatus?.update_available" type="button" size="sm" class="h-7 px-2 text-xs" :disabled="!aiCodexMcpCanInstall" @click="installMcp">
|
||||
<Loader2 v-if="mcpInstalling" class="mr-1 h-3 w-3 animate-spin" />
|
||||
{{ mcpInstalling ? t("settings.mcpInstalling") : aiCodexMcpActionLabel }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!aiIsCodexCli && aiSupportsAuthMethod" class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">Authentication</Label>
|
||||
<Select v-model="aiEditAuthMethod">
|
||||
<SelectTrigger class="col-span-2" inputClass="h-8 text-xs">
|
||||
@@ -2371,12 +2460,12 @@ watch(
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 items-center gap-3">
|
||||
<div v-if="!aiIsCodexCli" 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>
|
||||
|
||||
<div class="grid grid-cols-3 items-start gap-3">
|
||||
<div v-if="!aiIsCodexCli" class="grid grid-cols-3 items-start gap-3">
|
||||
<Label class="pt-2 text-right text-xs">Endpoint</Label>
|
||||
<div class="col-span-2 space-y-1.5">
|
||||
<Input v-model="aiEditEndpoint" :placeholder="aiEndpointPlaceholder" autocomplete="off" class="h-8 text-xs" />
|
||||
@@ -2384,6 +2473,14 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="aiIsCodexCli" class="grid grid-cols-3 items-start gap-3">
|
||||
<Label class="pt-2 text-right text-xs">{{ t("ai.codexCliPath") }}</Label>
|
||||
<div class="col-span-2 space-y-1.5">
|
||||
<Input v-model="aiEditCodexCliPath" autocomplete="off" class="h-8 text-xs" placeholder="codex" />
|
||||
<p class="text-[11px] text-muted-foreground">{{ t("ai.codexCliPathHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 items-start gap-3">
|
||||
<Label class="pt-2 text-right text-xs">{{ t("ai.model") }}</Label>
|
||||
<div class="col-span-2 space-y-1.5">
|
||||
@@ -2425,6 +2522,23 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="aiIsCodexCli" class="grid grid-cols-3 items-start gap-3">
|
||||
<Label class="pt-2 text-right text-xs">{{ t("ai.reasoningLevel") }}</Label>
|
||||
<div class="col-span-2 space-y-1.5">
|
||||
<Select v-model="aiEditReasoningLevel">
|
||||
<SelectTrigger inputClass="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="option in aiReasoningLevelOptions" :key="option.value" :value="option.value">
|
||||
{{ t(option.labelKey) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-[11px] text-muted-foreground">{{ t("ai.reasoningLevelHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="aiSupportsApiStyle" class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">API</Label>
|
||||
<div class="col-span-2 flex gap-2">
|
||||
@@ -2433,7 +2547,7 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 items-center gap-3">
|
||||
<div v-if="!aiIsCodexCli" class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">{{ t("ai.enableThinking") }}</Label>
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
<label class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
@@ -2451,7 +2565,7 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 items-start gap-3">
|
||||
<div v-if="!aiIsCodexCli" class="grid grid-cols-3 items-start gap-3">
|
||||
<Label class="text-right text-xs">{{ t("ai.contextWindow") }}</Label>
|
||||
<div class="col-span-2">
|
||||
<Input v-model.number="aiEditContextWindow" type="number" min="1000" step="1000" class="h-8 text-xs" :placeholder="t('ai.contextWindowAuto')" />
|
||||
@@ -2459,7 +2573,7 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 items-center gap-3">
|
||||
<div v-if="!aiIsCodexCli" class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">{{ t("ai.proxy") }}</Label>
|
||||
<label class="col-span-2 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<input v-model="aiEditProxyEnabled" type="checkbox" class="h-4 w-4 shrink-0 accent-primary" />
|
||||
@@ -2467,7 +2581,7 @@ watch(
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 items-center gap-3">
|
||||
<div v-if="!aiIsCodexCli" class="grid grid-cols-3 items-center gap-3">
|
||||
<Label class="text-right text-xs">{{ t("ai.proxyUrl") }}</Label>
|
||||
<Input v-model="aiEditProxyUrl" autocomplete="off" class="col-span-2" inputClass="h-8 text-xs" placeholder="socks5://127.0.0.1:7890" :disabled="!aiEditProxyEnabled" />
|
||||
</div>
|
||||
@@ -2737,7 +2851,7 @@ watch(
|
||||
|
||||
<DialogFooter v-else-if="activeSettingsTab === 'ai'" class="mx-0 mb-0 shrink-0 rounded-none border-t border-border/60 bg-transparent px-0 pb-0 pt-3 gap-3 sm:gap-3">
|
||||
<div class="flex flex-1 items-center gap-2">
|
||||
<Button size="sm" variant="outline" :disabled="aiTesting || (aiRequiresApiKey && !aiEditApiKey?.trim()) || !aiEditEndpoint?.trim() || !aiEditModel?.trim()" @click="aiTestConn">
|
||||
<Button size="sm" variant="outline" :disabled="aiTesting || (aiRequiresApiKey && !aiEditApiKey?.trim()) || (!aiIsCodexCli && !aiEditEndpoint?.trim()) || (!aiIsCodexCli && !aiEditModel?.trim())" @click="aiTestConn">
|
||||
<Loader2 v-if="aiTesting" class="h-3 w-3 animate-spin mr-1" />
|
||||
{{ t("connection.test") }}
|
||||
</Button>
|
||||
@@ -2745,8 +2859,20 @@ watch(
|
||||
<span>{{ t("connection.testSuccess") }}</span>
|
||||
<span v-if="aiTestLatency != null" class="text-green-500/70">{{ aiTestLatency }}ms</span>
|
||||
</span>
|
||||
<span v-else-if="aiTestResult === 'error'" class="text-xs text-destructive truncate max-w-[200px]" :title="aiTestError">
|
||||
{{ aiTestError }}
|
||||
<span v-else-if="aiTestResult === 'error'" class="min-w-0 max-w-[360px] flex items-center gap-1.5 text-xs text-destructive">
|
||||
<span class="select-text truncate" :title="aiTestError">{{ aiTestError }}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6 shrink-0 text-destructive/80 hover:text-destructive"
|
||||
:title="aiTestErrorCopied ? t('ai.copied') : t('ai.copyTestResult')"
|
||||
:aria-label="aiTestErrorCopied ? t('ai.copied') : t('ai.copyTestResult')"
|
||||
@click="copyAiTestError"
|
||||
>
|
||||
<CheckCircle2 v-if="aiTestErrorCopied" class="h-3.5 w-3.5" />
|
||||
<Copy v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="outline" @click="emit('update:open', false)">{{ t("common.close") }}</Button>
|
||||
|
||||
Reference in new issue
Block a user