mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
feat(cursor-ui): restore Cursor dashboard UX and icon assets
- rebuild /cursor page with model tiers, presets, and save-all flow - validate raw JSON edits via RawEditorSection before save operations - add sidebar image icon rendering and restore cursor.svg asset - include risk warning copy and OpenAI-first GPT-5.3 Codex preset Refs #555
This commit is contained in:
1 parent
7b73658f87
commit
7ab7a156eb
4 files changed
+733
-186
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cursor</title><path d="M22.106 5.68L12.5.135a.998.998 0 00-.998 0L1.893 5.68a.84.84 0 00-.419.726v11.186c0 .3.16.577.42.727l9.607 5.547a.999.999 0 00.998 0l9.608-5.547a.84.84 0 00.42-.727V6.407a.84.84 0 00-.42-.726zm-.603 1.176L12.228 22.92c-.063.108-.228.064-.228-.061V12.34a.59.59 0 00-.295-.51l-9.11-5.26c-.107-.062-.063-.228.062-.228h18.55c.264 0 .428.286.296.514z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 547 B |
@@ -11,8 +11,8 @@ import {
|
|||||||
BarChart3,
|
BarChart3,
|
||||||
Gauge,
|
Gauge,
|
||||||
Github,
|
Github,
|
||||||
Bot,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -35,8 +35,34 @@ import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
|
interface SidebarBadge {
|
||||||
|
text: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarChildItem {
|
||||||
|
path: string;
|
||||||
|
label: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarItem {
|
||||||
|
path: string;
|
||||||
|
label: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
iconSrc?: string;
|
||||||
|
badge?: SidebarBadge;
|
||||||
|
isCollapsible?: boolean;
|
||||||
|
children?: SidebarChildItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarGroupDef {
|
||||||
|
title: string;
|
||||||
|
items: SidebarItem[];
|
||||||
|
}
|
||||||
|
|
||||||
// Define navigation groups
|
// Define navigation groups
|
||||||
const navGroups = [
|
const navGroups: SidebarGroupDef[] = [
|
||||||
{
|
{
|
||||||
title: 'General',
|
title: 'General',
|
||||||
items: [
|
items: [
|
||||||
@@ -64,7 +90,7 @@ const navGroups = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '/copilot', icon: Github, label: 'GitHub Copilot' },
|
{ path: '/copilot', icon: Github, label: 'GitHub Copilot' },
|
||||||
{ path: '/cursor', icon: Bot, label: 'Cursor IDE' },
|
{ path: '/cursor', iconSrc: '/assets/sidebar/cursor.svg', label: 'Cursor IDE' },
|
||||||
{
|
{
|
||||||
path: '/accounts',
|
path: '/accounts',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
@@ -114,6 +140,17 @@ export function AppSidebar() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderMenuIcon = (item: Pick<SidebarItem, 'icon' | 'iconSrc'>) => {
|
||||||
|
if (item.iconSrc) {
|
||||||
|
return <img src={item.iconSrc} alt="" className="w-4 h-4 object-contain" />;
|
||||||
|
}
|
||||||
|
if (item.icon) {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return <Icon className="w-4 h-4" />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<SidebarHeader className="h-12 flex items-center justify-center">
|
<SidebarHeader className="h-12 flex items-center justify-center">
|
||||||
@@ -141,7 +178,7 @@ export function AppSidebar() {
|
|||||||
isActive={isParentActive(item.children)}
|
isActive={isParentActive(item.children)}
|
||||||
onClick={() => navigate(item.path)}
|
onClick={() => navigate(item.path)}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon className="w-4 h-4" />}
|
{renderMenuIcon(item)}
|
||||||
<span className="group-data-[collapsible=icon]:hidden">
|
<span className="group-data-[collapsible=icon]:hidden">
|
||||||
{getItemLabel(item)}
|
{getItemLabel(item)}
|
||||||
</span>
|
</span>
|
||||||
@@ -173,7 +210,7 @@ export function AppSidebar() {
|
|||||||
tooltip={getItemLabel(item)}
|
tooltip={getItemLabel(item)}
|
||||||
>
|
>
|
||||||
<Link to={item.path}>
|
<Link to={item.path}>
|
||||||
{item.icon && <item.icon className="w-4 h-4" />}
|
{renderMenuIcon(item)}
|
||||||
<span className="group-data-[collapsible=icon]:hidden flex-1">
|
<span className="group-data-[collapsible=icon]:hidden flex-1">
|
||||||
{getItemLabel(item)}
|
{getItemLabel(item)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export interface CursorConfig {
|
|||||||
port: number;
|
port: number;
|
||||||
auto_start: boolean;
|
auto_start: boolean;
|
||||||
ghost_mode: boolean;
|
ghost_mode: boolean;
|
||||||
|
model: string;
|
||||||
|
opus_model?: string;
|
||||||
|
sonnet_model?: string;
|
||||||
|
haiku_model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CursorModel {
|
export interface CursorModel {
|
||||||
|
|||||||
+686
-181
@@ -6,9 +6,9 @@
|
|||||||
import { useMemo, useState, type ElementType } from 'react';
|
import { useMemo, useState, type ElementType } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
Bot,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
FileCode,
|
Code2,
|
||||||
Key,
|
Key,
|
||||||
Loader2,
|
Loader2,
|
||||||
Play,
|
Play,
|
||||||
@@ -18,19 +18,30 @@ import {
|
|||||||
Save,
|
Save,
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Sparkles,
|
||||||
|
Zap,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useCursor } from '@/hooks/use-cursor';
|
import { useCursor } from '@/hooks/use-cursor';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
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 { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { CodeEditor } from '@/components/shared/code-editor';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { RawEditorSection } from '@/components/copilot/config-form/raw-editor-section';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -44,20 +55,166 @@ interface CursorConfigDraft {
|
|||||||
port: string;
|
port: string;
|
||||||
auto_start: boolean;
|
auto_start: boolean;
|
||||||
ghost_mode: boolean;
|
ghost_mode: boolean;
|
||||||
|
model: string;
|
||||||
|
opus_model: string;
|
||||||
|
sonnet_model: string;
|
||||||
|
haiku_model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawSettingsParseResult {
|
||||||
|
isValid: boolean;
|
||||||
|
settings?: { env?: Record<string, string> };
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildConfigDraft(config?: {
|
function buildConfigDraft(config?: {
|
||||||
port?: number;
|
port?: number;
|
||||||
auto_start?: boolean;
|
auto_start?: boolean;
|
||||||
ghost_mode?: boolean;
|
ghost_mode?: boolean;
|
||||||
|
model?: string;
|
||||||
|
opus_model?: string;
|
||||||
|
sonnet_model?: string;
|
||||||
|
haiku_model?: string;
|
||||||
}): CursorConfigDraft {
|
}): CursorConfigDraft {
|
||||||
return {
|
return {
|
||||||
port: String(config?.port ?? 20129),
|
port: String(config?.port ?? 20129),
|
||||||
auto_start: config?.auto_start ?? false,
|
auto_start: config?.auto_start ?? false,
|
||||||
ghost_mode: config?.ghost_mode ?? true,
|
ghost_mode: config?.ghost_mode ?? true,
|
||||||
|
model: config?.model?.trim() || 'gpt-5.3-codex',
|
||||||
|
opus_model: config?.opus_model?.trim() || '',
|
||||||
|
sonnet_model: config?.sonnet_model?.trim() || '',
|
||||||
|
haiku_model: config?.haiku_model?.trim() || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickModelByPatterns(
|
||||||
|
models: Array<{ id: string }>,
|
||||||
|
patterns: RegExp[],
|
||||||
|
fallback: string
|
||||||
|
): string {
|
||||||
|
const matched = models.find((model) => patterns.some((pattern) => pattern.test(model.id)));
|
||||||
|
return matched?.id ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeModelKey(value: string): string {
|
||||||
|
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickModelByAliases(
|
||||||
|
models: Array<{ id: string; name: string }>,
|
||||||
|
aliases: string[],
|
||||||
|
fallback: string
|
||||||
|
): string {
|
||||||
|
const normalizedAliasSet = new Set(aliases.map(normalizeModelKey));
|
||||||
|
const direct = models.find((model) => normalizedAliasSet.has(normalizeModelKey(model.id)));
|
||||||
|
if (direct) return direct.id;
|
||||||
|
|
||||||
|
const byName = models.find((model) => normalizedAliasSet.has(normalizeModelKey(model.name)));
|
||||||
|
if (byName) return byName.id;
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRawSettings(value: string): RawSettingsParseResult {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value || '{}');
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: 'Raw settings must be a JSON object',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
settings: parsed as { env?: Record<string, string> },
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: (error as Error).message || 'Invalid JSON',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function CursorModelSelector({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
models,
|
||||||
|
disabled,
|
||||||
|
allowDefaultFallback = false,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
value: string;
|
||||||
|
models: Array<{ id: string; name: string; provider: string }>;
|
||||||
|
disabled?: boolean;
|
||||||
|
allowDefaultFallback?: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
const selectorValue = value || (allowDefaultFallback ? '__default' : '');
|
||||||
|
const selected = models.find((model) => model.id === value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium">{label}</Label>
|
||||||
|
<p className="text-[10px] text-muted-foreground">{description}</p>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={selectorValue}
|
||||||
|
onValueChange={(nextValue) => {
|
||||||
|
if (allowDefaultFallback && nextValue === '__default') {
|
||||||
|
onChange('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange(nextValue);
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<SelectValue placeholder="Select model">
|
||||||
|
{selectorValue && (
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="truncate font-mono text-xs">
|
||||||
|
{allowDefaultFallback && selectorValue === '__default'
|
||||||
|
? 'Use Default Model'
|
||||||
|
: selected?.name || selectorValue}
|
||||||
|
</span>
|
||||||
|
{selected?.provider && (
|
||||||
|
<Badge variant="outline" className="text-[9px] h-4 px-1 capitalize">
|
||||||
|
{selected.provider}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-[300px]">
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel className="text-xs text-muted-foreground">
|
||||||
|
Available Models ({models.length})
|
||||||
|
</SelectLabel>
|
||||||
|
{allowDefaultFallback && <SelectItem value="__default">Use Default Model</SelectItem>}
|
||||||
|
{models.map((model) => (
|
||||||
|
<SelectItem key={model.id} value={model.id}>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="truncate text-xs font-mono">{model.name || model.id}</span>
|
||||||
|
<Badge variant="outline" className="text-[9px] h-4 px-1 capitalize">
|
||||||
|
{model.provider}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function StatusItem({
|
function StatusItem({
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
label,
|
label,
|
||||||
@@ -131,11 +288,47 @@ export function CursorPage() {
|
|||||||
const effectivePort = configDirty ? configDraft.port : pristineConfigDraft.port;
|
const effectivePort = configDirty ? configDraft.port : pristineConfigDraft.port;
|
||||||
const effectiveAutoStart = configDirty ? configDraft.auto_start : pristineConfigDraft.auto_start;
|
const effectiveAutoStart = configDirty ? configDraft.auto_start : pristineConfigDraft.auto_start;
|
||||||
const effectiveGhostMode = configDirty ? configDraft.ghost_mode : pristineConfigDraft.ghost_mode;
|
const effectiveGhostMode = configDirty ? configDraft.ghost_mode : pristineConfigDraft.ghost_mode;
|
||||||
|
const effectiveModel = configDirty ? configDraft.model : pristineConfigDraft.model;
|
||||||
|
const effectiveOpusModel = configDirty ? configDraft.opus_model : pristineConfigDraft.opus_model;
|
||||||
|
const effectiveSonnetModel = configDirty
|
||||||
|
? configDraft.sonnet_model
|
||||||
|
: pristineConfigDraft.sonnet_model;
|
||||||
|
const effectiveHaikuModel = configDirty
|
||||||
|
? configDraft.haiku_model
|
||||||
|
: pristineConfigDraft.haiku_model;
|
||||||
const effectiveRawConfigText = rawConfigDirty
|
const effectiveRawConfigText = rawConfigDirty
|
||||||
? rawConfigText
|
? rawConfigText
|
||||||
: JSON.stringify(rawSettings?.settings ?? {}, null, 2);
|
: JSON.stringify(rawSettings?.settings ?? {}, null, 2);
|
||||||
const rawSettingsReady = Boolean(rawSettings);
|
const rawSettingsReady = Boolean(rawSettings);
|
||||||
const disableRawSave = isSavingRawSettings || rawSettingsLoading || !rawSettingsReady;
|
const rawParseResult = useMemo(
|
||||||
|
() => parseRawSettings(effectiveRawConfigText),
|
||||||
|
[effectiveRawConfigText]
|
||||||
|
);
|
||||||
|
const isRawJsonValid = rawParseResult.isValid;
|
||||||
|
const hasChanges = configDirty || rawConfigDirty;
|
||||||
|
const canSave = !rawConfigDirty || (rawSettingsReady && isRawJsonValid);
|
||||||
|
const orderedModels = useMemo(() => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const sorted = [...models].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
const deduped = sorted.filter((model) => {
|
||||||
|
if (seen.has(model.id)) return false;
|
||||||
|
seen.add(model.id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (effectiveModel && !sorted.some((model) => model.id === effectiveModel)) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: effectiveModel,
|
||||||
|
name: effectiveModel,
|
||||||
|
provider: 'custom',
|
||||||
|
},
|
||||||
|
...deduped,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return deduped;
|
||||||
|
}, [models, effectiveModel]);
|
||||||
|
|
||||||
const updateConfigDraft = (updater: (draft: CursorConfigDraft) => CursorConfigDraft) => {
|
const updateConfigDraft = (updater: (draft: CursorConfigDraft) => CursorConfigDraft) => {
|
||||||
setConfigDraft((previousDraft) => {
|
setConfigDraft((previousDraft) => {
|
||||||
@@ -151,11 +344,17 @@ export function CursorPage() {
|
|||||||
[status?.enabled]
|
[status?.enabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSaveConfig = async () => {
|
const handleSaveConfig = async ({
|
||||||
|
suppressSuccessToast = false,
|
||||||
|
}: { suppressSuccessToast?: boolean } = {}) => {
|
||||||
const parsedPort = Number(effectivePort);
|
const parsedPort = Number(effectivePort);
|
||||||
if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
||||||
toast.error('Port must be an integer between 1 and 65535');
|
toast.error('Port must be an integer between 1 and 65535');
|
||||||
return;
|
return false;
|
||||||
|
}
|
||||||
|
if (!effectiveModel.trim()) {
|
||||||
|
toast.error('Default model is required');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -163,6 +362,10 @@ export function CursorPage() {
|
|||||||
port: parsedPort,
|
port: parsedPort,
|
||||||
auto_start: effectiveAutoStart,
|
auto_start: effectiveAutoStart,
|
||||||
ghost_mode: effectiveGhostMode,
|
ghost_mode: effectiveGhostMode,
|
||||||
|
model: effectiveModel,
|
||||||
|
opus_model: effectiveOpusModel || undefined,
|
||||||
|
sonnet_model: effectiveSonnetModel || undefined,
|
||||||
|
haiku_model: effectiveHaikuModel || undefined,
|
||||||
});
|
});
|
||||||
setConfigDirty(false);
|
setConfigDirty(false);
|
||||||
setConfigDraft(
|
setConfigDraft(
|
||||||
@@ -170,14 +373,104 @@ export function CursorPage() {
|
|||||||
port: parsedPort,
|
port: parsedPort,
|
||||||
auto_start: effectiveAutoStart,
|
auto_start: effectiveAutoStart,
|
||||||
ghost_mode: effectiveGhostMode,
|
ghost_mode: effectiveGhostMode,
|
||||||
|
model: effectiveModel,
|
||||||
|
opus_model: effectiveOpusModel || undefined,
|
||||||
|
sonnet_model: effectiveSonnetModel || undefined,
|
||||||
|
haiku_model: effectiveHaikuModel || undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
toast.success('Cursor config saved');
|
if (!suppressSuccessToast) {
|
||||||
|
toast.success('Cursor config and model mapping saved');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error((error as Error).message || 'Failed to save config');
|
toast.error((error as Error).message || 'Failed to save config');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyPreset = (preset: 'codex53' | 'claude46' | 'gemini3') => {
|
||||||
|
const fallbackModel = effectiveModel || currentModel || models[0]?.id || 'gpt-5.3-codex';
|
||||||
|
const codex53 = pickModelByAliases(
|
||||||
|
models,
|
||||||
|
['gpt-5.3-codex', 'gpt53codex', 'GPT-5.3 Codex'],
|
||||||
|
pickModelByPatterns(models, [/gpt[-.]?5.*codex/i], fallbackModel)
|
||||||
|
);
|
||||||
|
const codexMax = pickModelByAliases(
|
||||||
|
models,
|
||||||
|
['gpt-5.1-codex-max', 'gpt51codexmax', 'GPT-5.1 Codex Max'],
|
||||||
|
pickModelByPatterns(models, [/gpt[-.]?5.*codex.*max/i], codex53)
|
||||||
|
);
|
||||||
|
const codexFast = pickModelByAliases(
|
||||||
|
models,
|
||||||
|
['gpt-5-fast', 'gpt5fast', 'GPT-5 Fast'],
|
||||||
|
pickModelByPatterns(models, [/gpt[-.]?5.*fast/i], codex53)
|
||||||
|
);
|
||||||
|
const codexMini = pickModelByAliases(
|
||||||
|
models,
|
||||||
|
['gpt-5-mini', 'gpt5mini', 'GPT-5 Mini'],
|
||||||
|
pickModelByPatterns(models, [/gpt[-.]?5.*mini/i], codexFast)
|
||||||
|
);
|
||||||
|
const opus46 = pickModelByAliases(
|
||||||
|
models,
|
||||||
|
['claude-4.6-opus', 'claude46opus', 'Claude 4.6 Opus'],
|
||||||
|
pickModelByPatterns(models, [/claude[-.]?4\.?6.*opus/i, /claude.*opus/i], codex53)
|
||||||
|
);
|
||||||
|
const sonnet45 = pickModelByAliases(
|
||||||
|
models,
|
||||||
|
['claude-4.5-sonnet', 'claude45sonnet', 'Claude 4.5 Sonnet'],
|
||||||
|
pickModelByPatterns(models, [/claude[-.]?4\.?5.*sonnet/i, /claude.*sonnet/i], codex53)
|
||||||
|
);
|
||||||
|
const haiku45 = pickModelByAliases(
|
||||||
|
models,
|
||||||
|
['claude-4.5-haiku', 'claude45haiku', 'Claude 4.5 Haiku'],
|
||||||
|
pickModelByPatterns(models, [/claude[-.]?4\.?5.*haiku/i, /haiku/i], sonnet45)
|
||||||
|
);
|
||||||
|
const gemini3Pro = pickModelByAliases(
|
||||||
|
models,
|
||||||
|
['gemini-3-pro', 'gemini3pro', 'Gemini 3 Pro'],
|
||||||
|
pickModelByPatterns(models, [/gemini[-.]?3.*pro/i], codex53)
|
||||||
|
);
|
||||||
|
const gemini3Flash = pickModelByAliases(
|
||||||
|
models,
|
||||||
|
['gemini-3-flash', 'gemini3flash', 'Gemini 3 Flash'],
|
||||||
|
pickModelByPatterns(models, [/gemini[-.]?3.*flash/i, /gemini[-.]?2\.?5.*flash/i], gemini3Pro)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (preset === 'codex53') {
|
||||||
|
updateConfigDraft((draft) => ({
|
||||||
|
...draft,
|
||||||
|
model: codex53,
|
||||||
|
opus_model: codexMax,
|
||||||
|
sonnet_model: codex53,
|
||||||
|
haiku_model: codexMini,
|
||||||
|
}));
|
||||||
|
toast.success('Applied GPT-5.3 Codex preset');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset === 'claude46') {
|
||||||
|
updateConfigDraft((draft) => ({
|
||||||
|
...draft,
|
||||||
|
model: opus46,
|
||||||
|
opus_model: opus46,
|
||||||
|
sonnet_model: sonnet45,
|
||||||
|
haiku_model: haiku45,
|
||||||
|
}));
|
||||||
|
toast.success('Applied Claude 4.6 preset');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfigDraft((draft) => ({
|
||||||
|
...draft,
|
||||||
|
model: gemini3Pro,
|
||||||
|
opus_model: gemini3Pro,
|
||||||
|
sonnet_model: gemini3Pro,
|
||||||
|
haiku_model: gemini3Flash,
|
||||||
|
}));
|
||||||
|
toast.success('Applied Gemini 3 preset');
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleEnabled = async (enabled: boolean) => {
|
const handleToggleEnabled = async (enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
await updateConfigAsync({ enabled });
|
await updateConfigAsync({ enabled });
|
||||||
@@ -242,32 +535,29 @@ export function CursorPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveRawSettings = async () => {
|
const handleSaveRawSettings = async ({
|
||||||
|
suppressSuccessToast = false,
|
||||||
|
}: { suppressSuccessToast?: boolean } = {}) => {
|
||||||
if (!rawSettingsReady) {
|
if (!rawSettingsReady) {
|
||||||
toast.error('Raw settings are still loading. Please wait and try again.');
|
toast.error('Raw settings are still loading. Please wait and try again.');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: unknown;
|
if (!rawParseResult.isValid || !rawParseResult.settings) {
|
||||||
try {
|
toast.error(rawParseResult.error || 'Invalid JSON');
|
||||||
parsed = JSON.parse(effectiveRawConfigText || '{}');
|
return false;
|
||||||
} catch (error) {
|
|
||||||
toast.error((error as Error).message || 'Invalid JSON');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
||||||
toast.error('Raw settings must be a JSON object');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await saveRawSettingsAsync({
|
await saveRawSettingsAsync({
|
||||||
settings: parsed as { env?: Record<string, string> },
|
settings: rawParseResult.settings,
|
||||||
expectedMtime: rawSettings?.mtime,
|
expectedMtime: rawSettings?.mtime,
|
||||||
});
|
});
|
||||||
setRawConfigDirty(false);
|
setRawConfigDirty(false);
|
||||||
toast.success('Raw settings saved');
|
if (!suppressSuccessToast) {
|
||||||
|
toast.success('Raw settings saved');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = (error as Error).message || 'Failed to save raw settings';
|
const message = (error as Error).message || 'Failed to save raw settings';
|
||||||
if (message === 'CONFLICT') {
|
if (message === 'CONFLICT') {
|
||||||
@@ -275,9 +565,41 @@ export function CursorPage() {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveAll = async () => {
|
||||||
|
if (!hasChanges) return;
|
||||||
|
|
||||||
|
const saveConfig = configDirty;
|
||||||
|
const saveRawSettings = rawConfigDirty;
|
||||||
|
|
||||||
|
if (saveConfig) {
|
||||||
|
const saved = await handleSaveConfig({
|
||||||
|
suppressSuccessToast: saveRawSettings,
|
||||||
|
});
|
||||||
|
if (!saved) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveRawSettings) {
|
||||||
|
const saved = await handleSaveRawSettings({
|
||||||
|
suppressSuccessToast: saveConfig,
|
||||||
|
});
|
||||||
|
if (!saved) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveConfig && saveRawSettings) {
|
||||||
|
toast.success('Cursor configuration saved');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeaderRefresh = () => {
|
||||||
|
setRawConfigDirty(false);
|
||||||
|
refetchStatus();
|
||||||
|
refetchRawSettings();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-[calc(100vh-100px)] flex">
|
<div className="h-[calc(100vh-100px)] flex">
|
||||||
@@ -285,7 +607,11 @@ export function CursorPage() {
|
|||||||
<div className="p-4 border-b bg-background">
|
<div className="p-4 border-b bg-background">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Bot className="w-5 h-5 text-primary" />
|
<img
|
||||||
|
src="/assets/sidebar/cursor.svg"
|
||||||
|
alt=""
|
||||||
|
className="w-5 h-5 object-contain shrink-0"
|
||||||
|
/>
|
||||||
<h1 className="font-semibold">Cursor</h1>
|
<h1 className="font-semibold">Cursor</h1>
|
||||||
{integrationBadge}
|
{integrationBadge}
|
||||||
</div>
|
</div>
|
||||||
@@ -304,6 +630,20 @@ export function CursorPage() {
|
|||||||
|
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<div className="p-3 space-y-4">
|
<div className="p-3 space-y-4">
|
||||||
|
<div className="rounded-md border border-yellow-500/50 bg-yellow-500/15 p-3 space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-yellow-600 dark:text-yellow-400 shrink-0" />
|
||||||
|
<span className="text-xs font-semibold text-yellow-800 dark:text-yellow-200">
|
||||||
|
Unofficial API - Use at Your Own Risk
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul className="text-[11px] text-yellow-700 dark:text-yellow-300 space-y-0.5 pl-6 list-disc">
|
||||||
|
<li>Reverse-engineered integration may break anytime</li>
|
||||||
|
<li>Abuse or excessive usage may risk account restrictions</li>
|
||||||
|
<li>No warranty, no responsibility from CCS</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={ShieldCheck}
|
icon={ShieldCheck}
|
||||||
@@ -427,169 +767,334 @@ export function CursorPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0 bg-background overflow-hidden">
|
<div className="flex-1 min-w-0 bg-background overflow-hidden">
|
||||||
<div className="h-full overflow-auto p-4 md:p-6">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<Tabs defaultValue="config" className="space-y-4">
|
<div className="px-6 py-4 border-b bg-background flex items-center justify-between shrink-0">
|
||||||
<TabsList className="grid grid-cols-3 w-full max-w-md">
|
<div className="flex items-center gap-3">
|
||||||
<TabsTrigger value="config">Config</TabsTrigger>
|
<div>
|
||||||
<TabsTrigger value="models">Models</TabsTrigger>
|
<div className="flex items-center gap-2">
|
||||||
<TabsTrigger value="raw">Raw Settings</TabsTrigger>
|
<h2 className="text-lg font-semibold">Cursor Configuration</h2>
|
||||||
</TabsList>
|
{rawSettings && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
cursor.settings.json
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{rawSettings && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Last modified:{' '}
|
||||||
|
{rawSettings.exists
|
||||||
|
? new Date(rawSettings.mtime).toLocaleString()
|
||||||
|
: 'Never saved'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleHeaderRefresh}
|
||||||
|
disabled={statusLoading || rawSettingsLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={cn(
|
||||||
|
'w-4 h-4',
|
||||||
|
(statusLoading || rawSettingsLoading) && 'animate-spin'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSaveAll}
|
||||||
|
disabled={isUpdatingConfig || isSavingRawSettings || !hasChanges || !canSave}
|
||||||
|
>
|
||||||
|
{isUpdatingConfig || isSavingRawSettings ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="w-4 h-4 mr-1" />
|
||||||
|
Save
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TabsContent value="config" className="space-y-4">
|
<div className="flex-1 flex divide-x overflow-hidden">
|
||||||
<Card>
|
<div className="w-[540px] shrink-0 flex flex-col overflow-hidden bg-muted/5">
|
||||||
<CardHeader>
|
<Tabs defaultValue="config" className="h-full flex flex-col">
|
||||||
<CardTitle>Cursor Runtime Config</CardTitle>
|
<div className="px-4 pt-4 shrink-0">
|
||||||
<CardDescription>
|
<TabsList className="w-full">
|
||||||
Controls daemon behavior for local Cursor OpenAI-compatible endpoint.
|
<TabsTrigger value="config" className="flex-1">
|
||||||
</CardDescription>
|
Model Config
|
||||||
</CardHeader>
|
</TabsTrigger>
|
||||||
<CardContent className="space-y-5">
|
<TabsTrigger value="settings" className="flex-1">
|
||||||
<div className="space-y-2">
|
Settings
|
||||||
<Label htmlFor="cursor-port">Port</Label>
|
</TabsTrigger>
|
||||||
<Input
|
<TabsTrigger value="info" className="flex-1">
|
||||||
id="cursor-port"
|
Info
|
||||||
type="number"
|
</TabsTrigger>
|
||||||
min={1}
|
</TabsList>
|
||||||
max={65535}
|
</div>
|
||||||
value={effectivePort}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateConfigDraft((draft) => ({ ...draft, port: e.target.value }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
<div className="flex-1 overflow-hidden flex flex-col">
|
||||||
<div>
|
<TabsContent
|
||||||
<Label htmlFor="cursor-auto-start">Auto-start</Label>
|
value="config"
|
||||||
<p className="text-xs text-muted-foreground">
|
className="flex-1 mt-0 border-0 p-0 data-[state=inactive]:hidden flex flex-col overflow-hidden"
|
||||||
Start cursor daemon automatically when integration is used.
|
>
|
||||||
</p>
|
<ScrollArea className="flex-1">
|
||||||
</div>
|
<div className="p-4 space-y-6">
|
||||||
<Switch
|
<div>
|
||||||
id="cursor-auto-start"
|
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
|
||||||
checked={effectiveAutoStart}
|
<Sparkles className="w-4 h-4" />
|
||||||
onCheckedChange={(value) => {
|
Presets
|
||||||
updateConfigDraft((draft) => ({ ...draft, auto_start: value }));
|
</h3>
|
||||||
}}
|
<p className="text-xs text-muted-foreground mb-3">
|
||||||
/>
|
Apply pre-configured model mappings
|
||||||
</div>
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
<Button
|
||||||
<div>
|
variant="outline"
|
||||||
<Label htmlFor="cursor-ghost-mode">Ghost Mode</Label>
|
size="sm"
|
||||||
<p className="text-xs text-muted-foreground">
|
className="text-xs h-7 gap-1"
|
||||||
Requests `x-ghost-mode` to reduce telemetry.
|
onClick={() => applyPreset('codex53')}
|
||||||
</p>
|
title="OpenAI-only mapping: GPT-5.3 Codex / Codex Max / GPT-5 Mini"
|
||||||
</div>
|
>
|
||||||
<Switch
|
<Zap className="w-3 h-3" />
|
||||||
id="cursor-ghost-mode"
|
GPT-5.3 Codex
|
||||||
checked={effectiveGhostMode}
|
</Button>
|
||||||
onCheckedChange={(value) => {
|
<Button
|
||||||
updateConfigDraft((draft) => ({ ...draft, ghost_mode: value }));
|
variant="outline"
|
||||||
}}
|
size="sm"
|
||||||
/>
|
className="text-xs h-7 gap-1"
|
||||||
</div>
|
onClick={() => applyPreset('claude46')}
|
||||||
|
title="Claude-first mapping: Opus 4.6 / Sonnet 4.5 / Haiku 4.5"
|
||||||
<div className="flex justify-end">
|
>
|
||||||
<Button onClick={handleSaveConfig} disabled={isUpdatingConfig}>
|
<Zap className="w-3 h-3" />
|
||||||
{isUpdatingConfig ? (
|
Claude 4.6
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
</Button>
|
||||||
) : (
|
<Button
|
||||||
<Save className="w-4 h-4 mr-2" />
|
variant="outline"
|
||||||
)}
|
size="sm"
|
||||||
Save Config
|
className="text-xs h-7 gap-1"
|
||||||
</Button>
|
onClick={() => applyPreset('gemini3')}
|
||||||
</div>
|
title="Gemini-first mapping: Gemini 3 Pro + Gemini 3 Flash"
|
||||||
</CardContent>
|
>
|
||||||
</Card>
|
<Zap className="w-3 h-3" />
|
||||||
</TabsContent>
|
Gemini 3 Pro
|
||||||
|
</Button>
|
||||||
<TabsContent value="models" className="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Available Models</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Cursor forwards the requested model from each client request.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{modelsLoading ? (
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
Loading models...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{models.map((model) => (
|
|
||||||
<div
|
|
||||||
key={model.id}
|
|
||||||
className="rounded-lg border px-3 py-2 flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">{model.id}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{model.name} • {model.provider}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{model.id === currentModel && <Badge>Default</Badge>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="raw" className="space-y-4">
|
<Separator />
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<FileCode className="w-4 h-4 text-primary" />
|
|
||||||
cursor.settings.json
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{rawSettings?.path ?? '~/.ccs/cursor.settings.json'}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<CodeEditor
|
|
||||||
value={effectiveRawConfigText}
|
|
||||||
onChange={(value) => {
|
|
||||||
setRawConfigDirty(true);
|
|
||||||
setRawConfigText(value);
|
|
||||||
}}
|
|
||||||
language="json"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex gap-2 justify-end">
|
<div>
|
||||||
<Button
|
<h3 className="text-sm font-medium mb-2">Model Mapping</h3>
|
||||||
variant="outline"
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
onClick={() => {
|
Configure which models to use for each tier
|
||||||
setRawConfigDirty(false);
|
</p>
|
||||||
refetchRawSettings();
|
<div className="space-y-4">
|
||||||
}}
|
<CursorModelSelector
|
||||||
disabled={rawSettingsLoading}
|
label="Default Model"
|
||||||
>
|
description="Used when no specific tier is requested."
|
||||||
<RefreshCw
|
value={effectiveModel}
|
||||||
className={cn('w-4 h-4 mr-2', rawSettingsLoading && 'animate-spin')}
|
models={orderedModels}
|
||||||
/>
|
disabled={modelsLoading}
|
||||||
Refresh
|
onChange={(value) => {
|
||||||
</Button>
|
updateConfigDraft((draft) => ({ ...draft, model: value }));
|
||||||
<Button onClick={handleSaveRawSettings} disabled={disableRawSave}>
|
}}
|
||||||
{isSavingRawSettings ? (
|
/>
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
<CursorModelSelector
|
||||||
) : (
|
label="Opus (Most capable)"
|
||||||
<Save className="w-4 h-4 mr-2" />
|
description="Complex reasoning and highest quality responses."
|
||||||
)}
|
value={effectiveOpusModel}
|
||||||
Save Raw Settings
|
models={orderedModels}
|
||||||
</Button>
|
disabled={modelsLoading}
|
||||||
</div>
|
allowDefaultFallback
|
||||||
</CardContent>
|
onChange={(value) => {
|
||||||
</Card>
|
updateConfigDraft((draft) => ({ ...draft, opus_model: value }));
|
||||||
</TabsContent>
|
}}
|
||||||
</Tabs>
|
/>
|
||||||
|
<CursorModelSelector
|
||||||
|
label="Sonnet (Balanced)"
|
||||||
|
description="General coding and day-to-day chat workloads."
|
||||||
|
value={effectiveSonnetModel}
|
||||||
|
models={orderedModels}
|
||||||
|
disabled={modelsLoading}
|
||||||
|
allowDefaultFallback
|
||||||
|
onChange={(value) => {
|
||||||
|
updateConfigDraft((draft) => ({ ...draft, sonnet_model: value }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CursorModelSelector
|
||||||
|
label="Haiku (Fast)"
|
||||||
|
description="Low-latency and lightweight request paths."
|
||||||
|
value={effectiveHaikuModel}
|
||||||
|
models={orderedModels}
|
||||||
|
disabled={modelsLoading}
|
||||||
|
allowDefaultFallback
|
||||||
|
onChange={(value) => {
|
||||||
|
updateConfigDraft((draft) => ({ ...draft, haiku_model: value }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium">Runtime Settings</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cursor-port" className="text-xs">
|
||||||
|
Port
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="cursor-port"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={65535}
|
||||||
|
className="max-w-[150px] h-8"
|
||||||
|
value={effectivePort}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateConfigDraft((draft) => ({
|
||||||
|
...draft,
|
||||||
|
port: e.target.value,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="cursor-auto-start" className="text-xs">
|
||||||
|
Auto-start Daemon
|
||||||
|
</Label>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
Start Cursor daemon automatically when integration is used.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="cursor-auto-start"
|
||||||
|
checked={effectiveAutoStart}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
updateConfigDraft((draft) => ({ ...draft, auto_start: value }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="cursor-ghost-mode" className="text-xs">
|
||||||
|
Ghost Mode
|
||||||
|
</Label>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
Request `x-ghost-mode` to reduce telemetry.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="cursor-ghost-mode"
|
||||||
|
checked={effectiveGhostMode}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
updateConfigDraft((draft) => ({ ...draft, ghost_mode: value }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<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 mb-3">Available Models</h3>
|
||||||
|
{modelsLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Loading models...
|
||||||
|
</div>
|
||||||
|
) : models.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No model metadata available yet. Start daemon and refresh.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{models.map((model) => (
|
||||||
|
<div
|
||||||
|
key={model.id}
|
||||||
|
className="rounded-lg border px-3 py-2 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">{model.id}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{model.name} • {model.provider}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{model.id === currentModel && <Badge>Default</Badge>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">Cursor IDE</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
|
||||||
|
<span className="font-medium text-muted-foreground">File Path</span>
|
||||||
|
<code className="bg-muted px-1.5 py-0.5 rounded text-xs break-all">
|
||||||
|
{rawSettings?.path ?? '~/.ccs/cursor.settings.json'}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Model mapping writes `ANTHROPIC_MODEL`,
|
||||||
|
`ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and
|
||||||
|
`ANTHROPIC_DEFAULT_HAIKU_MODEL` in `cursor.settings.json`.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0 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>
|
||||||
|
<RawEditorSection
|
||||||
|
rawJsonContent={effectiveRawConfigText}
|
||||||
|
isRawJsonValid={isRawJsonValid}
|
||||||
|
rawJsonEdits={rawConfigDirty ? rawConfigText : null}
|
||||||
|
rawSettingsEnv={rawSettings?.settings?.env}
|
||||||
|
onChange={(value) => {
|
||||||
|
setRawConfigDirty(true);
|
||||||
|
setRawConfigText(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user