import { CheckCircle2, Cloud, Github, Monitor, RefreshCw, Shield } from "lucide-react"; import type React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { Badge } from "../components/ui/badge"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { Skeleton } from "../components/ui/skeleton"; import { useGithubAppConfig } from "../hooks/useGithubApp"; import { api } from "../lib/api"; import { accountAuthClient, type LinkedAccount, type SessionEntry, useSession } from "../lib/auth-client"; import { cn } from "../lib/utils"; const APP_VERSION = __APP_VERSION__; // ─── Account page root ─────────────────────────────────────────────────────── export function AccountPage() { const { data: session } = useSession(); const user = session?.user as { email?: string | null; emailVerified?: boolean; createdAt?: Date | string | null } | undefined; const currentToken = (session?.session as { token?: string } | undefined)?.token; const githubAppConfig = useGithubAppConfig(); const [accounts, setAccounts] = useState(null); const [accountsLoading, setAccountsLoading] = useState(true); const [accountsError, setAccountsError] = useState(null); const [sessions, setSessions] = useState(null); const [sessionsLoading, setSessionsLoading] = useState(true); const [sessionsError, setSessionsError] = useState(null); const loadAccounts = useCallback(async () => { setAccountsLoading(true); setAccountsError(null); const { data, error } = await accountAuthClient.listAccounts(); setAccountsLoading(false); if (error) { const msg = error.message || "Failed to load login methods"; setAccountsError(msg); toast.error(msg); } else { setAccounts(data); } }, []); const loadSessions = useCallback(async () => { setSessionsLoading(true); setSessionsError(null); const { data, error } = await accountAuthClient.listSessions(); setSessionsLoading(false); if (error) { const msg = error.message || "Failed to load sessions"; setSessionsError(msg); toast.error(msg); } else { setSessions(data); } }, []); useEffect(() => { loadAccounts(); loadSessions(); }, [loadAccounts, loadSessions]); const hasCredentialAccount = accounts?.some((a) => a.providerId === "credential") ?? false; const githubAccount = accounts?.find((a) => a.providerId === "github"); const amaAccount = accounts?.find((a) => a.providerId === "ama"); // Once the AMA account is linked, provision the owner's AMA project + vault so // resources exist before they create an agent or machine. The endpoint is // idempotent; provision once per linked account id. const provisionedAccountId = useRef(null); useEffect(() => { if (!amaAccount || provisionedAccountId.current === amaAccount.id) return; provisionedAccountId.current = amaAccount.id; api.ama.provision().catch(() => { // Best-effort: agent/machine creation will surface a clear error if AMA // resources are still missing. provisionedAccountId.current = null; }); }, [amaAccount]); const [githubConnecting, setGithubConnecting] = useState(false); const [githubError, setGithubError] = useState(null); async function handleConnectGitHub() { setGithubConnecting(true); setGithubError(null); const { error } = await accountAuthClient.linkSocial({ provider: "github", callbackURL: "/settings/account" }); setGithubConnecting(false); if (error) { const msg = error.message || "Failed to connect GitHub"; setGithubError(msg); toast.error(msg); } } const [amaConnecting, setAmaConnecting] = useState(false); const [amaError, setAmaError] = useState(null); async function handleConnectAma() { setAmaConnecting(true); setAmaError(null); const { data, error } = await accountAuthClient.oauth2.link({ providerId: "ama", callbackURL: "/settings/account" }); if (error) { setAmaConnecting(false); const msg = error.message || "Failed to connect AMA"; setAmaError(msg); toast.error(msg); return; } // The link endpoint returns the AMA authorization URL to redirect to. if (data?.url) { window.location.href = data.url; return; } setAmaConnecting(false); } async function handleDisconnectAma() { setAmaConnecting(true); setAmaError(null); const { error } = await accountAuthClient.unlinkAccount({ providerId: "ama" }); setAmaConnecting(false); if (error) { const msg = error.message || "Failed to disconnect AMA"; setAmaError(msg); toast.error(msg); return; } toast.success("AMA disconnected"); loadAccounts(); } return (

Account

Manage login methods, security, and active sessions.

{/* Identity summary */}
{user?.email ?? "—"} {user?.createdAt && ( {new Date(user.createdAt).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })} )} {accountsLoading ? ( ) : accountsError ? ( {accountsError} ) : accounts && accounts.length > 0 ? (
{accounts.map((a) => ( ))}
) : ( None found )}
v{APP_VERSION}
{/* GitHub connection */}
{accountsLoading ? ( ) : accountsError ? (

{accountsError}

) : githubAccount ? (

GitHub connected

Account ID: {githubAccount.accountId}

{githubError && (

{githubError}

)}
) : (

GitHub not connected

Connect GitHub to enable agent identity sync and GPG key integration.

{githubError && (

{githubError}

)}
)}
{/* GitHub App — distinct from the OAuth connection above: OAuth handles identity/GPG sync, the App grants repo push/PR + delivers PR webhooks. */} {githubAppConfig?.configured && githubAppConfig.install_url && (

GitHub App

{githubAppConfig.installed ? (

Connected{githubAppConfig.accounts[0] ? ` to ${githubAppConfig.accounts.map((a) => `@${a}`).join(", ")}` : ""}. Agent push/PR and PR status sync are enabled.

) : (

Install the app on your repositories to enable agent push/PR and PR status sync.

)}
{githubAppConfig.installed ? "Manage" : "Install App"}
)}
{/* AMA connection */}
{accountsLoading ? ( ) : accountsError ? (

{accountsError}

) : amaAccount ? (

AMA connected

Account ID: {amaAccount.accountId}

{amaError && (

{amaError}

)}
) : (

AMA not connected

Connect AMA to enable cloud scheduling and dispatch agents to your own AMA account.

{amaError && (

{amaError}

)}
)}
{/* Change password */} {/* Active sessions */}
); } // ─── Change password section ───────────────────────────────────────────────── function ChangePasswordSection({ hasCredentialAccount, accountsLoading, accountsError, }: { hasCredentialAccount: boolean; accountsLoading: boolean; accountsError: string | null; }) { const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const confirmMismatch = confirmPassword.length > 0 && newPassword !== confirmPassword; const canSubmit = currentPassword.length > 0 && newPassword.length >= 8 && newPassword === confirmPassword && !saving; async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!canSubmit) return; setError(null); setSuccess(false); setSaving(true); const { error: err } = await accountAuthClient.changePassword({ currentPassword, newPassword, revokeOtherSessions: false }); setSaving(false); if (err) { setError(err.message || "Failed to change password"); toast.error("Failed to change password"); return; } setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); setSuccess(true); toast.success("Password changed"); } return (
{accountsLoading ? ( ) : accountsError ? (

{accountsError}

) : !hasCredentialAccount ? (

Your account uses OAuth only. Password change is not available for OAuth-only accounts.

) : (
0 && newPassword.length < 8} > {newPassword.length > 0 && newPassword.length < 8 &&

Password must be at least 8 characters.

}
{confirmMismatch &&

Passwords do not match.

}
{error && (

{error}

)} {success && (

Password changed successfully.

)}
)}
); } // ─── Sessions section ──────────────────────────────────────────────────────── function SessionsSection({ sessions, sessionsLoading, sessionsError, currentToken, onRevoked, }: { sessions: SessionEntry[] | null; sessionsLoading: boolean; sessionsError: string | null; currentToken: string | undefined; onRevoked: () => void; }) { const [revoking, setRevoking] = useState(false); const otherSessions = useMemo(() => sessions?.filter((s) => s.token !== currentToken) ?? [], [sessions, currentToken]); async function handleRevokeOthers() { setRevoking(true); const { error } = await accountAuthClient.revokeOtherSessions(); setRevoking(false); if (error) { toast.error(error.message || "Failed to revoke sessions"); return; } toast.success("Other sessions revoked"); onRevoked(); } return (
{sessionsLoading ? ( <> ) : sessionsError ? (

{sessionsError}

) : sessions && sessions.length > 0 ? ( <> {sessions.map((s) => ( ))} {otherSessions.length > 0 && (
)} ) : (

No active sessions found.

)}
); } function SessionRow({ session, isCurrent }: { session: SessionEntry; isCurrent: boolean }) { const ua = parseUserAgent(session.userAgent); const date = new Date(session.createdAt).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); return (
{ua} {isCurrent && ( Current )}

{session.ipAddress ? `${session.ipAddress} · ` : ""}Started {date}

); } function parseUserAgent(ua?: string | null): string { if (!ua) return "Unknown device"; if (/iPhone|iPad|iOS/i.test(ua)) return "iOS"; if (/Android/i.test(ua)) return "Android"; if (/Windows/i.test(ua)) return "Windows browser"; if (/Macintosh|Mac OS/i.test(ua)) return "macOS browser"; if (/Linux/i.test(ua)) return "Linux browser"; return "Browser"; } // ─── Shared sub-components ─────────────────────────────────────────────────── function SectionHeader({ icon: Icon, title }: { icon: React.ElementType; title: string }) { return (

{title}

); } function InfoRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function ProviderBadge({ providerId }: { providerId: string }) { const label = providerId === "credential" ? "Email/Password" : providerId.charAt(0).toUpperCase() + providerId.slice(1); return ( {providerId === "github" && } {label} ); } function PasswordField({ label, id, value, onChange, autoComplete, "aria-invalid": ariaInvalid, children, }: { label: string; id: string; value: string; onChange: (v: string) => void; autoComplete: string; "aria-invalid"?: boolean; children?: React.ReactNode; }) { return (
onChange(e.target.value)} autoComplete={autoComplete} aria-invalid={ariaInvalid} /> {children}
); } function EmailVerificationBadge({ verified }: { verified: boolean }) { if (verified) { return ( Verified ); } return ( Unverified ); }