Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lat.md/provider-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ function App(): React.JSX.Element {
const [splashStatus, setSplashStatus] = useState<string | undefined>(
undefined,
);
const [setupProfile, setSetupProfile] = useState<string | undefined>(
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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -223,6 +227,7 @@ function App(): React.JSX.Element {
return (
<Setup
onComplete={() => setScreen("main")}
profile={setupProfile}
verifyWarning={verifyWarning}
onReinstall={handleVerifyReinstall}
onDismissVerifyWarning={handleDismissVerifyWarning}
Expand Down
66 changes: 66 additions & 0 deletions src/renderer/src/screens/Setup/Setup.test.tsx
Original file line number Diff line number Diff line change
@@ -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 => (
<span data-testid={`brand-${provider}`} />
),
}));

vi.mock("../../components/VerifyWarningBanner", () => ({
default: (): React.JSX.Element => <div data-testid="verify-warning" />,
}));

function installHermesAPI(): {
setEnv: ReturnType<typeof vi.fn>;
setModelConfig: ReturnType<typeof vi.fn>;
} {
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(<Setup profile="work" onComplete={onComplete} />);

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();
});
});
8 changes: 6 additions & 2 deletions src/renderer/src/screens/Setup/Setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ 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;
}

function Setup({
onComplete,
profile,
verifyWarning,
onReinstall,
onDismissVerifyWarning,
Expand Down Expand Up @@ -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;
Expand All @@ -75,6 +78,7 @@ function Setup({
configProvider,
configModel,
configBaseUrl,
profile,
);

onComplete();
Expand Down
Loading