diff --git a/frontend/workspace/src/components/settings/Credentials.tsx b/frontend/workspace/src/components/settings/Credentials.tsx index 4baecb4..6bb3bf2 100644 --- a/frontend/workspace/src/components/settings/Credentials.tsx +++ b/frontend/workspace/src/components/settings/Credentials.tsx @@ -3,6 +3,7 @@ // secret is write-only: values are never returned after saving. import { type Component, type JSX, For, Show, createMemo, createSignal, onMount } from "solid-js" import { Button } from "@synsci/ui/button" +import type { Provider } from "@synsci/sdk/v2/client" import { useGlobalSDK } from "@/context/global-sdk" import { usePlatform } from "@/context/platform" import { useProviders } from "@/hooks/use-providers" @@ -34,6 +35,27 @@ const PROVIDER_LABEL: Record = { } const BYOK_PROVIDERS = ["anthropic", "openai", "google", "openrouter", "groq", "mistral", "xai", "deepseek"] as const +// Where a connected provider's credential actually lives. Only "api" keys sit in +// the local auth store — the others reappear after a remove, so remove is gated. +const SOURCE_INFO: Record = { + api: { label: "local", removable: true, title: "API key stored in the local auth store on this machine" }, + env: { + label: "env", + removable: false, + title: "API key from an environment variable or dashboard sync — unset it where it is defined to remove it", + }, + config: { + label: "config", + removable: false, + title: "API key set in openscience.json — edit the config file to remove it", + }, + custom: { + label: "custom", + removable: false, + title: "Custom provider defined in openscience.json — edit the config file to remove it", + }, +} + export const Credentials: Component = () => { const sdk = useGlobalSDK() const platform = usePlatform() @@ -129,6 +151,9 @@ export const Credentials: Component = () => { const [keyValue, setKeyValue] = createSignal("") const [savingKey, setSavingKey] = createSignal(false) const connectedProviders = createMemo(() => providers.connected().filter((p) => p.id !== "synsci")) + // The list endpoint's generated type omits `source`, but the payload carries it + // for every connected provider (see Provider in @synsci/sdk/v2/client). + const sourceInfo = (p: { id: string }) => SOURCE_INFO[(p as { source?: Provider["source"] }).source ?? "api"] const saveKey = async () => { if (savingKey()) return const key = keyValue().trim() @@ -372,10 +397,32 @@ export const Credentials: Component = () => {
{PROVIDER_LABEL[p.id] ?? p.id} + + {sourceInfo(p).label} +
- + + + + } + > + + )} diff --git a/frontend/workspace/src/components/settings/General.tsx b/frontend/workspace/src/components/settings/General.tsx index 590a861..01c39fc 100644 --- a/frontend/workspace/src/components/settings/General.tsx +++ b/frontend/workspace/src/components/settings/General.tsx @@ -8,13 +8,14 @@ import { Component, Show, createMemo, createSignal, onMount, type JSX } from "solid-js" import { Button } from "@synsci/ui/button" import { Select } from "@synsci/ui/select" +import { showToast } from "@synsci/ui/toast" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { useModels } from "@/context/models" import { usePlatform } from "@/context/platform" import { useServer } from "@/context/server" import { URLS } from "@/config/urls" -import { FONT_SANS } from "@/styles/tokens" +import { FONT_CODE, FONT_SANS } from "@/styles/tokens" import { AppearanceSections } from "../settings-general" import { settingsApi } from "./api" @@ -85,8 +86,12 @@ export default function General() { if (!window.confirm("Disconnect this local server from OpenScience?")) return setBusy(true) try { - await sdk.client.account.logout() + const res = await sdk.client.account.logout() + if (res.error) + throw new Error(typeof res.error === "string" ? res.error : "The server could not clear the session") setAccount({ session: false }) + } catch (err) { + showToast({ variant: "error", title: "Sign out failed", description: message(err) }) } finally { setBusy(false) } @@ -166,6 +171,15 @@ export default function General() { sign out + +
+

+ Signed out — run{" "} + openscience connect login in a + terminal to reconnect this machine. +

+
+
@@ -238,6 +252,10 @@ export default function General() { ) } +function message(err: unknown) { + return err instanceof Error ? err.message : String(err) +} + const Section: Component<{ title: string; description?: string; children: JSX.Element }> = (props) => (
diff --git a/frontend/workspace/src/components/settings/Usage.tsx b/frontend/workspace/src/components/settings/Usage.tsx index a1c8135..6bcc09d 100644 --- a/frontend/workspace/src/components/settings/Usage.tsx +++ b/frontend/workspace/src/components/settings/Usage.tsx @@ -6,7 +6,7 @@ // the actual cost + tokens recorded on assistant messages across sessions. // • Extra usage budget → /settings/preferences (real JSON store). // • Buy credits → opens the Atlas top-up / checkout (URLS.dashboardCli). -import { Component, For, Show, createMemo, createSignal, onMount, type JSX } from "solid-js" +import { Component, For, Show, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js" import { useParams } from "@solidjs/router" import { Button } from "@synsci/ui/button" import { useGlobalSDK } from "@/context/global-sdk" @@ -78,7 +78,14 @@ export default function Usage() { setBudgetDraft(prefs.extra_budget_usd ? String(prefs.extra_budget_usd) : "") } catch {} } - onMount(() => void load()) + // Refetch when the window regains focus so a dashboard top-up (buy credits + // opens a new tab) shows up as soon as the user comes back. + const focus = () => void load() + onMount(() => { + void load() + window.addEventListener("focus", focus) + }) + onCleanup(() => window.removeEventListener("focus", focus)) const saveBudget = async () => { const value = Math.max(0, Number(budgetDraft()) || 0) @@ -94,6 +101,10 @@ export default function Usage() { } } + // Distinguish "no data yet" and "signed out" from real account values — the + // panel must not present "Free"/"byok" defaults as if they came from Atlas. + const loading = () => account() === undefined + const signedOut = () => account()?.session === false const plan = () => (account()?.user?.subscription_plan as string | undefined) ?? "Free" const balance = () => account()?.balance_usd const managed = () => account()?.billing_mode?.mode === "managed" @@ -138,19 +149,32 @@ export default function Usage() {
Plan - {plan()} + {loading() ? "…" : "—"}} + > + {plan()} +
Wallet balance - - {typeof balance() === "number" && balance()! >= 0 ? money(balance()!) : "—"} - + = 0} + fallback={{loading() ? "…" : "—"}} + > + {money(balance()!)} +
Billing - - {account()?.billing_mode?.mode ?? "byok"} - + {loading() ? "…" : "—"}} + > + + {account()?.billing_mode?.mode ?? "byok"} + +

- - Managed calls debit this wallet. Top up any time — credits never expire. + + + Not connected — run{" "} + openscience connect login{" "} + in a terminal to see your plan and wallet. + + } + > + + Managed calls debit this wallet. Top up any time — credits never expire. + +