Skip to content

Commit 12f825a

Browse files
Add Cursor provider session and model selection support
- 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
1 parent e11fb6e commit 12f825a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+5310
-105
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Minimal NDJSON JSON-RPC "agent" for ACP client tests.
4+
* Reads stdin lines; writes responses/notifications to stdout.
5+
*/
6+
import * as readline from "node:readline";
7+
import { appendFileSync } from "node:fs";
8+
9+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
10+
const requestLogPath = process.env.T3_ACP_REQUEST_LOG_PATH;
11+
const emitToolCalls = process.env.T3_ACP_EMIT_TOOL_CALLS === "1";
12+
const sessionId = "mock-session-1";
13+
let currentModeId = "ask";
14+
let nextRequestId = 1;
15+
const availableModes = [
16+
{
17+
id: "ask",
18+
name: "Ask",
19+
description: "Request permission before making any changes",
20+
},
21+
{
22+
id: "architect",
23+
name: "Architect",
24+
description: "Design and plan software systems without implementation",
25+
},
26+
{
27+
id: "code",
28+
name: "Code",
29+
description: "Write and modify code with full tool access",
30+
},
31+
];
32+
const pendingPermissionRequests = new Map();
33+
34+
function send(obj) {
35+
process.stdout.write(`${JSON.stringify(obj)}\n`);
36+
}
37+
38+
function modeState() {
39+
return {
40+
currentModeId,
41+
availableModes,
42+
};
43+
}
44+
45+
function sendSessionUpdate(update, session = sessionId) {
46+
send({
47+
jsonrpc: "2.0",
48+
method: "session/update",
49+
params: {
50+
sessionId: session,
51+
update,
52+
},
53+
});
54+
}
55+
56+
rl.on("line", (line) => {
57+
const trimmed = line.trim();
58+
if (!trimmed) return;
59+
let msg;
60+
try {
61+
msg = JSON.parse(trimmed);
62+
} catch {
63+
return;
64+
}
65+
if (!msg || typeof msg !== "object") return;
66+
if (requestLogPath) {
67+
appendFileSync(requestLogPath, `${JSON.stringify(msg)}\n`, "utf8");
68+
}
69+
70+
const id = msg.id;
71+
const method = msg.method;
72+
73+
if (method === undefined && id !== undefined && pendingPermissionRequests.has(id)) {
74+
const pending = pendingPermissionRequests.get(id);
75+
pendingPermissionRequests.delete(id);
76+
sendSessionUpdate(
77+
{
78+
sessionUpdate: "tool_call_update",
79+
toolCallId: pending.toolCallId,
80+
title: "Terminal",
81+
kind: "execute",
82+
status: "completed",
83+
rawOutput: {
84+
exitCode: 0,
85+
stdout: '{ "name": "t3" }',
86+
stderr: "",
87+
},
88+
},
89+
pending.sessionId,
90+
);
91+
sendSessionUpdate(
92+
{
93+
sessionUpdate: "agent_message_chunk",
94+
content: { type: "text", text: "hello from mock" },
95+
},
96+
pending.sessionId,
97+
);
98+
send({
99+
jsonrpc: "2.0",
100+
id: pending.promptRequestId,
101+
result: { stopReason: "end_turn" },
102+
});
103+
return;
104+
}
105+
106+
if (method === "initialize" && id !== undefined) {
107+
send({
108+
jsonrpc: "2.0",
109+
id,
110+
result: {
111+
protocolVersion: 1,
112+
agentCapabilities: { loadSession: true },
113+
},
114+
});
115+
return;
116+
}
117+
118+
if (method === "authenticate" && id !== undefined) {
119+
send({ jsonrpc: "2.0", id, result: { authenticated: true } });
120+
return;
121+
}
122+
123+
if (method === "session/new" && id !== undefined) {
124+
send({
125+
jsonrpc: "2.0",
126+
id,
127+
result: {
128+
sessionId,
129+
modes: modeState(),
130+
},
131+
});
132+
return;
133+
}
134+
135+
if (method === "session/load" && id !== undefined) {
136+
const requestedSessionId = msg.params?.sessionId ?? sessionId;
137+
sendSessionUpdate(
138+
{
139+
sessionUpdate: "user_message_chunk",
140+
content: { type: "text", text: "replay" },
141+
},
142+
requestedSessionId,
143+
);
144+
send({
145+
jsonrpc: "2.0",
146+
id,
147+
result: {
148+
modes: modeState(),
149+
},
150+
});
151+
return;
152+
}
153+
154+
if (method === "session/prompt" && id !== undefined) {
155+
const requestedSessionId = msg.params?.sessionId ?? sessionId;
156+
if (emitToolCalls) {
157+
const toolCallId = "tool-call-1";
158+
const permissionRequestId = nextRequestId++;
159+
sendSessionUpdate(
160+
{
161+
sessionUpdate: "tool_call",
162+
toolCallId,
163+
title: "Terminal",
164+
kind: "execute",
165+
status: "pending",
166+
rawInput: {
167+
command: ["cat", "server/package.json"],
168+
},
169+
},
170+
requestedSessionId,
171+
);
172+
sendSessionUpdate(
173+
{
174+
sessionUpdate: "tool_call_update",
175+
toolCallId,
176+
status: "in_progress",
177+
},
178+
requestedSessionId,
179+
);
180+
pendingPermissionRequests.set(permissionRequestId, {
181+
promptRequestId: id,
182+
sessionId: requestedSessionId,
183+
toolCallId,
184+
});
185+
send({
186+
jsonrpc: "2.0",
187+
id: permissionRequestId,
188+
method: "session/request_permission",
189+
params: {
190+
sessionId: requestedSessionId,
191+
toolCall: {
192+
toolCallId,
193+
title: "`cat server/package.json`",
194+
kind: "execute",
195+
status: "pending",
196+
content: [
197+
{
198+
type: "content",
199+
content: {
200+
type: "text",
201+
text: "Not in allowlist: cat server/package.json",
202+
},
203+
},
204+
],
205+
},
206+
options: [
207+
{ optionId: "allow-once", name: "Allow once", kind: "allow_once" },
208+
{ optionId: "allow-always", name: "Allow always", kind: "allow_always" },
209+
{ optionId: "reject-once", name: "Reject", kind: "reject_once" },
210+
],
211+
},
212+
});
213+
return;
214+
}
215+
sendSessionUpdate(
216+
{
217+
sessionUpdate: "plan",
218+
explanation: `Mock plan while in ${currentModeId}`,
219+
entries: [
220+
{
221+
content: "Inspect mock ACP state",
222+
priority: "high",
223+
status: "completed",
224+
},
225+
{
226+
content: "Implement the requested change",
227+
priority: "high",
228+
status: "in_progress",
229+
},
230+
],
231+
},
232+
requestedSessionId,
233+
);
234+
sendSessionUpdate(
235+
{
236+
sessionUpdate: "agent_message_chunk",
237+
content: { type: "text", text: "hello from mock" },
238+
},
239+
requestedSessionId,
240+
);
241+
send({
242+
jsonrpc: "2.0",
243+
id,
244+
result: { stopReason: "end_turn" },
245+
});
246+
return;
247+
}
248+
249+
if ((method === "session/set_mode" || method === "session/mode/set") && id !== undefined) {
250+
const nextModeId =
251+
typeof msg.params?.modeId === "string"
252+
? msg.params.modeId
253+
: typeof msg.params?.mode === "string"
254+
? msg.params.mode
255+
: undefined;
256+
if (typeof nextModeId === "string" && nextModeId.trim()) {
257+
currentModeId = nextModeId.trim();
258+
sendSessionUpdate({
259+
sessionUpdate: "current_mode_update",
260+
currentModeId,
261+
});
262+
}
263+
send({ jsonrpc: "2.0", id, result: null });
264+
return;
265+
}
266+
267+
if (method === "session/cancel" && id !== undefined) {
268+
send({ jsonrpc: "2.0", id, result: null });
269+
return;
270+
}
271+
272+
if (id !== undefined) {
273+
send({
274+
jsonrpc: "2.0",
275+
id,
276+
error: { code: -32601, message: `Unhandled method: ${String(method)}` },
277+
});
278+
}
279+
});

0 commit comments

Comments
 (0)