feat: add Copilot connection status card in settings

- New CopilotStatusCard component with status-specific visuals for
  ready, cli-missing, auth-required, and error states
- Expandable technical details section with CLI path and timestamps
- Refresh button wired to refreshSidecarCapabilities IPC
- Integrated into SettingsPanel above orchestration patterns

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-22 13:44:10 +01:00
co-authored by Copilot
parent d68b1b99f3
commit c237b9e82d
3 changed files with 250 additions and 0 deletions
+14
View File
@@ -57,6 +57,7 @@ export default function App() {
const [error, setError] = useState<string>();
const [sidecarCapabilities, setSidecarCapabilities] = useState<SidecarCapabilities>();
const [sessionActivities, setSessionActivities] = useState<SessionActivityMap>({});
const [isRefreshingCapabilities, setIsRefreshingCapabilities] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [showNewSession, setShowNewSession] = useState(false);
@@ -152,6 +153,16 @@ export default function App() {
);
}
const refreshCapabilities = async () => {
setIsRefreshingCapabilities(true);
try {
const capabilities = await api.refreshSidecarCapabilities();
setSidecarCapabilities(capabilities);
} finally {
setIsRefreshingCapabilities(false);
}
};
// Determine main content
let content: React.ReactNode;
let detailPanel: React.ReactNode | undefined;
@@ -203,6 +214,7 @@ export default function App() {
const overlay = showSettings ? (
<SettingsPanel
availableModels={availableModels}
isRefreshingCapabilities={isRefreshingCapabilities}
onClose={() => setShowSettings(false)}
onDeletePattern={async (id) => {
await api.deletePattern(id);
@@ -215,10 +227,12 @@ export default function App() {
resolveReasoningEffort(defaultModel, 'high'),
);
}}
onRefreshCapabilities={refreshCapabilities}
onSavePattern={async (pattern) => {
await api.savePattern({ pattern });
}}
patterns={workspace.patterns}
sidecarCapabilities={sidecarCapabilities}
/>
) : null;
@@ -0,0 +1,214 @@
import { useState } from 'react';
import {
CheckCircle2,
AlertTriangle,
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;
borderClasses: string;
bgClasses: string;
actionIcon?: React.ReactNode;
actionLabel?: string;
}
function getStatusConfig(status: SidecarConnectionStatus): StatusConfig {
switch (status) {
case 'ready':
return {
icon: <CheckCircle2 className="size-4 text-emerald-400" />,
label: 'Connected',
accentClasses: 'text-emerald-400',
borderClasses: 'border-emerald-500/20',
bgClasses: 'bg-emerald-500/5',
};
case 'copilot-cli-missing':
return {
icon: <Download className="size-4 text-amber-400" />,
label: 'CLI Not Found',
accentClasses: 'text-amber-400',
borderClasses: 'border-amber-500/20',
bgClasses: 'bg-amber-500/5',
actionIcon: <Terminal className="size-3" />,
actionLabel: 'Install Copilot CLI',
};
case 'copilot-auth-required':
return {
icon: <LogIn className="size-4 text-blue-400" />,
label: 'Sign-in Required',
accentClasses: 'text-blue-400',
borderClasses: 'border-blue-500/20',
bgClasses: 'bg-blue-500/5',
actionIcon: <Terminal className="size-3" />,
actionLabel: 'Run copilot auth login',
};
case 'copilot-error':
return {
icon: <XCircle className="size-4 text-red-400" />,
label: 'Connection Error',
accentClasses: 'text-red-400',
borderClasses: 'border-red-500/20',
bgClasses: 'bg-red-500/5',
};
}
}
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 '';
}
}
export function CopilotStatusCard({
connection,
modelCount,
isRefreshing,
onRefresh,
}: CopilotStatusCardProps) {
const [showDetails, setShowDetails] = useState(false);
if (!connection) {
return (
<div className="rounded-xl border border-[var(--color-border)] bg-zinc-900/40 p-4">
<div className="flex items-center gap-2">
<Cpu className="size-4 text-zinc-600" />
<span className="text-[13px] text-zinc-500">Checking Copilot connection</span>
<RefreshCw className="ml-auto size-3.5 animate-spin text-zinc-600" />
</div>
</div>
);
}
const config = getStatusConfig(connection.status);
const isHealthy = connection.status === 'ready';
const hasDetail = connection.detail || connection.copilotCliPath;
const checkedLabel = formatCheckedAt(connection.checkedAt);
return (
<div className={`rounded-xl border ${config.borderClasses} ${config.bgClasses} p-4 transition-colors`}>
{/* Main status row */}
<div className="flex items-start gap-3">
<div className="mt-0.5 shrink-0">{config.icon}</div>
<div className="min-w-0 flex-1">
{/* Title row */}
<div className="flex items-center gap-2">
<span className={`text-[13px] font-semibold ${config.accentClasses}`}>
{config.label}
</span>
{isHealthy && (
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-400">
{modelCount} model{modelCount === 1 ? '' : 's'}
</span>
)}
</div>
{/* Summary */}
<p className="mt-0.5 text-[12px] leading-relaxed text-zinc-400">
{connection.summary}
</p>
{/* Action hint for non-ready states */}
{!isHealthy && config.actionLabel && (
<div className="mt-2 flex items-center gap-1.5 rounded-lg bg-zinc-800/60 px-2.5 py-1.5 text-[11px] text-zinc-400">
{config.actionIcon}
<span>{config.actionLabel}</span>
</div>
)}
</div>
{/* Refresh button */}
<button
className="flex size-7 shrink-0 items-center justify-center rounded-lg text-zinc-500 transition hover:bg-zinc-800 hover:text-zinc-300 disabled:opacity-50"
disabled={isRefreshing}
onClick={onRefresh}
title="Refresh connection status"
type="button"
>
<RefreshCw className={`size-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
</button>
</div>
{/* Expandable details */}
{hasDetail && (
<div className="mt-3 border-t border-zinc-800/60 pt-2">
<button
className="flex w-full items-center gap-1 text-[11px] text-zinc-600 transition hover:text-zinc-400"
onClick={() => setShowDetails((prev) => !prev)}
type="button"
>
{showDetails ? (
<ChevronDown className="size-3" />
) : (
<ChevronRight className="size-3" />
)}
<span>Technical details</span>
{checkedLabel && (
<span className="ml-auto text-[10px] text-zinc-700">
checked {checkedLabel}
</span>
)}
</button>
{showDetails && (
<div className="mt-2 space-y-1.5 rounded-lg bg-zinc-900/60 px-3 py-2 text-[11px] font-mono text-zinc-500">
{connection.copilotCliPath && (
<div className="flex items-baseline gap-2">
<span className="shrink-0 text-zinc-600">CLI path</span>
<span className="truncate text-zinc-400">{connection.copilotCliPath}</span>
</div>
)}
{connection.detail && (
<div className="flex items-baseline gap-2">
<span className="shrink-0 text-zinc-600">Detail</span>
<span className="break-words text-zinc-400">{connection.detail}</span>
</div>
)}
{checkedLabel && (
<div className="flex items-baseline gap-2">
<span className="shrink-0 text-zinc-600">Checked</span>
<span className="text-zinc-400">{connection.checkedAt}</span>
</div>
)}
</div>
)}
</div>
)}
</div>
);
}
+22
View File
@@ -3,11 +3,16 @@ import { ChevronRight, Plus, X } from 'lucide-react';
import type { ModelDefinition } from '@shared/domain/models';
import type { PatternDefinition } from '@shared/domain/pattern';
import type { SidecarCapabilities } from '@shared/contracts/sidecar';
import { CopilotStatusCard } from '@renderer/components/CopilotStatusCard';
import { PatternEditor } from '@renderer/components/PatternEditor';
interface SettingsPanelProps {
availableModels: ReadonlyArray<ModelDefinition>;
patterns: PatternDefinition[];
sidecarCapabilities?: SidecarCapabilities;
isRefreshingCapabilities: boolean;
onRefreshCapabilities: () => void;
onClose: () => void;
onSavePattern: (pattern: PatternDefinition) => Promise<void>;
onDeletePattern: (patternId: string) => Promise<void>;
@@ -22,6 +27,9 @@ function modeBadgeClasses(pattern: PatternDefinition) {
export function SettingsPanel({
availableModels,
patterns,
sidecarCapabilities,
isRefreshingCapabilities,
onRefreshCapabilities,
onClose,
onSavePattern,
onDeletePattern,
@@ -73,6 +81,20 @@ export function SettingsPanel({
{/* Body */}
<div className="flex-1 overflow-y-auto px-6 py-5">
<div className="mx-auto max-w-2xl">
{/* Copilot Connection Status */}
<div className="mb-6">
<h3 className="mb-2 text-sm font-semibold text-zinc-200">GitHub Copilot</h3>
<CopilotStatusCard
connection={sidecarCapabilities?.connection}
isRefreshing={isRefreshingCapabilities}
modelCount={sidecarCapabilities?.models.length ?? 0}
onRefresh={onRefreshCapabilities}
/>
</div>
{/* Separator */}
<div className="mb-6 border-t border-[var(--color-border)]" />
<div className="mb-4 flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold text-zinc-200">Orchestration Patterns</h3>