mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +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(() =>
|
||||
import('@/pages/cliproxy-control-panel').then((m) => ({ default: m.CliproxyControlPanelPage }))
|
||||
);
|
||||
const CopilotPage = lazy(() => import('@/pages/copilot').then((m) => ({ default: m.CopilotPage })));
|
||||
const AccountsPage = lazy(() =>
|
||||
import('@/pages/accounts').then((m) => ({ default: m.AccountsPage }))
|
||||
);
|
||||
@@ -43,6 +44,7 @@ export default function App() {
|
||||
<Route path="/api" element={<ApiPage />} />
|
||||
<Route path="/cliproxy" element={<CliproxyPage />} />
|
||||
<Route path="/cliproxy/control-panel" element={<CliproxyControlPanelPage />} />
|
||||
<Route path="/copilot" element={<CopilotPage />} />
|
||||
<Route path="/accounts" element={<AccountsPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/health" element={<HealthPage />} />
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ChevronRight,
|
||||
BarChart3,
|
||||
Gauge,
|
||||
Github,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -54,6 +55,7 @@ const navGroups = [
|
||||
{ path: '/cliproxy/control-panel', icon: Gauge, label: 'Control Panel' },
|
||||
],
|
||||
},
|
||||
{ path: '/copilot', icon: Github, label: 'GitHub Copilot' },
|
||||
{
|
||||
path: '/accounts',
|
||||
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