diff --git a/src/renderer/components/CopilotStatusCard.tsx b/src/renderer/components/CopilotStatusCard.tsx index fc3763a..d3651d2 100644 --- a/src/renderer/components/CopilotStatusCard.tsx +++ b/src/renderer/components/CopilotStatusCard.tsx @@ -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({ )} - {/* Action hint for non-ready states */} - {!isHealthy && config.actionLabel && ( + {/* Installation guide for missing CLI */} + {connection.status === 'copilot-cli-missing' && ( +
+ +
+ )} + + {/* Action hint for other non-ready states */} + {!isHealthy && connection.status !== 'copilot-cli-missing' && config.actionLabel && (
{config.actionIcon}

{config.actionLabel}

diff --git a/src/renderer/components/WelcomePane.tsx b/src/renderer/components/WelcomePane.tsx index 9367615..252fcb8 100644 --- a/src/renderer/components/WelcomePane.tsx +++ b/src/renderer/components/WelcomePane.tsx @@ -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 ( + + ); +} + export function WelcomePane({ hasProjects, connectionStatus, @@ -165,7 +195,10 @@ export function WelcomePane({ {/* Action cards */} {/* Primary CTA adapts to state */} - {!isConnected && ( + {connectionStatus === 'copilot-cli-missing' && ( + + )} + {!isConnected && connectionStatus !== 'copilot-cli-missing' && ( } title="Connect GitHub Copilot" diff --git a/src/renderer/components/settings/CliInstallGuide.tsx b/src/renderer/components/settings/CliInstallGuide.tsx new file mode 100644 index 0000000..5a60b8e --- /dev/null +++ b/src/renderer/components/settings/CliInstallGuide.tsx @@ -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 ( + + ); +} + +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 ( +
+ {/* Method label row */} +
+ + {method.label} + + {method.recommended && ( + + + Recommended + + )} +
+ + {/* Command block */} +
+
+ + + {method.command} + +
+ +
+
+ ); +} + +export function CliInstallGuide({ onRefresh, isRefreshing }: CliInstallGuideProps) { + const [activePlatform, setActivePlatform] = useState(detectedPlatform); + + const activeInfo = installInstructions.find((i) => i.platform === activePlatform)!; + + return ( +
+ {/* Step 1: Install */} +
+
+ + 1 + + + Install the Copilot CLI + +
+ + {/* Platform tabs */} +
+ {installInstructions.map((info) => ( + setActivePlatform(info.platform)} + /> + ))} +
+ + {/* Commands for active platform */} +
+ {activeInfo.methods.map((method, i) => ( + + ))} +
+
+ + {/* Step 2: Authenticate */} +
+
+ + 2 + + + Sign in to GitHub + +
+ + +
+ + {/* Step 3: Refresh */} +
+
+ + 3 + + + Refresh connection + +
+ + +
+
+ ); +} + +function AuthCommandBlock() { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(() => { + void navigator.clipboard.writeText(authCommand); + setCopied(true); + setTimeout(() => setCopied(false), 1800); + }, []); + + return ( +
+
+ + + {authCommand} + +
+ +
+ ); +} diff --git a/src/renderer/lib/cliInstallInstructions.ts b/src/renderer/lib/cliInstallInstructions.ts new file mode 100644 index 0000000..b3198c9 --- /dev/null +++ b/src/renderer/lib/cliInstallInstructions.ts @@ -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)!; +} diff --git a/src/renderer/lib/platform.ts b/src/renderer/lib/platform.ts index 954617b..edcc96d 100644 --- a/src/renderer/lib/platform.ts +++ b/src/renderer/lib/platform.ts @@ -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';