Skip to content

Add Cursor CLI provider support#556

Closed
Mizaro wants to merge 1 commit intopingdotgg:mainfrom
Mizaro:feat/cursor-cli-support
Closed

Add Cursor CLI provider support#556
Mizaro wants to merge 1 commit intopingdotgg:mainfrom
Mizaro:feat/cursor-cli-support

Conversation

@Mizaro
Copy link
Copy Markdown

@Mizaro Mizaro commented Mar 8, 2026

Summary

  • add Cursor as a first-class provider in shared contracts, server health, adapter registration, and the web provider picker
  • implement a Cursor CLI adapter that runs \cursor-agent\, streams runtime events, supports interrupts, and accepts provider-specific binary overrides
  • expose Cursor settings/custom models in the app and forward provider CLI options with turn-start commands

Verification

  • bun typecheck
  • bun lint

Note

Add Cursor CLI provider support and register CursorAdapterLive in ProviderAdapterRegistryLive via ProviderAdapterRegistry.ts and wire providerOptions through orchestration and UI

Introduce a cursor provider with a CursorAdapterLive that manages sessions and turns via the Cursor CLI, extend contracts to include cursor in ProviderKind and providerOptions, wire providerOptions through orchestration, add health checks for the Cursor CLI, and expose cursor models and settings in the web UI.

📍Where to Start

Start with CursorAdapterLive in CursorAdapter.ts, then follow provider registration in ProviderAdapterRegistry.ts and providerOptions propagation in ProviderCommandReactor.ts.

📊 Macroscope summarized 0255041. 19 files reviewed, 11 issues evaluated, 1 issue filtered, 4 comments posted

🗂️ Filtered Issues

apps/web/src/composerDraftStore.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 607: Memory leak: When setProjectDraftThreadId garbage-collects an orphaned draft thread, it fails to revoke the Blob URLs associated with that draft's images before removing it from state. This causes blob: URLs to remain allocated in the browser's memory until the tab closes. Similar leaks exist in clearProjectDraftThreadId and clearProjectDraftThreadById. You should iterate over state.draftsByThreadId[previousThreadIdForProject].images and call revokeObjectPreviewUrl for each image before deleting the draft, matching the cleanup logic in clearThreadDraft. [ Out of scope ]

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7d29627c-3596-4a83-bf20-27e2c64358a8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +244 to +246
let buffer = "";
stream.on("data", (chunk: Buffer | string) => {
buffer += chunk.toString();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low Layers/CursorAdapter.ts:244

attachLineReader concatenates raw Buffer chunks as strings before buffering, so multi-byte UTF-8 characters split across chunk boundaries are corrupted. Each partial chunk is decoded independently, causing replacement characters or malformed sequences in the final buffer. Consider using stream.setEncoding('utf8') or string_decoder.StringDecoder to handle multi-byte characters correctly across chunks.

-  let buffer = "";
-  stream.on("data", (chunk: Buffer | string) => {
-    buffer += chunk.toString();
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/CursorAdapter.ts around lines 244-246:

`attachLineReader` concatenates raw `Buffer` chunks as strings before buffering, so multi-byte UTF-8 characters split across chunk boundaries are corrupted. Each partial chunk is decoded independently, causing replacement characters or malformed sequences in the final buffer. Consider using `stream.setEncoding('utf8')` or `string_decoder.StringDecoder` to handle multi-byte characters correctly across chunks.

Evidence trail:
apps/server/src/provider/Layers/CursorAdapter.ts lines 240-262 at REVIEWED_COMMIT. The `attachLineReader` function on line 240 handles stream data events at line 245-246 with `buffer += chunk.toString()` where chunk is `Buffer | string`. This pattern of calling toString() on each Buffer chunk independently before concatenation is the exact issue described - multi-byte UTF-8 characters split across chunk boundaries will be corrupted.

].join("\n");
}

function buildCursorArgs(input: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Layers/CursorAdapter.ts:211

When buildCursorArgs returns the prompt as a raw command-line argument, and spawnProcess is invoked with shell: true on Windows (line 419), newlines in the prompt are interpreted by cmd.exe as command separators. This causes arbitrary command execution and guaranteed process failure on Windows. Additionally, prompts exceeding 8191 characters will crash on Windows due to command-line length limits. The prompt should be passed via standard input or a temporary file instead.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/CursorAdapter.ts around line 211:

When `buildCursorArgs` returns the prompt as a raw command-line argument, and `spawnProcess` is invoked with `shell: true` on Windows (line 419), newlines in the prompt are interpreted by `cmd.exe` as command separators. This causes arbitrary command execution and guaranteed process failure on Windows. Additionally, prompts exceeding 8191 characters will crash on Windows due to command-line length limits. The prompt should be passed via standard input or a temporary file instead.

Evidence trail:
apps/server/src/provider/Layers/CursorAdapter.ts lines 211-223 (buildCursorArgs returns prompt as `-p` argument), lines 408-420 (spawnProcess call with shell: process.platform === "win32"), lines 199-209 (buildCursorPrompt adds newlines in plan mode), line 285 (spawnProcess defaults to Node.js spawn from line 3-4 import)

Comment on lines +228 to +238
function killChild(child: ChildProcessHandle, signal: NodeJS.Signals = "SIGTERM"): void {
if (process.platform === "win32" && child.pid !== undefined) {
try {
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
return;
} catch {
// Fall back to direct kill.
}
}
child.kill(signal);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CursorAdapter.ts:228

killChild uses spawnSync('taskkill', ...) on Windows and returns immediately if no exception is thrown, but spawnSync does not throw on failure — it returns an object with error or non-zero status. When taskkill fails (e.g., binary missing, access denied), the function silently exits without falling back to child.kill(), potentially leaving the child process running and the session stuck. Consider checking result.error or result.status !== 0 before returning, and fall back to child.kill() on any failure.

 function killChild(child: ChildProcessHandle, signal: NodeJS.Signals = "SIGTERM"): void {
   if (process.platform === "win32" && child.pid !== undefined) {
     try {
-      spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
-      return;
+      const result = spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
+        stdio: "ignore",
+      });
+      if (!result.error && result.status === 0) {
+        return;
+      }
     } catch {
       // Fall back to direct kill.
     }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/CursorAdapter.ts around lines 228-238:

`killChild` uses `spawnSync('taskkill', ...)` on Windows and returns immediately if no exception is thrown, but `spawnSync` does not throw on failure — it returns an object with `error` or non-zero `status`. When `taskkill` fails (e.g., binary missing, access denied), the function silently exits without falling back to `child.kill()`, potentially leaving the child process running and the session stuck. Consider checking `result.error` or `result.status !== 0` before returning, and fall back to `child.kill()` on any failure.

Evidence trail:
1. Code at apps/server/src/provider/Layers/CursorAdapter.ts lines 228-238 shows `killChild` function using `try-catch` with `spawnSync` and returning immediately after the call.
2. Node.js documentation (https://nodejs.org/api/child_process.html) confirms that `spawnSync` returns an object with `status`, `signal`, and `error` properties instead of throwing exceptions on failure.

Comment on lines +326 to +328
const existing = sessions.get(input.threadId);
const createdAt = existing?.createdAt ?? nowIso();
const updatedAt = nowIso();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CursorAdapter.ts:326

startSession overwrites an existing session in sessions without terminating its activeTurn. If a turn is running, the child process continues executing but becomes unreachable, causing zombie processes that exhaust server resources. Consider checking for and killing any existing active turn before replacing the session.

          const existing = sessions.get(input.threadId);
+          if (existing?.activeTurn) {
+            killChild(existing.activeTurn.child);
+          }
           const createdAt = existing?.createdAt ?? nowIso();
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/CursorAdapter.ts around lines 326-328:

`startSession` overwrites an existing session in `sessions` without terminating its `activeTurn`. If a turn is running, the child process continues executing but becomes unreachable, causing zombie processes that exhaust server resources. Consider checking for and killing any existing active turn before replacing the session.

Evidence trail:
- apps/server/src/provider/Layers/CursorAdapter.ts lines 47-59 (CursorSessionState interface with activeTurn field)
- apps/server/src/provider/Layers/CursorAdapter.ts lines 315-372 (startSession function - creates `next` without activeTurn, sets it without killing existing activeTurn)
- apps/server/src/provider/Layers/CursorAdapter.ts lines 326, 331-355 (existing session is retrieved, but activeTurn is not preserved or terminated)
- apps/server/src/provider/Layers/CursorAdapter.ts lines 376-378 (example of proper activeTurn cleanup with killChild)
- apps/server/src/provider/Layers/CursorAdapter.ts lines 949-951, 968-970 (other places with proper cleanup)

@brrock
Copy link
Copy Markdown

brrock commented Mar 8, 2026

The t3 team already have a cursor pr up #178, they are more likely to merge their own then yours

@Mizaro
Copy link
Copy Markdown
Author

Mizaro commented Mar 8, 2026

@brrock Thanks!

@Mizaro Mizaro marked this pull request as draft March 8, 2026 21:37
@Mizaro Mizaro closed this Mar 8, 2026
@Mizaro Mizaro deleted the feat/cursor-cli-support branch March 15, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants