From c237b9e82d359aba9f015e6e56c713cbac9d770f Mon Sep 17 00:00:00 2001 From: David Kaya Date: Sun, 22 Mar 2026 13:44:10 +0100 Subject: [PATCH] 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> --- src/renderer/App.tsx | 14 ++ src/renderer/components/CopilotStatusCard.tsx | 214 ++++++++++++++++++ src/renderer/components/SettingsPanel.tsx | 22 ++ 3 files changed, 250 insertions(+) create mode 100644 src/renderer/components/CopilotStatusCard.tsx diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 0589708..c61d6c7 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -57,6 +57,7 @@ export default function App() { const [error, setError] = useState(); const [sidecarCapabilities, setSidecarCapabilities] = useState(); const [sessionActivities, setSessionActivities] = useState({}); + 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 ? ( 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; diff --git a/src/renderer/components/CopilotStatusCard.tsx b/src/renderer/components/CopilotStatusCard.tsx new file mode 100644 index 0000000..68bb097 --- /dev/null +++ b/src/renderer/components/CopilotStatusCard.tsx @@ -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: , + label: 'Connected', + accentClasses: 'text-emerald-400', + borderClasses: 'border-emerald-500/20', + bgClasses: 'bg-emerald-500/5', + }; + case 'copilot-cli-missing': + return { + icon: , + label: 'CLI Not Found', + accentClasses: 'text-amber-400', + borderClasses: 'border-amber-500/20', + bgClasses: 'bg-amber-500/5', + actionIcon: , + actionLabel: 'Install Copilot CLI', + }; + case 'copilot-auth-required': + return { + icon: , + label: 'Sign-in Required', + accentClasses: 'text-blue-400', + borderClasses: 'border-blue-500/20', + bgClasses: 'bg-blue-500/5', + actionIcon: , + actionLabel: 'Run copilot auth login', + }; + case 'copilot-error': + return { + icon: , + 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 ( +
+
+ + Checking Copilot connection… + +
+
+ ); + } + + const config = getStatusConfig(connection.status); + const isHealthy = connection.status === 'ready'; + const hasDetail = connection.detail || connection.copilotCliPath; + const checkedLabel = formatCheckedAt(connection.checkedAt); + + return ( +
+ {/* Main status row */} +
+
{config.icon}
+ +
+ {/* Title row */} +
+ + {config.label} + + {isHealthy && ( + + {modelCount} model{modelCount === 1 ? '' : 's'} + + )} +
+ + {/* Summary */} +

+ {connection.summary} +

+ + {/* Action hint for non-ready states */} + {!isHealthy && config.actionLabel && ( +
+ {config.actionIcon} + {config.actionLabel} +
+ )} +
+ + {/* Refresh button */} + +
+ + {/* Expandable details */} + {hasDetail && ( +
+ + + {showDetails && ( +
+ {connection.copilotCliPath && ( +
+ CLI path + {connection.copilotCliPath} +
+ )} + {connection.detail && ( +
+ Detail + {connection.detail} +
+ )} + {checkedLabel && ( +
+ Checked + {connection.checkedAt} +
+ )} +
+ )} +
+ )} +
+ ); +} diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx index 3cc0189..4a93f1e 100644 --- a/src/renderer/components/SettingsPanel.tsx +++ b/src/renderer/components/SettingsPanel.tsx @@ -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; patterns: PatternDefinition[]; + sidecarCapabilities?: SidecarCapabilities; + isRefreshingCapabilities: boolean; + onRefreshCapabilities: () => void; onClose: () => void; onSavePattern: (pattern: PatternDefinition) => Promise; onDeletePattern: (patternId: string) => Promise; @@ -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 */}
+ {/* Copilot Connection Status */} +
+

GitHub Copilot

+ +
+ + {/* Separator */} +
+

Orchestration Patterns