From dd67b25df1cba0b1f108e33340a888c1e5b15225 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 26 Apr 2026 14:26:34 -0300 Subject: [PATCH] fix(cli-tools): preserve opencode config and raw key copy (#1626) - Use jsonc-parser to update only provider.omniroute in opencode.json, preserving MCP servers, comments, and other provider entries - Add /api/cli-tools/keys endpoint returning raw keys for CLI tools UI (session-auth protected via requireCliToolsAuth) - Fix OpenCode guide step 3 to use ICU-style {baseUrl} placeholder instead of broken {{baseUrl}} across all 40+ locales - Restore valid OpenCode light/dark SVG logos (were broken HTML downloads) - Add customCliTab translation key to en.json - Add modelLabels support for human-readable model names in config - Fix 9 additional locale files (bn, fa, gu, in, mr, sw, ta, te, ur) that were added after the original PR and still had double-braces Co-authored-by: JasonLandbridge --- package-lock.json | 7 + package.json | 1 + public/providers/opencode-dark.svg | 1 + public/providers/opencode-light.svg | 1 + .../cli-tools/CLIToolsPageClient.tsx | 2 +- .../cli-tools/components/DefaultToolCard.tsx | 122 ++++++++++++------ .../guide-settings/[toolId]/route.ts | 22 ++-- src/app/api/cli-tools/keys/route.ts | 23 ++++ src/i18n/messages/ar.json | 2 +- src/i18n/messages/bg.json | 2 +- src/i18n/messages/bn.json | 2 +- src/i18n/messages/cs.json | 2 +- src/i18n/messages/da.json | 2 +- src/i18n/messages/de.json | 2 +- src/i18n/messages/en.json | 3 +- src/i18n/messages/es.json | 2 +- src/i18n/messages/fa.json | 2 +- src/i18n/messages/fi.json | 2 +- src/i18n/messages/fr.json | 2 +- src/i18n/messages/gu.json | 2 +- src/i18n/messages/he.json | 2 +- src/i18n/messages/hi.json | 2 +- src/i18n/messages/hu.json | 2 +- src/i18n/messages/id.json | 2 +- src/i18n/messages/in.json | 2 +- src/i18n/messages/it.json | 2 +- src/i18n/messages/ja.json | 2 +- src/i18n/messages/ko.json | 2 +- src/i18n/messages/mr.json | 2 +- src/i18n/messages/ms.json | 2 +- src/i18n/messages/nl.json | 2 +- src/i18n/messages/no.json | 2 +- src/i18n/messages/phi.json | 2 +- src/i18n/messages/pl.json | 2 +- src/i18n/messages/pt-BR.json | 2 +- src/i18n/messages/pt.json | 2 +- src/i18n/messages/ro.json | 2 +- src/i18n/messages/ru.json | 4 +- src/i18n/messages/sk.json | 2 +- src/i18n/messages/sv.json | 2 +- src/i18n/messages/sw.json | 2 +- src/i18n/messages/ta.json | 2 +- src/i18n/messages/te.json | 2 +- src/i18n/messages/th.json | 2 +- src/i18n/messages/tr.json | 2 +- src/i18n/messages/uk-UA.json | 2 +- src/i18n/messages/ur.json | 2 +- src/i18n/messages/vi.json | 2 +- src/i18n/messages/zh-CN.json | 2 +- src/shared/constants/cliTools.ts | 3 +- src/shared/services/opencodeConfig.ts | 58 ++++++++- src/shared/validation/schemas.ts | 1 + tests/unit/cli-tools-i18n.test.ts | 34 +++++ tests/unit/cli-tools-keys-route.test.ts | 78 +++++++++++ tests/unit/guide-settings-route.test.ts | 108 +++++++++++++++- ...t40-opencode-cli-tools-integration.test.ts | 74 +++++++++++ 56 files changed, 522 insertions(+), 98 deletions(-) create mode 100644 public/providers/opencode-dark.svg create mode 100644 public/providers/opencode-light.svg create mode 100644 src/app/api/cli-tools/keys/route.ts create mode 100644 tests/unit/cli-tools-i18n.test.ts create mode 100644 tests/unit/cli-tools-keys-route.test.ts diff --git a/package-lock.json b/package-lock.json index 2187faf501..b6cae3091c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "https-proxy-agent": "^9.0.0", "jose": "^6.1.3", "js-yaml": "^4.1.0", + "jsonc-parser": "^3.3.1", "lowdb": "^7.0.1", "monaco-editor": "^0.55.1", "next": "^16.2.3", @@ -9164,6 +9165,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", diff --git a/package.json b/package.json index 68a5a192af..ac444ef0ba 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "https-proxy-agent": "^9.0.0", "jose": "^6.1.3", "js-yaml": "^4.1.0", + "jsonc-parser": "^3.3.1", "lowdb": "^7.0.1", "monaco-editor": "^0.55.1", "next": "^16.2.3", diff --git a/public/providers/opencode-dark.svg b/public/providers/opencode-dark.svg new file mode 100644 index 0000000000..b79c7332e2 --- /dev/null +++ b/public/providers/opencode-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/providers/opencode-light.svg b/public/providers/opencode-light.svg new file mode 100644 index 0000000000..b79140a507 --- /dev/null +++ b/public/providers/opencode-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx index 6eadda323e..2b2c8d27c3 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx @@ -90,7 +90,7 @@ export default function CLIToolsPageClient({ machineId: _machineId }) { const fetchApiKeys = async () => { try { - const res = await fetch("/api/keys"); + const res = await fetch("/api/cli-tools/keys"); if (res.ok) { const data = await res.json(); setApiKeys(data.keys || []); diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.tsx index 2fa8f408cc..89e976fa65 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.tsx @@ -6,6 +6,7 @@ import Image from "next/image"; import { useTranslations } from "next-intl"; import { copyToClipboard } from "@/shared/utils/clipboard"; import { buildOpenCodeConfigDocument } from "@/shared/services/opencodeConfig"; +import { useTheme } from "@/shared/hooks/useTheme"; export default function DefaultToolCard({ toolId, @@ -37,6 +38,7 @@ export default function DefaultToolCard({ const [message, setMessage] = useState(null); const [saving, setSaving] = useState(false); const runtimeFetchStartedRef = useRef(false); + const { isDark } = useTheme(); // (#523) Initialize state with key *id* instead of masked key string const [selectedApiKeyId, setSelectedApiKeyId] = useState(() => @@ -44,6 +46,54 @@ export default function DefaultToolCard({ ); const isMultiModelTool = tool.modelSelectionMode === "multiple"; const usesOpenCodePreview = tool.previewConfigMode === "opencode"; + const selectedKeyObj = apiKeys?.find((k) => k.id === selectedApiKeyId); + + const resolveApiKeyValue = useCallback( + () => selectedKeyObj?.rawKey || (!cloudEnabled ? "sk_omniroute" : t("yourApiKeyPlaceholder")), + [cloudEnabled, selectedKeyObj?.rawKey, t] + ); + + const getSelectedModelEntries = useCallback(() => { + const selectedValues = isMultiModelTool + ? modelValues.length > 0 + ? modelValues + : modelValue + ? [modelValue] + : [] + : modelValue + ? [modelValue] + : []; + + const availableModels = Array.isArray(activeProviders) + ? activeProviders.flatMap((provider) => provider?.models || []) + : []; + const modelMap = new Map( + availableModels.filter((model) => model?.value).map((model) => [model.value, model]) + ); + + return selectedValues.map((value) => { + const matched = modelMap.get(value); + return { + value, + label: matched?.name || matched?.label || value, + }; + }); + }, [activeProviders, isMultiModelTool, modelValue, modelValues]); + + const getSelectedModelLabels = useCallback( + () => getSelectedModelEntries().map((entry) => entry.label), + [getSelectedModelEntries] + ); + + const getSelectedModelLabelMap = useCallback( + () => Object.fromEntries(getSelectedModelEntries().map((entry) => [entry.value, entry.label])), + [getSelectedModelEntries] + ); + + const normalizedBaseUrl = baseUrl || "http://localhost:20128"; + const baseUrlWithV1 = normalizedBaseUrl.endsWith("/v1") + ? normalizedBaseUrl + : `${normalizedBaseUrl}/v1`; // Persist and restore model selection per tool via localStorage useEffect(() => { @@ -75,7 +125,9 @@ export default function DefaultToolCard({ const prefix = savedKey.slice(0, 8); const suffix = savedKey.slice(-4); const matchedKey = apiKeys.find( - (k) => k.key && k.key.startsWith(prefix) && k.key.endsWith(suffix) + (k) => + (k.rawKey && k.rawKey.startsWith(prefix) && k.rawKey.endsWith(suffix)) || + (k.key && k.key.startsWith(prefix) && k.key.endsWith(suffix)) ); if (matchedKey) setSelectedApiKeyId(matchedKey.id); } @@ -134,23 +186,14 @@ export default function DefaultToolCard({ const replaceVars = useCallback( (text) => { - // (#523) Look up the key object by id to get the masked display value. - const selectedKeyObj = apiKeys?.find((k) => k.id === selectedApiKeyId); - let keyToUse = - selectedKeyObj?.key || (!cloudEnabled ? "sk_omniroute" : t("yourApiKeyPlaceholder")); - if (keyToUse.includes("***")) keyToUse = ""; - - const normalizedBaseUrl = baseUrl || "http://localhost:20128"; - const baseUrlWithV1 = normalizedBaseUrl.endsWith("/v1") - ? normalizedBaseUrl - : `${normalizedBaseUrl}/v1`; + const keyToUse = resolveApiKeyValue(); return text .replace(/\{\{baseUrl\}\}/g, baseUrlWithV1) .replace(/\{\{apiKey\}\}/g, keyToUse) - .replace(/\{\{model\}\}/g, modelValue || t("modelPlaceholder")); + .replace(/\{\{model\}\}/g, getSelectedModelLabels()[0] || t("modelPlaceholder")); }, - [apiKeys, baseUrl, cloudEnabled, modelValue, selectedApiKeyId, t] + [baseUrl, getSelectedModelLabels, resolveApiKeyValue, t] ); const handleCopy = async (text, field) => { @@ -168,33 +211,24 @@ export default function DefaultToolCard({ if (!tool.codeBlock?.code) return ""; if (!usesOpenCodePreview) return replaceVars(tool.codeBlock.code); - const selectedKeyObj = apiKeys?.find((k) => k.id === selectedApiKeyId); - let keyToUse = - selectedKeyObj?.key || (!cloudEnabled ? "sk_omniroute" : t("yourApiKeyPlaceholder")); - if (keyToUse.includes("***")) keyToUse = ""; - const normalizedBaseUrl = baseUrl || "http://localhost:20128"; - const baseUrlWithV1 = normalizedBaseUrl.endsWith("/v1") - ? normalizedBaseUrl - : `${normalizedBaseUrl}/v1`; - + const keyToUse = resolveApiKeyValue(); return JSON.stringify( buildOpenCodeConfigDocument({ baseUrl: baseUrlWithV1, apiKey: keyToUse, models: getSelectedModels(), model: getSelectedModels()[0], + modelLabels: getSelectedModelLabelMap(), }), null, 2 ); }, [ - apiKeys, baseUrl, - cloudEnabled, getSelectedModels, + getSelectedModelLabelMap, replaceVars, - selectedApiKeyId, - t, + resolveApiKeyValue, tool.codeBlock?.code, usesOpenCodePreview, ]); @@ -229,11 +263,6 @@ export default function DefaultToolCard({ // (#523) Prefer keyId lookup so the backend writes the real key to disk. const selectedKeyId = selectedApiKeyId?.trim() || null; - const normalizedBaseUrl = baseUrl || "http://localhost:20128"; - const baseUrlWithV1 = normalizedBaseUrl.endsWith("/v1") - ? normalizedBaseUrl - : `${normalizedBaseUrl}/v1`; - const res = await fetch(`/api/cli-tools/guide-settings/${toolId}`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -243,6 +272,7 @@ export default function DefaultToolCard({ keyId: selectedKeyId, model: modelValue, models: isMultiModelTool ? getSelectedModels() : undefined, + modelLabels: getSelectedModelLabelMap(), }), }); const data = await res.json(); @@ -284,9 +314,7 @@ export default function DefaultToolCard({