mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 14:16:16 +00:00
feat(config): add Claude IDE extension setup flow
This commit is contained in:
1 parent
437b040f4c
commit
b82f10e639
18 files changed
+1120
-196
No files matched your search
@@ -28,6 +28,9 @@ const CliproxyControlPanelPage = lazy(() =>
|
||||
);
|
||||
const CopilotPage = lazy(() => import('@/pages/copilot').then((m) => ({ default: m.CopilotPage })));
|
||||
const CursorPage = lazy(() => import('@/pages/cursor').then((m) => ({ default: m.CursorPage })));
|
||||
const ClaudeExtensionPage = lazy(() =>
|
||||
import('@/pages/claude-extension').then((m) => ({ default: m.ClaudeExtensionPage }))
|
||||
);
|
||||
const DroidPage = lazy(() => import('@/pages/droid').then((m) => ({ default: m.DroidPage })));
|
||||
const AccountsPage = lazy(() =>
|
||||
import('@/pages/accounts').then((m) => ({ default: m.AccountsPage }))
|
||||
@@ -119,6 +122,14 @@ export default function App() {
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/claude-extension"
|
||||
element={
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<ClaudeExtensionPage />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/droid"
|
||||
element={
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
BarChart3,
|
||||
Gauge,
|
||||
Github,
|
||||
Puzzle,
|
||||
TerminalSquare,
|
||||
} from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
@@ -101,6 +102,7 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] {
|
||||
},
|
||||
{ path: '/copilot', icon: Github, label: t('nav.githubCopilot') },
|
||||
{ path: '/cursor', iconSrc: '/assets/sidebar/cursor.svg', label: t('nav.cursorIde') },
|
||||
{ path: '/claude-extension', icon: Puzzle, label: t('nav.claudeExtension') },
|
||||
{
|
||||
path: '/accounts',
|
||||
icon: Users,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { withApiBase } from '@/lib/api-client';
|
||||
|
||||
export interface ClaudeExtensionProfileOption {
|
||||
name: string;
|
||||
profileType: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ClaudeExtensionHostOption {
|
||||
id: 'vscode' | 'cursor' | 'windsurf';
|
||||
label: string;
|
||||
settingsKey: string;
|
||||
disableLoginPromptKey?: string;
|
||||
settingsTargetLabel: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ClaudeExtensionSetupPayload {
|
||||
profile: {
|
||||
requestedProfile: string;
|
||||
resolvedProfileName: string;
|
||||
profileType: string;
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
host: ClaudeExtensionHostOption;
|
||||
env: Array<{ name: string; value: string }>;
|
||||
warnings: string[];
|
||||
notes: string[];
|
||||
removeEnvKeys: string[];
|
||||
sharedSettings: {
|
||||
path: string;
|
||||
command: string;
|
||||
json: string;
|
||||
};
|
||||
ideSettings: {
|
||||
targetLabel: string;
|
||||
json: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function requestJson<T>(url: string): Promise<T> {
|
||||
const res = await fetch(withApiBase(url));
|
||||
if (!res.ok) {
|
||||
const payload = (await res.json().catch(() => null)) as { error?: string } | null;
|
||||
throw new Error(payload?.error || `Request failed (${res.status})`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function useClaudeExtensionOptions() {
|
||||
return useQuery({
|
||||
queryKey: ['claude-extension-options'],
|
||||
queryFn: () =>
|
||||
requestJson<{ profiles: ClaudeExtensionProfileOption[]; hosts: ClaudeExtensionHostOption[] }>(
|
||||
'/claude-extension/profiles'
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function useClaudeExtensionSetup(profile?: string, host: string = 'vscode') {
|
||||
return useQuery({
|
||||
queryKey: ['claude-extension-setup', profile, host],
|
||||
enabled: Boolean(profile),
|
||||
queryFn: () =>
|
||||
requestJson<ClaudeExtensionSetupPayload>(
|
||||
`/claude-extension/setup?profile=${encodeURIComponent(profile || '')}&host=${encodeURIComponent(host)}`
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -25,6 +25,7 @@ const resources = {
|
||||
controlPanel: 'Control Panel',
|
||||
githubCopilot: 'GitHub Copilot',
|
||||
cursorIde: 'Cursor IDE',
|
||||
claudeExtension: 'Claude Extension',
|
||||
accounts: 'Accounts',
|
||||
allAccounts: 'All Accounts',
|
||||
sharedData: 'Shared Data',
|
||||
@@ -1195,6 +1196,7 @@ const resources = {
|
||||
controlPanel: '控制面板',
|
||||
githubCopilot: 'GitHub Copilot',
|
||||
cursorIde: 'Cursor IDE',
|
||||
claudeExtension: 'Claude Extension',
|
||||
accounts: '账号',
|
||||
allAccounts: '全部账号',
|
||||
sharedData: '共享数据',
|
||||
@@ -2324,6 +2326,7 @@ const resources = {
|
||||
controlPanel: 'Bảng điều khiển',
|
||||
githubCopilot: 'GitHub Copilot',
|
||||
cursorIde: 'Cursor IDE',
|
||||
claudeExtension: 'Claude Extension',
|
||||
accounts: 'Tài khoản',
|
||||
allAccounts: 'Tất cả tài khoản',
|
||||
sharedData: 'Dữ liệu dùng chung',
|
||||
@@ -3513,6 +3516,7 @@ const resources = {
|
||||
controlPanel: 'コントロールパネル',
|
||||
githubCopilot: 'GitHub Copilot',
|
||||
cursorIde: 'Cursor IDE',
|
||||
claudeExtension: 'Claude Extension',
|
||||
accounts: 'アカウント',
|
||||
allAccounts: 'すべてのアカウント',
|
||||
sharedData: '共有データ',
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { useState } from 'react';
|
||||
import { AlertTriangle, Code2, Copy, FolderCog, Loader2, Sparkles } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { CopyButton } from '@/components/ui/copy-button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useClaudeExtensionOptions, useClaudeExtensionSetup } from '@/hooks/use-claude-extension';
|
||||
|
||||
function CodeBlockCard({
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<Card className="border-border/60">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">{title}</CardTitle>
|
||||
<CardDescription className="mt-1">{description}</CardDescription>
|
||||
</div>
|
||||
<CopyButton value={value} label={`Copy ${title}`} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="overflow-x-auto rounded-md border bg-muted/40 p-4 text-xs leading-6 text-foreground">
|
||||
{value}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClaudeExtensionPage() {
|
||||
const optionsQuery = useClaudeExtensionOptions();
|
||||
const [profile, setProfile] = useState<string>('');
|
||||
const [host, setHost] = useState<string>('vscode');
|
||||
const selectedProfile = profile || optionsQuery.data?.profiles?.[0]?.name || '';
|
||||
const setupQuery = useClaudeExtensionSetup(selectedProfile, host);
|
||||
const activeError = (optionsQuery.error as Error | null) ?? (setupQuery.error as Error | null);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
Native setup
|
||||
</Badge>
|
||||
<Badge variant="outline">VS Code first</Badge>
|
||||
</div>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Claude IDE Extension</h1>
|
||||
<p className="max-w-3xl text-sm text-muted-foreground">
|
||||
Generate the correct Claude extension configuration for CCS API profiles, account
|
||||
instances, CLIProxy modes, Copilot, and default/native Claude flows.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Target selection</CardTitle>
|
||||
<CardDescription>
|
||||
Choose the CCS profile and IDE host you want to configure.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">CCS profile</div>
|
||||
<Select
|
||||
value={selectedProfile}
|
||||
onValueChange={setProfile}
|
||||
disabled={optionsQuery.isLoading}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a profile" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{optionsQuery.data?.profiles.map((option) => (
|
||||
<SelectItem key={option.name} value={option.name}>
|
||||
{option.label} ({option.profileType})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{
|
||||
optionsQuery.data?.profiles.find((option) => option.name === selectedProfile)
|
||||
?.description
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">IDE host</div>
|
||||
<Select value={host} onValueChange={setHost} disabled={optionsQuery.isLoading}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a host" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{optionsQuery.data?.hosts.map((option) => (
|
||||
<SelectItem key={option.id} value={option.id}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{optionsQuery.data?.hosts.find((option) => option.id === host)?.description}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{optionsQuery.isLoading || (selectedProfile && setupQuery.isLoading) ? (
|
||||
<div className="flex h-48 items-center justify-center text-muted-foreground">
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
Resolving extension setup...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeError ? (
|
||||
<Card className="border-destructive/40">
|
||||
<CardContent className="flex items-start gap-3 pt-6 text-sm text-destructive">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<div>{activeError.message}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{setupQuery.data ? (
|
||||
<>
|
||||
<div className="grid gap-4 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Code2 className="h-4 w-4" />
|
||||
Resolved profile
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="font-medium">{setupQuery.data.profile.label}</div>
|
||||
<div className="text-muted-foreground">{setupQuery.data.profile.description}</div>
|
||||
<Badge variant="outline">{setupQuery.data.profile.profileType}</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<FolderCog className="h-4 w-4" />
|
||||
Shared settings path
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="font-mono text-xs break-all">
|
||||
{setupQuery.data.sharedSettings.path}
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
Preferred when you want Claude Code CLI and the IDE extension to share one CCS
|
||||
profile.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Copy className="h-4 w-4" />
|
||||
IDE target
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="font-medium">{setupQuery.data.host.label}</div>
|
||||
<div className="font-mono text-xs">{setupQuery.data.host.settingsKey}</div>
|
||||
<div className="text-muted-foreground">
|
||||
{setupQuery.data.ideSettings.targetLabel}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{setupQuery.data.warnings.length > 0 ? (
|
||||
<Card className="border-amber-400/40 bg-amber-50/50 dark:bg-amber-950/10">
|
||||
<CardContent className="space-y-2 pt-6 text-sm">
|
||||
{setupQuery.data.warnings.map((warning) => (
|
||||
<div key={warning} className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600" />
|
||||
<span>{warning}</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<CodeBlockCard
|
||||
title="Preferred shared Claude settings"
|
||||
description={`Run ${setupQuery.data.sharedSettings.command}, or copy the JSON shape shown here for manual edits.`}
|
||||
value={setupQuery.data.sharedSettings.json}
|
||||
/>
|
||||
<CodeBlockCard
|
||||
title="IDE-local settings.json snippet"
|
||||
description={`Replace the full ${setupQuery.data.host.settingsKey} value in ${setupQuery.data.ideSettings.targetLabel}.`}
|
||||
value={setupQuery.data.ideSettings.json}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">CLI shortcut</CardTitle>
|
||||
<CardDescription>
|
||||
Use the native shared-settings path directly from the terminal.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<CopyButton
|
||||
value={setupQuery.data.sharedSettings.command}
|
||||
label="Copy persist command"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<pre className="overflow-x-auto rounded-md border bg-muted/40 p-4 text-xs">
|
||||
{setupQuery.data.sharedSettings.command}
|
||||
</pre>
|
||||
{setupQuery.data.notes.length > 0 ? (
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
{setupQuery.data.notes.map((note) => (
|
||||
<div key={note}>- {note}</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,8 @@ export { AnalyticsPage } from './analytics';
|
||||
|
||||
export { CursorPage } from './cursor';
|
||||
|
||||
export { ClaudeExtensionPage } from './claude-extension';
|
||||
|
||||
export { UpdatesPage } from './updates';
|
||||
|
||||
export { DroidPage } from './droid';
|
||||
Reference in new issue
Block a user