feat(config): add Claude IDE extension setup flow

This commit is contained in:
Tam Nhu Tran committed 2026-03-15 15:58:37 -04:00
1 parent 437b040f4c
commit b82f10e639
18 files changed
+1120 -196

No files matched your search

+11
View File
@@ -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={
+2
View File
@@ -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,
+72
View File
@@ -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)}`
),
});
}
+4
View File
@@ -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: '共有データ',
+249
View File
@@ -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>
);
}
+2
View File
@@ -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';