mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 16:17:02 +00:00
feat(cursor): split legacy bridge from cliproxy provider
This commit is contained in:
1 parent
1bad3b0305
commit
3b5941c60b
36 files changed
+974
-155
No files matched your search
+5
-1
@@ -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();
|
||||
|
||||
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user