feat(cursor): split legacy bridge from cliproxy provider

This commit is contained in:
Tam Nhu Tran committed 2026-04-15 17:04:37 -04:00
1 parent 1bad3b0305
commit 3b5941c60b
36 files changed
+974 -155

No files matched your search

+5 -1
View File
@@ -1,5 +1,5 @@
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from 'sonner';
import { queryClient } from '@/lib/query-client';
@@ -129,6 +129,10 @@ export default function App() {
/>
<Route
path="/cursor"
element={<Navigate to="/cliproxy?provider=cursor" replace />}
/>
<Route
path="/legacy/cursor"
element={
<Suspense fallback={<PageLoader />}>
<CursorPage />
@@ -25,6 +25,17 @@ interface ProviderEditorHeaderProps {
onSave: () => void;
}
function formatSettingsPathBadgeLabel(pathValue: string): string {
const normalized = pathValue.replace(/\\/g, '/');
const cliproxySegment = '/cliproxy/providers/';
const cliproxyIndex = normalized.lastIndexOf(cliproxySegment);
if (cliproxyIndex !== -1) {
return normalized.slice(cliproxyIndex + 1);
}
return normalized.replace(/^.*\//, '');
}
export function ProviderEditorHeader({
displayName,
logoProvider,
@@ -63,7 +74,7 @@ export function ProviderEditorHeader({
)}
{!isRemoteMode && data?.path && (
<Badge variant="outline" className="text-xs">
{data.path.replace(/^.*[\\/]/, '')}
{formatSettingsPathBadgeLabel(data.path)}
</Badge>
)}
</div>
@@ -15,6 +15,7 @@ import {
isAnthropicModelEnvKey,
} from '@/lib/extended-context-utils';
import { supportsExtendedContext } from '@/lib/model-catalogs';
import { isValidProvider } from '@/lib/provider-config';
/** Required env vars for CLIProxy providers (informational only - runtime fills defaults) */
const REQUIRED_ENV_KEYS = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN'] as const;
@@ -39,12 +40,18 @@ export function useProviderEditor(
queryFn: async () => {
const res = await fetch(`/api/settings/${provider}/raw`);
if (!res.ok) {
const fallbackPath =
provider === 'cursor'
? `~/.ccs/cliproxy/providers/${provider}.settings.json`
: isValidProvider(provider)
? `~/.ccs/${provider}.settings.json`
: `~/.ccs/profiles/${provider}/settings.json`;
// Return empty settings for unconfigured providers
return {
profile: provider,
settings: { env: {} },
mtime: Date.now(),
path: `~/.ccs/profiles/${provider}/settings.json`,
path: fallbackPath,
};
}
return res.json();
+5 -1
View File
@@ -126,7 +126,11 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] {
{
title: t('nav.deprecated'),
items: [
{ path: '/cursor', iconSrc: '/assets/sidebar/cursor.svg', label: t('nav.cursorIde') },
{
path: '/legacy/cursor',
iconSrc: '/assets/sidebar/cursor.svg',
label: `${t('nav.cursorIde')} (Legacy)`,
},
],
},
{
+19 -11
View File
@@ -57,6 +57,8 @@ interface CursorAuthResult {
message: string;
}
const LEGACY_CURSOR_API_BASE = '/legacy/cursor';
export interface CursorProbeResult {
ok: boolean;
stage: 'config' | 'auth' | 'daemon' | 'runtime';
@@ -87,25 +89,25 @@ function getProbeErrorMessage(value: unknown): string | null {
}
async function fetchCursorStatus(): Promise<CursorStatus> {
const res = await fetch(withApiBase('/cursor/status'));
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/status`));
if (!res.ok) throw new Error('Failed to fetch cursor status');
return res.json();
}
async function fetchCursorConfig(): Promise<CursorConfig> {
const res = await fetch(withApiBase('/cursor/settings'));
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/settings`));
if (!res.ok) throw new Error('Failed to fetch cursor config');
return res.json();
}
async function fetchCursorModels(): Promise<CursorModelsResponse> {
const res = await fetch(withApiBase('/cursor/models'));
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/models`));
if (!res.ok) throw new Error('Failed to fetch cursor models');
return res.json();
}
async function fetchCursorRawSettings(): Promise<CursorRawSettings> {
const res = await fetch(withApiBase('/cursor/settings/raw'));
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/settings/raw`));
if (!res.ok) throw new Error('Failed to fetch cursor raw settings');
return res.json();
}
@@ -113,7 +115,7 @@ async function fetchCursorRawSettings(): Promise<CursorRawSettings> {
async function updateCursorConfig(
updates: Partial<CursorConfig>
): Promise<{ success: boolean; cursor: CursorConfig }> {
const res = await fetch(withApiBase('/cursor/settings'), {
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/settings`), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
@@ -126,7 +128,7 @@ async function saveCursorRawSettings(data: {
settings: CursorRawSettings['settings'];
expectedMtime?: number;
}): Promise<{ success: boolean; mtime: number }> {
const res = await fetch(withApiBase('/cursor/settings/raw'), {
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/settings/raw`), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
@@ -137,7 +139,9 @@ async function saveCursorRawSettings(data: {
}
async function autoDetectCursorAuth(): Promise<CursorAuthResult> {
const res = await fetch(withApiBase('/cursor/auth/auto-detect'), { method: 'POST' });
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/auth/auto-detect`), {
method: 'POST',
});
if (!res.ok) {
const error = await res.json().catch(() => ({ error: 'Auto-detect failed' }));
throw new Error(error.error || 'Auto-detect failed');
@@ -149,7 +153,7 @@ async function importCursorAuthManual(data: {
accessToken: string;
machineId: string;
}): Promise<CursorAuthResult> {
const res = await fetch(withApiBase('/cursor/auth/import'), {
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/auth/import`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
@@ -162,19 +166,23 @@ async function importCursorAuthManual(data: {
}
async function startCursorDaemon(): Promise<{ success: boolean; pid?: number; error?: string }> {
const res = await fetch(withApiBase('/cursor/daemon/start'), { method: 'POST' });
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/daemon/start`), {
method: 'POST',
});
if (!res.ok) throw new Error('Failed to start cursor daemon');
return res.json();
}
async function stopCursorDaemon(): Promise<{ success: boolean; error?: string }> {
const res = await fetch(withApiBase('/cursor/daemon/stop'), { method: 'POST' });
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/daemon/stop`), {
method: 'POST',
});
if (!res.ok) throw new Error('Failed to stop cursor daemon');
return res.json();
}
async function probeCursorRuntime(): Promise<CursorProbeResult> {
const res = await fetch(withApiBase('/cursor/probe'), { method: 'POST' });
const res = await fetch(withApiBase(`${LEGACY_CURSOR_API_BASE}/probe`), { method: 'POST' });
const payload = await res.json().catch(() => null);
if (isCursorProbeResult(payload)) {
+2 -2
View File
@@ -1283,7 +1283,7 @@ export function CursorPage() {
<span className="font-medium text-muted-foreground">
{t('cursorPage.provider')}
</span>
<span className="font-mono">Cursor IDE</span>
<span className="font-mono">Cursor IDE (Legacy)</span>
</div>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">
@@ -1295,7 +1295,7 @@ export function CursorPage() {
</div>
{/* TODO i18n: missing key for model mapping env var info paragraph */}
<p className="text-xs text-muted-foreground">
Model mapping writes `ANTHROPIC_MODEL`,
Legacy bridge model mapping writes `ANTHROPIC_MODEL`,
`ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and
`ANTHROPIC_DEFAULT_HAIKU_MODEL` in `cursor.settings.json`.
</p>
+11 -9
View File
@@ -32,7 +32,7 @@ describe('useCursor', () => {
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url.endsWith('/api/cursor/status')) {
if (url.endsWith('/api/legacy/cursor/status')) {
return Promise.resolve(
createJsonResponse({
enabled: true,
@@ -48,7 +48,7 @@ describe('useCursor', () => {
);
}
if (url.endsWith('/api/cursor/settings')) {
if (url.endsWith('/api/legacy/cursor/settings')) {
return Promise.resolve(
createJsonResponse({
enabled: true,
@@ -60,7 +60,7 @@ describe('useCursor', () => {
);
}
if (url.endsWith('/api/cursor/models')) {
if (url.endsWith('/api/legacy/cursor/models')) {
return Promise.resolve(
createJsonResponse({
models: [{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', provider: 'openai' }],
@@ -69,7 +69,7 @@ describe('useCursor', () => {
);
}
if (url.endsWith('/api/cursor/settings/raw')) {
if (url.endsWith('/api/legacy/cursor/settings/raw')) {
return Promise.resolve(
createJsonResponse({
settings: {},
@@ -80,7 +80,7 @@ describe('useCursor', () => {
);
}
if (url.endsWith('/api/cursor/probe') && init?.method === 'POST') {
if (url.endsWith('/api/legacy/cursor/probe') && init?.method === 'POST') {
return Promise.resolve(createJsonResponse(probeFailure, 503));
}
@@ -102,14 +102,16 @@ describe('useCursor', () => {
await waitFor(() => expect(result.current.probeResult).toMatchObject(probeFailure));
await waitFor(() =>
expect(
fetchMock.mock.calls.filter(([input]) => String(input).endsWith('/api/cursor/status'))
.length
fetchMock.mock.calls.filter(([input]) =>
String(input).endsWith('/api/legacy/cursor/status')
).length
).toBeGreaterThanOrEqual(2)
);
await waitFor(() =>
expect(
fetchMock.mock.calls.filter(([input]) => String(input).endsWith('/api/cursor/models'))
.length
fetchMock.mock.calls.filter(([input]) =>
String(input).endsWith('/api/legacy/cursor/models')
).length
).toBeGreaterThanOrEqual(2)
);
});