cursor stuff

This commit is contained in:
2026-04-16 12:40:51 +01:00
parent 9a7669af28
commit 0bfdbd350e
3 changed files with 54 additions and 21 deletions
+49 -19
View File
@@ -52,6 +52,9 @@ const LOGIN_URL_TIMEOUT_MS = 15_000;
const AUTH_POLL_INTERVAL_MS = 2_000;
const AUTH_POLL_TIMEOUT_MS = 5 * 60_000;
const CURSOR_FORCE = process.env.PI_CURSOR_ACP_FORCE !== "false";
/** Same resolution as community providers: https://github.com/netandreus/pi-cursor-provider */
const CURSOR_AGENT_CMD =
process.env.PI_CURSOR_AGENT_PATH ?? process.env.CURSOR_AGENT_PATH ?? process.env.AGENT_PATH ?? "cursor-agent";
const TOOL_NAME_ALIASES = new Map<string, string>([
["readtoolcall", "read"],
@@ -336,7 +339,7 @@ async function execCapture(command: string, args: string[], stdin?: string): Pro
async function discoverModels(): Promise<DiscoveredModel[]> {
try {
const result = await execCapture("cursor-agent", ["models"]);
const result = await execCapture(CURSOR_AGENT_CMD, ["models"]);
const models = parseModels(result.stdout);
return models.length > 0 ? models : FALLBACK_MODELS;
} catch {
@@ -491,6 +494,13 @@ function renderToolContent(content: unknown): string {
}
function classifyToolResult(content: unknown, isError: boolean | undefined): ToolLoopErrorClass {
// Pi sets `isError` on failed tool runs. Do not substring-scan successful payloads:
// source files often contain "invalid", "schema", "type error", etc., which would
// misclassify reads as "validation" and trip the tool loop guard (see SPA-754-style sessions).
if (isError !== true) {
return "success";
}
const text = renderToolContent(content).toLowerCase();
if (
text.includes("missing required")
@@ -1092,8 +1102,16 @@ function collectAssistantResponse(events: StreamJsonEvent[]): {
if (text) {
if (isPartial) {
sawAssistantPartials = true;
assistantText += text;
assistantChunks.push(text);
// Partials are cumulative — only take the delta that extends current text.
if (text.startsWith(assistantText) && text.length > assistantText.length) {
const delta = text.slice(assistantText.length);
assistantChunks.push(delta);
assistantText = text;
} else if (!assistantText) {
assistantChunks.push(text);
assistantText = text;
}
// If text doesn't extend assistantText, skip (reset/duplicate).
} else if (!sawAssistantPartials) {
assistantText = text;
}
@@ -1102,8 +1120,14 @@ function collectAssistantResponse(events: StreamJsonEvent[]): {
if (thinking) {
if (isPartial) {
sawThinkingPartials = true;
thinkingText += thinking;
thinkingChunks.push(thinking);
if (thinking.startsWith(thinkingText) && thinking.length > thinkingText.length) {
const delta = thinking.slice(thinkingText.length);
thinkingChunks.push(delta);
thinkingText = thinking;
} else if (!thinkingText) {
thinkingChunks.push(thinking);
thinkingText = thinking;
}
} else if (!sawThinkingPartials) {
thinkingText = thinking;
}
@@ -1143,7 +1167,9 @@ function getNovelStreamingDelta(previous: string, next: string): { delta: string
snapshot: next,
};
}
return { delta: next, snapshot: next };
// cursor-agent sometimes "resets" mid-stream (e.g. new message block).
// Do NOT emit or update snapshot — treat as stale/duplicate to prevent re-sending.
return { delta: "", snapshot: previous };
}
async function handleStreamingChatRequest(
@@ -1173,7 +1199,7 @@ async function handleStreamingChatRequest(
];
if (CURSOR_FORCE) args.push("--force");
const child = spawn("cursor-agent", args, { stdio: ["pipe", "pipe", "pipe"] });
const child = spawn(CURSOR_AGENT_CMD, args, { stdio: ["pipe", "pipe", "pipe"] });
let stdoutBuffer = "";
let stdoutText = "";
let stderrText = "";
@@ -1226,8 +1252,6 @@ async function handleStreamingChatRequest(
streamedAny = true;
sendSse(res, createChatCompletionChunk(model, { reasoning_content: delta }, null, responseMeta));
}
} else {
lastThinkingSnapshot = "";
}
if (text) {
const { delta, snapshot } = getNovelStreamingDelta(lastAssistantSnapshot, text);
@@ -1236,15 +1260,21 @@ async function handleStreamingChatRequest(
streamedAny = true;
sendSse(res, createChatCompletionChunk(model, { content: delta }, null, responseMeta));
}
} else {
lastAssistantSnapshot = "";
}
};
child.on("error", (error) => {
if (finished) return;
finished = true;
sendSse(res, createChatCompletionChunk(model, { content: `cursor-agent failed to start: ${error.message}` }, "stop", responseMeta));
sendSse(
res,
createChatCompletionChunk(
model,
{ content: `${CURSOR_AGENT_CMD} failed to start: ${error.message}` },
"stop",
responseMeta,
),
);
endSse(res);
});
@@ -1327,10 +1357,10 @@ async function handleChatRequest(res: ServerResponse, workspace: string, body: a
model,
];
if (CURSOR_FORCE) args.push("--force");
result = await execCapture("cursor-agent", args, prompt);
result = await execCapture(CURSOR_AGENT_CMD, args, prompt);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
sendJson(res, 200, createChatCompletionResponse(model, `cursor-agent failed to start: ${message}`));
sendJson(res, 200, createChatCompletionResponse(model, `${CURSOR_AGENT_CMD} failed to start: ${message}`));
return;
}
@@ -1457,7 +1487,7 @@ async function refreshProvider(pi: ExtensionAPI, workspace: string) {
async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
return await new Promise((resolve, reject) => {
const child = spawn("cursor-agent", ["login"], { stdio: ["ignore", "pipe", "pipe"] });
const child = spawn(CURSOR_AGENT_CMD, ["login"], { stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
let settled = false;
@@ -1500,7 +1530,7 @@ async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
child.on("close", async (code) => {
if (settled) return;
if (code !== 0) {
finish(() => reject(new Error(stderr.trim() || `cursor-agent login exited with code ${code}`)));
finish(() => reject(new Error(stderr.trim() || `${CURSOR_AGENT_CMD} login exited with code ${code}`)));
return;
}
@@ -1523,7 +1553,7 @@ async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
async function refreshCursorCredentials(credentials: OAuthCredentials): Promise<OAuthCredentials> {
if (!hasCursorAuth()) {
throw new Error("Cursor is not logged in. Run /login cursor-acp or cursor-agent login.");
throw new Error(`Cursor is not logged in. Run /login cursor-acp or ${CURSOR_AGENT_CMD} login.`);
}
return {
refresh: credentials.refresh || "cursor-auth",
@@ -1562,11 +1592,11 @@ export default function (pi: ExtensionAPI) {
pi.registerCommand("cursor-acp-status", {
description: "Show Cursor ACP provider status",
handler: async (_args, ctx) => {
const version = await execCapture("cursor-agent", ["--version"]).catch(() => null);
const version = await execCapture(CURSOR_AGENT_CMD, ["--version"]).catch(() => null);
const models = await discoverModels();
const status = [
`provider: ${PROVIDER_ID}`,
`cursor-agent: ${version?.stdout.trim() || "not installed"}`,
`cli (${CURSOR_AGENT_CMD}): ${version?.stdout.trim() || "not installed"}`,
`logged in: ${hasCursorAuth() ? "yes" : "no"}`,
`proxy: ${runtime?.baseUrl ?? "not started"}`,
`models: ${models.length}`,