mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-25 20:33:58 +02:00
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:
@@ -57,6 +57,7 @@ export default function App() {
|
|||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [sidecarCapabilities, setSidecarCapabilities] = useState<SidecarCapabilities>();
|
const [sidecarCapabilities, setSidecarCapabilities] = useState<SidecarCapabilities>();
|
||||||
const [sessionActivities, setSessionActivities] = useState<SessionActivityMap>({});
|
const [sessionActivities, setSessionActivities] = useState<SessionActivityMap>({});
|
||||||
|
const [isRefreshingCapabilities, setIsRefreshingCapabilities] = useState(false);
|
||||||
|
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [showNewSession, setShowNewSession] = 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
|
// Determine main content
|
||||||
let content: React.ReactNode;
|
let content: React.ReactNode;
|
||||||
let detailPanel: React.ReactNode | undefined;
|
let detailPanel: React.ReactNode | undefined;
|
||||||
@@ -203,6 +214,7 @@ export default function App() {
|
|||||||
const overlay = showSettings ? (
|
const overlay = showSettings ? (
|
||||||
<SettingsPanel
|
<SettingsPanel
|
||||||
availableModels={availableModels}
|
availableModels={availableModels}
|
||||||
|
isRefreshingCapabilities={isRefreshingCapabilities}
|
||||||
onClose={() => setShowSettings(false)}
|
onClose={() => setShowSettings(false)}
|
||||||
onDeletePattern={async (id) => {
|
onDeletePattern={async (id) => {
|
||||||
await api.deletePattern(id);
|
await api.deletePattern(id);
|
||||||
@@ -215,10 +227,12 @@ export default function App() {
|
|||||||
resolveReasoningEffort(defaultModel, 'high'),
|
resolveReasoningEffort(defaultModel, 'high'),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
onRefreshCapabilities={refreshCapabilities}
|
||||||
onSavePattern={async (pattern) => {
|
onSavePattern={async (pattern) => {
|
||||||
await api.savePattern({ pattern });
|
await api.savePattern({ pattern });
|
||||||
}}
|
}}
|
||||||
patterns={workspace.patterns}
|
patterns={workspace.patterns}
|
||||||
|
sidecarCapabilities={sidecarCapabilities}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,11 +3,16 @@ import { ChevronRight, Plus, X } from 'lucide-react';
|
|||||||
|
|
||||||
import type { ModelDefinition } from '@shared/domain/models';
|
import type { ModelDefinition } from '@shared/domain/models';
|
||||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
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';
|
import { PatternEditor } from '@renderer/components/PatternEditor';
|
||||||
|
|
||||||
interface SettingsPanelProps {
|
interface SettingsPanelProps {
|
||||||
availableModels: ReadonlyArray<ModelDefinition>;
|
availableModels: ReadonlyArray<ModelDefinition>;
|
||||||
patterns: PatternDefinition[];
|
patterns: PatternDefinition[];
|
||||||
|
sidecarCapabilities?: SidecarCapabilities;
|
||||||
|
isRefreshingCapabilities: boolean;
|
||||||
|
onRefreshCapabilities: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSavePattern: (pattern: PatternDefinition) => Promise<void>;
|
onSavePattern: (pattern: PatternDefinition) => Promise<void>;
|
||||||
onDeletePattern: (patternId: string) => Promise<void>;
|
onDeletePattern: (patternId: string) => Promise<void>;
|
||||||
@@ -22,6 +27,9 @@ function modeBadgeClasses(pattern: PatternDefinition) {
|
|||||||
export function SettingsPanel({
|
export function SettingsPanel({
|
||||||
availableModels,
|
availableModels,
|
||||||
patterns,
|
patterns,
|
||||||
|
sidecarCapabilities,
|
||||||
|
isRefreshingCapabilities,
|
||||||
|
onRefreshCapabilities,
|
||||||
onClose,
|
onClose,
|
||||||
onSavePattern,
|
onSavePattern,
|
||||||
onDeletePattern,
|
onDeletePattern,
|
||||||
@@ -73,6 +81,20 @@ export function SettingsPanel({
|
|||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-5">
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||||
<div className="mx-auto max-w-2xl">
|
<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 className="mb-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-zinc-200">Orchestration Patterns</h3>
|
<h3 className="text-sm font-semibold text-zinc-200">Orchestration Patterns</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user