mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-29 07:58:47 +02:00
feat: show platform-specific Copilot CLI install instructions when CLI is missing
Replace the generic 'Install the copilot CLI' one-liner with a rich, platform-tabbed installation guide. Auto-detects macOS/Windows/Linux and shows the recommended install command (Homebrew, WinGet, or install script) plus alternatives (npm). Includes copy-to-clipboard, auth step, and a refresh button. - Extend platform.ts with isWindows, isLinux, detectedPlatform - Add cliInstallInstructions.ts with per-platform install data - Add CliInstallGuide component in settings/ with platform tabs - Integrate into CopilotStatusCard for copilot-cli-missing state - Enhance WelcomePane with CliMissingCard showing quick-start command Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { CliInstallGuide } from '@renderer/components/settings/CliInstallGuide';
|
||||
import type {
|
||||
SidecarConnectionDiagnostics,
|
||||
SidecarConnectionStatus,
|
||||
@@ -391,8 +392,15 @@ export function CopilotStatusCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action hint for non-ready states */}
|
||||
{!isHealthy && config.actionLabel && (
|
||||
{/* Installation guide for missing CLI */}
|
||||
{connection.status === 'copilot-cli-missing' && (
|
||||
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-glass)] p-4">
|
||||
<CliInstallGuide isRefreshing={isRefreshing} onRefresh={onRefresh} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action hint for other non-ready states */}
|
||||
{!isHealthy && connection.status !== 'copilot-cli-missing' && config.actionLabel && (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-glass)] px-3 py-2.5">
|
||||
<div className="mt-0.5 shrink-0">{config.actionIcon}</div>
|
||||
<p className="text-[12px] leading-relaxed text-[var(--color-text-secondary)]">{config.actionLabel}</p>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { CheckCircle2, Circle, FolderPlus, MessageSquarePlus, Settings, Zap } from 'lucide-react';
|
||||
import { CheckCircle2, Circle, Download, FolderPlus, MessageSquarePlus, Settings, Zap } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
import type { SidecarConnectionStatus } from '@shared/contracts/sidecar';
|
||||
import { detectedPlatform } from '@renderer/lib/platform';
|
||||
import { getInstallInfoForPlatform } from '@renderer/lib/cliInstallInstructions';
|
||||
import appIconUrl from '../../../assets/icons/icon.png';
|
||||
|
||||
interface WelcomePaneProps {
|
||||
@@ -71,6 +73,34 @@ function SetupStep({ label, done, active }: SetupStepProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function CliMissingCard({ onOpenSettings }: { onOpenSettings: () => void }) {
|
||||
const info = getInstallInfoForPlatform(detectedPlatform);
|
||||
const recommended = info.methods.find((m) => m.recommended) ?? info.methods[0];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenSettings}
|
||||
className="group flex w-full cursor-pointer items-start gap-4 rounded-xl border border-[var(--color-border-glow)] bg-[var(--color-accent-muted)] px-5 py-4 text-left shadow-[0_0_24px_rgba(36,92,249,0.1)] backdrop-blur-sm transition-all duration-200 hover:shadow-[0_0_32px_rgba(36,92,249,0.15)]"
|
||||
>
|
||||
<div className="brand-gradient-bg flex size-9 shrink-0 items-center justify-center rounded-full">
|
||||
<Download className="size-4 text-white" />
|
||||
</div>
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<span className="block text-[13px] font-medium text-[var(--color-text-primary)]">
|
||||
Install the Copilot CLI
|
||||
</span>
|
||||
<code className="block truncate rounded-md bg-[var(--color-surface-1)] px-2 py-1 font-mono text-[11px] text-[var(--color-text-secondary)]">
|
||||
{recommended.command}
|
||||
</code>
|
||||
<span className="block text-[11px] text-[var(--color-text-muted)]">
|
||||
View full instructions for {info.displayName} →
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function WelcomePane({
|
||||
hasProjects,
|
||||
connectionStatus,
|
||||
@@ -165,7 +195,10 @@ export function WelcomePane({
|
||||
{/* Action cards */}
|
||||
<motion.div {...fadeUp(isFirstRun && !allDone ? 0.2 : 0.16)} className="flex w-full flex-col gap-2.5">
|
||||
{/* Primary CTA adapts to state */}
|
||||
{!isConnected && (
|
||||
{connectionStatus === 'copilot-cli-missing' && (
|
||||
<CliMissingCard onOpenSettings={onOpenSettings} />
|
||||
)}
|
||||
{!isConnected && connectionStatus !== 'copilot-cli-missing' && (
|
||||
<ActionCard
|
||||
icon={<Zap className="size-4 text-white" />}
|
||||
title="Connect GitHub Copilot"
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Check, Copy, ChevronRight, KeyRound, Sparkles } from 'lucide-react';
|
||||
|
||||
import { detectedPlatform, type DetectedPlatform } from '@renderer/lib/platform';
|
||||
import {
|
||||
installInstructions,
|
||||
authCommand,
|
||||
type PlatformInstallInfo,
|
||||
type InstallMethod,
|
||||
} from '@renderer/lib/cliInstallInstructions';
|
||||
|
||||
interface CliInstallGuideProps {
|
||||
onRefresh: () => void;
|
||||
isRefreshing: boolean;
|
||||
}
|
||||
|
||||
function PlatformTab({
|
||||
info,
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
info: PlatformInstallInfo;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`relative rounded-md px-3 py-1.5 text-[11px] font-semibold tracking-wide transition-all duration-200 ${
|
||||
active
|
||||
? 'bg-[var(--color-surface-3)] text-[var(--color-text-primary)] shadow-sm'
|
||||
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
|
||||
}`}
|
||||
aria-pressed={active}
|
||||
>
|
||||
{info.displayName}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandBlock({
|
||||
method,
|
||||
index,
|
||||
}: {
|
||||
method: InstallMethod;
|
||||
index: number;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
void navigator.clipboard.writeText(method.command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1800);
|
||||
}, [method.command]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group/cmd space-y-1.5"
|
||||
style={{ animationDelay: `${index * 60}ms` }}
|
||||
>
|
||||
{/* Method label row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-medium text-[var(--color-text-secondary)]">
|
||||
{method.label}
|
||||
</span>
|
||||
{method.recommended && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-[var(--color-accent)]/10 px-2 py-0.5 text-[9px] font-bold uppercase tracking-widest text-[var(--color-accent)]">
|
||||
<Sparkles className="size-2.5" />
|
||||
Recommended
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Command block */}
|
||||
<div className="relative flex items-center overflow-hidden rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-1)]">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 px-3 py-2">
|
||||
<ChevronRight className="size-3 shrink-0 text-[var(--color-accent)]/60" />
|
||||
<code className="min-w-0 select-all truncate font-mono text-[12px] leading-relaxed text-[var(--color-text-primary)]">
|
||||
{method.command}
|
||||
</code>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="flex shrink-0 items-center gap-1 border-l border-[var(--color-border-subtle)] px-2.5 py-2 text-[var(--color-text-muted)] transition-colors duration-150 hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-secondary)]"
|
||||
title="Copy command"
|
||||
aria-label={`Copy command: ${method.command}`}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-3 text-[var(--color-status-success)]" />
|
||||
) : (
|
||||
<Copy className="size-3" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CliInstallGuide({ onRefresh, isRefreshing }: CliInstallGuideProps) {
|
||||
const [activePlatform, setActivePlatform] = useState<DetectedPlatform>(detectedPlatform);
|
||||
|
||||
const activeInfo = installInstructions.find((i) => i.platform === activePlatform)!;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Step 1: Install */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex size-5 items-center justify-center rounded-full bg-[var(--color-accent)]/15 text-[10px] font-bold text-[var(--color-accent)]">
|
||||
1
|
||||
</span>
|
||||
<span className="text-[12px] font-semibold text-[var(--color-text-primary)]">
|
||||
Install the Copilot CLI
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Platform tabs */}
|
||||
<div className="flex items-center gap-1 rounded-lg bg-[var(--color-surface-2)] p-1">
|
||||
{installInstructions.map((info) => (
|
||||
<PlatformTab
|
||||
key={info.platform}
|
||||
active={activePlatform === info.platform}
|
||||
info={info}
|
||||
onClick={() => setActivePlatform(info.platform)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Commands for active platform */}
|
||||
<div className="space-y-3">
|
||||
{activeInfo.methods.map((method, i) => (
|
||||
<CommandBlock key={method.label} index={i} method={method} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 2: Authenticate */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex size-5 items-center justify-center rounded-full bg-[var(--color-accent)]/15 text-[10px] font-bold text-[var(--color-accent)]">
|
||||
2
|
||||
</span>
|
||||
<span className="text-[12px] font-semibold text-[var(--color-text-primary)]">
|
||||
Sign in to GitHub
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AuthCommandBlock />
|
||||
</div>
|
||||
|
||||
{/* Step 3: Refresh */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex size-5 items-center justify-center rounded-full bg-[var(--color-accent)]/15 text-[10px] font-bold text-[var(--color-accent)]">
|
||||
3
|
||||
</span>
|
||||
<span className="text-[12px] font-semibold text-[var(--color-text-primary)]">
|
||||
Refresh connection
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg border border-[var(--color-accent)]/30 bg-[var(--color-accent)]/8 px-3 py-2 text-[12px] font-medium text-[var(--color-accent)] transition-all duration-200 hover:bg-[var(--color-accent)]/15 disabled:opacity-50"
|
||||
>
|
||||
{isRefreshing ? 'Checking…' : 'Check connection'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthCommandBlock() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
void navigator.clipboard.writeText(authCommand);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1800);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center overflow-hidden rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-1)]">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 px-3 py-2">
|
||||
<KeyRound className="size-3 shrink-0 text-[var(--color-accent)]/60" />
|
||||
<code className="min-w-0 select-all truncate font-mono text-[12px] leading-relaxed text-[var(--color-text-primary)]">
|
||||
{authCommand}
|
||||
</code>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="flex shrink-0 items-center gap-1 border-l border-[var(--color-border-subtle)] px-2.5 py-2 text-[var(--color-text-muted)] transition-colors duration-150 hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-secondary)]"
|
||||
title="Copy command"
|
||||
aria-label="Copy authentication command"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-3 text-[var(--color-status-success)]" />
|
||||
) : (
|
||||
<Copy className="size-3" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { DetectedPlatform } from './platform';
|
||||
|
||||
export interface InstallMethod {
|
||||
label: string;
|
||||
command: string;
|
||||
recommended?: boolean;
|
||||
}
|
||||
|
||||
export interface PlatformInstallInfo {
|
||||
platform: DetectedPlatform;
|
||||
displayName: string;
|
||||
methods: InstallMethod[];
|
||||
}
|
||||
|
||||
export const installInstructions: PlatformInstallInfo[] = [
|
||||
{
|
||||
platform: 'macos',
|
||||
displayName: 'macOS',
|
||||
methods: [
|
||||
{ label: 'Homebrew', command: 'brew install copilot-cli', recommended: true },
|
||||
{ label: 'Install script', command: 'curl -fsSL https://gh.io/copilot-install | bash' },
|
||||
{ label: 'npm', command: 'npm install -g @github/copilot' },
|
||||
],
|
||||
},
|
||||
{
|
||||
platform: 'windows',
|
||||
displayName: 'Windows',
|
||||
methods: [
|
||||
{ label: 'WinGet', command: 'winget install GitHub.Copilot', recommended: true },
|
||||
{ label: 'npm', command: 'npm install -g @github/copilot' },
|
||||
],
|
||||
},
|
||||
{
|
||||
platform: 'linux',
|
||||
displayName: 'Linux',
|
||||
methods: [
|
||||
{ label: 'Install script', command: 'curl -fsSL https://gh.io/copilot-install | bash', recommended: true },
|
||||
{ label: 'Homebrew', command: 'brew install copilot-cli' },
|
||||
{ label: 'npm', command: 'npm install -g @github/copilot' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const authCommand = 'copilot auth login';
|
||||
|
||||
export function getInstallInfoForPlatform(platform: DetectedPlatform): PlatformInstallInfo {
|
||||
return installInstructions.find((i) => i.platform === platform)!;
|
||||
}
|
||||
@@ -1 +1,8 @@
|
||||
export type DetectedPlatform = 'macos' | 'windows' | 'linux';
|
||||
|
||||
export const isMac = navigator.platform.startsWith('Mac');
|
||||
export const isWindows = navigator.platform.startsWith('Win');
|
||||
export const isLinux = !isMac && !isWindows;
|
||||
|
||||
export const detectedPlatform: DetectedPlatform =
|
||||
isMac ? 'macos' : isWindows ? 'windows' : 'linux';
|
||||
|
||||
Reference in New Issue
Block a user