import { useState } from 'react';
import {
CheckCircle2,
XCircle,
RefreshCw,
ChevronDown,
ChevronRight,
Terminal,
LogIn,
Download,
Cpu,
} from 'lucide-react';
import type {
SidecarConnectionDiagnostics,
SidecarConnectionStatus,
} from '@shared/contracts/sidecar';
interface CopilotStatusCardProps {
connection?: SidecarConnectionDiagnostics;
modelCount: number;
isRefreshing: boolean;
onRefresh: () => void;
}
interface StatusConfig {
icon: React.ReactNode;
label: string;
accentClasses: string;
dotClasses: string;
actionIcon?: React.ReactNode;
actionLabel?: string;
}
function getStatusConfig(status: SidecarConnectionStatus): StatusConfig {
switch (status) {
case 'ready':
return {
icon: ,
label: 'Connected to GitHub Copilot',
accentClasses: 'text-emerald-400',
dotClasses: 'bg-emerald-400',
};
case 'copilot-cli-missing':
return {
icon: ,
label: 'Copilot CLI not found',
accentClasses: 'text-amber-400',
dotClasses: 'bg-amber-400',
actionIcon: ,
actionLabel: 'Install the copilot CLI and ensure it is on your PATH',
};
case 'copilot-auth-required':
return {
icon: ,
label: 'Sign-in required',
accentClasses: 'text-blue-400',
dotClasses: 'bg-blue-400',
actionIcon: ,
actionLabel: 'Run copilot auth login in your terminal, then refresh',
};
case 'copilot-error':
return {
icon: ,
label: 'Connection error',
accentClasses: 'text-red-400',
dotClasses: 'bg-red-400',
};
}
}
function formatCheckedAt(iso: string): string {
try {
const date = new Date(iso);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMin = Math.floor(diffMs / 60_000);
if (diffMin < 1) return 'just now';
if (diffMin < 60) return `${diffMin}m ago`;
const diffHr = Math.floor(diffMin / 60);
if (diffHr < 24) return `${diffHr}h ago`;
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
} catch {
return '';
}
}
function shortenPath(fullPath: string): string {
const parts = fullPath.replace(/\\/g, '/').split('/');
if (parts.length <= 3) return fullPath;
return `…/${parts.slice(-2).join('/')}`;
}
export function CopilotStatusCard({
connection,
modelCount,
isRefreshing,
onRefresh,
}: CopilotStatusCardProps) {
const [showDetails, setShowDetails] = useState(false);
if (!connection) {
return (
Checking connection…
);
}
const config = getStatusConfig(connection.status);
const isHealthy = connection.status === 'ready';
const hasDetail = connection.copilotCliPath;
const checkedLabel = formatCheckedAt(connection.checkedAt);
return (
{/* Status indicator */}
{config.label}
{isHealthy && (
· {modelCount} model{modelCount === 1 ? '' : 's'} available
)}
{checkedLabel && (
{checkedLabel}
)}
{/* Action hint for non-ready states */}
{!isHealthy && config.actionLabel && (
{config.actionIcon}
{config.actionLabel}
)}
{/* CLI path (always visible when healthy, expandable otherwise) */}
{isHealthy && hasDetail && (
{showDetails && (
{connection.copilotCliPath && (
CLI path
{connection.copilotCliPath}
)}
Last checked
{new Date(connection.checkedAt).toLocaleString()}
)}
)}
{/* Error detail for non-ready states */}
{!isHealthy && hasDetail && (
CLI path
{shortenPath(connection.copilotCliPath!)}
{connection.detail && (
Error detail
{connection.detail}
)}
)}
);
}