feat(ui): add copilot-api install button and version display

- add CopilotInfo type and install mutation to useCopilot hook

- show version in status card when installed

- add Install button with loading state when not installed
This commit is contained in:
kaitranntt
2025-12-18 02:27:35 -05:00
parent fee241d00b
commit f813ad06f6
2 changed files with 73 additions and 2 deletions
@@ -8,7 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useCopilot } from '@/hooks/use-copilot';
import { CheckCircle2, XCircle, AlertTriangle, Loader2 } from 'lucide-react';
import { CheckCircle2, XCircle, AlertTriangle, Loader2, Download } from 'lucide-react';
export function CopilotStatusCard() {
const {
@@ -20,6 +20,8 @@ export function CopilotStatusCard() {
isStartingDaemon,
stopDaemon,
isStoppingDaemon,
install,
isInstalling,
} = useCopilot();
if (statusLoading) {
@@ -80,7 +82,7 @@ export function CopilotStatusCard() {
<XCircle className="h-5 w-5 text-red-500" />
)}
<span className="text-sm">
copilot-api {status.installed ? 'Installed' : 'Not Installed'}
copilot-api {status.installed ? `v${status.version}` : 'Not Installed'}
</span>
</div>
@@ -116,6 +118,22 @@ export function CopilotStatusCard() {
{/* Actions */}
<div className="flex flex-wrap gap-2 pt-2">
{!status.installed && (
<Button onClick={() => install(undefined)} disabled={isInstalling} size="sm">
{isInstalling ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Installing...
</>
) : (
<>
<Download className="mr-2 h-4 w-4" />
Install copilot-api
</>
)}
</Button>
)}
{!status.authenticated && (
<Button
onClick={() => startAuth()}
+53
View File
@@ -12,6 +12,7 @@ const API_BASE = '/api';
export interface CopilotStatus {
enabled: boolean;
installed: boolean;
version: string | null;
authenticated: boolean;
daemon_running: boolean;
port: number;
@@ -22,6 +23,20 @@ export interface CopilotStatus {
wait_on_limit: boolean;
}
export interface CopilotInfo {
installed: boolean;
version: string | null;
path: string;
pinnedVersion: string | null;
}
export interface CopilotInstallResult {
success: boolean;
installed: boolean;
version: string | null;
path: string;
}
export interface CopilotConfig {
enabled: boolean;
auto_start: boolean;
@@ -120,6 +135,22 @@ async function stopCopilotDaemon(): Promise<{ success: boolean; error?: string }
return res.json();
}
async function fetchCopilotInfo(): Promise<CopilotInfo> {
const res = await fetch(`${API_BASE}/copilot/info`);
if (!res.ok) throw new Error('Failed to fetch copilot info');
return res.json();
}
async function installCopilotApi(version?: string): Promise<CopilotInstallResult> {
const res = await fetch(`${API_BASE}/copilot/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(version ? { version } : {}),
});
if (!res.ok) throw new Error('Failed to install copilot-api');
return res.json();
}
// Hook
export function useCopilot() {
const queryClient = useQueryClient();
@@ -146,6 +177,11 @@ export function useCopilot() {
queryFn: fetchCopilotRawSettings,
});
const infoQuery = useQuery({
queryKey: ['copilot-info'],
queryFn: fetchCopilotInfo,
});
// Mutations
const updateConfigMutation = useMutation({
mutationFn: updateCopilotConfig,
@@ -189,6 +225,14 @@ export function useCopilot() {
},
});
const installMutation = useMutation({
mutationFn: installCopilotApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['copilot-status'] });
queryClient.invalidateQueries({ queryKey: ['copilot-info'] });
},
});
return {
// Status
status: statusQuery.data,
@@ -227,5 +271,14 @@ export function useCopilot() {
stopDaemon: stopDaemonMutation.mutate,
isStoppingDaemon: stopDaemonMutation.isPending,
// Install
info: infoQuery.data,
infoLoading: infoQuery.isLoading,
refetchInfo: infoQuery.refetch,
install: installMutation.mutate,
installAsync: installMutation.mutateAsync,
isInstalling: installMutation.isPending,
};
}