diff --git a/apps/server/src/provider/Layers/CursorProvider.test.ts b/apps/server/src/provider/Layers/CursorProvider.test.ts index 60a7312eea3..78f62ac2123 100644 --- a/apps/server/src/provider/Layers/CursorProvider.test.ts +++ b/apps/server/src/provider/Layers/CursorProvider.test.ts @@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; +import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import { describe, expect, it } from "vite-plus/test"; import type * as EffectAcpSchema from "effect-acp/schema"; import type { CursorSettings } from "@t3tools/contracts"; @@ -24,7 +25,11 @@ import { } from "./CursorProvider.ts"; const runNode = ( - effect: Effect.Effect, + effect: Effect.Effect< + A, + E, + ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path + >, ): Promise => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))); const resolveMockAgentPath = Effect.fn("resolveMockAgentPath")(function* () { @@ -293,6 +298,18 @@ const baseCursorSettings: CursorSettings = { apiEndpoint: "", customModels: [], }; +const cursorAcpDiscoveryFailedMessage = [ + "Cursor ACP model discovery failed.", + "Cursor CLI setup may be incomplete; install or enable the Cursor CLI, restart T3 Code, and try again.", + "See https://cursor.com/docs/cli/installation.", + "Check server logs for ACP details.", +].join(" "); +const missingCursorBinaryPath = "/definitely/not/installed/t3-cursor-agent"; +const cursorCliCommandMissingMessage = [ + `Cursor CLI command \`${missingCursorBinaryPath}\` was not found.`, + `Install or enable the Cursor CLI, make sure \`${missingCursorBinaryPath}\` is on PATH, then restart T3 Code.`, + "See https://cursor.com/docs/cli/installation.", +].join(" "); describe("getCursorFallbackModels", () => { it("does not publish any built-in cursor models before ACP discovery", () => { @@ -338,12 +355,11 @@ describe("buildCursorProviderSnapshot", () => { auth: { status: "unauthenticated" }, message: "Cursor Agent is not authenticated. Run `agent login` and try again.", }, - discoveryWarning: "Cursor ACP model discovery failed. Check server logs for details.", + discoveryWarning: cursorAcpDiscoveryFailedMessage, }), ).toMatchObject({ status: "error", - message: - "Cursor Agent is not authenticated. Run `agent login` and try again. Cursor ACP model discovery failed. Check server logs for details.", + message: `Cursor Agent is not authenticated. Run \`agent login\` and try again. ${cursorAcpDiscoveryFailedMessage}`, models: [ { slug: "claude-sonnet-4-6", @@ -411,10 +427,28 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => { }); describe("checkCursorProviderStatus", () => { + it("reports the install docs when the Cursor CLI command is missing", async () => { + const provider = await runNode( + checkCursorProviderStatus({ + enabled: true, + binaryPath: missingCursorBinaryPath, + apiEndpoint: "", + customModels: [], + }), + ); + + expect(provider).toMatchObject({ + installed: false, + status: "error", + auth: { status: "unknown" }, + message: cursorCliCommandMissingMessage, + }); + }); + it("passes the injected environment to ACP model discovery", async () => { const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture()); - const provider = await Effect.runPromise( + const provider = await runNode( checkCursorProviderStatus( { enabled: true, @@ -426,7 +460,7 @@ describe("checkCursorProviderStatus", () => { ...process.env, T3_ACP_REQUEST_LOG_PATH: requestLogPath, }, - ).pipe(Effect.provide(NodeServices.layer)), + ), ); expect(provider.models.map((model) => model.slug)).toEqual([ @@ -443,13 +477,13 @@ describe("discoverCursorModelsViaAcp", () => { it("keeps the ACP probe runtime alive long enough to discover models", async () => { const wrapperPath = await runNode(makeMockAgentWrapper()); - const models = await Effect.runPromise( + const models = await runNode( discoverCursorModelsViaAcp({ enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], - }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), + }).pipe(Effect.scoped), ); expect(models.map((model) => model.slug)).toEqual([ @@ -465,13 +499,13 @@ describe("discoverCursorModelsViaAcp", () => { makeExitLogFixture("cursor-provider-exit-log-"), ); - await Effect.runPromise( + await runNode( discoverCursorModelsViaAcp({ enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], - }).pipe(Effect.provide(NodeServices.layer)), + }), ); const exitLog = await runNode(waitForFileContent(exitLogPath)); diff --git a/apps/server/src/provider/Layers/CursorProvider.ts b/apps/server/src/provider/Layers/CursorProvider.ts index ff96ece9349..cd9b93a4734 100644 --- a/apps/server/src/provider/Layers/CursorProvider.ts +++ b/apps/server/src/provider/Layers/CursorProvider.ts @@ -62,6 +62,13 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000; const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08; +const CURSOR_CLI_INSTALLATION_DOCS_URL = "https://cursor.com/docs/cli/installation"; +const CURSOR_ACP_MODEL_DISCOVERY_FAILED_MESSAGE = [ + "Cursor ACP model discovery failed.", + "Cursor CLI setup may be incomplete; install or enable the Cursor CLI, restart T3 Code, and try again.", + `See ${CURSOR_CLI_INSTALLATION_DOCS_URL}.`, + "Check server logs for ACP details.", +].join(" "); export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = { _meta: { parameterizedModelPicker: true, @@ -608,6 +615,14 @@ function joinProviderMessages(...messages: ReadonlyArray): s return parts.length > 0 ? parts.join(" ") : undefined; } +function buildCursorCliCommandMissingMessage(binaryPath: string): string { + return [ + `Cursor CLI command \`${binaryPath}\` was not found.`, + `Install or enable the Cursor CLI, make sure \`${binaryPath}\` is on PATH, then restart T3 Code.`, + `See ${CURSOR_CLI_INSTALLATION_DOCS_URL}.`, + ].join(" "); +} + export function buildCursorProviderSnapshot(input: { readonly checkedAt: string; readonly cursorSettings: CursorSettings; @@ -1020,7 +1035,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( status: "error", auth: { status: "unknown" }, message: isCommandMissingCause(error) - ? "Cursor Agent CLI (`agent`) is not installed or not on PATH." + ? buildCursorCliCommandMissingMessage(cursorSettings.binaryPath) : "Failed to execute Cursor Agent CLI health check.", }, }); @@ -1079,7 +1094,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( yield* Effect.logWarning("Cursor ACP model discovery failed", { errorTag: causeErrorTag(discoveryExit.cause), }); - discoveryWarning = "Cursor ACP model discovery failed. Check server logs for details."; + discoveryWarning = CURSOR_ACP_MODEL_DISCOVERY_FAILED_MESSAGE; } else if (Option.isNone(discoveryExit.value)) { discoveryWarning = `Cursor ACP model discovery timed out after ${CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS}ms.`; } else if (discoveryExit.value.value.length === 0) {