From 59b84017f3e5cfd1627a17dd7800313895f51bce Mon Sep 17 00:00:00 2001 From: hanlujie Date: Thu, 9 Jul 2026 11:36:32 +0800 Subject: [PATCH] Fix setup credentials for active profile --- lat.md/provider-setup.md | 2 + src/renderer/src/App.tsx | 5 ++ src/renderer/src/screens/Setup/Setup.test.tsx | 66 +++++++++++++++++++ src/renderer/src/screens/Setup/Setup.tsx | 8 ++- 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/screens/Setup/Setup.test.tsx diff --git a/lat.md/provider-setup.md b/lat.md/provider-setup.md index f76a8c1d4..f3e767cf9 100644 --- a/lat.md/provider-setup.md +++ b/lat.md/provider-setup.md @@ -4,6 +4,8 @@ The first-run screen where the user picks an AI provider and enters credentials The provider list is data-driven from `PROVIDERS.setup` in [[src/renderer/src/constants.ts]]. Each entry carries an `envKey`, `configProvider`, `baseUrl`, and `needsKey`; selecting a card drives which form fields show (API key, or the Local server/base-URL flow). +The first-run gate is profile-aware: `checkInstall` decides whether setup is needed for the active profile, so [[src/renderer/src/App.tsx#App]] passes that profile into [[src/renderer/src/screens/Setup/Setup.tsx#Setup]]. Setup writes both `.env` and `config.yaml` to the same profile, preventing a named active profile from losing the key to the default workspace. + ## Hermes One is the first-priority provider **Hermes One Inference** (`https://inference.hermesone.org/v1`) is Hermes One's own OpenAI-compatible gateway, listed **first** in `PROVIDERS.setup`, `PROVIDER_CARDS`, and the `SETTINGS_SECTIONS` "LLM Providers" items — so it leads the Add-provider picker. diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 41365baa4..a417c0fc0 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -33,6 +33,9 @@ function App(): React.JSX.Element { const [splashStatus, setSplashStatus] = useState( undefined, ); + const [setupProfile, setSetupProfile] = useState( + undefined, + ); const isMac = window.electron?.process?.platform === "darwin"; // Bumped on every runInstallCheck so a superseded run (e.g. the user hit // "Switch to local mode" while an SSH tunnel attempt was still in flight) @@ -72,6 +75,7 @@ function App(): React.JSX.Element { } else { setSplashStatus("Checking local install…"); const status = await window.hermesAPI.checkInstall(); + setSetupProfile(status.activeProfile || "default"); if (!status.installed) { next = "welcome"; } else if (!status.hasApiKey) { @@ -223,6 +227,7 @@ function App(): React.JSX.Element { return ( setScreen("main")} + profile={setupProfile} verifyWarning={verifyWarning} onReinstall={handleVerifyReinstall} onDismissVerifyWarning={handleDismissVerifyWarning} diff --git a/src/renderer/src/screens/Setup/Setup.test.tsx b/src/renderer/src/screens/Setup/Setup.test.tsx new file mode 100644 index 000000000..c8272138b --- /dev/null +++ b/src/renderer/src/screens/Setup/Setup.test.tsx @@ -0,0 +1,66 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import type React from "react"; +import { describe, expect, it, vi } from "vitest"; +import { DEFAULT_DASHSCOPE_BASE_URL } from "../../constants"; +import Setup from "./Setup"; + +vi.mock("../../components/useI18n", () => ({ + useI18n: () => ({ + t: (key: string): string => key, + }), +})); + +vi.mock("../../components/common/BrandLogo", () => ({ + default: ({ provider }: { provider: string }): React.JSX.Element => ( + + ), +})); + +vi.mock("../../components/VerifyWarningBanner", () => ({ + default: (): React.JSX.Element =>
, +})); + +function installHermesAPI(): { + setEnv: ReturnType; + setModelConfig: ReturnType; +} { + const api = { + setEnv: vi.fn().mockResolvedValue(true), + setModelConfig: vi.fn().mockResolvedValue(true), + openExternal: vi.fn(), + }; + Object.defineProperty(window, "hermesAPI", { + configurable: true, + value: api, + }); + return api; +} + +describe("Setup", () => { + it("saves DashScope credentials to the active setup profile", async () => { + const api = installHermesAPI(); + const onComplete = vi.fn(); + render(); + + fireEvent.click(screen.getByText("Alibaba DashScope")); + fireEvent.change(screen.getByPlaceholderText("sk-..."), { + target: { value: "sk-dashscope" }, + }); + fireEvent.click(screen.getByText("setup.continue")); + + await waitFor(() => { + expect(api.setEnv).toHaveBeenCalledWith( + "DASHSCOPE_API_KEY", + "sk-dashscope", + "work", + ); + }); + expect(api.setModelConfig).toHaveBeenCalledWith( + "alibaba", + "", + DEFAULT_DASHSCOPE_BASE_URL, + "work", + ); + expect(onComplete).toHaveBeenCalled(); + }); +}); diff --git a/src/renderer/src/screens/Setup/Setup.tsx b/src/renderer/src/screens/Setup/Setup.tsx index c39184d79..4f43d8e85 100644 --- a/src/renderer/src/screens/Setup/Setup.tsx +++ b/src/renderer/src/screens/Setup/Setup.tsx @@ -8,6 +8,8 @@ import { expectedEnvKeyForUrl } from "../../../../shared/url-key-map"; interface SetupProps { onComplete: () => void; + /** Active profile being configured; omitted means the default profile. */ + profile?: string; verifyWarning?: boolean; onReinstall?: () => void; onDismissVerifyWarning?: () => void; @@ -15,6 +17,7 @@ interface SetupProps { function Setup({ onComplete, + profile, verifyWarning, onReinstall, onDismissVerifyWarning, @@ -61,10 +64,10 @@ function Setup({ try { if (provider.needsKey && provider.envKey) { - await window.hermesAPI.setEnv(provider.envKey, apiKey.trim()); + await window.hermesAPI.setEnv(provider.envKey, apiKey.trim(), profile); } else if (isLocal && apiKey.trim()) { const envKey = resolveCustomEnvKey(baseUrl.trim()); - await window.hermesAPI.setEnv(envKey, apiKey.trim()); + await window.hermesAPI.setEnv(envKey, apiKey.trim(), profile); } const configProvider = isLocal ? "custom" : provider.configProvider; @@ -75,6 +78,7 @@ function Setup({ configProvider, configModel, configBaseUrl, + profile, ); onComplete();