mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 04:17:54 +00:00
feat(ui): add copilot dashboard page
- use-copilot.ts: React hook for copilot state management - copilot-status-card.tsx: status display with auth/daemon controls - copilot-config-form.tsx: configuration form with model selector - copilot.tsx: main page combining status and config - add /copilot route and sidebar navigation
This commit is contained in:
@@ -21,6 +21,7 @@ const CliproxyPage = lazy(() =>
|
|||||||
const CliproxyControlPanelPage = lazy(() =>
|
const CliproxyControlPanelPage = lazy(() =>
|
||||||
import('@/pages/cliproxy-control-panel').then((m) => ({ default: m.CliproxyControlPanelPage }))
|
import('@/pages/cliproxy-control-panel').then((m) => ({ default: m.CliproxyControlPanelPage }))
|
||||||
);
|
);
|
||||||
|
const CopilotPage = lazy(() => import('@/pages/copilot').then((m) => ({ default: m.CopilotPage })));
|
||||||
const AccountsPage = lazy(() =>
|
const AccountsPage = lazy(() =>
|
||||||
import('@/pages/accounts').then((m) => ({ default: m.AccountsPage }))
|
import('@/pages/accounts').then((m) => ({ default: m.AccountsPage }))
|
||||||
);
|
);
|
||||||
@@ -43,6 +44,7 @@ export default function App() {
|
|||||||
<Route path="/api" element={<ApiPage />} />
|
<Route path="/api" element={<ApiPage />} />
|
||||||
<Route path="/cliproxy" element={<CliproxyPage />} />
|
<Route path="/cliproxy" element={<CliproxyPage />} />
|
||||||
<Route path="/cliproxy/control-panel" element={<CliproxyControlPanelPage />} />
|
<Route path="/cliproxy/control-panel" element={<CliproxyControlPanelPage />} />
|
||||||
|
<Route path="/copilot" element={<CopilotPage />} />
|
||||||
<Route path="/accounts" element={<AccountsPage />} />
|
<Route path="/accounts" element={<AccountsPage />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
<Route path="/health" element={<HealthPage />} />
|
<Route path="/health" element={<HealthPage />} />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Gauge,
|
Gauge,
|
||||||
|
Github,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@@ -54,6 +55,7 @@ const navGroups = [
|
|||||||
{ path: '/cliproxy/control-panel', icon: Gauge, label: 'Control Panel' },
|
{ path: '/cliproxy/control-panel', icon: Gauge, label: 'Control Panel' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{ path: '/copilot', icon: Github, label: 'GitHub Copilot' },
|
||||||
{
|
{
|
||||||
path: '/accounts',
|
path: '/accounts',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Copilot Config Form
|
||||||
|
*
|
||||||
|
* Form for configuring GitHub Copilot integration settings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { useCopilot } from '@/hooks/use-copilot';
|
||||||
|
import { Loader2, Save } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export function CopilotConfigForm() {
|
||||||
|
const { config, configLoading, models, modelsLoading, updateConfigAsync, isUpdating } =
|
||||||
|
useCopilot();
|
||||||
|
|
||||||
|
// Track local overrides for form fields
|
||||||
|
const [localOverrides, setLocalOverrides] = useState<{
|
||||||
|
enabled?: boolean;
|
||||||
|
autoStart?: boolean;
|
||||||
|
port?: number;
|
||||||
|
accountType?: 'individual' | 'business' | 'enterprise';
|
||||||
|
model?: string;
|
||||||
|
rateLimit?: string;
|
||||||
|
waitOnLimit?: boolean;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
// Use local overrides if set, otherwise use config values
|
||||||
|
const enabled = localOverrides.enabled ?? config?.enabled ?? false;
|
||||||
|
const autoStart = localOverrides.autoStart ?? config?.auto_start ?? false;
|
||||||
|
const port = localOverrides.port ?? config?.port ?? 4141;
|
||||||
|
const accountType = localOverrides.accountType ?? config?.account_type ?? 'individual';
|
||||||
|
const model = localOverrides.model ?? config?.model ?? 'claude-opus-4-5-20250514';
|
||||||
|
const rateLimit = localOverrides.rateLimit ?? config?.rate_limit?.toString() ?? '';
|
||||||
|
const waitOnLimit = localOverrides.waitOnLimit ?? config?.wait_on_limit ?? true;
|
||||||
|
|
||||||
|
const updateField = <K extends keyof typeof localOverrides>(
|
||||||
|
key: K,
|
||||||
|
value: (typeof localOverrides)[K]
|
||||||
|
) => {
|
||||||
|
setLocalOverrides((prev) => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
await updateConfigAsync({
|
||||||
|
enabled,
|
||||||
|
auto_start: autoStart,
|
||||||
|
port,
|
||||||
|
account_type: accountType,
|
||||||
|
model,
|
||||||
|
rate_limit: rateLimit ? parseInt(rateLimit, 10) : null,
|
||||||
|
wait_on_limit: waitOnLimit,
|
||||||
|
});
|
||||||
|
// Clear local overrides after successful save
|
||||||
|
setLocalOverrides({});
|
||||||
|
toast.success('Copilot configuration has been updated.');
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to save settings.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (configLoading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Configuration</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Configuration</CardTitle>
|
||||||
|
<CardDescription>Configure GitHub Copilot integration settings</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Enable Toggle */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="enabled">Enable Copilot</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Allow using GitHub Copilot subscription with Claude Code
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="enabled"
|
||||||
|
checked={enabled}
|
||||||
|
onCheckedChange={(v) => updateField('enabled', v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Port */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="port">Port</Label>
|
||||||
|
<Input
|
||||||
|
id="port"
|
||||||
|
type="number"
|
||||||
|
value={port}
|
||||||
|
onChange={(e) => updateField('port', parseInt(e.target.value, 10))}
|
||||||
|
min={1024}
|
||||||
|
max={65535}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Local port for copilot-api proxy (default: 4141)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Account Type */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="account-type">Account Type</Label>
|
||||||
|
<Select
|
||||||
|
value={accountType}
|
||||||
|
onValueChange={(v) => updateField('accountType', v as typeof accountType)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="account-type">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="individual">Individual</SelectItem>
|
||||||
|
<SelectItem value="business">Business</SelectItem>
|
||||||
|
<SelectItem value="enterprise">Enterprise</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-sm text-muted-foreground">Your GitHub Copilot subscription type</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="model">Model</Label>
|
||||||
|
<Select value={model} onValueChange={(v) => updateField('model', v)}>
|
||||||
|
<SelectTrigger id="model">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<SelectItem value={model} disabled>
|
||||||
|
Loading...
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
models.map((m) => (
|
||||||
|
<SelectItem key={m.id} value={m.id}>
|
||||||
|
{m.name} ({m.provider})
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Model to use via Copilot (default: Claude Opus 4.5)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rate Limit */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="rate-limit">Rate Limit (seconds)</Label>
|
||||||
|
<Input
|
||||||
|
id="rate-limit"
|
||||||
|
type="number"
|
||||||
|
value={rateLimit}
|
||||||
|
onChange={(e) => updateField('rateLimit', e.target.value)}
|
||||||
|
placeholder="No limit"
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Minimum seconds between requests (leave empty for no limit)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wait on Limit */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="wait-on-limit">Wait on Rate Limit</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Wait instead of error when rate limit is hit
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="wait-on-limit"
|
||||||
|
checked={waitOnLimit}
|
||||||
|
onCheckedChange={(v) => updateField('waitOnLimit', v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auto Start */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="auto-start">Auto-start Daemon</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Automatically start copilot-api when using profile
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="auto-start"
|
||||||
|
checked={autoStart}
|
||||||
|
onCheckedChange={(v) => updateField('autoStart', v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<Button onClick={handleSave} disabled={isUpdating} className="w-full">
|
||||||
|
{isUpdating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
Save Changes
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Copilot Status Card
|
||||||
|
*
|
||||||
|
* Displays GitHub Copilot integration status overview.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export function CopilotStatusCard() {
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
statusLoading,
|
||||||
|
startAuth,
|
||||||
|
isAuthenticating,
|
||||||
|
startDaemon,
|
||||||
|
isStartingDaemon,
|
||||||
|
stopDaemon,
|
||||||
|
isStoppingDaemon,
|
||||||
|
} = useCopilot();
|
||||||
|
|
||||||
|
if (statusLoading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>GitHub Copilot Status</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>GitHub Copilot Status</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-muted-foreground">Failed to load status</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
GitHub Copilot Status
|
||||||
|
{status.enabled ? (
|
||||||
|
<Badge variant="default">Enabled</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary">Disabled</Badge>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Use your GitHub Copilot subscription with Claude Code</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Warning Banner */}
|
||||||
|
<div className="flex items-start gap-2 rounded-md border border-yellow-500/20 bg-yellow-500/10 p-3">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-yellow-500 shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-yellow-700 dark:text-yellow-300">
|
||||||
|
This uses a reverse-engineered API. Excessive usage may trigger GitHub abuse detection.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Grid */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
{/* Installed */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{status.installed ? (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-5 w-5 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm">
|
||||||
|
copilot-api {status.installed ? 'Installed' : 'Not Installed'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Authenticated */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{status.authenticated ? (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-5 w-5 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm">
|
||||||
|
{status.authenticated ? 'Authenticated' : 'Not Authenticated'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Daemon */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{status.daemon_running ? (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-5 w-5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm">Daemon {status.daemon_running ? 'Running' : 'Stopped'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Info */}
|
||||||
|
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
|
||||||
|
<span>Port: {status.port}</span>
|
||||||
|
<span>Model: {status.model}</span>
|
||||||
|
<span>Auto-start: {status.auto_start ? 'Yes' : 'No'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex flex-wrap gap-2 pt-2">
|
||||||
|
{!status.authenticated && (
|
||||||
|
<Button
|
||||||
|
onClick={() => startAuth()}
|
||||||
|
disabled={isAuthenticating || !status.installed}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Authenticating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Authenticate with GitHub'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status.daemon_running ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => stopDaemon()}
|
||||||
|
disabled={isStoppingDaemon}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isStoppingDaemon ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Stopping...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Stop Daemon'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => startDaemon()}
|
||||||
|
disabled={isStartingDaemon || !status.authenticated}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isStartingDaemon ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Starting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Start Daemon'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* Copilot API Hook
|
||||||
|
*
|
||||||
|
* React hook for managing GitHub Copilot integration state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
const API_BASE = '/api';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export interface CopilotStatus {
|
||||||
|
enabled: boolean;
|
||||||
|
installed: boolean;
|
||||||
|
authenticated: boolean;
|
||||||
|
daemon_running: boolean;
|
||||||
|
port: number;
|
||||||
|
model: string;
|
||||||
|
account_type: 'individual' | 'business' | 'enterprise';
|
||||||
|
auto_start: boolean;
|
||||||
|
rate_limit: number | null;
|
||||||
|
wait_on_limit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopilotConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
auto_start: boolean;
|
||||||
|
port: number;
|
||||||
|
account_type: 'individual' | 'business' | 'enterprise';
|
||||||
|
rate_limit: number | null;
|
||||||
|
wait_on_limit: boolean;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopilotModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
provider: 'openai' | 'anthropic';
|
||||||
|
isDefault?: boolean;
|
||||||
|
isCurrent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API functions
|
||||||
|
async function fetchCopilotStatus(): Promise<CopilotStatus> {
|
||||||
|
const res = await fetch(`${API_BASE}/copilot/status`);
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch copilot status');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCopilotConfig(): Promise<CopilotConfig> {
|
||||||
|
const res = await fetch(`${API_BASE}/copilot/config`);
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch copilot config');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCopilotModels(): Promise<{ models: CopilotModel[]; current: string }> {
|
||||||
|
const res = await fetch(`${API_BASE}/copilot/models`);
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch copilot models');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCopilotConfig(config: Partial<CopilotConfig>): Promise<{ success: boolean }> {
|
||||||
|
const res = await fetch(`${API_BASE}/copilot/config`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(config),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to update copilot config');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startCopilotAuth(): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const res = await fetch(`${API_BASE}/copilot/auth/start`, { method: 'POST' });
|
||||||
|
if (!res.ok) throw new Error('Failed to start auth');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startCopilotDaemon(): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||||
|
const res = await fetch(`${API_BASE}/copilot/daemon/start`, { method: 'POST' });
|
||||||
|
if (!res.ok) throw new Error('Failed to start daemon');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopCopilotDaemon(): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const res = await fetch(`${API_BASE}/copilot/daemon/stop`, { method: 'POST' });
|
||||||
|
if (!res.ok) throw new Error('Failed to stop daemon');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook
|
||||||
|
export function useCopilot() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
const statusQuery = useQuery({
|
||||||
|
queryKey: ['copilot-status'],
|
||||||
|
queryFn: fetchCopilotStatus,
|
||||||
|
refetchInterval: 5000, // Refresh every 5 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
const configQuery = useQuery({
|
||||||
|
queryKey: ['copilot-config'],
|
||||||
|
queryFn: fetchCopilotConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelsQuery = useQuery({
|
||||||
|
queryKey: ['copilot-models'],
|
||||||
|
queryFn: fetchCopilotModels,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
const updateConfigMutation = useMutation({
|
||||||
|
mutationFn: updateCopilotConfig,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['copilot-status'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['copilot-config'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const startAuthMutation = useMutation({
|
||||||
|
mutationFn: startCopilotAuth,
|
||||||
|
onSuccess: () => {
|
||||||
|
// Delay refetch to allow auth to complete
|
||||||
|
setTimeout(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['copilot-status'] });
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const startDaemonMutation = useMutation({
|
||||||
|
mutationFn: startCopilotDaemon,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['copilot-status'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopDaemonMutation = useMutation({
|
||||||
|
mutationFn: stopCopilotDaemon,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['copilot-status'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Status
|
||||||
|
status: statusQuery.data,
|
||||||
|
statusLoading: statusQuery.isLoading,
|
||||||
|
statusError: statusQuery.error,
|
||||||
|
refetchStatus: statusQuery.refetch,
|
||||||
|
|
||||||
|
// Config
|
||||||
|
config: configQuery.data,
|
||||||
|
configLoading: configQuery.isLoading,
|
||||||
|
|
||||||
|
// Models
|
||||||
|
models: modelsQuery.data?.models ?? [],
|
||||||
|
currentModel: modelsQuery.data?.current,
|
||||||
|
modelsLoading: modelsQuery.isLoading,
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
updateConfig: updateConfigMutation.mutate,
|
||||||
|
updateConfigAsync: updateConfigMutation.mutateAsync,
|
||||||
|
isUpdating: updateConfigMutation.isPending,
|
||||||
|
|
||||||
|
startAuth: startAuthMutation.mutate,
|
||||||
|
isAuthenticating: startAuthMutation.isPending,
|
||||||
|
|
||||||
|
startDaemon: startDaemonMutation.mutate,
|
||||||
|
isStartingDaemon: startDaemonMutation.isPending,
|
||||||
|
|
||||||
|
stopDaemon: stopDaemonMutation.mutate,
|
||||||
|
isStoppingDaemon: stopDaemonMutation.isPending,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copilot Page
|
||||||
|
*
|
||||||
|
* GitHub Copilot integration settings page.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CopilotStatusCard } from '@/components/copilot/copilot-status-card';
|
||||||
|
import { CopilotConfigForm } from '@/components/copilot/copilot-config-form';
|
||||||
|
|
||||||
|
export function CopilotPage() {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-6 space-y-6 max-w-4xl">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold">GitHub Copilot</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Use your GitHub Copilot subscription with Claude Code
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CopilotStatusCard />
|
||||||
|
<CopilotConfigForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user