"use client";
import { useState, useCallback } from "react";
import {
IconCheck,
IconChevronDown,
IconChevronRight,
IconInfoCircle,
IconX,
IconRefresh,
IconKey,
IconTrash,
IconEye,
IconEyeOff,
IconTerminal2,
} from "@tabler/icons-react";
import { Badge } from "@kandev/ui/badge";
import { Button } from "@kandev/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@kandev/ui/collapsible";
import { Input } from "@kandev/ui/input";
import { Spinner } from "@kandev/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useAppStore } from "@/components/state-provider";
import { useGitHubStatus } from "@/hooks/domains/github/use-github-status";
import { useToast } from "@/components/toast-provider";
import { configureGitHubToken, clearGitHubToken } from "@/lib/api/domains/github-api";
import type { AuthDiagnostics } from "@/lib/types/github";
import { GitHubRateLimitDisplay } from "./github-rate-limit";
import { HostShellDialog } from "@/components/settings/host-shell-dialog";
function DiagnosticsOutput({ diagnostics }: { diagnostics: AuthDiagnostics }) {
return (
{diagnostics.command}
exit code: {diagnostics.exit_code}
{diagnostics.exit_code !== 0 && (
A non-zero exit code means the command failed. Review the output below for details.
)}
{diagnostics.output.trim()}
);
}
function TokenConfigForm({ onSuccess }: { onSuccess: () => void }) {
const [token, setToken] = useState("");
const [showToken, setShowToken] = useState(false);
const [saving, setSaving] = useState(false);
const { toast } = useToast();
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
if (!token.trim()) return;
setSaving(true);
try {
await configureGitHubToken(token.trim());
toast({ description: "GitHub token configured successfully", variant: "success" });
setToken("");
onSuccess();
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to configure token";
toast({ description: message, variant: "error" });
} finally {
setSaving(false);
}
},
[token, toast, onSuccess],
);
return (
);
}
type StatusForNotConnected = {
diagnostics?: AuthDiagnostics;
};
function NotConnectedView({
status,
refresh,
ghAuthOpen,
setGhAuthOpen,
}: {
status: StatusForNotConnected | null;
refresh: () => void;
ghAuthOpen: boolean;
setGhAuthOpen: (open: boolean) => void;
}) {
// gh installed → CLI sign-in is the recommended path (browser-driven OAuth,
// no PAT to manage). When it's missing we drop to PAT-only.
const ghInstalled = useAppStore((state) =>
state.availableAgents.tools.some((t) => t.name === "gh" && t.available),
);
const [tokenOpen, setTokenOpen] = useState(false);
const [diagOpen, setDiagOpen] = useState(false);
return (
Not connected to GitHub
Refresh
Kandev also accepts a GITHUB_TOKEN environment variable on startup. The
UI options below cover the common cases.
{ghInstalled ? (
setGhAuthOpen(true)} />
) : (
)}
{tokenOpen ? (
) : (
)}
{ghInstalled ? "Use a personal access token instead" : "Configure GitHub token"}
{/* TokenConfigForm already renders the "Create a Personal Access
Token with repo + read:org scopes" hint below its input. */}
{status?.diagnostics && (
)}
);
}
function DiagnosticsDisclosure({
diagnostics,
open,
onOpenChange,
}: {
diagnostics: AuthDiagnostics;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
return (
{open ? (
) : (
)}
Diagnostics ({diagnostics.command}, exit{" "}
{diagnostics.exit_code})
);
}
function PrimaryCLISignIn({ onClick }: { onClick: () => void }) {
return (
Sign in with the GitHub CLI
Recommended. Opens a terminal that runs{" "}
gh auth login with browser-based
authentication.
Open terminal and sign in
);
}
function CLIUnavailableHint() {
return (
The gh CLI is not installed on this host - a
personal access token is the only sign-in option available here. To install it later, see{" "}
cli.github.com
.
);
}
export function GitHubStatusCard() {
const { status, loaded, loading, refresh } = useGitHubStatus();
const { toast } = useToast();
const [clearing, setClearing] = useState(false);
const [ghAuthOpen, setGhAuthOpen] = useState(false);
const handleClearToken = useCallback(async () => {
setClearing(true);
try {
await clearGitHubToken();
toast({ description: "GitHub token removed", variant: "success" });
refresh();
} catch {
toast({ description: "Failed to clear token", variant: "error" });
} finally {
setClearing(false);
}
}, [toast, refresh]);
if (loading || !loaded) {
return (
Checking GitHub connection...
);
}
if (!status || !status.authenticated) {
return (
);
}
return (
Connected as {status.username}
{status.auth_method === "gh_cli" ? "gh CLI" : "PAT"}
{status.token_configured && (
{clearing ? : }
)}
{status.token_configured && (
Token stored in secrets. This token is used by remote agents for GitHub operations.
)}
);
}