From 7b73658f8700c7d6f3804f00df29de4c14243028 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 14 Feb 2026 16:57:04 +0700 Subject: [PATCH 1/7] feat(cursor): sync model mapping across config and raw settings - add cursor model fields to unified config defaults and YAML comments - expand Cursor model catalog/defaults and return configured current model - keep cursor.settings.json env model fields in sync with config updates - align unit tests for new model defaults and route merge behavior Refs #555 --- src/config/unified-config-loader.ts | 6 + src/config/unified-config-types.ts | 9 ++ src/cursor/cursor-models.ts | 129 ++++++++++++--- src/web-server/routes/cursor-routes.ts | 3 +- .../routes/cursor-settings-routes.ts | 149 ++++++++++++++++-- tests/unit/cursor/cursor-models.test.ts | 23 +-- .../web-server/cursor-settings-routes.test.ts | 57 ++++++- 7 files changed, 322 insertions(+), 54 deletions(-) diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index 773259c4..790117b0 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -306,6 +306,10 @@ function mergeWithDefaults(partial: Partial): UnifiedConfig { port: partial.cursor?.port ?? DEFAULT_CURSOR_CONFIG.port, auto_start: partial.cursor?.auto_start ?? DEFAULT_CURSOR_CONFIG.auto_start, ghost_mode: partial.cursor?.ghost_mode ?? DEFAULT_CURSOR_CONFIG.ghost_mode, + model: partial.cursor?.model ?? DEFAULT_CURSOR_CONFIG.model, + opus_model: partial.cursor?.opus_model, + sonnet_model: partial.cursor?.sonnet_model, + haiku_model: partial.cursor?.haiku_model, }, // Global env - injected into all non-Claude subscription profiles global_env: { @@ -603,6 +607,8 @@ function generateYamlWithComments(config: UnifiedConfig): string { lines.push('# port: Port for cursor proxy daemon (default: 20129)'); lines.push('# auto_start: Auto-start daemon when CCS starts (default: false)'); lines.push('# ghost_mode: Disable telemetry for privacy (default: true)'); + lines.push('# model: Default model ID (used for ANTHROPIC_MODEL)'); + lines.push('# opus_model/sonnet_model/haiku_model: Optional tier model mapping'); lines.push('# ----------------------------------------------------------------------------'); lines.push( yaml.dump({ cursor: config.cursor }, { indent: 2, lineWidth: -1, quotingType: '"' }).trim() diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index a9faf6d6..6a7797d0 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -304,6 +304,14 @@ export interface CursorConfig { auto_start: boolean; /** Enable ghost mode to disable telemetry (default: true) */ ghost_mode: boolean; + /** Default model ID used by Cursor integration */ + model: string; + /** Optional tier mapping for Claude-compatible model routing */ + opus_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + sonnet_model?: string; + /** Optional tier mapping for Claude-compatible model routing */ + haiku_model?: string; } /** @@ -723,6 +731,7 @@ export const DEFAULT_CURSOR_CONFIG: CursorConfig = { port: 20129, auto_start: false, ghost_mode: true, + model: 'gpt-5.3-codex', }; /** diff --git a/src/cursor/cursor-models.ts b/src/cursor/cursor-models.ts index de878cdc..c1355fa0 100644 --- a/src/cursor/cursor-models.ts +++ b/src/cursor/cursor-models.ts @@ -13,61 +13,143 @@ import { isDaemonRunning } from './cursor-daemon'; export const DEFAULT_CURSOR_PORT = 20129; /** Default model ID */ -export const DEFAULT_CURSOR_MODEL = 'gpt-4.1'; +export const DEFAULT_CURSOR_MODEL = 'gpt-5.3-codex'; /** * Default models available through Cursor IDE. * Used as fallback when daemon is not reachable. - * Source: Cursor IDE supported models (Feb 2025) + * Source: Cursor docs model catalog (Feb 2026) */ export const DEFAULT_CURSOR_MODELS: CursorModel[] = [ // Anthropic Models { - id: 'claude-sonnet-4', - name: 'Claude Sonnet 4', + id: 'claude-4.6-opus', + name: 'Claude 4.6 Opus', provider: 'anthropic', }, { - id: 'claude-sonnet-4.5', - name: 'Claude Sonnet 4.5', + id: 'claude-4.6-opus-fast-mode', + name: 'Claude 4.6 Opus (Fast mode)', provider: 'anthropic', }, { - id: 'claude-opus-4', - name: 'Claude Opus 4', + id: 'claude-4.5-sonnet', + name: 'Claude 4.5 Sonnet', provider: 'anthropic', }, + { + id: 'claude-4.5-opus', + name: 'Claude 4.5 Opus', + provider: 'anthropic', + }, + { + id: 'claude-4.5-haiku', + name: 'Claude 4.5 Haiku', + provider: 'anthropic', + }, + { + id: 'claude-4-sonnet', + name: 'Claude 4 Sonnet', + provider: 'anthropic', + }, + { + id: 'claude-4-sonnet-1m', + name: 'Claude 4 Sonnet 1M', + provider: 'anthropic', + }, + + // Cursor Models + { + id: 'composer-1.5', + name: 'Composer 1.5', + provider: 'cursor', + }, + { + id: 'composer-1', + name: 'Composer 1', + provider: 'cursor', + }, // OpenAI Models { - id: 'gpt-4.1', - name: 'GPT-4.1', + id: 'gpt-5.3-codex', + name: 'GPT-5.3 Codex', provider: 'openai', isDefault: true, }, + { + id: 'gpt-5.2-codex', + name: 'GPT-5.2 Codex', + provider: 'openai', + }, + { + id: 'gpt-5.2', + name: 'GPT-5.2', + provider: 'openai', + }, + { + id: 'gpt-5.1-codex', + name: 'GPT-5.1 Codex', + provider: 'openai', + }, + { + id: 'gpt-5.1-codex-max', + name: 'GPT-5.1 Codex Max', + provider: 'openai', + }, + { + id: 'gpt-5.1-codex-mini', + name: 'GPT-5.1 Codex Mini', + provider: 'openai', + }, + { + id: 'gpt-5-codex', + name: 'GPT-5-Codex', + provider: 'openai', + }, + { + id: 'gpt-5', + name: 'GPT-5', + provider: 'openai', + }, + { + id: 'gpt-5-fast', + name: 'GPT-5 Fast', + provider: 'openai', + }, { id: 'gpt-5-mini', name: 'GPT-5 Mini', provider: 'openai', }, - { - id: 'o3-mini', - name: 'O3 Mini', - provider: 'openai', - }, // Google Models { - id: 'gemini-2.5-pro', - name: 'Gemini 2.5 Pro', + id: 'gemini-3-pro', + name: 'Gemini 3 Pro', + provider: 'google', + }, + { + id: 'gemini-3-pro-image-preview', + name: 'Gemini 3 Pro Image Preview', + provider: 'google', + }, + { + id: 'gemini-3-flash', + name: 'Gemini 3 Flash', + provider: 'google', + }, + { + id: 'gemini-2.5-flash', + name: 'Gemini 2.5 Flash', provider: 'google', }, - // Cursor Custom Models + // xAI Models { - id: 'cursor-small', - name: 'Cursor Small', - provider: 'cursor', + id: 'grok-code', + name: 'Grok Code', + provider: 'xai', }, ]; @@ -154,7 +236,7 @@ export async function getAvailableModels(port: number): Promise { /** * Get the default model. - * Uses gpt-4.1 as it's commonly available. + * Uses GPT-5.3 Codex as default. */ export function getDefaultModel(): string { return DEFAULT_CURSOR_MODEL; @@ -167,7 +249,8 @@ export function detectProvider(modelId: string): string { if (modelId.includes('claude')) return 'anthropic'; if (modelId.includes('gpt') || /^o[1-9]\d*(-|$)/.test(modelId)) return 'openai'; if (modelId.includes('gemini')) return 'google'; - if (modelId.includes('cursor')) return 'cursor'; + if (modelId.includes('cursor') || modelId.includes('composer')) return 'cursor'; + if (modelId.includes('grok')) return 'xai'; return 'unknown'; } diff --git a/src/web-server/routes/cursor-routes.ts b/src/web-server/routes/cursor-routes.ts index 0ba13344..b28c3705 100644 --- a/src/web-server/routes/cursor-routes.ts +++ b/src/web-server/routes/cursor-routes.ts @@ -7,7 +7,6 @@ import { Router } from 'express'; import { getDaemonStatus, getAvailableModels, - getDefaultModel, startDaemon, stopDaemon, checkAuthStatus, @@ -150,7 +149,7 @@ router.get('/models', async (_req: Request, res: Response): Promise => { try { const cursorConfig = getCursorConfig(); const models = await getAvailableModels(cursorConfig.port); - res.json({ models, current: getDefaultModel() }); + res.json({ models, current: cursorConfig.model }); } catch (error) { res.status(500).json({ error: (error as Error).message }); } diff --git a/src/web-server/routes/cursor-settings-routes.ts b/src/web-server/routes/cursor-settings-routes.ts index ce1d229a..25bbd685 100644 --- a/src/web-server/routes/cursor-settings-routes.ts +++ b/src/web-server/routes/cursor-settings-routes.ts @@ -13,6 +13,7 @@ import { saveUnifiedConfig, getCursorConfig, } from '../../config/unified-config-loader'; +import type { CursorConfig } from '../../config/unified-config-types'; const router = Router(); @@ -34,8 +35,78 @@ function parseLocalCursorPort(settings: unknown): number | null { } } +function parseRequiredModel(value: unknown): string | null { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function parseOptionalModel(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +function getDefaultCursorSettings(cursorConfig: CursorConfig): { env: Record } { + const model = cursorConfig.model || DEFAULT_CURSOR_CONFIG.model; + return { + env: { + ANTHROPIC_BASE_URL: `http://127.0.0.1:${cursorConfig.port}`, + ANTHROPIC_AUTH_TOKEN: 'cursor-managed', + ANTHROPIC_MODEL: model, + ANTHROPIC_DEFAULT_OPUS_MODEL: cursorConfig.opus_model || model, + ANTHROPIC_DEFAULT_SONNET_MODEL: cursorConfig.sonnet_model || model, + ANTHROPIC_DEFAULT_HAIKU_MODEL: cursorConfig.haiku_model || model, + }, + }; +} + +function syncRawSettingsFromCursorConfig(cursorConfig: CursorConfig): void { + const settingsPath = path.join(getCcsDir(), 'cursor.settings.json'); + + let settings: Record = {}; + if (fs.existsSync(settingsPath)) { + try { + const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + settings = parsed as Record; + } + } catch { + settings = {}; + } + } + + const envSource = settings.env; + const env = + typeof envSource === 'object' && envSource !== null && !Array.isArray(envSource) + ? { ...(envSource as Record) } + : {}; + + const model = cursorConfig.model || DEFAULT_CURSOR_CONFIG.model; + const localPort = parseLocalCursorPort({ env }); + if (!env.ANTHROPIC_BASE_URL || localPort !== null) { + env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${cursorConfig.port}`; + } + if (!env.ANTHROPIC_AUTH_TOKEN) { + env.ANTHROPIC_AUTH_TOKEN = 'cursor-managed'; + } + env.ANTHROPIC_MODEL = model; + env.ANTHROPIC_DEFAULT_OPUS_MODEL = cursorConfig.opus_model || model; + env.ANTHROPIC_DEFAULT_SONNET_MODEL = cursorConfig.sonnet_model || model; + env.ANTHROPIC_DEFAULT_HAIKU_MODEL = cursorConfig.haiku_model || model; + + const nextSettings = { + ...settings, + env, + }; + + const tempPath = settingsPath + '.tmp'; + fs.writeFileSync(tempPath, JSON.stringify(nextSettings, null, 2) + '\n'); + fs.renameSync(tempPath, settingsPath); +} + /** - * GET /api/cursor/settings - Get cursor config (port, auto_start, ghost_mode) + * GET /api/cursor/settings - Get cursor config */ router.get('/', (_req: Request, res: Response): void => { try { @@ -82,8 +153,40 @@ router.put('/', (req: Request, res: Response): void => { res.status(400).json({ error: 'ghost_mode must be a boolean' }); return; } + if ('model' in updates && !parseRequiredModel(updates.model)) { + res.status(400).json({ error: 'model must be a non-empty string' }); + return; + } + if ( + 'opus_model' in updates && + updates.opus_model !== undefined && + updates.opus_model !== null && + typeof updates.opus_model !== 'string' + ) { + res.status(400).json({ error: 'opus_model must be a string' }); + return; + } + if ( + 'sonnet_model' in updates && + updates.sonnet_model !== undefined && + updates.sonnet_model !== null && + typeof updates.sonnet_model !== 'string' + ) { + res.status(400).json({ error: 'sonnet_model must be a string' }); + return; + } + if ( + 'haiku_model' in updates && + updates.haiku_model !== undefined && + updates.haiku_model !== null && + typeof updates.haiku_model !== 'string' + ) { + res.status(400).json({ error: 'haiku_model must be a string' }); + return; + } const config = loadOrCreateUnifiedConfig(); + const normalizedModel = parseRequiredModel(updates.model); // Merge updates with existing config // Only known fields are merged — unknown properties are ignored @@ -94,9 +197,23 @@ router.put('/', (req: Request, res: Response): void => { updates.auto_start ?? config.cursor?.auto_start ?? DEFAULT_CURSOR_CONFIG.auto_start, ghost_mode: updates.ghost_mode ?? config.cursor?.ghost_mode ?? DEFAULT_CURSOR_CONFIG.ghost_mode, + model: normalizedModel ?? config.cursor?.model ?? DEFAULT_CURSOR_CONFIG.model, + opus_model: + 'opus_model' in updates + ? parseOptionalModel(updates.opus_model) + : config.cursor?.opus_model, + sonnet_model: + 'sonnet_model' in updates + ? parseOptionalModel(updates.sonnet_model) + : config.cursor?.sonnet_model, + haiku_model: + 'haiku_model' in updates + ? parseOptionalModel(updates.haiku_model) + : config.cursor?.haiku_model, }; saveUnifiedConfig(config); + syncRawSettingsFromCursorConfig(config.cursor); res.json({ success: true, cursor: config.cursor }); } catch (error) { res.status(500).json({ error: (error as Error).message }); @@ -114,14 +231,7 @@ router.get('/raw', (_req: Request, res: Response): void => { // If file doesn't exist, return default structure if (!fs.existsSync(settingsPath)) { - // Create settings structure matching Cursor pattern - // Use 127.0.0.1 instead of localhost for more reliable local connections - const defaultSettings = { - env: { - ANTHROPIC_BASE_URL: `http://127.0.0.1:${cursorConfig.port}`, - ANTHROPIC_AUTH_TOKEN: 'cursor-managed', - }, - }; + const defaultSettings = getDefaultCursorSettings(cursorConfig); res.json({ settings: defaultSettings, @@ -186,14 +296,19 @@ router.put('/raw', (req: Request, res: Response): void => { // Keep unified config aligned with raw settings edits (parity with Copilot raw editor). const parsedPort = parseLocalCursorPort(settings); - if (parsedPort) { - const config = loadOrCreateUnifiedConfig(); - config.cursor = { - ...(config.cursor ?? DEFAULT_CURSOR_CONFIG), - port: parsedPort, - }; - saveUnifiedConfig(config); - } + const config = loadOrCreateUnifiedConfig(); + const env = (settings as { env?: Record }).env ?? {}; + const model = parseRequiredModel(env.ANTHROPIC_MODEL) ?? config.cursor?.model; + + config.cursor = { + ...(config.cursor ?? DEFAULT_CURSOR_CONFIG), + ...(parsedPort !== null ? { port: parsedPort } : {}), + ...(model ? { model } : {}), + opus_model: parseOptionalModel(env.ANTHROPIC_DEFAULT_OPUS_MODEL), + sonnet_model: parseOptionalModel(env.ANTHROPIC_DEFAULT_SONNET_MODEL), + haiku_model: parseOptionalModel(env.ANTHROPIC_DEFAULT_HAIKU_MODEL), + }; + saveUnifiedConfig(config); const stat = fs.statSync(settingsPath); res.json({ success: true, mtime: stat.mtimeMs }); diff --git a/tests/unit/cursor/cursor-models.test.ts b/tests/unit/cursor/cursor-models.test.ts index e510a590..e5127f02 100644 --- a/tests/unit/cursor/cursor-models.test.ts +++ b/tests/unit/cursor/cursor-models.test.ts @@ -35,8 +35,8 @@ describe('DEFAULT_CURSOR_PORT', () => { }); describe('DEFAULT_CURSOR_MODEL', () => { - it('is gpt-4.1', () => { - expect(DEFAULT_CURSOR_MODEL).toBe('gpt-4.1'); + it('is gpt-5.3-codex', () => { + expect(DEFAULT_CURSOR_MODEL).toBe('gpt-5.3-codex'); }); }); @@ -48,12 +48,12 @@ describe('getDefaultModel', () => { describe('detectProvider', () => { it('detects anthropic models', () => { - expect(detectProvider('claude-sonnet-4')).toBe('anthropic'); - expect(detectProvider('claude-opus-4')).toBe('anthropic'); + expect(detectProvider('claude-4.5-sonnet')).toBe('anthropic'); + expect(detectProvider('claude-4.6-opus')).toBe('anthropic'); }); it('detects openai models', () => { - expect(detectProvider('gpt-4.1')).toBe('openai'); + expect(detectProvider('gpt-5.3-codex')).toBe('openai'); expect(detectProvider('gpt-5-mini')).toBe('openai'); expect(detectProvider('o3-mini')).toBe('openai'); }); @@ -65,11 +65,16 @@ describe('detectProvider', () => { }); it('detects google models', () => { - expect(detectProvider('gemini-2.5-pro')).toBe('google'); + expect(detectProvider('gemini-3-pro')).toBe('google'); }); it('detects cursor models', () => { - expect(detectProvider('cursor-small')).toBe('cursor'); + expect(detectProvider('composer-1.5')).toBe('cursor'); + expect(detectProvider('cursor-model')).toBe('cursor'); + }); + + it('detects xai models', () => { + expect(detectProvider('grok-code')).toBe('xai'); }); it('defaults to unknown for unrecognized models', () => { @@ -79,8 +84,8 @@ describe('detectProvider', () => { describe('formatModelName', () => { it('returns catalog name for known models', () => { - expect(formatModelName('claude-sonnet-4')).toBe('Claude Sonnet 4'); - expect(formatModelName('gpt-4.1')).toBe('GPT-4.1'); + expect(formatModelName('claude-4.6-opus')).toBe('Claude 4.6 Opus'); + expect(formatModelName('gpt-5.3-codex')).toBe('GPT-5.3 Codex'); }); it('converts kebab-case to title case for unknown models', () => { diff --git a/tests/unit/web-server/cursor-settings-routes.test.ts b/tests/unit/web-server/cursor-settings-routes.test.ts index c2e5dfac..fb163a7a 100644 --- a/tests/unit/web-server/cursor-settings-routes.test.ts +++ b/tests/unit/web-server/cursor-settings-routes.test.ts @@ -128,6 +128,13 @@ describe('Cursor Settings Routes Logic', () => { port: updates.port ?? config.cursor?.port ?? 3000, auto_start: updates.auto_start ?? config.cursor?.auto_start ?? false, ghost_mode: updates.ghost_mode ?? config.cursor?.ghost_mode ?? false, + model: updates.model ?? config.cursor?.model ?? 'gpt-5.3-codex', + opus_model: + updates.opus_model !== undefined ? updates.opus_model : config.cursor?.opus_model, + sonnet_model: + updates.sonnet_model !== undefined ? updates.sonnet_model : config.cursor?.sonnet_model, + haiku_model: + updates.haiku_model !== undefined ? updates.haiku_model : config.cursor?.haiku_model, }; expect(cursorConfig.port).toBe(5000); @@ -137,7 +144,13 @@ describe('Cursor Settings Routes Logic', () => { it('updates port only', () => { const config = loadOrCreateUnifiedConfig(); - config.cursor = { enabled: false, port: 3000, auto_start: false, ghost_mode: false }; + config.cursor = { + enabled: false, + port: 3000, + auto_start: false, + ghost_mode: false, + model: 'gpt-5.3-codex', + }; saveUnifiedConfig(config); const updates = { port: 4000 }; @@ -146,6 +159,13 @@ describe('Cursor Settings Routes Logic', () => { port: updates.port ?? config.cursor?.port ?? 3000, auto_start: updates.auto_start ?? config.cursor?.auto_start ?? false, ghost_mode: updates.ghost_mode ?? config.cursor?.ghost_mode ?? false, + model: updates.model ?? config.cursor?.model ?? 'gpt-5.3-codex', + opus_model: + updates.opus_model !== undefined ? updates.opus_model : config.cursor?.opus_model, + sonnet_model: + updates.sonnet_model !== undefined ? updates.sonnet_model : config.cursor?.sonnet_model, + haiku_model: + updates.haiku_model !== undefined ? updates.haiku_model : config.cursor?.haiku_model, }; expect(cursorConfig.port).toBe(4000); @@ -155,7 +175,13 @@ describe('Cursor Settings Routes Logic', () => { it('updates auto_start only', () => { const config = loadOrCreateUnifiedConfig(); - config.cursor = { enabled: false, port: 3000, auto_start: false, ghost_mode: false }; + config.cursor = { + enabled: false, + port: 3000, + auto_start: false, + ghost_mode: false, + model: 'gpt-5.3-codex', + }; saveUnifiedConfig(config); const updates = { auto_start: true }; @@ -164,6 +190,13 @@ describe('Cursor Settings Routes Logic', () => { port: updates.port ?? config.cursor?.port ?? 3000, auto_start: updates.auto_start ?? config.cursor?.auto_start ?? false, ghost_mode: updates.ghost_mode ?? config.cursor?.ghost_mode ?? false, + model: updates.model ?? config.cursor?.model ?? 'gpt-5.3-codex', + opus_model: + updates.opus_model !== undefined ? updates.opus_model : config.cursor?.opus_model, + sonnet_model: + updates.sonnet_model !== undefined ? updates.sonnet_model : config.cursor?.sonnet_model, + haiku_model: + updates.haiku_model !== undefined ? updates.haiku_model : config.cursor?.haiku_model, }; expect(cursorConfig.port).toBe(3000); @@ -173,7 +206,13 @@ describe('Cursor Settings Routes Logic', () => { it('updates ghost_mode only', () => { const config = loadOrCreateUnifiedConfig(); - config.cursor = { enabled: false, port: 3000, auto_start: false, ghost_mode: false }; + config.cursor = { + enabled: false, + port: 3000, + auto_start: false, + ghost_mode: false, + model: 'gpt-5.3-codex', + }; saveUnifiedConfig(config); const updates = { ghost_mode: true }; @@ -182,6 +221,13 @@ describe('Cursor Settings Routes Logic', () => { port: updates.port ?? config.cursor?.port ?? 3000, auto_start: updates.auto_start ?? config.cursor?.auto_start ?? false, ghost_mode: updates.ghost_mode ?? config.cursor?.ghost_mode ?? false, + model: updates.model ?? config.cursor?.model ?? 'gpt-5.3-codex', + opus_model: + updates.opus_model !== undefined ? updates.opus_model : config.cursor?.opus_model, + sonnet_model: + updates.sonnet_model !== undefined ? updates.sonnet_model : config.cursor?.sonnet_model, + haiku_model: + updates.haiku_model !== undefined ? updates.haiku_model : config.cursor?.haiku_model, }; expect(cursorConfig.port).toBe(3000); @@ -203,11 +249,16 @@ describe('Cursor Settings Routes Logic', () => { env: { ANTHROPIC_BASE_URL: `http://127.0.0.1:${cursorPort}`, ANTHROPIC_AUTH_TOKEN: 'cursor-managed', + ANTHROPIC_MODEL: 'gpt-5.3-codex', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5.3-codex', }, }; expect(defaultSettings.env.ANTHROPIC_BASE_URL).toContain('http://127.0.0.1:'); expect(defaultSettings.env.ANTHROPIC_AUTH_TOKEN).toBe('cursor-managed'); + expect(defaultSettings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); }); it('reads existing file', () => { From 7ab7a156ebfb696e1d2f275b58c32115800283c2 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 14 Feb 2026 16:57:27 +0700 Subject: [PATCH 2/7] 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 --- ui/public/assets/sidebar/cursor.svg | 1 + ui/src/components/layout/app-sidebar.tsx | 47 +- ui/src/hooks/use-cursor.ts | 4 + ui/src/pages/cursor.tsx | 867 ++++++++++++++++++----- 4 files changed, 733 insertions(+), 186 deletions(-) create mode 100644 ui/public/assets/sidebar/cursor.svg diff --git a/ui/public/assets/sidebar/cursor.svg b/ui/public/assets/sidebar/cursor.svg new file mode 100644 index 00000000..a5b2ee3b --- /dev/null +++ b/ui/public/assets/sidebar/cursor.svg @@ -0,0 +1 @@ +Cursor \ No newline at end of file diff --git a/ui/src/components/layout/app-sidebar.tsx b/ui/src/components/layout/app-sidebar.tsx index ddfdad91..aaa04a7d 100644 --- a/ui/src/components/layout/app-sidebar.tsx +++ b/ui/src/components/layout/app-sidebar.tsx @@ -11,8 +11,8 @@ import { BarChart3, Gauge, Github, - Bot, } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; import { Sidebar, SidebarContent, @@ -35,8 +35,34 @@ import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; 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 -const navGroups = [ +const navGroups: SidebarGroupDef[] = [ { title: 'General', items: [ @@ -64,7 +90,7 @@ const navGroups = [ ], }, { 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', icon: Users, @@ -114,6 +140,17 @@ export function AppSidebar() { ); }; + const renderMenuIcon = (item: Pick) => { + if (item.iconSrc) { + return ; + } + if (item.icon) { + const Icon = item.icon; + return ; + } + return null; + }; + return ( @@ -141,7 +178,7 @@ export function AppSidebar() { isActive={isParentActive(item.children)} onClick={() => navigate(item.path)} > - {item.icon && } + {renderMenuIcon(item)} {getItemLabel(item)} @@ -173,7 +210,7 @@ export function AppSidebar() { tooltip={getItemLabel(item)} > - {item.icon && } + {renderMenuIcon(item)} {getItemLabel(item)} diff --git a/ui/src/hooks/use-cursor.ts b/ui/src/hooks/use-cursor.ts index e7d43976..20ed9be8 100644 --- a/ui/src/hooks/use-cursor.ts +++ b/ui/src/hooks/use-cursor.ts @@ -26,6 +26,10 @@ export interface CursorConfig { port: number; auto_start: boolean; ghost_mode: boolean; + model: string; + opus_model?: string; + sonnet_model?: string; + haiku_model?: string; } export interface CursorModel { diff --git a/ui/src/pages/cursor.tsx b/ui/src/pages/cursor.tsx index 40d35a9f..d37a0c2d 100644 --- a/ui/src/pages/cursor.tsx +++ b/ui/src/pages/cursor.tsx @@ -6,9 +6,9 @@ import { useMemo, useState, type ElementType } from 'react'; import { toast } from 'sonner'; import { - Bot, + AlertTriangle, CheckCircle2, - FileCode, + Code2, Key, Loader2, Play, @@ -18,19 +18,30 @@ import { Save, Server, ShieldCheck, + Sparkles, + Zap, XCircle, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useCursor } from '@/hooks/use-cursor'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 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 { Dialog, DialogContent, @@ -44,20 +55,166 @@ interface CursorConfigDraft { port: string; auto_start: boolean; ghost_mode: boolean; + model: string; + opus_model: string; + sonnet_model: string; + haiku_model: string; +} + +interface RawSettingsParseResult { + isValid: boolean; + settings?: { env?: Record }; + error?: string; } function buildConfigDraft(config?: { port?: number; auto_start?: boolean; ghost_mode?: boolean; + model?: string; + opus_model?: string; + sonnet_model?: string; + haiku_model?: string; }): CursorConfigDraft { return { port: String(config?.port ?? 20129), auto_start: config?.auto_start ?? false, 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 }, + }; + } 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 ( +
+
+ +

{description}

+
+ +
+ ); +} + function StatusItem({ icon: Icon, label, @@ -131,11 +288,47 @@ export function CursorPage() { const effectivePort = configDirty ? configDraft.port : pristineConfigDraft.port; const effectiveAutoStart = configDirty ? configDraft.auto_start : pristineConfigDraft.auto_start; 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 ? rawConfigText : JSON.stringify(rawSettings?.settings ?? {}, null, 2); 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(); + 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) => { setConfigDraft((previousDraft) => { @@ -151,11 +344,17 @@ export function CursorPage() { [status?.enabled] ); - const handleSaveConfig = async () => { + const handleSaveConfig = async ({ + suppressSuccessToast = false, + }: { suppressSuccessToast?: boolean } = {}) => { const parsedPort = Number(effectivePort); if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 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 { @@ -163,6 +362,10 @@ export function CursorPage() { port: parsedPort, auto_start: effectiveAutoStart, ghost_mode: effectiveGhostMode, + model: effectiveModel, + opus_model: effectiveOpusModel || undefined, + sonnet_model: effectiveSonnetModel || undefined, + haiku_model: effectiveHaikuModel || undefined, }); setConfigDirty(false); setConfigDraft( @@ -170,14 +373,104 @@ export function CursorPage() { port: parsedPort, auto_start: effectiveAutoStart, 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) { 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) => { try { await updateConfigAsync({ enabled }); @@ -242,32 +535,29 @@ export function CursorPage() { } }; - const handleSaveRawSettings = async () => { + const handleSaveRawSettings = async ({ + suppressSuccessToast = false, + }: { suppressSuccessToast?: boolean } = {}) => { if (!rawSettingsReady) { toast.error('Raw settings are still loading. Please wait and try again.'); - return; + return false; } - let parsed: unknown; - try { - parsed = JSON.parse(effectiveRawConfigText || '{}'); - } 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; + if (!rawParseResult.isValid || !rawParseResult.settings) { + toast.error(rawParseResult.error || 'Invalid JSON'); + return false; } try { await saveRawSettingsAsync({ - settings: parsed as { env?: Record }, + settings: rawParseResult.settings, expectedMtime: rawSettings?.mtime, }); setRawConfigDirty(false); - toast.success('Raw settings saved'); + if (!suppressSuccessToast) { + toast.success('Raw settings saved'); + } + return true; } catch (error) { const message = (error as Error).message || 'Failed to save raw settings'; if (message === 'CONFLICT') { @@ -275,9 +565,41 @@ export function CursorPage() { } else { 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 ( <>
@@ -285,7 +607,11 @@ export function CursorPage() {
- +

Cursor

{integrationBadge}
@@ -304,6 +630,20 @@ export function CursorPage() {
+
+
+ + + Unofficial API - Use at Your Own Risk + +
+
    +
  • Reverse-engineered integration may break anytime
  • +
  • Abuse or excessive usage may risk account restrictions
  • +
  • No warranty, no responsibility from CCS
  • +
+
+
-
- - - Config - Models - Raw Settings - +
+
+
+
+
+

Cursor Configuration

+ {rawSettings && ( + + cursor.settings.json + + )} +
+ {rawSettings && ( +

+ Last modified:{' '} + {rawSettings.exists + ? new Date(rawSettings.mtime).toLocaleString() + : 'Never saved'} +

+ )} +
+
+
+ + +
+
- - - - Cursor Runtime Config - - Controls daemon behavior for local Cursor OpenAI-compatible endpoint. - - - -
- - { - updateConfigDraft((draft) => ({ ...draft, port: e.target.value })); - }} - /> -
+
+
+ +
+ + + Model Config + + + Settings + + + Info + + +
-
-
- -

- Start cursor daemon automatically when integration is used. -

-
- { - updateConfigDraft((draft) => ({ ...draft, auto_start: value })); - }} - /> -
- -
-
- -

- Requests `x-ghost-mode` to reduce telemetry. -

-
- { - updateConfigDraft((draft) => ({ ...draft, ghost_mode: value })); - }} - /> -
- -
- -
- - - - - - - - Available Models - - Cursor forwards the requested model from each client request. - - - - {modelsLoading ? ( -
- - Loading models... -
- ) : ( -
- {models.map((model) => ( -
-
-

{model.id}

-

- {model.name} • {model.provider} -

-
-
- {model.id === currentModel && Default} +
+ + +
+
+

+ + Presets +

+

+ Apply pre-configured model mappings +

+
+ + +
- ))} -
- )} - - -
- - - - - - cursor.settings.json - - - {rawSettings?.path ?? '~/.ccs/cursor.settings.json'} - - - - { - setRawConfigDirty(true); - setRawConfigText(value); - }} - language="json" - /> + -
- - -
-
-
-
- +
+

Model Mapping

+

+ Configure which models to use for each tier +

+
+ { + updateConfigDraft((draft) => ({ ...draft, model: value })); + }} + /> + { + updateConfigDraft((draft) => ({ ...draft, opus_model: value })); + }} + /> + { + updateConfigDraft((draft) => ({ ...draft, sonnet_model: value })); + }} + /> + { + updateConfigDraft((draft) => ({ ...draft, haiku_model: value })); + }} + /> +
+
+
+ + + + + +
+
+

Runtime Settings

+ +
+ + { + updateConfigDraft((draft) => ({ + ...draft, + port: e.target.value, + })); + }} + /> +
+ +
+
+ +

+ Start Cursor daemon automatically when integration is used. +

+
+ { + updateConfigDraft((draft) => ({ ...draft, auto_start: value })); + }} + /> +
+ +
+
+ +

+ Request `x-ghost-mode` to reduce telemetry. +

+
+ { + updateConfigDraft((draft) => ({ ...draft, ghost_mode: value })); + }} + /> +
+
+
+
+
+ + + +
+
+

Available Models

+ {modelsLoading ? ( +
+ + Loading models... +
+ ) : models.length === 0 ? ( +

+ No model metadata available yet. Start daemon and refresh. +

+ ) : ( +
+ {models.map((model) => ( +
+
+

{model.id}

+

+ {model.name} • {model.provider} +

+
+ {model.id === currentModel && Default} +
+ ))} +
+ )} +
+ +
+
+ Provider + Cursor IDE +
+
+ File Path + + {rawSettings?.path ?? '~/.ccs/cursor.settings.json'} + +
+

+ Model mapping writes `ANTHROPIC_MODEL`, + `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and + `ANTHROPIC_DEFAULT_HAIKU_MODEL` in `cursor.settings.json`. +

+
+
+
+
+
+ +
+ +
+
+ + + Raw Configuration (JSON) + +
+ { + setRawConfigDirty(true); + setRawConfigText(value); + }} + /> +
+
From 4f086aa34adb32be42527e0f3a77c4369e88df72 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 14 Feb 2026 17:48:00 +0700 Subject: [PATCH 3/7] fix(cursor-settings): remove sync fs calls in settings sync - switch cursor settings file reads/writes to fs.promises - make settings sync and route handler async with awaited file ops - restores maintainability baseline for synchronous fs references --- .../routes/cursor-settings-routes.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/web-server/routes/cursor-settings-routes.ts b/src/web-server/routes/cursor-settings-routes.ts index 25bbd685..8a120dfe 100644 --- a/src/web-server/routes/cursor-settings-routes.ts +++ b/src/web-server/routes/cursor-settings-routes.ts @@ -5,6 +5,7 @@ import type { Request, Response } from 'express'; import { Router } from 'express'; import * as fs from 'fs'; +import { promises as fsp } from 'fs'; import * as path from 'path'; import { getCcsDir } from '../../utils/config-manager'; import { DEFAULT_CURSOR_CONFIG } from '../../config/unified-config-types'; @@ -61,19 +62,17 @@ function getDefaultCursorSettings(cursorConfig: CursorConfig): { env: Record { const settingsPath = path.join(getCcsDir(), 'cursor.settings.json'); let settings: Record = {}; - if (fs.existsSync(settingsPath)) { - try { - const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); - if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { - settings = parsed as Record; - } - } catch { - settings = {}; + try { + const parsed = JSON.parse(await fsp.readFile(settingsPath, 'utf-8')); + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + settings = parsed as Record; } + } catch { + settings = {}; } const envSource = settings.env; @@ -101,8 +100,8 @@ function syncRawSettingsFromCursorConfig(cursorConfig: CursorConfig): void { }; const tempPath = settingsPath + '.tmp'; - fs.writeFileSync(tempPath, JSON.stringify(nextSettings, null, 2) + '\n'); - fs.renameSync(tempPath, settingsPath); + await fsp.writeFile(tempPath, JSON.stringify(nextSettings, null, 2) + '\n'); + await fsp.rename(tempPath, settingsPath); } /** @@ -120,7 +119,7 @@ router.get('/', (_req: Request, res: Response): void => { /** * PUT /api/cursor/settings - Update cursor config */ -router.put('/', (req: Request, res: Response): void => { +router.put('/', async (req: Request, res: Response): Promise => { try { const updates = req.body; @@ -213,7 +212,7 @@ router.put('/', (req: Request, res: Response): void => { }; saveUnifiedConfig(config); - syncRawSettingsFromCursorConfig(config.cursor); + await syncRawSettingsFromCursorConfig(config.cursor); res.json({ success: true, cursor: config.cursor }); } catch (error) { res.status(500).json({ error: (error as Error).message }); From decdffea61f543beca508ce209b543bbeee81a4c Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 14 Feb 2026 17:48:18 +0700 Subject: [PATCH 4/7] fix(cursor-ui): restore info card order and list scrolling - move Cursor info metadata card above available models - add min-h-0/flex containment to preserve scroll behavior - fix clipped content in cursor config dashboard layout --- ui/src/pages/cursor.tsx | 50 ++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/ui/src/pages/cursor.tsx b/ui/src/pages/cursor.tsx index d37a0c2d..f6c4fa74 100644 --- a/ui/src/pages/cursor.tsx +++ b/ui/src/pages/cursor.tsx @@ -766,8 +766,8 @@ export function CursorPage() {
-
-
+
+
@@ -823,8 +823,8 @@ export function CursorPage() {
-
-
+
+
@@ -840,7 +840,7 @@ export function CursorPage() {
-
+
- +
+
+
+ Provider + Cursor IDE +
+
+ File Path + + {rawSettings?.path ?? '~/.ccs/cursor.settings.json'} + +
+

+ Model mapping writes `ANTHROPIC_MODEL`, + `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and + `ANTHROPIC_DEFAULT_HAIKU_MODEL` in `cursor.settings.json`. +

+
+

Available Models

{modelsLoading ? ( @@ -1051,24 +1069,6 @@ export function CursorPage() {
)}
- -
-
- Provider - Cursor IDE -
-
- File Path - - {rawSettings?.path ?? '~/.ccs/cursor.settings.json'} - -
-

- Model mapping writes `ANTHROPIC_MODEL`, - `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and - `ANTHROPIC_DEFAULT_HAIKU_MODEL` in `cursor.settings.json`. -

-
From 15f7bce43213935d3e2b12a0d7b2ab88d4db7dd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Feb 2026 10:52:05 +0000 Subject: [PATCH 5/7] chore(release): 7.44.0-dev.1 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4c12602..85641d42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.44.0", + "version": "7.44.0-dev.1", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", From b40fe866ec293fc08dbb877a000a87addd3ab16d Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 15 Feb 2026 05:39:46 +0700 Subject: [PATCH 6/7] fix(cursor-ui): label cursor dashboard as beta - add visible Beta badge in Cursor page header - clarifies feature is not production-tested yet --- ui/src/pages/cursor.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/src/pages/cursor.tsx b/ui/src/pages/cursor.tsx index f6c4fa74..0997dddd 100644 --- a/ui/src/pages/cursor.tsx +++ b/ui/src/pages/cursor.tsx @@ -613,6 +613,12 @@ export function CursorPage() { className="w-5 h-5 object-contain shrink-0" />

Cursor

+ + Beta + {integrationBadge}