mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +00:00
feat(copilot): redesign config form to match CLIProxy pattern
- Split-view layout (40% left / 60% right) matching CLIProxy design - Left panel: Model Config tab with FlexibleModelSelector components, Settings tab, and Info tab - Right panel: Raw JSON editor for copilot.settings.json - Add preset buttons for quick model tier mapping - Bidirectional sync between UI and raw JSON - Simplify copilot.tsx page layout with narrower sidebar
This commit is contained in:
1 parent
882792a4fb
commit
7886259c36
2 files changed
+875
-176
No files matched your search
@@ -2,28 +2,129 @@
|
|||||||
* Copilot Config Form
|
* Copilot Config Form
|
||||||
*
|
*
|
||||||
* Form for configuring GitHub Copilot integration settings.
|
* Form for configuring GitHub Copilot integration settings.
|
||||||
|
* Split-view layout matching CLIProxy provider editor:
|
||||||
|
* - Left: Friendly UI with model mapping selectors
|
||||||
|
* - Right: Raw JSON editor for copilot.settings.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useMemo, useCallback, lazy, Suspense } from 'react';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||||
|
import { CopyButton } from '@/components/ui/copy-button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
|
SelectGroup,
|
||||||
|
SelectLabel,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useCopilot } from '@/hooks/use-copilot';
|
import { useCopilot } from '@/hooks/use-copilot';
|
||||||
import { Loader2, Save } from 'lucide-react';
|
import { Loader2, Save, Code2, X, Info, RefreshCw, Sparkles, Zap, Check } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
|
|
||||||
|
// Lazy load CodeEditor
|
||||||
|
const CodeEditor = lazy(() =>
|
||||||
|
import('@/components/code-editor').then((m) => ({ default: m.CodeEditor }))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Model presets for quick configuration
|
||||||
|
const MODEL_PRESETS = [
|
||||||
|
{
|
||||||
|
name: 'Claude Opus 4.5',
|
||||||
|
default: 'claude-opus-4-5-20250514',
|
||||||
|
opus: 'claude-opus-4-5-20250514',
|
||||||
|
sonnet: 'claude-sonnet-4-20250514',
|
||||||
|
haiku: 'claude-haiku-3-5-20250514',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Claude Sonnet 4',
|
||||||
|
default: 'claude-sonnet-4-20250514',
|
||||||
|
opus: 'claude-sonnet-4-20250514',
|
||||||
|
sonnet: 'claude-sonnet-4-20250514',
|
||||||
|
haiku: 'claude-haiku-3-5-20250514',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GPT-4o',
|
||||||
|
default: 'gpt-4o',
|
||||||
|
opus: 'gpt-4o',
|
||||||
|
sonnet: 'gpt-4o',
|
||||||
|
haiku: 'gpt-4o-mini',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface FlexibleModelSelectorProps {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
value: string | undefined;
|
||||||
|
onChange: (model: string) => void;
|
||||||
|
models: { id: string }[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FlexibleModelSelector({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
models,
|
||||||
|
disabled,
|
||||||
|
}: FlexibleModelSelectorProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium">{label}</label>
|
||||||
|
{description && <p className="text-[10px] text-muted-foreground">{description}</p>}
|
||||||
|
</div>
|
||||||
|
<Select value={value || ''} onValueChange={onChange} disabled={disabled}>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<SelectValue placeholder="Select model">
|
||||||
|
{value && <span className="truncate font-mono text-xs">{value}</span>}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-[300px]">
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel className="text-xs text-muted-foreground">
|
||||||
|
Available Models ({models.length})
|
||||||
|
</SelectLabel>
|
||||||
|
{models.map((model) => (
|
||||||
|
<SelectItem key={model.id} value={model.id}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="truncate font-mono text-xs">{model.id}</span>
|
||||||
|
{value === model.id && <Check className="w-3 h-3 text-primary ml-auto" />}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function CopilotConfigForm() {
|
export function CopilotConfigForm() {
|
||||||
const { config, configLoading, models, modelsLoading, updateConfigAsync, isUpdating } =
|
const {
|
||||||
useCopilot();
|
config,
|
||||||
|
configLoading,
|
||||||
|
models,
|
||||||
|
modelsLoading,
|
||||||
|
rawSettings,
|
||||||
|
rawSettingsLoading,
|
||||||
|
updateConfigAsync,
|
||||||
|
isUpdating,
|
||||||
|
saveRawSettingsAsync,
|
||||||
|
isSavingRawSettings,
|
||||||
|
refetchRawSettings,
|
||||||
|
} = useCopilot();
|
||||||
|
|
||||||
// Track local overrides for form fields
|
// Track local overrides for form fields
|
||||||
const [localOverrides, setLocalOverrides] = useState<{
|
const [localOverrides, setLocalOverrides] = useState<{
|
||||||
@@ -34,16 +135,26 @@ export function CopilotConfigForm() {
|
|||||||
model?: string;
|
model?: string;
|
||||||
rateLimit?: string;
|
rateLimit?: string;
|
||||||
waitOnLimit?: boolean;
|
waitOnLimit?: boolean;
|
||||||
|
opusModel?: string;
|
||||||
|
sonnetModel?: string;
|
||||||
|
haikuModel?: string;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
|
// Raw JSON editor state
|
||||||
|
const [rawJsonEdits, setRawJsonEdits] = useState<string | null>(null);
|
||||||
|
const [conflictDialog, setConflictDialog] = useState(false);
|
||||||
|
|
||||||
// Use local overrides if set, otherwise use config values
|
// Use local overrides if set, otherwise use config values
|
||||||
const enabled = localOverrides.enabled ?? config?.enabled ?? false;
|
const enabled = localOverrides.enabled ?? config?.enabled ?? false;
|
||||||
const autoStart = localOverrides.autoStart ?? config?.auto_start ?? false;
|
const autoStart = localOverrides.autoStart ?? config?.auto_start ?? false;
|
||||||
const port = localOverrides.port ?? config?.port ?? 4141;
|
const port = localOverrides.port ?? config?.port ?? 4141;
|
||||||
const accountType = localOverrides.accountType ?? config?.account_type ?? 'individual';
|
const accountType = localOverrides.accountType ?? config?.account_type ?? 'individual';
|
||||||
const model = localOverrides.model ?? config?.model ?? 'claude-opus-4-5-20250514';
|
const currentModel = localOverrides.model ?? config?.model ?? 'claude-opus-4-5-20250514';
|
||||||
const rateLimit = localOverrides.rateLimit ?? config?.rate_limit?.toString() ?? '';
|
const rateLimit = localOverrides.rateLimit ?? config?.rate_limit?.toString() ?? '';
|
||||||
const waitOnLimit = localOverrides.waitOnLimit ?? config?.wait_on_limit ?? true;
|
const waitOnLimit = localOverrides.waitOnLimit ?? config?.wait_on_limit ?? true;
|
||||||
|
const opusModel = localOverrides.opusModel ?? config?.opus_model ?? '';
|
||||||
|
const sonnetModel = localOverrides.sonnetModel ?? config?.sonnet_model ?? '';
|
||||||
|
const haikuModel = localOverrides.haikuModel ?? config?.haiku_model ?? '';
|
||||||
|
|
||||||
const updateField = <K extends keyof typeof localOverrides>(
|
const updateField = <K extends keyof typeof localOverrides>(
|
||||||
key: K,
|
key: K,
|
||||||
@@ -52,182 +163,521 @@ export function CopilotConfigForm() {
|
|||||||
setLocalOverrides((prev) => ({ ...prev, [key]: value }));
|
setLocalOverrides((prev) => ({ ...prev, [key]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Batch update for presets
|
||||||
|
const applyPreset = (preset: (typeof MODEL_PRESETS)[0]) => {
|
||||||
|
setLocalOverrides((prev) => ({
|
||||||
|
...prev,
|
||||||
|
model: preset.default,
|
||||||
|
opusModel: preset.opus,
|
||||||
|
sonnetModel: preset.sonnet,
|
||||||
|
haikuModel: preset.haiku,
|
||||||
|
}));
|
||||||
|
toast.success(`Applied "${preset.name}" preset`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Raw JSON content
|
||||||
|
const rawJsonContent = useMemo(() => {
|
||||||
|
if (rawJsonEdits !== null) return rawJsonEdits;
|
||||||
|
if (rawSettings?.settings) return JSON.stringify(rawSettings.settings, null, 2);
|
||||||
|
return '{\n "env": {}\n}';
|
||||||
|
}, [rawJsonEdits, rawSettings]);
|
||||||
|
|
||||||
|
const handleRawJsonChange = useCallback((value: string) => {
|
||||||
|
setRawJsonEdits(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Check if JSON is valid
|
||||||
|
const isRawJsonValid = useMemo(() => {
|
||||||
|
try {
|
||||||
|
JSON.parse(rawJsonContent);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [rawJsonContent]);
|
||||||
|
|
||||||
|
// Check for unsaved changes
|
||||||
|
const hasChanges = useMemo(() => {
|
||||||
|
const hasLocalChanges = Object.keys(localOverrides).length > 0;
|
||||||
|
const hasJsonChanges =
|
||||||
|
rawJsonEdits !== null && rawJsonEdits !== JSON.stringify(rawSettings?.settings, null, 2);
|
||||||
|
return hasLocalChanges || hasJsonChanges;
|
||||||
|
}, [localOverrides, rawJsonEdits, rawSettings]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
await updateConfigAsync({
|
// Save config changes
|
||||||
enabled,
|
if (Object.keys(localOverrides).length > 0) {
|
||||||
auto_start: autoStart,
|
await updateConfigAsync({
|
||||||
port,
|
enabled,
|
||||||
account_type: accountType,
|
auto_start: autoStart,
|
||||||
model,
|
port,
|
||||||
rate_limit: rateLimit ? parseInt(rateLimit, 10) : null,
|
account_type: accountType,
|
||||||
wait_on_limit: waitOnLimit,
|
model: currentModel,
|
||||||
});
|
rate_limit: rateLimit ? parseInt(rateLimit, 10) : null,
|
||||||
// Clear local overrides after successful save
|
wait_on_limit: waitOnLimit,
|
||||||
|
opus_model: opusModel || undefined,
|
||||||
|
sonnet_model: sonnetModel || undefined,
|
||||||
|
haiku_model: haikuModel || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save raw JSON changes
|
||||||
|
if (rawJsonEdits !== null && isRawJsonValid) {
|
||||||
|
const settingsToSave = JSON.parse(rawJsonContent);
|
||||||
|
await saveRawSettingsAsync({
|
||||||
|
settings: settingsToSave,
|
||||||
|
expectedMtime: rawSettings?.mtime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear local state
|
||||||
setLocalOverrides({});
|
setLocalOverrides({});
|
||||||
toast.success('Copilot configuration has been updated.');
|
setRawJsonEdits(null);
|
||||||
} catch {
|
toast.success('Copilot configuration saved');
|
||||||
toast.error('Failed to save settings.');
|
} catch (error) {
|
||||||
|
if ((error as Error).message === 'CONFLICT') {
|
||||||
|
setConflictDialog(true);
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to save settings');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (configLoading) {
|
const handleConflictResolve = async (overwrite: boolean) => {
|
||||||
|
setConflictDialog(false);
|
||||||
|
if (overwrite) {
|
||||||
|
await refetchRawSettings();
|
||||||
|
handleSave();
|
||||||
|
} else {
|
||||||
|
setRawJsonEdits(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (configLoading || rawSettingsLoading) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className="space-y-6">
|
||||||
<CardHeader>
|
<Skeleton className="h-10 w-full" />
|
||||||
<CardTitle>Configuration</CardTitle>
|
<Skeleton className="h-10 w-full" />
|
||||||
</CardHeader>
|
<Skeleton className="h-10 w-full" />
|
||||||
<CardContent className="flex items-center justify-center py-8">
|
<Skeleton className="h-10 w-full" />
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// Left column: Friendly UI
|
||||||
<Card>
|
const renderFriendlyUI = () => (
|
||||||
<CardHeader>
|
<div className="h-full flex flex-col">
|
||||||
<CardTitle>Configuration</CardTitle>
|
<Tabs defaultValue="config" className="h-full flex flex-col">
|
||||||
<CardDescription>Configure GitHub Copilot integration settings</CardDescription>
|
<div className="px-4 pt-4 shrink-0">
|
||||||
</CardHeader>
|
<TabsList className="w-full">
|
||||||
<CardContent className="space-y-6">
|
<TabsTrigger value="config" className="flex-1">
|
||||||
{/* Enable Toggle */}
|
Model Config
|
||||||
<div className="flex items-center justify-between">
|
</TabsTrigger>
|
||||||
<div className="space-y-0.5">
|
<TabsTrigger value="settings" className="flex-1">
|
||||||
<Label htmlFor="enabled">Enable Copilot</Label>
|
Settings
|
||||||
<p className="text-sm text-muted-foreground">
|
</TabsTrigger>
|
||||||
Allow using GitHub Copilot subscription with Claude Code
|
<TabsTrigger value="info" className="flex-1">
|
||||||
</p>
|
Info
|
||||||
</div>
|
</TabsTrigger>
|
||||||
<Switch
|
</TabsList>
|
||||||
id="enabled"
|
|
||||||
checked={enabled}
|
|
||||||
onCheckedChange={(v) => updateField('enabled', v)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Port */}
|
<div className="flex-1 overflow-hidden flex flex-col">
|
||||||
<div className="space-y-2">
|
{/* Model Config Tab */}
|
||||||
<Label htmlFor="port">Port</Label>
|
<TabsContent
|
||||||
<Input
|
value="config"
|
||||||
id="port"
|
className="flex-1 mt-0 border-0 p-0 data-[state=inactive]:hidden flex flex-col overflow-hidden"
|
||||||
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">
|
<ScrollArea className="flex-1">
|
||||||
<SelectValue />
|
<div className="p-4 space-y-6">
|
||||||
</SelectTrigger>
|
{/* Quick Presets */}
|
||||||
<SelectContent>
|
<div>
|
||||||
<SelectItem value="individual">Individual</SelectItem>
|
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
|
||||||
<SelectItem value="business">Business</SelectItem>
|
<Sparkles className="w-4 h-4" />
|
||||||
<SelectItem value="enterprise">Enterprise</SelectItem>
|
Presets
|
||||||
</SelectContent>
|
</h3>
|
||||||
</Select>
|
<p className="text-xs text-muted-foreground mb-3">
|
||||||
<p className="text-sm text-muted-foreground">Your GitHub Copilot subscription type</p>
|
Apply pre-configured model mappings
|
||||||
</div>
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{MODEL_PRESETS.map((preset) => (
|
||||||
|
<Button
|
||||||
|
key={preset.name}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs h-7 gap-1"
|
||||||
|
onClick={() => applyPreset(preset)}
|
||||||
|
>
|
||||||
|
<Zap className="w-3 h-3" />
|
||||||
|
{preset.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Model */}
|
<Separator />
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="model">Model</Label>
|
{/* Model Mapping */}
|
||||||
<Select value={model} onValueChange={(v) => updateField('model', v)}>
|
<div>
|
||||||
<SelectTrigger id="model">
|
<h3 className="text-sm font-medium mb-2">Model Mapping</h3>
|
||||||
<SelectValue />
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
</SelectTrigger>
|
Configure which models to use for each tier
|
||||||
<SelectContent>
|
</p>
|
||||||
{modelsLoading ? (
|
<div className="space-y-4">
|
||||||
<SelectItem value={model} disabled>
|
<FlexibleModelSelector
|
||||||
Loading...
|
label="Default Model"
|
||||||
</SelectItem>
|
description="Used when no specific tier is requested"
|
||||||
) : (
|
value={currentModel}
|
||||||
models.map((m) => (
|
onChange={(model) => updateField('model', model)}
|
||||||
<SelectItem key={m.id} value={m.id}>
|
models={models}
|
||||||
{m.name} ({m.provider})
|
disabled={modelsLoading}
|
||||||
</SelectItem>
|
/>
|
||||||
))
|
<FlexibleModelSelector
|
||||||
|
label="Opus (Most capable)"
|
||||||
|
description="For complex reasoning tasks"
|
||||||
|
value={opusModel || currentModel}
|
||||||
|
onChange={(model) => updateField('opusModel', model)}
|
||||||
|
models={models}
|
||||||
|
disabled={modelsLoading}
|
||||||
|
/>
|
||||||
|
<FlexibleModelSelector
|
||||||
|
label="Sonnet (Balanced)"
|
||||||
|
description="Balance of speed and capability"
|
||||||
|
value={sonnetModel || currentModel}
|
||||||
|
onChange={(model) => updateField('sonnetModel', model)}
|
||||||
|
models={models}
|
||||||
|
disabled={modelsLoading}
|
||||||
|
/>
|
||||||
|
<FlexibleModelSelector
|
||||||
|
label="Haiku (Fast)"
|
||||||
|
description="Quick responses for simple tasks"
|
||||||
|
value={haikuModel || currentModel}
|
||||||
|
onChange={(model) => updateField('haikuModel', model)}
|
||||||
|
models={models}
|
||||||
|
disabled={modelsLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Settings Tab */}
|
||||||
|
<TabsContent
|
||||||
|
value="settings"
|
||||||
|
className="flex-1 mt-0 border-0 p-0 data-[state=inactive]:hidden flex flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
<div className="p-4 space-y-6">
|
||||||
|
{/* Enable Toggle */}
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="enabled" className="text-sm font-medium">
|
||||||
|
Enable Copilot
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Allow using GitHub Copilot subscription
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="enabled"
|
||||||
|
checked={enabled}
|
||||||
|
onCheckedChange={(v) => updateField('enabled', v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Settings */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium">Basic Settings</h3>
|
||||||
|
|
||||||
|
{/* Port */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="port" className="text-xs">
|
||||||
|
Port
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="port"
|
||||||
|
type="number"
|
||||||
|
value={port}
|
||||||
|
onChange={(e) => updateField('port', parseInt(e.target.value, 10))}
|
||||||
|
min={1024}
|
||||||
|
max={65535}
|
||||||
|
className="max-w-[150px] h-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Account Type */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="account-type" className="text-xs">
|
||||||
|
Account Type
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={accountType}
|
||||||
|
onValueChange={(v) => updateField('accountType', v as typeof accountType)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="account-type" className="max-w-[150px] h-8">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="individual">Individual</SelectItem>
|
||||||
|
<SelectItem value="business">Business</SelectItem>
|
||||||
|
<SelectItem value="enterprise">Enterprise</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Rate Limiting */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium">Rate Limiting</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="rate-limit" className="text-xs">
|
||||||
|
Rate Limit (seconds)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="rate-limit"
|
||||||
|
type="number"
|
||||||
|
value={rateLimit}
|
||||||
|
onChange={(e) => updateField('rateLimit', e.target.value)}
|
||||||
|
placeholder="No limit"
|
||||||
|
min={0}
|
||||||
|
className="max-w-[150px] h-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="wait-on-limit" className="text-xs">
|
||||||
|
Wait on Rate Limit
|
||||||
|
</Label>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
Wait instead of error when limit hit
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="wait-on-limit"
|
||||||
|
checked={waitOnLimit}
|
||||||
|
onCheckedChange={(v) => updateField('waitOnLimit', v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Daemon Settings */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium">Daemon Settings</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="auto-start" className="text-xs">
|
||||||
|
Auto-start Daemon
|
||||||
|
</Label>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
Start copilot-api when using profile
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="auto-start"
|
||||||
|
checked={autoStart}
|
||||||
|
onCheckedChange={(v) => updateField('autoStart', v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Info Tab */}
|
||||||
|
<TabsContent
|
||||||
|
value="info"
|
||||||
|
className="h-full mt-0 border-0 p-0 data-[state=inactive]:hidden"
|
||||||
|
>
|
||||||
|
<ScrollArea className="h-full">
|
||||||
|
<div className="p-4 space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium flex items-center gap-2 mb-3">
|
||||||
|
<Info className="w-4 h-4" />
|
||||||
|
Configuration Info
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
|
||||||
|
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
|
||||||
|
<span className="font-medium text-muted-foreground">Provider</span>
|
||||||
|
<span className="font-mono">GitHub Copilot</span>
|
||||||
|
</div>
|
||||||
|
{rawSettings && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
|
||||||
|
<span className="font-medium text-muted-foreground">File Path</span>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<code className="bg-muted px-1.5 py-0.5 rounded text-xs break-all">
|
||||||
|
{rawSettings.path}
|
||||||
|
</code>
|
||||||
|
<CopyButton value={rawSettings.path} size="icon" className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
|
||||||
|
<span className="font-medium text-muted-foreground">Status</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
rawSettings.exists
|
||||||
|
? 'w-fit text-green-600 border-green-200 bg-green-50'
|
||||||
|
: 'w-fit text-muted-foreground'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{rawSettings.exists ? 'File exists' : 'Using defaults'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-3">Quick Usage</h3>
|
||||||
|
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
|
||||||
|
<UsageCommand label="Run with Copilot" command="ccs copilot" />
|
||||||
|
<UsageCommand label="Authenticate" command="ccs copilot auth" />
|
||||||
|
<UsageCommand label="Start daemon" command="ccs copilot --start" />
|
||||||
|
<UsageCommand label="Stop daemon" command="ccs copilot --stop" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Right column: Raw JSON Editor
|
||||||
|
const renderRawEditor = () => (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||||
|
<span className="ml-2 text-muted-foreground">Loading editor...</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
{!isRawJsonValid && rawJsonEdits !== null && (
|
||||||
|
<div className="mb-2 px-3 py-2 bg-destructive/10 text-destructive text-sm rounded-md flex items-center gap-2 mx-6 mt-4 shrink-0">
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
Invalid JSON syntax
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 overflow-hidden px-6 pb-6 pt-4">
|
||||||
|
<div className="h-full border rounded-md overflow-hidden bg-background">
|
||||||
|
<CodeEditor
|
||||||
|
value={rawJsonContent}
|
||||||
|
onChange={handleRawJsonChange}
|
||||||
|
language="json"
|
||||||
|
minHeight="100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-4 border-b bg-background flex items-center justify-between shrink-0">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-lg font-semibold">Copilot Configuration</h2>
|
||||||
|
{rawSettings && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
copilot.settings.json
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</div>
|
||||||
</Select>
|
{rawSettings && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Model to use via Copilot (default: Claude Opus 4.5)
|
Last modified:{' '}
|
||||||
</p>
|
{rawSettings.exists ? new Date(rawSettings.mtime).toLocaleString() : 'Never saved'}
|
||||||
</div>
|
</p>
|
||||||
|
)}
|
||||||
{/* 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>
|
</div>
|
||||||
<Switch
|
|
||||||
id="wait-on-limit"
|
|
||||||
checked={waitOnLimit}
|
|
||||||
onCheckedChange={(v) => updateField('waitOnLimit', v)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => refetchRawSettings()}
|
||||||
|
disabled={rawSettingsLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${rawSettingsLoading ? 'animate-spin' : ''}`} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isUpdating || isSavingRawSettings || !hasChanges || !isRawJsonValid}
|
||||||
|
>
|
||||||
|
{isUpdating || isSavingRawSettings ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="w-4 h-4 mr-1" />
|
||||||
|
Save
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Auto Start */}
|
{/* Split Layout (40% Left / 60% Right) */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex-1 grid grid-cols-[40%_60%] divide-x overflow-hidden">
|
||||||
<div className="space-y-0.5">
|
{/* Left Column: Friendly UI */}
|
||||||
<Label htmlFor="auto-start">Auto-start Daemon</Label>
|
<div className="flex flex-col overflow-hidden bg-muted/5">{renderFriendlyUI()}</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Automatically start copilot-api when using profile
|
{/* Right Column: Raw Editor */}
|
||||||
</p>
|
<div className="flex flex-col overflow-hidden">
|
||||||
|
<div className="px-6 py-2 bg-muted/30 border-b flex items-center gap-2 shrink-0 h-[45px]">
|
||||||
|
<Code2 className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Raw Configuration (JSON)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
{renderRawEditor()}
|
||||||
id="auto-start"
|
|
||||||
checked={autoStart}
|
|
||||||
onCheckedChange={(v) => updateField('autoStart', v)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Save Button */}
|
<ConfirmDialog
|
||||||
<Button onClick={handleSave} disabled={isUpdating} className="w-full">
|
open={conflictDialog}
|
||||||
{isUpdating ? (
|
title="File Modified Externally"
|
||||||
<>
|
description="This settings file was modified by another process. Overwrite with your changes or discard?"
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
confirmText="Overwrite"
|
||||||
Saving...
|
variant="destructive"
|
||||||
</>
|
onConfirm={() => handleConflictResolve(true)}
|
||||||
) : (
|
onCancel={() => handleConflictResolve(false)}
|
||||||
<>
|
/>
|
||||||
<Save className="mr-2 h-4 w-4" />
|
</div>
|
||||||
Save Changes
|
);
|
||||||
</>
|
}
|
||||||
)}
|
|
||||||
</Button>
|
/** Usage command with copy button */
|
||||||
</CardContent>
|
function UsageCommand({ label, command }: { label: string; command: string }) {
|
||||||
</Card>
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground">{label}</label>
|
||||||
|
<div className="mt-1 flex gap-2">
|
||||||
|
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
|
||||||
|
{command}
|
||||||
|
</code>
|
||||||
|
<CopyButton value={command} size="icon" className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+263
-14
@@ -1,24 +1,273 @@
|
|||||||
/**
|
/**
|
||||||
* Copilot Page
|
* Copilot Page - Master-Detail Layout
|
||||||
*
|
* Left sidebar: Status overview + Quick actions
|
||||||
* GitHub Copilot integration settings page.
|
* Right panel: Split-view configuration form (matches CLIProxy design)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CopilotStatusCard } from '@/components/copilot/copilot-status-card';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import {
|
||||||
|
Github,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
RefreshCw,
|
||||||
|
Power,
|
||||||
|
PowerOff,
|
||||||
|
Key,
|
||||||
|
Server,
|
||||||
|
Cpu,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useCopilot } from '@/hooks/use-copilot';
|
||||||
import { CopilotConfigForm } from '@/components/copilot/copilot-config-form';
|
import { CopilotConfigForm } from '@/components/copilot/copilot-config-form';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export function CopilotPage() {
|
// Status section component
|
||||||
|
function StatusSection({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-6 space-y-6 max-w-4xl">
|
<div className="space-y-2">
|
||||||
<div>
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide px-3">
|
||||||
<h1 className="text-2xl font-bold">GitHub Copilot</h1>
|
{title}
|
||||||
<p className="text-muted-foreground">
|
</div>
|
||||||
Use your GitHub Copilot subscription with Claude Code
|
<div className="space-y-1">{children}</div>
|
||||||
</p>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status item component
|
||||||
|
function StatusItem({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
variant = 'default',
|
||||||
|
}: {
|
||||||
|
icon: React.ElementType;
|
||||||
|
label: string;
|
||||||
|
status: boolean;
|
||||||
|
statusText?: string;
|
||||||
|
variant?: 'default' | 'warning';
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 px-3 py-2 rounded-lg bg-muted/50">
|
||||||
|
<Icon className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="text-sm">{label}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{status ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle2
|
||||||
|
className={cn(
|
||||||
|
'w-4 h-4',
|
||||||
|
variant === 'warning' ? 'text-yellow-500' : 'text-green-500'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-xs',
|
||||||
|
variant === 'warning' ? 'text-yellow-500' : 'text-green-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{statusText || 'Yes'}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<XCircle className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-xs text-muted-foreground">{statusText || 'No'}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state when loading
|
||||||
|
function LoadingSidebar() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 p-4">
|
||||||
|
<Skeleton className="h-8 w-full" />
|
||||||
|
<Skeleton className="h-12 w-full" />
|
||||||
|
<Skeleton className="h-12 w-full" />
|
||||||
|
<Skeleton className="h-12 w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CopilotPage() {
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
statusLoading,
|
||||||
|
refetchStatus,
|
||||||
|
startAuth,
|
||||||
|
isAuthenticating,
|
||||||
|
startDaemon,
|
||||||
|
isStartingDaemon,
|
||||||
|
stopDaemon,
|
||||||
|
isStoppingDaemon,
|
||||||
|
} = useCopilot();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[calc(100vh-100px)] flex">
|
||||||
|
{/* Left Sidebar - Status Overview */}
|
||||||
|
<div className="w-64 border-r flex flex-col bg-muted/30 shrink-0">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b bg-background">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Github className="w-5 h-5 text-primary" />
|
||||||
|
<h1 className="font-semibold">Copilot</h1>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => refetchStatus()}
|
||||||
|
disabled={statusLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn('w-4 h-4', statusLoading && 'animate-spin')} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">GitHub Copilot proxy</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Overview */}
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
{statusLoading ? (
|
||||||
|
<LoadingSidebar />
|
||||||
|
) : (
|
||||||
|
<div className="p-3 space-y-4">
|
||||||
|
{/* Warning Banner */}
|
||||||
|
<div className="flex items-start gap-2 rounded-md border border-yellow-500/20 bg-yellow-500/10 p-2.5">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-yellow-500 shrink-0 mt-0.5" />
|
||||||
|
<p className="text-xs text-yellow-700 dark:text-yellow-300">
|
||||||
|
Reverse-engineered API
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Integration Status */}
|
||||||
|
<StatusSection title="Integration">
|
||||||
|
<StatusItem
|
||||||
|
icon={Power}
|
||||||
|
label="Enabled"
|
||||||
|
status={status?.enabled ?? false}
|
||||||
|
statusText={status?.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
/>
|
||||||
|
<StatusItem
|
||||||
|
icon={Server}
|
||||||
|
label="copilot-api"
|
||||||
|
status={status?.installed ?? false}
|
||||||
|
statusText={status?.installed ? 'Installed' : 'Missing'}
|
||||||
|
/>
|
||||||
|
</StatusSection>
|
||||||
|
|
||||||
|
{/* Authentication */}
|
||||||
|
<StatusSection title="Auth">
|
||||||
|
<StatusItem
|
||||||
|
icon={Key}
|
||||||
|
label="GitHub"
|
||||||
|
status={status?.authenticated ?? false}
|
||||||
|
statusText={status?.authenticated ? 'Connected' : 'Not Connected'}
|
||||||
|
/>
|
||||||
|
{!status?.authenticated && status?.installed && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full mt-2"
|
||||||
|
onClick={() => startAuth()}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
>
|
||||||
|
{isAuthenticating ? 'Authenticating...' : 'Authenticate'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</StatusSection>
|
||||||
|
|
||||||
|
{/* Daemon */}
|
||||||
|
<StatusSection title="Daemon">
|
||||||
|
<StatusItem
|
||||||
|
icon={Cpu}
|
||||||
|
label="Status"
|
||||||
|
status={status?.daemon_running ?? false}
|
||||||
|
statusText={status?.daemon_running ? 'Running' : 'Stopped'}
|
||||||
|
/>
|
||||||
|
<div className="px-3 py-1 text-xs text-muted-foreground">
|
||||||
|
Port: {status?.port ?? 4141}
|
||||||
|
</div>
|
||||||
|
{status?.authenticated && (
|
||||||
|
<div className="px-1">
|
||||||
|
{status?.daemon_running ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => stopDaemon()}
|
||||||
|
disabled={isStoppingDaemon}
|
||||||
|
>
|
||||||
|
<PowerOff className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
{isStoppingDaemon ? 'Stopping...' : 'Stop'}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => startDaemon()}
|
||||||
|
disabled={isStartingDaemon}
|
||||||
|
>
|
||||||
|
<Power className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
{isStartingDaemon ? 'Starting...' : 'Start'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StatusSection>
|
||||||
|
|
||||||
|
{/* Quick Status */}
|
||||||
|
<StatusSection title="Config">
|
||||||
|
<div className="px-3 py-2 rounded-lg bg-muted/50 space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">Model</span>
|
||||||
|
<span className="font-mono truncate max-w-[100px] text-[10px]">
|
||||||
|
{status?.model ?? 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">Account</span>
|
||||||
|
<Badge variant="secondary" className="text-[10px] h-4">
|
||||||
|
{status?.account_type ?? 'individual'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StatusSection>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>Proxy</span>
|
||||||
|
{status?.daemon_running ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<CheckCircle2 className="w-3 h-3 text-green-500" />
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<XCircle className="w-3 h-3 text-muted-foreground" />
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Panel - Split-view Configuration Form */}
|
||||||
|
<div className="flex-1 flex flex-col min-w-0 bg-background overflow-hidden">
|
||||||
|
<CopilotConfigForm />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CopilotStatusCard />
|
|
||||||
<CopilotConfigForm />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user