diff --git a/ui/web/src/i18n/locales/en/cli-credentials.json b/ui/web/src/i18n/locales/en/cli-credentials.json index 5a5176a9..e57af89c 100644 --- a/ui/web/src/i18n/locales/en/cli-credentials.json +++ b/ui/web/src/i18n/locales/en/cli-credentials.json @@ -33,7 +33,11 @@ "agentIdHint": "optional — leave blank for global", "commaSeparated": "comma-separated", "binaryNameRequired": "Binary name is required.", - "failedToSave": "Failed to save." + "failedToSave": "Failed to save.", + "addEnvVar": "Add Variable", + "noEnvVarsHint": "Click \"Add Variable\" to define environment variables for this CLI tool.", + "envKeyPlaceholder": "ENV_VAR_NAME", + "envValuePlaceholder": "value" }, "placeholders": { "binaryName": "e.g. gh", diff --git a/ui/web/src/i18n/locales/vi/cli-credentials.json b/ui/web/src/i18n/locales/vi/cli-credentials.json index 1cbf5462..d95c1103 100644 --- a/ui/web/src/i18n/locales/vi/cli-credentials.json +++ b/ui/web/src/i18n/locales/vi/cli-credentials.json @@ -33,7 +33,11 @@ "agentIdHint": "tùy chọn — để trống cho toàn cục", "commaSeparated": "phân cách bằng dấu phẩy", "binaryNameRequired": "Tên binary là bắt buộc.", - "failedToSave": "Không thể lưu." + "failedToSave": "Không thể lưu.", + "addEnvVar": "Thêm biến", + "noEnvVarsHint": "Nhấn \"Thêm biến\" để khai báo biến môi trường cho công cụ CLI này.", + "envKeyPlaceholder": "TÊN_BIẾN", + "envValuePlaceholder": "giá trị" }, "placeholders": { "binaryName": "vd: gh", diff --git a/ui/web/src/i18n/locales/zh/cli-credentials.json b/ui/web/src/i18n/locales/zh/cli-credentials.json index 0e7cdd2b..45983926 100644 --- a/ui/web/src/i18n/locales/zh/cli-credentials.json +++ b/ui/web/src/i18n/locales/zh/cli-credentials.json @@ -33,7 +33,11 @@ "agentIdHint": "可选 — 留空表示全局", "commaSeparated": "逗号分隔", "binaryNameRequired": "二进制名称为必填项。", - "failedToSave": "保存失败。" + "failedToSave": "保存失败。", + "addEnvVar": "添加变量", + "noEnvVarsHint": "点击\"添加变量\"为此 CLI 工具定义环境变量。", + "envKeyPlaceholder": "变量名", + "envValuePlaceholder": "值" }, "placeholders": { "binaryName": "例如 gh", diff --git a/ui/web/src/pages/cli-credentials/cli-credential-form-dialog.tsx b/ui/web/src/pages/cli-credentials/cli-credential-form-dialog.tsx index 8e39d431..c0f59ba5 100644 --- a/ui/web/src/pages/cli-credentials/cli-credential-form-dialog.tsx +++ b/ui/web/src/pages/cli-credentials/cli-credential-form-dialog.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, @@ -11,8 +11,14 @@ import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Plus, X } from "lucide-react"; import type { SecureCLIBinary, CLICredentialInput, CLIPreset } from "./hooks/use-cli-credentials"; +interface ManualEnvEntry { + key: string; + value: string; +} + interface Props { open: boolean; onOpenChange: (open: boolean) => void; @@ -38,19 +44,20 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset const [agentId, setAgentId] = useState(""); const [enabled, setEnabled] = useState(true); const [envValues, setEnvValues] = useState>({}); + const [manualEnvEntries, setManualEnvEntries] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const isEdit = !!credential; - // Build typed entry list to avoid noUncheckedIndexedAccess issues const presetEntries: Array<[string, CLIPreset]> = Object.entries(presets).filter( (e): e is [string, CLIPreset] => e[1] !== undefined, ); - // Current preset definition (for env var fields) const activePreset: CLIPreset | null = selectedPreset !== NONE_PRESET ? (presets[selectedPreset] ?? null) : null; + const isManualMode = selectedPreset === NONE_PRESET; + useEffect(() => { if (!open) return; setSelectedPreset(NONE_PRESET); @@ -64,6 +71,7 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset setAgentId(credential?.agent_id ?? ""); setEnabled(credential?.enabled ?? true); setEnvValues({}); + setManualEnvEntries([]); setError(""); }, [open, credential]); @@ -79,11 +87,36 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset setTimeout(p.timeout); setTips(p.tips); setEnvValues({}); + setManualEnvEntries([]); }; + const addManualEnvEntry = useCallback(() => { + setManualEnvEntries((prev) => [...prev, { key: "", value: "" }]); + }, []); + + const removeManualEnvEntry = useCallback((index: number) => { + setManualEnvEntries((prev) => prev.filter((_, i) => i !== index)); + }, []); + + const updateManualEnvEntry = useCallback((index: number, field: "key" | "value", val: string) => { + setManualEnvEntries((prev) => + prev.map((entry, i) => (i === index ? { ...entry, [field]: val } : entry)), + ); + }, []); + const splitCommaList = (v: string): string[] => v.split(",").map((s) => s.trim()).filter(Boolean); + const buildEnvPayload = (): Record => { + if (!isManualMode) return envValues; + const env: Record = {}; + for (const entry of manualEnvEntries) { + const k = entry.key.trim(); + if (k && entry.value) env[k] = entry.value; + } + return env; + }; + const handleSubmit = async () => { if (!binaryName.trim()) { setError(t("form.binaryNameRequired")); @@ -104,7 +137,8 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset enabled, }; if (selectedPreset !== NONE_PRESET) payload.preset = selectedPreset; - if (Object.keys(envValues).length > 0) payload.env = envValues; + const env = buildEnvPayload(); + if (Object.keys(env).length > 0) payload.env = env; await onSubmit(payload); onOpenChange(false); } catch (err) { @@ -179,6 +213,53 @@ export function CliCredentialFormDialog({ open, onOpenChange, credential, preset )} + {/* Manual env var entries (no preset selected) */} + {isManualMode && ( +
+
+

{t("form.envVars")}

+ +
+ {manualEnvEntries.length === 0 && ( +

{t("form.noEnvVarsHint")}

+ )} + {manualEnvEntries.map((entry, idx) => ( +
+
+ updateManualEnvEntry(idx, "key", e.target.value)} + className="text-base md:text-sm font-mono" + /> +
+
+ updateManualEnvEntry(idx, "value", e.target.value)} + className="text-base md:text-sm" + /> +
+ +
+ ))} +
+ )} +