Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,35 @@ function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> {
];
}

function modelConfigOptionsFor(modelId: string): ReadonlyArray<AcpSchema.SessionConfigOption> {
const previousModelId = currentModelId;
try {
currentModelId = modelId;
return configOptions().filter(
(option) => option.category !== "mode" && option.category !== "model",
);
} finally {
currentModelId = previousModelId;
}
}

function availableModels(): ReadonlyArray<{
readonly value: string;
readonly name: string;
readonly configOptions: ReadonlyArray<AcpSchema.SessionConfigOption>;
}> {
return [
{ value: "default", name: "Auto" },
{ value: "composer-2", name: "Composer 2" },
{ value: "gpt-5.4", name: "GPT-5.4" },
{ value: "claude-opus-4-6", name: "Opus 4.6" },
].map((model) => ({
value: model.value,
name: model.name,
configOptions: modelConfigOptionsFor(model.value),
}));
}

const availableModes: ReadonlyArray<AcpSchema.SessionMode> = [
{
id: "ask",
Expand Down Expand Up @@ -517,6 +546,12 @@ const program = Effect.gen(function* () {
);

yield* agent.handleUnknownExtRequest((method, params) => {
if (method === "cursor/list_available_models") {
return Effect.succeed({
models: availableModels(),
});
}

if (method !== "session/mode/set") {
return Effect.fail(AcpError.AcpRequestError.methodNotFound(method));
}
Expand Down
16 changes: 6 additions & 10 deletions apps/server/src/provider/Drivers/CursorDriver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/**
* CursorDriver — `ProviderDriver` for the Cursor Agent (`agent`) runtime.
*
* Cursor exposes an ACP-based CLI. The driver is still a plain value, but
* its snapshot uses `makeManagedServerProvider`'s optional `enrichSnapshot`
* hook to run the slow ACP model-capability probe in the background without
* blocking the initial `ready`-state publish.
* Cursor exposes an ACP-based CLI. Model catalog and capability refreshes
* happen during the managed provider status check via Cursor's
* `list_available_models` extension method.
*
* Text generation is supported via the ACP runtime — `makeCursorTextGeneration`
* drives `runtime.prompt` with a structured-output schema and collects the
Expand Down Expand Up @@ -139,20 +138,17 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
initialSnapshot: (settings) =>
buildInitialCursorProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
checkProvider,
// Preserve the background ACP model-capability probe that used to
// live on `CursorProviderLive`. Only fires when the snapshot reports
// an authenticated, enabled provider with at least one non-custom
// model whose capabilities haven't been captured yet.
// Model catalog and capabilities come exclusively from Cursor's
// list_available_models extension method during provider checks.
enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
enrichCursorSnapshot({
settings,
environment: processEnv,
snapshot: currentSnapshot,
maintenanceCapabilities,
publishSnapshot,
stampIdentity,
httpClient,
}).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner)),
}),
refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
}).pipe(
Effect.mapError(
Expand Down
134 changes: 1 addition & 133 deletions apps/server/src/provider/Layers/CursorProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import { describe, expect, it } from "vitest";
import type * as EffectAcpSchema from "effect-acp/schema";
import type { CursorSettings, ServerProviderModel } from "@t3tools/contracts";
import type { CursorSettings } from "@t3tools/contracts";
import { createModelCapabilities } from "@t3tools/shared/model";

import {
buildCursorProviderSnapshot,
buildCursorCapabilitiesFromConfigOptions,
buildCursorDiscoveredModelsFromConfigOptions,
checkCursorProviderStatus,
discoverCursorModelCapabilitiesViaAcp,
discoverCursorModelsViaAcp,
getCursorFallbackModels,
getCursorParameterizedModelPickerUnsupportedMessage,
Expand Down Expand Up @@ -295,57 +293,13 @@ const parameterizedClaudeModelOptionConfigOptions = [
},
] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>;

const sessionNewCursorConfigOptions = [
{
type: "select",
currentValue: "agent",
options: [
{ name: "Agent", value: "agent", description: "Full agent capabilities with tool access" },
],
category: "mode",
id: "mode",
name: "Mode",
description: "Controls how the agent executes tasks",
},
{
type: "select",
currentValue: "composer-2",
options: [
{ name: "Auto", value: "default" },
{ name: "Composer 2", value: "composer-2" },
{ name: "GPT-5.4", value: "gpt-5.4" },
{ name: "Sonnet 4.6", value: "claude-sonnet-4-6" },
{ name: "Opus 4.6", value: "claude-opus-4-6" },
{ name: "Codex 5.3 Spark", value: "gpt-5.3-codex-spark" },
],
category: "model",
id: "model",
name: "Model",
description: "Controls which model is used for responses",
},
{
type: "select",
currentValue: "true",
options: [
{ name: "Off", value: "false" },
{ name: "Fast", value: "true" },
],
category: "model_config",
id: "fast",
name: "Fast",
description: "Faster speeds.",
},
] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>;

const baseCursorSettings: CursorSettings = {
enabled: true,
binaryPath: "agent",
apiEndpoint: "",
customModels: [],
};

const emptyCapabilities = createModelCapabilities({ optionDescriptors: [] });

describe("getCursorFallbackModels", () => {
it("does not publish any built-in cursor models before ACP discovery", () => {
expect(
Expand Down Expand Up @@ -462,51 +416,6 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => {
});
});

describe("buildCursorDiscoveredModelsFromConfigOptions", () => {
it("publishes ACP model choices immediately from session/new config options", () => {
expect(buildCursorDiscoveredModelsFromConfigOptions(sessionNewCursorConfigOptions)).toEqual([
{
slug: "default",
name: "Auto",
isCustom: false,
capabilities: emptyCapabilities,
},
{
slug: "composer-2",
name: "Composer 2",
isCustom: false,
capabilities: createModelCapabilities({
optionDescriptors: [booleanDescriptor("fastMode", "Fast", true)],
}),
},
{
slug: "gpt-5.4",
name: "GPT-5.4",
isCustom: false,
capabilities: emptyCapabilities,
},
{
slug: "claude-sonnet-4-6",
name: "Sonnet 4.6",
isCustom: false,
capabilities: emptyCapabilities,
},
{
slug: "claude-opus-4-6",
name: "Opus 4.6",
isCustom: false,
capabilities: emptyCapabilities,
},
{
slug: "gpt-5.3-codex-spark",
name: "Codex 5.3 Spark",
isCustom: false,
capabilities: emptyCapabilities,
},
]);
});
});

describe("checkCursorProviderStatus", () => {
it("passes the injected environment to ACP model discovery", async () => {
const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture());
Expand Down Expand Up @@ -576,47 +485,6 @@ describe("discoverCursorModelsViaAcp", () => {
});
});

describe("discoverCursorModelCapabilitiesViaAcp", () => {
it("closes all ACP probe runtimes after capability enrichment completes", async () => {
const { exitLogPath, wrapperPath } = await runNode(
makeExitLogFixture("cursor-capabilities-exit-log-"),
);
const existingModels: ReadonlyArray<ServerProviderModel> = [
{ slug: "default", name: "Auto", isCustom: false, capabilities: emptyCapabilities },
{ slug: "composer-2", name: "Composer 2", isCustom: false, capabilities: emptyCapabilities },
{ slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, capabilities: emptyCapabilities },
{
slug: "claude-opus-4-6",
name: "Opus 4.6",
isCustom: false,
capabilities: emptyCapabilities,
},
];

const models = await Effect.runPromise(
discoverCursorModelCapabilitiesViaAcp(
{
enabled: true,
binaryPath: wrapperPath,
apiEndpoint: "",
customModels: [],
},
existingModels,
).pipe(Effect.provide(NodeServices.layer)),
);

expect(models.map((model) => model.slug)).toEqual([
"default",
"composer-2",
"gpt-5.4",
"claude-opus-4-6",
]);

const exitLog = await runNode(waitForFileContent(exitLogPath));
expect(exitLog.match(/SIGTERM/g)?.length ?? 0).toBe(4);
});
});

describe("parseCursorAboutOutput", () => {
it("parses json about output and forwards subscription metadata", () => {
expect(
Expand Down
Loading
Loading