feat: show account identity, version badge, and org pills in settings

Extend CopilotStatusCard to surface the new backend diagnostics:
- Account identity row (login + host) when authenticated
- Organization pills with +N overflow
- CLI version freshness badge (latest/outdated/unknown)
- Latest-version hint in expandable details when outdated
- Mark version/account roadmap items as complete

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-22 15:19:24 +01:00
co-authored by Copilot
parent af3a60501c
commit b73f5dfb41
2 changed files with 107 additions and 3 deletions
+2 -2
View File
@@ -83,8 +83,8 @@ Implemented foundation:
Still worth adding:
- [ ] outdated Copilot CLI state
- [ ] active GitHub account or organization context when available
- [x] outdated Copilot CLI state
- [x] active GitHub account or organization context when available
- [ ] clear model availability explanation when a model is unavailable
- [ ] reconnect and troubleshooting actions beyond refresh
- [ ] optional per-project or per-pattern account selection later if Kopaya supports multiple Copilot identities
+105 -1
View File
@@ -9,11 +9,15 @@ import {
LogIn,
Download,
Cpu,
ArrowUpCircle,
User,
Building2,
} from 'lucide-react';
import type {
SidecarConnectionDiagnostics,
SidecarConnectionStatus,
SidecarCopilotCliVersionStatus,
} from '@shared/contracts/sidecar';
interface CopilotStatusCardProps {
@@ -94,6 +98,85 @@ function shortenPath(fullPath: string): string {
return `…/${parts.slice(-2).join('/')}`;
}
function VersionBadge({ status, installedVersion }: { status: SidecarCopilotCliVersionStatus; installedVersion?: string }) {
const versionLabel = installedVersion ? `v${installedVersion}` : undefined;
switch (status) {
case 'latest':
return (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-400">
<CheckCircle2 className="size-2.5" />
{versionLabel ?? 'Up to date'}
</span>
);
case 'outdated':
return (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-500/10 px-2 py-0.5 text-[10px] font-medium text-amber-400">
<ArrowUpCircle className="size-2.5" />
Update available
</span>
);
case 'unknown':
return (
<span className="inline-flex items-center gap-1 rounded-full bg-zinc-800 px-2 py-0.5 text-[10px] font-medium text-zinc-500">
{versionLabel ?? 'Version unknown'}
</span>
);
}
}
function AccountSection({ connection }: { connection: SidecarConnectionDiagnostics }) {
const { account } = connection;
if (!account) return null;
const hasLogin = !!account.login;
const hasOrgs = account.organizations && account.organizations.length > 0;
if (!hasLogin && !account.statusMessage) return null;
const MAX_VISIBLE_ORGS = 5;
const visibleOrgs = hasOrgs ? account.organizations!.slice(0, MAX_VISIBLE_ORGS) : [];
const remainingOrgs = hasOrgs ? account.organizations!.length - MAX_VISIBLE_ORGS : 0;
return (
<div className="space-y-2.5">
{/* Identity row */}
<div className="flex items-center gap-2">
<User className="size-3.5 text-zinc-500" />
{hasLogin ? (
<div className="flex items-center gap-1.5 text-[12px]">
<span className="font-medium text-zinc-200">{account.login}</span>
{account.host && (
<span className="text-zinc-500">· {account.host}</span>
)}
</div>
) : (
<span className="text-[12px] text-zinc-500">{account.statusMessage}</span>
)}
</div>
{/* Organizations */}
{hasOrgs && (
<div className="flex items-start gap-2">
<Building2 className="mt-0.5 size-3.5 shrink-0 text-zinc-500" />
<div className="flex flex-wrap items-center gap-1">
{visibleOrgs.map((org) => (
<span
className="rounded-md bg-zinc-800 px-1.5 py-0.5 text-[10px] font-medium text-zinc-400"
key={org}
>
{org}
</span>
))}
{remainingOrgs > 0 && (
<span className="text-[10px] text-zinc-600">+{remainingOrgs} more</span>
)}
</div>
</div>
)}
</div>
);
}
export function CopilotStatusCard({
connection,
modelCount,
@@ -116,6 +199,8 @@ export function CopilotStatusCard({
const isHealthy = connection.status === 'ready';
const hasDetail = connection.copilotCliPath;
const checkedLabel = formatCheckedAt(connection.checkedAt);
const hasVersionInfo = !!connection.copilotCliVersion;
const hasAccountInfo = !!connection.account;
return (
<div className="space-y-3">
@@ -131,6 +216,12 @@ export function CopilotStatusCard({
</span>
)}
<div className="ml-auto flex items-center gap-2">
{hasVersionInfo && (
<VersionBadge
installedVersion={connection.copilotCliVersion!.installedVersion}
status={connection.copilotCliVersion!.status}
/>
)}
{checkedLabel && (
<span className="text-[11px] text-zinc-600">{checkedLabel}</span>
)}
@@ -146,6 +237,11 @@ export function CopilotStatusCard({
</div>
</div>
{/* Account info (when healthy) */}
{isHealthy && hasAccountInfo && (
<AccountSection connection={connection} />
)}
{/* Action hint for non-ready states */}
{!isHealthy && config.actionLabel && (
<div className="flex items-start gap-2 rounded-lg border border-zinc-800 bg-zinc-900/60 px-3 py-2.5">
@@ -154,7 +250,7 @@ export function CopilotStatusCard({
</div>
)}
{/* CLI path (always visible when healthy, expandable otherwise) */}
{/* Expandable details (healthy state) */}
{isHealthy && hasDetail && (
<div className="space-y-2">
<button
@@ -176,6 +272,14 @@ export function CopilotStatusCard({
</p>
</div>
)}
{hasVersionInfo && connection.copilotCliVersion!.status === 'outdated' && connection.copilotCliVersion!.latestVersion && (
<div className="border-b border-zinc-800/40 px-3 py-2">
<span className="text-[10px] font-medium uppercase tracking-wider text-zinc-600">Latest version</span>
<p className="mt-0.5 font-mono text-[11px] text-zinc-400">
{connection.copilotCliVersion!.latestVersion}
</p>
</div>
)}
<div className="px-3 py-2">
<span className="text-[10px] font-medium uppercase tracking-wider text-zinc-600">Last checked</span>
<p className="mt-0.5 text-[11px] text-zinc-400">