Marketplace UI: accounts — gh on host, gh in a container, access tokens

Per preflight F5, Remove is disabled with a hint for an account a
marketplace uses rather than offering a confirm modal that promises a
removal the backend refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 09:03:38 -07:00
co-authored by Claude Opus 5.5
parent 487443c27c
commit f3909084f6
6 changed files with 673 additions and 4 deletions
@@ -0,0 +1,75 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { useAppState } from "../../store/appState";
import type { AppSettings } from "../../lib/types";
import type { MarketplaceApi } from "../../hooks/useMarketplace";
const testMarketplaceAccount = vi.fn();
const removeMarketplaceAccount = vi.fn();
vi.mock("../../lib/tauri-commands", () => ({
testMarketplaceAccount: (id: string) => testMarketplaceAccount(id),
removeMarketplaceAccount: (id: string) => removeMarketplaceAccount(id),
}));
vi.mock("./AddAccountModal", () => ({ default: () => <div>add account modal</div> }));
import AccountsPane from "./AccountsPane";
const settings = {
marketplace_accounts: [
{ id: "a1", label: "Personal", host: "github.com", method: "gh_host", username: "me" },
{ id: "a2", label: "Gitea", host: "repo.example.com", method: "token", username: "jk" },
],
marketplaces: [{ id: "m1", name: "Team", url: "https://repo.example.com/t/m.git", branch: null, account_id: "a2" }],
global_marketplace_installs: [],
} as unknown as AppSettings;
describe("AccountsPane", () => {
beforeEach(() => {
vi.clearAllMocks();
useAppState.setState({ appSettings: settings, toasts: [] });
});
it("lists accounts with their method and usage", () => {
render(<AccountsPane mp={{} as MarketplaceApi} />);
expect(screen.getByText("Personal")).toBeInTheDocument();
expect(screen.getByText(/gh on this computer/)).toBeInTheDocument();
expect(screen.getByText(/Used by Team$/)).toBeInTheDocument();
});
it("tests an account", async () => {
testMarketplaceAccount.mockResolvedValue("me");
render(<AccountsPane mp={{} as MarketplaceApi} />);
fireEvent.click(screen.getByRole("button", { name: "Test Personal" }));
await waitFor(() => expect(useAppState.getState().toasts[0]).toMatchObject({ kind: "success" }));
expect(useAppState.getState().toasts[0].message).toContain("me");
});
// F5: the backend refuses to remove an account a marketplace uses, so the
// UI must not promise otherwise with a confirm modal — Remove is disabled
// with a hint instead, and there is no confirm step to click through.
it("disables Remove for an account in use, with a hint", () => {
render(<AccountsPane mp={{} as MarketplaceApi} />);
const removeGitea = screen.getByRole("button", { name: "Remove Gitea" });
expect(removeGitea).toHaveAttribute("aria-disabled", "true");
expect(screen.getByText(/Used by Team.*change or remove that marketplace first/)).toBeInTheDocument();
fireEvent.click(removeGitea);
expect(removeMarketplaceAccount).not.toHaveBeenCalled();
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("removes an unused account", async () => {
removeMarketplaceAccount.mockResolvedValue({ ...settings, marketplace_accounts: [settings.marketplace_accounts[1]] });
render(<AccountsPane mp={{} as MarketplaceApi} />);
const removePersonal = screen.getByRole("button", { name: "Remove Personal" });
expect(removePersonal).not.toHaveAttribute("aria-disabled");
fireEvent.click(removePersonal);
await waitFor(() => expect(removeMarketplaceAccount).toHaveBeenCalledWith("a1"));
await waitFor(() => expect(useAppState.getState().appSettings!.marketplace_accounts).toHaveLength(1));
});
it("opens the add dialog", () => {
render(<AccountsPane mp={{} as MarketplaceApi} />);
fireEvent.click(screen.getByRole("button", { name: "Add account" }));
expect(screen.getByText("add account modal")).toBeInTheDocument();
});
});
+101 -4
View File
@@ -1,11 +1,108 @@
import { useState } from "react";
import type { MarketplaceApi } from "../../hooks/useMarketplace"; import type { MarketplaceApi } from "../../hooks/useMarketplace";
import { useAppState } from "../../store/appState"; import { useAppState } from "../../store/appState";
import { removeMarketplaceAccount, testMarketplaceAccount } from "../../lib/tauri-commands";
import type { AccountMethod, MarketplaceAccount } from "../../lib/types";
import Button from "../ui/Button";
import AddAccountModal from "./AddAccountModal";
const METHOD_LABEL: Record<AccountMethod, string> = {
gh_host: "GitHub — gh on this computer",
gh_container: "GitHub — signed in via container",
token: "Access token",
};
export default function AccountsPane(_props: { mp: MarketplaceApi }) { export default function AccountsPane(_props: { mp: MarketplaceApi }) {
const count = useAppState((s) => s.appSettings?.marketplace_accounts.length ?? 0); const appSettings = useAppState((s) => s.appSettings);
const setAppSettings = useAppState((s) => s.setAppSettings);
const pushToast = useAppState((s) => s.pushToast);
const [adding, setAdding] = useState(false);
const [testing, setTesting] = useState<string | null>(null);
const [removing, setRemoving] = useState<string | null>(null);
const accounts = appSettings?.marketplace_accounts ?? [];
const marketplaces = appSettings?.marketplaces ?? [];
const usedBy = (id: string) => marketplaces.filter((m) => m.account_id === id).map((m) => m.name);
const test = async (a: MarketplaceAccount) => {
setTesting(a.id);
try {
const login = await testMarketplaceAccount(a.id);
pushToast({ kind: "success", message: `${a.label} works — signed in as ${login}` });
} catch (e) {
pushToast({ kind: "error", message: `${a.label} could not sign in`, detail: String(e) });
} finally {
setTesting(null);
}
};
const remove = async (a: MarketplaceAccount) => {
setRemoving(a.id);
try {
setAppSettings(await removeMarketplaceAccount(a.id));
} catch (e) {
pushToast({ kind: "error", message: `Could not remove ${a.label}`, detail: String(e) });
} finally {
setRemoving(null);
}
};
return ( return (
<p className="p-4 text-xs text-[var(--text-secondary)]"> <div className="p-4 space-y-3 max-w-3xl">
{count} account{count === 1 ? "" : "s"}. <div className="flex items-center justify-between">
</p> <p className="text-xs text-[var(--text-secondary)]">
Accounts are used to fetch private marketplaces. Tokens are kept in your OS keychain and never enter
containers.
</p>
<Button size="md" variant="secondary" onClick={() => setAdding(true)}>
Add account
</Button>
</div>
{accounts.length === 0 && <p className="text-xs text-[var(--text-secondary)]">No accounts yet. Public repositories need none.</p>}
<ul className="space-y-2">
{accounts.map((a) => {
const users = usedBy(a.id);
const inUse = users.length > 0;
return (
<li
key={a.id}
className="flex items-center justify-between gap-2 p-2 rounded-[var(--radius-control)] border border-[var(--border-color)]"
>
<div className="min-w-0 text-xs">
<p className="font-medium">{a.label}</p>
<p className="text-[var(--text-secondary)]">
{METHOD_LABEL[a.method]} · {a.host}
{a.username ? ` · ${a.username}` : ""}
</p>
{inUse && <p className="text-[var(--text-secondary)]">Used by {users.join(", ")}</p>}
</div>
<div className="flex gap-1 flex-shrink-0">
<Button
size="sm"
variant="ghost"
aria-label={`Test ${a.label}`}
disabled={testing === a.id}
onClick={() => void test(a)}
>
{testing === a.id ? "Testing…" : "Test"}
</Button>
<Button
size="sm"
variant="ghost"
aria-label={`Remove ${a.label}`}
disabled={removing === a.id}
unavailable={inUse}
unavailableReason={`Used by ${users.join(", ")} — change or remove that marketplace first`}
onClick={() => void remove(a)}
>
Remove
</Button>
</div>
</li>
);
})}
</ul>
{adding && <AddAccountModal onClose={() => setAdding(false)} />}
</div>
); );
} }
@@ -0,0 +1,77 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { useAppState } from "../../store/appState";
import type { Project } from "../../lib/types";
const marketplaceGhHostAvailable = vi.fn();
const addMarketplaceGhHostAccount = vi.fn();
const addMarketplaceTokenAccount = vi.fn();
const getSettings = vi.fn();
vi.mock("../../lib/tauri-commands", () => ({
marketplaceGhHostAvailable: () => marketplaceGhHostAvailable(),
addMarketplaceGhHostAccount: (...a: unknown[]) => addMarketplaceGhHostAccount(...a),
addMarketplaceTokenAccount: (...a: unknown[]) => addMarketplaceTokenAccount(...a),
getSettings: () => getSettings(),
}));
vi.mock("./GhContainerLoginModal", () => ({
default: ({ projectId }: { projectId: string }) => <div>container login for {projectId}</div>,
}));
import AddAccountModal from "./AddAccountModal";
const running = { id: "p1", name: "api", status: "running", container_id: "c1" } as unknown as Project;
describe("AddAccountModal", () => {
beforeEach(() => {
vi.clearAllMocks();
getSettings.mockResolvedValue({ marketplace_accounts: [] });
useAppState.setState({ projects: [running], toasts: [] });
});
it("uses host gh when available", async () => {
marketplaceGhHostAvailable.mockResolvedValue(true);
addMarketplaceGhHostAccount.mockResolvedValue({ id: "a1" });
const onClose = vi.fn();
render(<AddAccountModal onClose={onClose} />);
expect(await screen.findByText(/gh is installed on this computer/)).toBeInTheDocument();
fireEvent.change(screen.getByLabelText("Label"), { target: { value: "Personal" } });
fireEvent.click(screen.getByRole("button", { name: "Add account" }));
await waitFor(() => expect(addMarketplaceGhHostAccount).toHaveBeenCalledWith("Personal", "github.com"));
await waitFor(() => expect(onClose).toHaveBeenCalled());
});
it("falls back to gh in a running container", async () => {
marketplaceGhHostAvailable.mockResolvedValue(false);
render(<AddAccountModal onClose={vi.fn()} />);
expect(await screen.findByLabelText("Run gh in")).toBeInTheDocument();
fireEvent.change(screen.getByLabelText("Label"), { target: { value: "Work" } });
fireEvent.click(screen.getByRole("button", { name: "Sign in" }));
expect(screen.getByText("container login for p1")).toBeInTheDocument();
});
it("adds a token account for any host", async () => {
marketplaceGhHostAvailable.mockResolvedValue(false);
addMarketplaceTokenAccount.mockResolvedValue({ id: "a2" });
render(<AddAccountModal onClose={vi.fn()} />);
fireEvent.click(await screen.findByRole("radio", { name: "Access token" }));
fireEvent.change(screen.getByLabelText("Label"), { target: { value: "Gitea" } });
fireEvent.change(screen.getByLabelText("Host"), { target: { value: "repo.anhonesthost.net" } });
fireEvent.change(screen.getByLabelText("Token"), { target: { value: "test-token-not-real" } });
fireEvent.click(screen.getByRole("button", { name: "Add account" }));
await waitFor(() =>
expect(addMarketplaceTokenAccount).toHaveBeenCalledWith("Gitea", "repo.anhonesthost.net", "test-token-not-real"),
);
});
it("shows a validation error from the backend", async () => {
marketplaceGhHostAvailable.mockResolvedValue(false);
addMarketplaceTokenAccount.mockRejectedValue("The token was rejected by repo.anhonesthost.net (HTTP 401)");
render(<AddAccountModal onClose={vi.fn()} />);
fireEvent.click(await screen.findByRole("radio", { name: "Access token" }));
fireEvent.change(screen.getByLabelText("Label"), { target: { value: "G" } });
fireEvent.change(screen.getByLabelText("Host"), { target: { value: "repo.anhonesthost.net" } });
fireEvent.change(screen.getByLabelText("Token"), { target: { value: "test-token-not-real" } });
fireEvent.click(screen.getByRole("button", { name: "Add account" }));
expect(await screen.findByText(/HTTP 401/)).toBeInTheDocument();
});
});
@@ -0,0 +1,190 @@
import { useEffect, useState } from "react";
import Modal from "../ui/Modal";
import Button from "../ui/Button";
import SegmentedControl from "../ui/SegmentedControl";
import Field, { inputClass, selectClass } from "../ui/Field";
import {
addMarketplaceGhHostAccount,
addMarketplaceTokenAccount,
getSettings,
marketplaceGhHostAvailable,
} from "../../lib/tauri-commands";
import { useAppState } from "../../store/appState";
import GhContainerLoginModal from "./GhContainerLoginModal";
type Method = "gh" | "token";
interface Props {
onClose: () => void;
}
export default function AddAccountModal({ onClose }: Props) {
const projects = useAppState((s) => s.projects);
const setAppSettings = useAppState((s) => s.setAppSettings);
const runnable = projects.filter((p) => p.status === "running" && p.container_id);
const [method, setMethod] = useState<Method>("gh");
const [hostGh, setHostGh] = useState<boolean | null>(null);
const [label, setLabel] = useState("");
const [host, setHost] = useState("github.com");
const [token, setToken] = useState("");
const [projectId, setProjectId] = useState(runnable[0]?.id ?? "");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [containerLogin, setContainerLogin] = useState(false);
useEffect(() => {
let cancelled = false;
marketplaceGhHostAvailable()
.then((v) => {
if (!cancelled) setHostGh(v);
})
.catch(() => {
if (!cancelled) setHostGh(false);
});
return () => {
cancelled = true;
};
}, []);
const reloadSettings = async () => setAppSettings(await getSettings());
const finish = async () => {
await reloadSettings();
onClose();
};
const submit = async () => {
setError(null);
if (method === "gh" && !hostGh) {
setContainerLogin(true);
return;
}
setBusy(true);
try {
if (method === "gh") {
await addMarketplaceGhHostAccount(label.trim(), host.trim());
} else {
const t = token.trim();
setToken("");
await addMarketplaceTokenAccount(label.trim(), host.trim(), t);
}
await finish();
} catch (e) {
setError(typeof e === "string" ? e : String(e));
} finally {
setBusy(false);
}
};
const hostValid = /^[A-Za-z0-9.-]+(:[0-9]+)?$/.test(host.trim());
const needsContainer = method === "gh" && hostGh === false;
const canSubmit =
!busy &&
hostGh !== null &&
label.trim() !== "" &&
hostValid &&
(method === "gh" ? !needsContainer || projectId !== "" : token.trim() !== "");
if (containerLogin) {
const project = runnable.find((p) => p.id === projectId);
return (
<GhContainerLoginModal
label={label.trim()}
host={host.trim()}
projectId={projectId}
projectName={project?.name ?? projectId}
onClose={onClose}
onDone={() => void finish()}
/>
);
}
return (
<Modal
title="Add account"
description="Accounts let Triple-C read private marketplace repositories. Credentials stay on this computer and never enter containers."
widthClassName="w-[36rem]"
dismissible={!busy}
onClose={onClose}
footer={
<>
<Button size="md" variant="ghost" onClick={onClose} disabled={busy}>
Cancel
</Button>
<Button size="md" variant="primary" onClick={() => void submit()} disabled={!canSubmit}>
{needsContainer ? "Sign in" : busy ? "Checking…" : "Add account"}
</Button>
</>
}
>
<div className="space-y-3">
<SegmentedControl<Method>
label="Sign-in method"
value={method}
onChange={(m) => {
setMethod(m);
setError(null);
}}
segments={[
{ value: "gh", label: "GitHub via gh" },
{ value: "token", label: "Access token" },
]}
/>
<Field label="Label">
{(id) => (
<input id={id} value={label} onChange={(e) => setLabel(e.target.value)} className={inputClass} placeholder="Work GitHub" />
)}
</Field>
<Field label="Host" hint={hostValid ? undefined : "Host name only, e.g. github.com or repo.example.com"}>
{(id) => <input id={id} value={host} onChange={(e) => setHost(e.target.value)} className={inputClass} />}
</Field>
{method === "gh" && hostGh === true && (
<p className="text-xs text-[var(--text-secondary)]">
gh is installed on this computer. Triple-C asks it for a token each time it fetches, so signing out of gh
also signs this account out. If gh is not logged in yet, run <code className="font-mono">gh auth login</code> first.
</p>
)}
{needsContainer &&
(runnable.length === 0 ? (
<p className="text-xs text-[var(--warning)]">
gh is not installed on this computer. Start a project so gh can run in its container, or use an access token.
</p>
) : (
<Field
label="Run gh in"
hint="gh is not installed on this computer, so the sign-in runs in this container. The token is kept in your OS keychain, not in the container."
>
{(id) => (
<select id={id} value={projectId} onChange={(e) => setProjectId(e.target.value)} className={selectClass}>
{runnable.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
)}
</Field>
))}
{method === "token" && (
<Field
label="Token"
hint="A personal access token with read access to the repository. For GitHub SSO orgs, authorise the token for the org."
>
{(id) => (
<input
id={id}
type="password"
autoComplete="off"
value={token}
onChange={(e) => setToken(e.target.value)}
className={inputClass}
/>
)}
</Field>
)}
{error && <p role="alert" className="text-xs text-[var(--error)] whitespace-pre-wrap">{error}</p>}
</div>
</Modal>
);
}
@@ -0,0 +1,72 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
const startMarketplaceGhContainerLogin = vi.fn();
const cancelMarketplaceGhLogin = vi.fn();
const openUrlExternal = vi.fn();
vi.mock("../../lib/tauri-commands", () => ({
startMarketplaceGhContainerLogin: (...a: unknown[]) => startMarketplaceGhContainerLogin(...a),
cancelMarketplaceGhLogin: () => cancelMarketplaceGhLogin(),
openUrlExternal: (u: string) => openUrlExternal(u),
}));
const handlers = new Map<string, (e: { payload: unknown }) => void>();
vi.mock("@tauri-apps/api/event", () => ({
listen: vi.fn(async (name: string, cb: (e: { payload: unknown }) => void) => {
handlers.set(name, cb);
return vi.fn();
}),
}));
import GhContainerLoginModal from "./GhContainerLoginModal";
describe("GhContainerLoginModal", () => {
beforeEach(() => {
vi.clearAllMocks();
handlers.clear();
});
it("shows the device code, opens the URL, and finishes", async () => {
let resolve!: (v: unknown) => void;
startMarketplaceGhContainerLogin.mockReturnValue(new Promise((r) => (resolve = r)));
const onDone = vi.fn();
render(
<GhContainerLoginModal label="Work" host="github.com" projectId="p1" projectName="api" onClose={vi.fn()} onDone={onDone} />,
);
await waitFor(() => expect(handlers.has("marketplace-gh-login-code")).toBe(true));
await waitFor(() => expect(startMarketplaceGhContainerLogin).toHaveBeenCalledWith("Work", "github.com", "p1"));
act(() =>
handlers.get("marketplace-gh-login-code")!({
payload: { account_id: "unknown-yet", code: "ABCD-1234", url: "https://github.com/login/device" },
}),
);
expect(screen.getByText("ABCD-1234")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Open GitHub" }));
expect(openUrlExternal).toHaveBeenCalledWith("https://github.com/login/device");
await act(async () => resolve({ id: "acc9", label: "Work", host: "github.com", method: "gh_container", username: "me" }));
await waitFor(() => expect(onDone).toHaveBeenCalled());
});
it("refuses to open a non-GitHub URL from the container", async () => {
startMarketplaceGhContainerLogin.mockReturnValue(new Promise(() => {}));
render(<GhContainerLoginModal label="W" host="github.com" projectId="p1" projectName="api" onClose={vi.fn()} onDone={vi.fn()} />);
await waitFor(() => expect(handlers.has("marketplace-gh-login-code")).toBe(true));
act(() =>
handlers.get("marketplace-gh-login-code")!({
payload: { account_id: "x", code: "ABCD-1234", url: "https://evil.example/login" },
}),
);
expect(screen.queryByRole("button", { name: "Open GitHub" })).not.toBeInTheDocument();
});
it("cancels", async () => {
startMarketplaceGhContainerLogin.mockReturnValue(new Promise(() => {}));
const onClose = vi.fn();
render(<GhContainerLoginModal label="W" host="github.com" projectId="p1" projectName="api" onClose={onClose} onDone={vi.fn()} />);
fireEvent.click(await screen.findByRole("button", { name: "Cancel sign-in" }));
expect(cancelMarketplaceGhLogin).toHaveBeenCalled();
expect(onClose).toHaveBeenCalled();
});
});
@@ -0,0 +1,158 @@
import { useEffect, useRef, useState } from "react";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import Modal from "../ui/Modal";
import Button from "../ui/Button";
import StatusIndicator from "../ui/StatusIndicator";
import {
cancelMarketplaceGhLogin,
openUrlExternal,
startMarketplaceGhContainerLogin,
} from "../../lib/tauri-commands";
import type { MarketplaceAccount } from "../../lib/types";
interface Props {
label: string;
host: string;
projectId: string;
projectName: string;
onClose: () => void;
onDone: (account: MarketplaceAccount) => void;
}
interface CodeEvent {
account_id: string;
code: string;
url: string;
}
interface OutputEvent {
account_id: string;
chunk: string;
}
const MAX_OUTPUT = 8000;
/** Only open device-login pages on the host being signed in to. */
function safeDeviceUrl(url: string, host: string): string | null {
try {
const u = new URL(url);
return u.protocol === "https:" && u.hostname === host ? u.toString() : null;
} catch {
return null;
}
}
/**
* Drives `gh auth login --web` inside a running container. The command only
* resolves when the login finishes, so the new account's id is unknown while it
* runs; the modal accepts every gh-login event while open. The backend allows
* one gh login at a time, so there is never another flow's event to confuse.
*/
export default function GhContainerLoginModal({ label, host, projectId, projectName, onClose, onDone }: Props) {
const [code, setCode] = useState<string | null>(null);
const [url, setUrl] = useState<string | null>(null);
const [output, setOutput] = useState("");
const [error, setError] = useState<string | null>(null);
const [running, setRunning] = useState(true);
const started = useRef(false);
useEffect(() => {
let cancelled = false;
const unlisteners: UnlistenFn[] = [];
const register = async <T,>(name: string, handle: (p: T) => void) => {
const un = await listen<T>(name, (e) => handle(e.payload));
if (cancelled) un();
else unlisteners.push(un);
};
void (async () => {
await register<CodeEvent>("marketplace-gh-login-code", (p) => {
setCode(p.code);
setUrl(p.url);
});
await register<OutputEvent>("marketplace-gh-login-output", (p) =>
setOutput((prev) => {
const next = prev + p.chunk;
return next.length > MAX_OUTPUT ? next.slice(next.length - MAX_OUTPUT) : next;
}),
);
if (cancelled || started.current) return;
started.current = true;
try {
const account = await startMarketplaceGhContainerLogin(label, host, projectId);
if (!cancelled) {
setRunning(false);
onDone(account);
}
} catch (e) {
if (!cancelled) {
setRunning(false);
setError(typeof e === "string" ? e : String(e));
}
}
})();
return () => {
cancelled = true;
for (const un of unlisteners) {
try {
un();
} catch {
/* already gone */
}
}
};
// Runs once per modal instance; the props do not change while it is open.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const cancel = () => {
void cancelMarketplaceGhLogin();
onClose();
};
const openable = url ? safeDeviceUrl(url, host) : null;
return (
<Modal
title={`Sign in to ${host} with gh`}
description={`Running gh auth login in "${projectName}". The sign-in is not kept in that container.`}
widthClassName="w-[40rem]"
dismissible={!running}
onClose={running ? cancel : onClose}
footer={
running ? (
<Button size="md" variant="ghost" onClick={cancel}>
Cancel sign-in
</Button>
) : (
<Button size="md" onClick={onClose}>
Close
</Button>
)
}
>
<div className="space-y-3">
{running && !code && <StatusIndicator tone="busy" label="Starting gh…" className="text-xs" />}
{code && running && (
<div className="space-y-2">
<p className="text-xs">Enter this code on the GitHub device page:</p>
<p className="font-mono text-lg tracking-widest select-all">{code}</p>
{openable ? (
<Button size="md" variant="primary" onClick={() => void openUrlExternal(openable)}>
Open GitHub
</Button>
) : (
url && <p className="text-xs text-[var(--error)]">The sign-in URL did not point at {host}; not opening it.</p>
)}
</div>
)}
{error && <p role="alert" className="text-xs text-[var(--error)] whitespace-pre-wrap">{error}</p>}
{output && (
<pre className="max-h-40 overflow-auto p-2 text-[11px] font-mono whitespace-pre-wrap rounded-[var(--radius-control)] bg-[var(--bg-primary)] border border-[var(--border-color)]">
{output}
</pre>
)}
</div>
</Modal>
);
}