Skip to content

Add ACP support with Cursor provider#1355

Open
juliusmarminge wants to merge 72 commits intomainfrom
t3code/greeting
Open

Add ACP support with Cursor provider#1355
juliusmarminge wants to merge 72 commits intomainfrom
t3code/greeting

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Mar 24, 2026

Summary

  • Adds Cursor as a first-class provider with ACP session lifecycle support, health checks, and adapter wiring in the server.
  • Implements Cursor model selection, including fast/plan mode mapping and session restart behavior when model options change.
  • Preserves provider/thread model state through orchestration, projection, and turn dispatch paths.
  • Updates the web app to surface Cursor traits, provider/model selection, and session drafting behavior.
  • Expands runtime ingestion so completed tool events retain structured tool metadata.

Testing

  • bun fmt
  • bun lint
  • bun typecheck
  • Added and updated tests across server, contracts, shared, and web layers for Cursor adapter behavior, orchestration routing, session model changes, and UI state handling.
  • Not run: bun run test

Note

Medium Risk
Adds a new cursor text-generation provider that spawns an external agent acp process and applies model/config options via ACP, which can fail or hang if the binary/protocol or config option mapping is wrong. Also changes git diff capture limits and several long-running integration tests’ timeouts, which could mask regressions or increase CI flakiness.

Overview
Adds Cursor-backed git text generation via ACP (CursorTextGenerationLive) and wires it into RoutingTextGeneration by extending TextGenerationProvider with cursor.

Introduces ACP test tooling (scripts/acp-mock-agent.ts, cursor-acp-model-mismatch-probe.ts) plus a new CursorTextGeneration test suite that validates ACP config-option based model selection and ensures the ACP child process is closed.

Makes a few supporting changes: desktop backend readiness now defaults to /.well-known/t3/environment, effect-acp is added as a dev dependency (and bundled via tsdown), checkpoint git diff output is capped via maxOutputBytes, Claude text generation switches to resolveClaudeApiModelId, and multiple integration/git tests are refactored to use longer, centralized timeouts.

Reviewed by Cursor Bugbot for commit e78ff43. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Cursor as a supported AI provider via ACP (Agent Communication Protocol)

  • Introduces a new effect-acp package implementing the ACP JSON-RPC protocol layer, including typed client/agent interfaces, error types, schema codegen, and in-process/stdio transport.
  • Adds CursorProviderLive and CursorAdapterLive to the server, enabling session management, model selection, streaming runtime events, tool calls, approval flows, and user input handling via Cursor CLI's ACP mode.
  • Extends contracts (ProviderKind, ModelSelection, CursorModelOptions, CursorSettings) to formally include cursor as a valid provider with reasoning, fastMode, thinking, and contextWindow options.
  • Wires Cursor into the web UI: composer provider registry, TraitsPicker, ProviderModelPicker, settings panel, and composerDraftStore all now handle the cursor provider.
  • Adds CursorTextGenerationLive to back git text generation (commit messages, PR content, branch names) via Cursor ACP with a 180s timeout.
  • Removes "restart-session" from ProviderSessionModelSwitchMode, so in-session Cursor model switches no longer restart the session.
  • Risk: Cursor support depends on the agent binary being installed and accessible; sessions will fail at spawn time if the binary is missing or misconfigured.

Macroscope summarized e78ff43.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 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: 65aea02a-bbd9-41d1-8950-d3c9275fd9bd

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
  • Commit unit tests in branch t3code/greeting

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

@juliusmarminge juliusmarminge marked this pull request as draft March 24, 2026 07:29
@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Mar 24, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Merge never clears cached provider model options
    • Replaced ?? fallback with key in incoming check so explicitly-present-but-undefined provider keys now clear cached values, and added cache deletion when merge produces undefined.
  • ✅ Fixed: Mock agent test uses strict equal with extra fields
    • Changed toEqual to toMatchObject so the assertion tolerates the extra modes field returned by the mock agent.

Create PR

Or push these changes by commenting:

@cursor push beb68c40d7
Preview (beb68c40d7)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -50,20 +50,17 @@
   cached: ProviderModelOptions | undefined,
   incoming: ProviderModelOptions | undefined,
 ): ProviderModelOptions | undefined {
-  if (!cached && !incoming) {
-    return undefined;
+  if (incoming === undefined) return cached;
+  if (cached === undefined) return incoming;
+
+  const providerKeys = ["codex", "claudeAgent", "cursor"] as const;
+  const next: Record<string, unknown> = {};
+  for (const key of providerKeys) {
+    const value = key in incoming ? incoming[key] : cached[key];
+    if (value !== undefined) {
+      next[key] = value;
+    }
   }
-  const next = {
-    ...(incoming?.codex !== undefined || cached?.codex !== undefined
-      ? { codex: incoming?.codex ?? cached?.codex }
-      : {}),
-    ...(incoming?.claudeAgent !== undefined || cached?.claudeAgent !== undefined
-      ? { claudeAgent: incoming?.claudeAgent ?? cached?.claudeAgent }
-      : {}),
-    ...(incoming?.cursor !== undefined || cached?.cursor !== undefined
-      ? { cursor: incoming?.cursor ?? cached?.cursor }
-      : {}),
-  } satisfies Partial<ProviderModelOptions>;
   return Object.keys(next).length > 0 ? (next as ProviderModelOptions) : undefined;
 }
 
@@ -405,8 +402,12 @@
       threadModelOptions.get(input.threadId),
       input.modelOptions,
     );
-    if (mergedModelOptions !== undefined) {
-      threadModelOptions.set(input.threadId, mergedModelOptions);
+    if (input.modelOptions !== undefined) {
+      if (mergedModelOptions !== undefined) {
+        threadModelOptions.set(input.threadId, mergedModelOptions);
+      } else {
+        threadModelOptions.delete(input.threadId);
+      }
     }
     const normalizedInput = toNonEmptyProviderInput(input.messageText);
     const normalizedAttachments = input.attachments ?? [];

diff --git a/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts b/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts
--- a/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts
+++ b/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts
@@ -32,7 +32,7 @@
         cwd: process.cwd(),
         mcpServers: [],
       });
-      expect(newResult).toEqual({ sessionId: "mock-session-1" });
+      expect(newResult).toMatchObject({ sessionId: "mock-session-1" });
 
       const promptResult = yield* conn.request("session/prompt", {
         sessionId: "mock-session-1",

- Introduce Cursor ACP adapter and model selection probe
- Preserve cursor session resume state across model changes
- Propagate provider and runtime tool metadata through orchestration and UI

Made-with: Cursor
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
…tion

Instead of restarting the ACP process when the model changes mid-thread,
use session/set_config_option to switch models within a live session.
Update sessionModelSwitch to "in-session" and add probe tests to verify
the real agent supports this method.

Made-with: Cursor
Made-with: Cursor

# Conflicts:
#	apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx
#	apps/web/src/components/chat/ProviderModelPicker.browser.tsx
#	apps/web/src/components/chat/ProviderModelPicker.tsx
#	apps/web/src/components/chat/TraitsPicker.tsx
#	apps/web/src/components/chat/composerProviderRegistry.test.tsx
#	apps/web/src/composerDraftStore.ts
#	packages/contracts/src/model.ts
#	packages/shared/src/model.test.ts
#	packages/shared/src/model.ts
- Removed unused CursorModelOptions and related logic from ChatView.
- Updated model selection handling to map concrete Cursor slugs to server-provided options.
- Simplified ProviderModelPicker by eliminating unnecessary cursor-related state and logic.
- Adjusted tests to reflect changes in model selection behavior for Cursor provider.

Made-with: Cursor
- Add a standalone ACP probe script for initialize/auth/session/new
- Switch Cursor provider status checks to `agent about` for version and auth
- Log the ACP session/new result in the probe test
- Canonicalize Claude and Cursor dispatch model slugs
- Update provider model selection, defaults, and tests
- route Cursor commit/PR/branch generation through the agent CLI
- resolve separate ACP and agent model IDs for Cursor models
- improve git action failure logging and surface command output
juliusmarminge and others added 4 commits April 9, 2026 02:08
- Parse `cursor about --format json` results
- Fall back to plain `about` when JSON formatting is unsupported
- Preserve subscription metadata in provider auth state
# Conflicts:
#	apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
#	apps/server/src/git/Layers/GitManager.test.ts
#	apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	apps/web/src/components/chat/composerProviderRegistry.tsx
#	apps/web/src/composerDraftStore.ts
#	apps/web/src/rpc/client.test.ts
#	apps/web/src/session-logic.ts
#	apps/web/src/store.ts
#	packages/contracts/src/orchestration.ts
#	packages/contracts/src/settings.ts
# Conflicts:
#	apps/server/integration/orchestrationEngine.integration.test.ts
#	apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
#	apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	bun.lock
#	package.json
#	packages/contracts/src/orchestration.ts
#	packages/contracts/src/settings.ts
juliusmarminge and others added 3 commits April 10, 2026 14:58
- Keep false/explicit defaults in draft and dispatch state
- Allow Cursor fast mode and thinking to be cleared on later turns
- Add coverage for Cursor option reset behavior
# Conflicts:
#	apps/server/src/git/Layers/GitCore.test.ts
#	apps/server/src/git/Layers/GitManager.test.ts
- Seed providers with initial disabled/loading snapshots
- Discover Cursor ACP models from session config options
- Update Traits picker to use the new Cursor model trait flow
- Add probe script and tests for Cursor provider behavior
juliusmarminge and others added 4 commits April 13, 2026 10:06
Co-authored-by: codex <codex@users.noreply.github.com>
- keep the selected provider model when updating sticky traits
- add regression coverage for Cursor draft model selection
- Cache discovered Cursor models with freshness checks
- Prefer Cursor model_option effort controls and preserve max
- Update Cursor ACP probes, contracts, and examples
Adopt the shared provider status cache for Cursor, remove the branch-local Cursor model cache, and preserve cached discovered models during provider hydration.

Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge marked this pull request as ready for review April 13, 2026 22:54
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 13, 2026

Approvability

Verdict: Needs human review

10 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

Stop probing every Cursor model via ACP during provider refresh, and preserve the last good discovered model snapshot when a later refresh returns sparse model data.

Co-authored-by: codex <codex@users.noreply.github.com>
Refresh the Cursor provider quickly from ACP session config, then enrich per-model capabilities in the background with bounded parallelism. Preserve the last good provider model snapshot so sparse refreshes do not wipe discovered capabilities.

Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Default path overridden by subsequent options spread
    • Swapped the order so ...options is spread first and the path default (using ??) comes after, ensuring the computed default wins when options.path is absent or undefined.

Create PR

Or push these changes by commenting:

@cursor push 4298a93567
Preview (4298a93567)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -375,8 +375,8 @@
 
   try {
     await waitForHttpReady(baseUrl, {
+      ...options,
       path: options?.path ?? "/.well-known/t3/environment",
-      ...options,
       signal: controller.signal,
     });
   } finally {

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 6c36109. Configure here.

@cursor
Copy link
Copy Markdown
Contributor

cursor bot commented Apr 14, 2026

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Child process leaked - runtime close never called
    • Wrapped makeCursorAcpRuntime with Effect.acquireRelease so that runtime.close is called as a scope finalizer when Effect.scoped exits, properly terminating the spawned child process.

Create PR

Or push these changes by commenting:

@cursor push 0f4c368c4c
Preview (0f4c368c4c)
diff --git a/apps/server/src/git/Layers/CursorTextGeneration.ts b/apps/server/src/git/Layers/CursorTextGeneration.ts
--- a/apps/server/src/git/Layers/CursorTextGeneration.ts
+++ b/apps/server/src/git/Layers/CursorTextGeneration.ts
@@ -126,12 +126,15 @@
       ).pipe(Effect.catch(() => Effect.undefined));
 
       const outputRef = yield* Ref.make("");
-      const runtime = yield* makeCursorAcpRuntime({
-        cursorSettings,
-        childProcessSpawner: commandSpawner,
-        cwd,
-        clientInfo: { name: "t3-code-git-text", version: "0.0.0" },
-      });
+      const runtime = yield* Effect.acquireRelease(
+        makeCursorAcpRuntime({
+          cursorSettings,
+          childProcessSpawner: commandSpawner,
+          cwd,
+          clientInfo: { name: "t3-code-git-text", version: "0.0.0" },
+        }),
+        (rt) => rt.close,
+      );
 
       yield* runtime.handleSessionUpdate((notification) => {
         const update = notification.update;

You can send follow-ups to the cloud agent here.

juliusmarminge and others added 2 commits April 13, 2026 19:40
Co-authored-by: codex <codex@users.noreply.github.com>
- add ACP exit logging for the mock agent
- ensure Cursor text generation closes its child runtime
- cover process shutdown in the live test
- ignore interrupt-only turn start failures and preserve session state
- make ACP runtime/client handling more robust with distinct ids and buffered events
- fix stale provider enrichment, empty model options, and Cursor model discovery
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

const runtime = ManagedRuntime.make(layer);

Line 269 declares const runtime inside the createHarness function, which shadows the module-level let runtime declared on lines 73-76. The outer runtime remains null forever, so the cleanup in afterEach (lines 86-88) never calls runtime.dispose(). This causes ManagedRuntime instances to leak between tests.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts around line 269:

Line 269 declares `const runtime` inside the `createHarness` function, which shadows the module-level `let runtime` declared on lines 73-76. The outer `runtime` remains `null` forever, so the cleanup in `afterEach` (lines 86-88) never calls `runtime.dispose()`. This causes `ManagedRuntime` instances to leak between tests.

Evidence trail:
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts lines 73-76 (module-level `let runtime = null`), lines 86-88 (`afterEach` cleanup checking `if (runtime) { await runtime.dispose(); }`), line 269 (`const runtime = ManagedRuntime.make(layer);` inside `createHarness`), lines 303-319 (return object from `createHarness` does not include `runtime`).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant