authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-06-23 07:33:01+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-22 22:33:01-07:00
log3083d712eefb8009f7366ba53040eb2d477bfa33
tree0a147e9ebb2322bb8fbd93075f9e5f46a1808084
parent6672a1d21060c6c7b927726b6b91fbfe8c8337f3
signature Signed by PGP key B5690EEEBB952194

[codex] fix: clarify Cursor CLI setup error (#3519)

Co-authored-by: Codex <codex@openai.com>

2 files changed, 61 insertions(+), 12 deletions(-)

apps/server/src/provider/Layers/CursorProvider.test.ts+44-10
......@@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
44import * as Effect from "effect/Effect";
55import * as FileSystem from "effect/FileSystem";
66import * as Path from "effect/Path";
7import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
78import { describe, expect, it } from "vite-plus/test";
89import type * as EffectAcpSchema from "effect-acp/schema";
910import type { CursorSettings } from "@t3tools/contracts";
......@@ -24,7 +25,11 @@ import {
2425} from "./CursorProvider.ts";
2526
2627const runNode = <A, E>(
27 effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path>,
28 effect: Effect.Effect<
29 A,
30 E,
31 ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path
32 >,
2833): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)));
2934
3035const resolveMockAgentPath = Effect.fn("resolveMockAgentPath")(function* () {
......@@ -293,6 +298,18 @@ const baseCursorSettings: CursorSettings = {
293298 apiEndpoint: "",
294299 customModels: [],
295300};
301const cursorAcpDiscoveryFailedMessage = [
302 "Cursor ACP model discovery failed.",
303 "Cursor CLI setup may be incomplete; install or enable the Cursor CLI, restart T3 Code, and try again.",
304 "See https://cursor.com/docs/cli/installation.",
305 "Check server logs for ACP details.",
306].join(" ");
307const missingCursorBinaryPath = "/definitely/not/installed/t3-cursor-agent";
308const cursorCliCommandMissingMessage = [
309 `Cursor CLI command \`${missingCursorBinaryPath}\` was not found.`,
310 `Install or enable the Cursor CLI, make sure \`${missingCursorBinaryPath}\` is on PATH, then restart T3 Code.`,
311 "See https://cursor.com/docs/cli/installation.",
312].join(" ");
296313
297314describe("getCursorFallbackModels", () => {
298315 it("does not publish any built-in cursor models before ACP discovery", () => {
......@@ -338,12 +355,11 @@ describe("buildCursorProviderSnapshot", () => {
338355 auth: { status: "unauthenticated" },
339356 message: "Cursor Agent is not authenticated. Run `agent login` and try again.",
340357 },
341 discoveryWarning: "Cursor ACP model discovery failed. Check server logs for details.",
358 discoveryWarning: cursorAcpDiscoveryFailedMessage,
342359 }),
343360 ).toMatchObject({
344361 status: "error",
345 message:
346 "Cursor Agent is not authenticated. Run `agent login` and try again. Cursor ACP model discovery failed. Check server logs for details.",
362 message: `Cursor Agent is not authenticated. Run \`agent login\` and try again. ${cursorAcpDiscoveryFailedMessage}`,
347363 models: [
348364 {
349365 slug: "claude-sonnet-4-6",
......@@ -411,10 +427,28 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => {
411427});
412428
413429describe("checkCursorProviderStatus", () => {
430 it("reports the install docs when the Cursor CLI command is missing", async () => {
431 const provider = await runNode(
432 checkCursorProviderStatus({
433 enabled: true,
434 binaryPath: missingCursorBinaryPath,
435 apiEndpoint: "",
436 customModels: [],
437 }),
438 );
439
440 expect(provider).toMatchObject({
441 installed: false,
442 status: "error",
443 auth: { status: "unknown" },
444 message: cursorCliCommandMissingMessage,
445 });
446 });
447
414448 it("passes the injected environment to ACP model discovery", async () => {
415449 const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture());
416450
417 const provider = await Effect.runPromise(
451 const provider = await runNode(
418452 checkCursorProviderStatus(
419453 {
420454 enabled: true,
......@@ -426,7 +460,7 @@ describe("checkCursorProviderStatus", () => {
426460 ...process.env,
427461 T3_ACP_REQUEST_LOG_PATH: requestLogPath,
428462 },
429 ).pipe(Effect.provide(NodeServices.layer)),
463 ),
430464 );
431465
432466 expect(provider.models.map((model) => model.slug)).toEqual([
......@@ -443,13 +477,13 @@ describe("discoverCursorModelsViaAcp", () => {
443477 it("keeps the ACP probe runtime alive long enough to discover models", async () => {
444478 const wrapperPath = await runNode(makeMockAgentWrapper());
445479
446 const models = await Effect.runPromise(
480 const models = await runNode(
447481 discoverCursorModelsViaAcp({
448482 enabled: true,
449483 binaryPath: wrapperPath,
450484 apiEndpoint: "",
451485 customModels: [],
452 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
486 }).pipe(Effect.scoped),
453487 );
454488
455489 expect(models.map((model) => model.slug)).toEqual([
......@@ -465,13 +499,13 @@ describe("discoverCursorModelsViaAcp", () => {
465499 makeExitLogFixture("cursor-provider-exit-log-"),
466500 );
467501
468 await Effect.runPromise(
502 await runNode(
469503 discoverCursorModelsViaAcp({
470504 enabled: true,
471505 binaryPath: wrapperPath,
472506 apiEndpoint: "",
473507 customModels: [],
474 }).pipe(Effect.provide(NodeServices.layer)),
508 }),
475509 );
476510
477511 const exitLog = await runNode(waitForFileContent(exitLogPath));
apps/server/src/provider/Layers/CursorProvider.ts+17-2
......@@ -62,6 +62,13 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({
6262
6363const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;
6464const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08;
65const CURSOR_CLI_INSTALLATION_DOCS_URL = "https://cursor.com/docs/cli/installation";
66const CURSOR_ACP_MODEL_DISCOVERY_FAILED_MESSAGE = [
67 "Cursor ACP model discovery failed.",
68 "Cursor CLI setup may be incomplete; install or enable the Cursor CLI, restart T3 Code, and try again.",
69 `See ${CURSOR_CLI_INSTALLATION_DOCS_URL}.`,
70 "Check server logs for ACP details.",
71].join(" ");
6572export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {
6673 _meta: {
6774 parameterizedModelPicker: true,
......@@ -608,6 +615,14 @@ function joinProviderMessages(...messages: ReadonlyArray<string | undefined>): s
608615 return parts.length > 0 ? parts.join(" ") : undefined;
609616}
610617
618function buildCursorCliCommandMissingMessage(binaryPath: string): string {
619 return [
620 `Cursor CLI command \`${binaryPath}\` was not found.`,
621 `Install or enable the Cursor CLI, make sure \`${binaryPath}\` is on PATH, then restart T3 Code.`,
622 `See ${CURSOR_CLI_INSTALLATION_DOCS_URL}.`,
623 ].join(" ");
624}
625
611626export function buildCursorProviderSnapshot(input: {
612627 readonly checkedAt: string;
613628 readonly cursorSettings: CursorSettings;
......@@ -1020,7 +1035,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
10201035 status: "error",
10211036 auth: { status: "unknown" },
10221037 message: isCommandMissingCause(error)
1023 ? "Cursor Agent CLI (`agent`) is not installed or not on PATH."
1038 ? buildCursorCliCommandMissingMessage(cursorSettings.binaryPath)
10241039 : "Failed to execute Cursor Agent CLI health check.",
10251040 },
10261041 });
......@@ -1079,7 +1094,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
10791094 yield* Effect.logWarning("Cursor ACP model discovery failed", {
10801095 errorTag: causeErrorTag(discoveryExit.cause),
10811096 });
1082 discoveryWarning = "Cursor ACP model discovery failed. Check server logs for details.";
1097 discoveryWarning = CURSOR_ACP_MODEL_DISCOVERY_FAILED_MESSAGE;
10831098 } else if (Option.isNone(discoveryExit.value)) {
10841099 discoveryWarning = `Cursor ACP model discovery timed out after ${CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS}ms.`;
10851100 } else if (discoveryExit.value.value.length === 0) {