Skip to content

Commit 10129ed

Browse files
Use server-driven ModelCapabilities for Cursor traits
Replace the hardcoded client-side CURSOR_MODEL_CAPABILITY_BY_FAMILY map with server-provided ModelCapabilities, matching the Codex/Claude pattern. - Add CursorProvider snapshot service with BUILT_IN_MODELS and per-model capabilities; register it in ProviderRegistry alongside Codex/Claude. - Delete CursorTraitsPicker and route Cursor through the generic TraitsPicker, adding cursor support for the reasoning/effort key. - Add normalizeCursorModelOptionsWithCapabilities to providerModels. Made-with: Cursor
1 parent 7165d9f commit 10129ed

File tree

9 files changed

+645
-287
lines changed

9 files changed

+645
-287
lines changed

apps/server/src/provider/Layers/CursorProvider.ts

Lines changed: 439 additions & 0 deletions
Large diffs are not rendered by default.

apps/server/src/provider/Layers/ProviderRegistry.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@ import { Effect, Equal, Layer, PubSub, Ref, Stream } from "effect";
88

99
import { ClaudeProviderLive } from "./ClaudeProvider";
1010
import { CodexProviderLive } from "./CodexProvider";
11+
import { CursorProviderLive } from "./CursorProvider";
1112
import type { ClaudeProviderShape } from "../Services/ClaudeProvider";
1213
import { ClaudeProvider } from "../Services/ClaudeProvider";
1314
import type { CodexProviderShape } from "../Services/CodexProvider";
1415
import { CodexProvider } from "../Services/CodexProvider";
16+
import type { CursorProviderShape } from "../Services/CursorProvider";
17+
import { CursorProvider } from "../Services/CursorProvider";
1518
import { ProviderRegistry, type ProviderRegistryShape } from "../Services/ProviderRegistry";
1619

1720
const loadProviders = (
1821
codexProvider: CodexProviderShape,
1922
claudeProvider: ClaudeProviderShape,
20-
): Effect.Effect<readonly [ServerProvider, ServerProvider]> =>
21-
Effect.all([codexProvider.getSnapshot, claudeProvider.getSnapshot], {
23+
cursorProvider: CursorProviderShape,
24+
): Effect.Effect<readonly [ServerProvider, ServerProvider, ServerProvider]> =>
25+
Effect.all([codexProvider.getSnapshot, claudeProvider.getSnapshot, cursorProvider.getSnapshot], {
2226
concurrency: "unbounded",
2327
});
2428

@@ -32,18 +36,19 @@ export const ProviderRegistryLive = Layer.effect(
3236
Effect.gen(function* () {
3337
const codexProvider = yield* CodexProvider;
3438
const claudeProvider = yield* ClaudeProvider;
39+
const cursorProvider = yield* CursorProvider;
3540
const changesPubSub = yield* Effect.acquireRelease(
3641
PubSub.unbounded<ReadonlyArray<ServerProvider>>(),
3742
PubSub.shutdown,
3843
);
3944
const providersRef = yield* Ref.make<ReadonlyArray<ServerProvider>>(
40-
yield* loadProviders(codexProvider, claudeProvider),
45+
yield* loadProviders(codexProvider, claudeProvider, cursorProvider),
4146
);
4247

4348
const syncProviders = (options?: { readonly publish?: boolean }) =>
4449
Effect.gen(function* () {
4550
const previousProviders = yield* Ref.get(providersRef);
46-
const providers = yield* loadProviders(codexProvider, claudeProvider);
51+
const providers = yield* loadProviders(codexProvider, claudeProvider, cursorProvider);
4752
yield* Ref.set(providersRef, providers);
4853

4954
if (options?.publish !== false && haveProvidersChanged(previousProviders, providers)) {
@@ -59,6 +64,9 @@ export const ProviderRegistryLive = Layer.effect(
5964
yield* Stream.runForEach(claudeProvider.streamChanges, () => syncProviders()).pipe(
6065
Effect.forkScoped,
6166
);
67+
yield* Stream.runForEach(cursorProvider.streamChanges, () => syncProviders()).pipe(
68+
Effect.forkScoped,
69+
);
6270

6371
return {
6472
getProviders: syncProviders({ publish: false }).pipe(
@@ -74,10 +82,14 @@ export const ProviderRegistryLive = Layer.effect(
7482
case "claudeAgent":
7583
yield* claudeProvider.refresh;
7684
break;
85+
case "cursor":
86+
yield* cursorProvider.refresh;
87+
break;
7788
default:
78-
yield* Effect.all([codexProvider.refresh, claudeProvider.refresh], {
79-
concurrency: "unbounded",
80-
});
89+
yield* Effect.all(
90+
[codexProvider.refresh, claudeProvider.refresh, cursorProvider.refresh],
91+
{ concurrency: "unbounded" },
92+
);
8193
break;
8294
}
8395
return yield* syncProviders();
@@ -90,4 +102,8 @@ export const ProviderRegistryLive = Layer.effect(
90102
},
91103
} satisfies ProviderRegistryShape;
92104
}),
93-
).pipe(Layer.provideMerge(CodexProviderLive), Layer.provideMerge(ClaudeProviderLive));
105+
).pipe(
106+
Layer.provideMerge(CodexProviderLive),
107+
Layer.provideMerge(ClaudeProviderLive),
108+
Layer.provideMerge(CursorProviderLive),
109+
);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ServiceMap } from "effect";
2+
3+
import type { ServerProviderShape } from "./ServerProvider";
4+
5+
export interface CursorProviderShape extends ServerProviderShape {}
6+
7+
export class CursorProvider extends ServiceMap.Service<CursorProvider, CursorProviderShape>()(
8+
"t3/provider/Services/CursorProvider",
9+
) {}

apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { afterEach, describe, expect, it, vi } from "vitest";
66
import { render } from "vitest-browser-react";
77

88
import { CompactComposerControlsMenu } from "./CompactComposerControlsMenu";
9-
import { CursorTraitsMenuContent } from "./CursorTraitsPicker";
109
import { TraitsMenuContent } from "./TraitsPicker";
1110
import { useComposerDraftStore } from "../../composerDraftStore";
1211

@@ -109,27 +108,42 @@ async function mountMenu(props?: { modelSelection?: ModelSelection; prompt?: str
109108
},
110109
},
111110
]
112-
: [];
111+
: provider === "cursor"
112+
? [
113+
{
114+
slug: "gpt-5.3-codex",
115+
name: "Codex 5.3",
116+
isCustom: false,
117+
capabilities: {
118+
reasoningEffortLevels: [
119+
{ value: "low", label: "Low" },
120+
{ value: "normal", label: "Normal", isDefault: true },
121+
{ value: "high", label: "High" },
122+
{ value: "xhigh", label: "Extra high" },
123+
],
124+
supportsFastMode: true,
125+
supportsThinkingToggle: false,
126+
promptInjectedEffortLevels: [],
127+
},
128+
},
129+
]
130+
: [];
113131
const screen = await render(
114132
<CompactComposerControlsMenu
115133
activePlan={false}
116134
interactionMode="default"
117135
planSidebarOpen={false}
118136
runtimeMode="approval-required"
119137
traitsMenuContent={
120-
provider === "cursor" ? (
121-
<CursorTraitsMenuContent threadId={threadId} model={model} cursorModelOptions={null} />
122-
) : (
123-
<TraitsMenuContent
124-
provider={provider}
125-
models={models}
126-
threadId={threadId}
127-
model={model}
128-
prompt={props?.prompt ?? ""}
129-
modelOptions={providerOptions}
130-
onPromptChange={onPromptChange}
131-
/>
132-
)
138+
<TraitsMenuContent
139+
provider={provider}
140+
models={models}
141+
threadId={threadId}
142+
model={model}
143+
prompt={props?.prompt ?? ""}
144+
modelOptions={providerOptions}
145+
onPromptChange={onPromptChange}
146+
/>
133147
}
134148
onToggleInteractionMode={vi.fn()}
135149
onTogglePlanSidebar={vi.fn()}

apps/web/src/components/chat/CursorTraitsPicker.tsx

Lines changed: 0 additions & 238 deletions
This file was deleted.

0 commit comments

Comments
 (0)