-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add ACP support with Cursor provider #1355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 43 commits
Commits
Show all changes
101 commits
Select commit
Hold shift + click to select a range
7165d9f
Add Cursor provider session and model selection support
juliusmarminge 10129ed
Use server-driven ModelCapabilities for Cursor traits
juliusmarminge d4561ad
Switch Cursor model selection to in-session via session/set_config_op…
juliusmarminge 97d5288
Merge remote-tracking branch 'origin/main' into t3code/greeting
juliusmarminge d747322
Refactor Cursor model handling in ChatView and ProviderModelPicker
juliusmarminge 55cef69
rm probes
juliusmarminge 86e60de
rm probe
juliusmarminge e7c5ac3
Probe Cursor ACP session setup in one script
juliusmarminge c37322e
rm unused probe
juliusmarminge e1f8939
Normalize provider model IDs and Cursor options
juliusmarminge f5e64d6
fix picker
juliusmarminge 4d4bee2
fix spawning
juliusmarminge 3a21c19
Add Cursor text generation and model mapping
juliusmarminge 88696b4
fix effect lsp issues
juliusmarminge ea47805
use constructors
juliusmarminge 97ed04d
kewl
juliusmarminge 0575c11
move claude model id lookup
juliusmarminge aa5b8d2
rm unused test
juliusmarminge 26ef535
kewl
juliusmarminge c029bac
nit
juliusmarminge f7b2b07
kewl
juliusmarminge accc67d
effect-acp
juliusmarminge 84f9533
improve sdk and use it
juliusmarminge 22594d6
nit
juliusmarminge 0c218d2
noExternal
juliusmarminge f76321f
Log ACP requests and preserve turn-start failures
juliusmarminge 1787185
tidy up effect-acp
juliusmarminge ba6604b
error on fmt
juliusmarminge 1f7a48a
small nit
juliusmarminge 9476dd2
Handle cancelled Cursor ACP turns and simplify model ids
juliusmarminge 058822b
Extract ACP runtime event helpers and preserve model slugs
juliusmarminge 146e102
Constrain ACP runtime event source types
juliusmarminge e9c611f
Merge origin/main into t3code/greeting
juliusmarminge f2614d0
Settle pending user input on session stop
juliusmarminge fe988ec
Log outgoing ACP notifications before sending
juliusmarminge c4e9e40
Remove child-process export from effect-acp package
juliusmarminge 65621de
Patch ACP protocol to use request envelopes
juliusmarminge cad3cbe
Propagate ACP child exits through protocol termination
juliusmarminge ff8577a
servicify
juliusmarminge 7192f24
Merge origin/main into t3code/greeting
juliusmarminge 81d1239
Align Effect catalog with merged main
juliusmarminge b994a72
fix ubild
juliusmarminge 5cb6d05
agent/client separation
juliusmarminge e44eea6
fix
juliusmarminge 399be25
kewl
juliusmarminge ece6436
fixes
juliusmarminge 981aeba
Merge origin/main into t3code/greeting
juliusmarminge 8e6fc4c
Merge origin/main into t3code/greeting
juliusmarminge 68e06f6
Merge origin/main into t3code/greeting
juliusmarminge f1ac1ff
Merge remote-tracking branch 'origin/main' into t3code/greeting
juliusmarminge 085dccc
Merge remote-tracking branch 'origin/main' into t3code/greeting
juliusmarminge e8d6a12
Merge origin/main into t3code/greeting
juliusmarminge f0d4415
Add ACP model picker support for Cursor
juliusmarminge 1dea9cc
Gate Cursor parameterized model picker on preview channel
juliusmarminge 6cbf692
Support JSON Cursor about output
juliusmarminge d9b8123
Merge remote-tracking branch 'origin/main' into t3code/greeting
01bfe63
Merge remote-tracking branch 'origin/main' into t3code/greeting
2a23bd2
Remove obsolete Effect patches
50d2528
Update Cursor provider test expectations
543e496
Preserve explicit Cursor option resets
juliusmarminge 7ee8909
Merge remote-tracking branch 'origin/main' into t3code/greeting
0c1793a
Refine provider discovery and Cursor ACP model handling
juliusmarminge 9d2409e
Merge origin/main into t3code/greeting
juliusmarminge dc7e19c
Preserve cursor model when traits change
juliusmarminge c689f21
Add Cursor model cache and max-effort support
juliusmarminge 191b24c
Merge origin/main into t3code/greeting
juliusmarminge 218cd8b
Speed up Cursor provider discovery
juliusmarminge e9bf9dc
Keep Cursor capability discovery off the refresh path
juliusmarminge 6c36109
Fix desktop backend readiness probe
juliusmarminge e94d19c
Merge origin/main into t3code/greeting
juliusmarminge c3f926c
Close cursor ACP runtime after generation
juliusmarminge e78ff43
Harden ACP session handling and provider refreshes
juliusmarminge a7ab294
Merge origin/main into t3code/greeting
juliusmarminge ce947ec
Fix validation fallout after main merge
juliusmarminge eb4f923
Harden Cursor ACP probe cleanup and warning handling
juliusmarminge d9657e6
Scope ACP runtime to session lifecycle
juliusmarminge a1a8b0d
Serialize same-thread Cursor session startup
juliusmarminge ff0e88a
Start provider probes on startup
juliusmarminge 188f902
Gate Cursor provider behind runtime flag
juliusmarminge 8e18135
Always enable Cursor flag
juliusmarminge 75cff58
fix: remove hardcoded true from isCursorEnabled feature flag
cursoragent a95008e
Merge origin/main into t3code/greeting
juliusmarminge 5ca5e9c
fix: remove hardcoded true from isCursorEnabled feature flag
cursor[bot] fda21fb
Merge origin/main into t3code/greeting
juliusmarminge c9207c7
Remove cursor feature-flag gating
juliusmarminge 489c2c9
Move Claude API model ID resolution into provider
juliusmarminge 32df4e7
Remove duplicate model and layer providers
juliusmarminge c0436c6
Refactor provider model picker option handling
juliusmarminge d4b40db
fixx
juliusmarminge 849e3dc
fix option logic
juliusmarminge ed2a46f
Skip redundant ACP session config writes
juliusmarminge 10c1faf
Expose ACP event stream via getter
juliusmarminge 47bd4be
Fallback to current model option in Cursor probes
juliusmarminge 2395967
Use runtime event accessor consistently
juliusmarminge de2c424
Handle provider request errors by detail text
juliusmarminge 7555ae6
kewl
juliusmarminge f91a965
me
juliusmarminge 1753bc6
retry capability probe
juliusmarminge 0c46385
Default Cursor off and show Early Access badge in settings
juliusmarminge b2f3a10
Honor explicit turn model when session model switching is unsupported
juliusmarminge aa696b5
Remove test for model override on unsupported session restart
juliusmarminge File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,342 @@ | ||
| #!/usr/bin/env bun | ||
| import { appendFileSync } from "node:fs"; | ||
|
|
||
| import * as Effect from "effect/Effect"; | ||
|
|
||
| import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | ||
|
|
||
| import * as EffectAcpAgent from "effect-acp/agent"; | ||
| import * as AcpError from "effect-acp/errors"; | ||
| import type * as AcpSchema from "effect-acp/schema"; | ||
|
|
||
| const requestLogPath = process.env.T3_ACP_REQUEST_LOG_PATH; | ||
| const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1"; | ||
| const emitAskQuestion = process.env.T3_ACP_EMIT_ASK_QUESTION === "1"; | ||
| const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1"; | ||
| const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1"; | ||
| const sessionId = "mock-session-1"; | ||
|
|
||
| let currentModeId = "ask"; | ||
| let currentModelId = "default"; | ||
| const cancelledSessions = new Set<string>(); | ||
|
|
||
| function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> { | ||
| return [ | ||
| { | ||
| id: "model", | ||
| name: "Model", | ||
| category: "model", | ||
| type: "select" as const, | ||
| currentValue: currentModelId, | ||
| options: [ | ||
| { value: "default", name: "Auto" }, | ||
| { value: "composer-2", name: "Composer 2" }, | ||
| { value: "composer-2[fast=true]", name: "Composer 2 Fast" }, | ||
| { value: "gpt-5.3-codex[reasoning=medium,fast=false]", name: "Codex 5.3" }, | ||
| ], | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| const availableModes: ReadonlyArray<AcpSchema.SessionMode> = [ | ||
| { | ||
| id: "ask", | ||
| name: "Ask", | ||
| description: "Request permission before making any changes", | ||
| }, | ||
| { | ||
| id: "architect", | ||
| name: "Architect", | ||
| description: "Design and plan software systems without implementation", | ||
| }, | ||
| { | ||
| id: "code", | ||
| name: "Code", | ||
| description: "Write and modify code with full tool access", | ||
| }, | ||
| ]; | ||
|
|
||
| function modeState(): AcpSchema.SessionModeState { | ||
| return { | ||
| currentModeId, | ||
| availableModes, | ||
| }; | ||
| } | ||
|
|
||
| const program = Effect.gen(function* () { | ||
| const agent = yield* EffectAcpAgent.AcpAgent; | ||
|
|
||
| yield* agent.handleInitialize(() => | ||
| Effect.succeed({ | ||
| protocolVersion: 1, | ||
| agentCapabilities: { loadSession: true }, | ||
| }), | ||
| ); | ||
|
|
||
| yield* agent.handleAuthenticate(() => Effect.succeed({})); | ||
|
|
||
| yield* agent.handleCreateSession(() => | ||
| Effect.succeed({ | ||
| sessionId, | ||
| modes: modeState(), | ||
| configOptions: configOptions(), | ||
| }), | ||
| ); | ||
|
|
||
| yield* agent.handleLoadSession((request) => | ||
| agent.client | ||
| .sessionUpdate({ | ||
| sessionId: String(request.sessionId ?? sessionId), | ||
| update: { | ||
| sessionUpdate: "user_message_chunk", | ||
| content: { type: "text", text: "replay" }, | ||
| }, | ||
| }) | ||
| .pipe( | ||
| Effect.as({ | ||
| modes: modeState(), | ||
| configOptions: configOptions(), | ||
| }), | ||
| ), | ||
| ); | ||
|
|
||
| yield* agent.handleSetSessionConfigOption((request) => | ||
| Effect.gen(function* () { | ||
| if (exitOnSetConfigOption) { | ||
| return yield* Effect.sync(() => { | ||
| process.exit(7); | ||
| }); | ||
| } | ||
| if (failSetConfigOption) { | ||
| return yield* AcpError.AcpRequestError.invalidParams( | ||
| "Mock invalid params for session/set_config_option", | ||
| { | ||
| method: "session/set_config_option", | ||
| params: request, | ||
| }, | ||
| ); | ||
| } | ||
| if (request.configId === "model" && typeof request.value === "string") { | ||
| currentModelId = request.value; | ||
| } | ||
| return { | ||
| configOptions: configOptions(), | ||
| }; | ||
| }), | ||
| ); | ||
|
|
||
| yield* agent.handleCancel(({ sessionId }) => | ||
| Effect.sync(() => { | ||
| cancelledSessions.add(String(sessionId)); | ||
| }), | ||
| ); | ||
|
|
||
| yield* agent.handlePrompt((request) => | ||
| Effect.gen(function* () { | ||
| const requestedSessionId = String(request.sessionId ?? sessionId); | ||
|
|
||
| if (emitToolCalls) { | ||
| const toolCallId = "tool-call-1"; | ||
|
|
||
| yield* agent.client.sessionUpdate({ | ||
| sessionId: requestedSessionId, | ||
| update: { | ||
| sessionUpdate: "tool_call", | ||
| toolCallId, | ||
| title: "Terminal", | ||
| kind: "execute", | ||
| status: "pending", | ||
| rawInput: { | ||
| command: ["cat", "server/package.json"], | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| yield* agent.client.sessionUpdate({ | ||
| sessionId: requestedSessionId, | ||
| update: { | ||
| sessionUpdate: "tool_call_update", | ||
| toolCallId, | ||
| status: "in_progress", | ||
| }, | ||
| }); | ||
|
|
||
| const permission = yield* agent.client.requestPermission({ | ||
| sessionId: requestedSessionId, | ||
| toolCall: { | ||
| toolCallId, | ||
| title: "`cat server/package.json`", | ||
| kind: "execute", | ||
| status: "pending", | ||
| content: [ | ||
| { | ||
| type: "content", | ||
| content: { | ||
| type: "text", | ||
| text: "Not in allowlist: cat server/package.json", | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| options: [ | ||
| { optionId: "allow-once", name: "Allow once", kind: "allow_once" }, | ||
| { optionId: "allow-always", name: "Allow always", kind: "allow_always" }, | ||
| { optionId: "reject-once", name: "Reject", kind: "reject_once" }, | ||
| ], | ||
| }); | ||
|
|
||
| const cancelled = | ||
| cancelledSessions.delete(requestedSessionId) || | ||
| permission.outcome.outcome === "cancelled"; | ||
|
|
||
| yield* agent.client.sessionUpdate({ | ||
| sessionId: requestedSessionId, | ||
| update: { | ||
| sessionUpdate: "tool_call_update", | ||
| toolCallId, | ||
| title: "Terminal", | ||
| kind: "execute", | ||
| status: "completed", | ||
| rawOutput: { | ||
| exitCode: 0, | ||
| stdout: '{ "name": "t3" }', | ||
| stderr: "", | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| yield* agent.client.sessionUpdate({ | ||
| sessionId: requestedSessionId, | ||
| update: { | ||
| sessionUpdate: "agent_message_chunk", | ||
| content: { type: "text", text: "hello from mock" }, | ||
| }, | ||
| }); | ||
|
|
||
| return { stopReason: cancelled ? "cancelled" : "end_turn" }; | ||
| } | ||
|
|
||
| if (emitAskQuestion) { | ||
| yield* agent.client.extRequest("cursor/ask_question", { | ||
| toolCallId: "ask-question-tool-call-1", | ||
| title: "Question", | ||
| questions: [ | ||
| { | ||
| id: "scope", | ||
| prompt: "Which scope?", | ||
| options: [ | ||
| { id: "workspace", label: "Workspace" }, | ||
| { id: "session", label: "Session" }, | ||
| ], | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| return { stopReason: "end_turn" }; | ||
| } | ||
|
|
||
| yield* agent.client.sessionUpdate({ | ||
| sessionId: requestedSessionId, | ||
| update: { | ||
| sessionUpdate: "plan", | ||
| entries: [ | ||
| { | ||
| content: "Inspect mock ACP state", | ||
| priority: "high", | ||
| status: "completed", | ||
| }, | ||
| { | ||
| content: "Implement the requested change", | ||
| priority: "high", | ||
| status: "in_progress", | ||
| }, | ||
| ], | ||
| }, | ||
| }); | ||
|
|
||
| yield* agent.client.sessionUpdate({ | ||
| sessionId: requestedSessionId, | ||
| update: { | ||
| sessionUpdate: "agent_message_chunk", | ||
| content: { type: "text", text: "hello from mock" }, | ||
| }, | ||
| }); | ||
|
|
||
| return { stopReason: "end_turn" }; | ||
| }), | ||
| ); | ||
|
|
||
| yield* agent.handleUnknownExtRequest((method, params) => { | ||
| if (method !== "session/mode/set") { | ||
| return Effect.fail(AcpError.AcpRequestError.methodNotFound(method)); | ||
| } | ||
|
|
||
| const nextModeId = | ||
| typeof params === "object" && | ||
| params !== null && | ||
| "modeId" in params && | ||
| typeof params.modeId === "string" | ||
| ? params.modeId | ||
| : typeof params === "object" && | ||
| params !== null && | ||
| "mode" in params && | ||
| typeof params.mode === "string" | ||
| ? params.mode | ||
| : undefined; | ||
| const requestedSessionId = | ||
| typeof params === "object" && | ||
| params !== null && | ||
| "sessionId" in params && | ||
| typeof params.sessionId === "string" | ||
| ? params.sessionId | ||
| : sessionId; | ||
|
|
||
| if (typeof nextModeId === "string" && nextModeId.trim()) { | ||
| currentModeId = nextModeId.trim(); | ||
| return agent.client | ||
| .sessionUpdate({ | ||
| sessionId: requestedSessionId, | ||
| update: { | ||
| sessionUpdate: "current_mode_update", | ||
| currentModeId, | ||
| }, | ||
| }) | ||
| .pipe(Effect.as({})); | ||
| } | ||
|
|
||
| return Effect.succeed({}); | ||
| }); | ||
|
|
||
| return yield* Effect.never; | ||
| }).pipe( | ||
| Effect.provide( | ||
| EffectAcpAgent.layerStdio( | ||
| requestLogPath | ||
| ? { | ||
| logIncoming: true, | ||
| logger: (event) => { | ||
| if (event.direction !== "incoming" || event.stage !== "raw") { | ||
| return Effect.void; | ||
| } | ||
| if (typeof event.payload !== "string") { | ||
| return Effect.void; | ||
| } | ||
| const payload = event.payload; | ||
| return Effect.sync(() => { | ||
| appendFileSync( | ||
| requestLogPath, | ||
| payload.endsWith("\n") ? payload : `${payload}\n`, | ||
| "utf8", | ||
| ); | ||
| }); | ||
| }, | ||
| } | ||
| : {}, | ||
| ), | ||
| ), | ||
| Effect.scoped, | ||
| Effect.provide(NodeServices.layer), | ||
| ); | ||
|
|
||
| NodeRuntime.runMain(program); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.