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
+3
View File
@@ -4,3 +4,6 @@ email = "thomasgl@pm.me"
[git] [git]
write-change-id-header = true write-change-id-header = true
[snapshot]
auto-update-stale = true
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"lastChangelogVersion": "0.66.1", "lastChangelogVersion": "0.66.1",
"defaultProvider": "openai-codex", "defaultProvider": "cursor-acp",
"defaultModel": "gpt-5.4", "defaultModel": "auto",
"defaultThinkingLevel": "medium", "defaultThinkingLevel": "medium",
"theme": "matugen", "theme": "matugen",
"lsp": { "lsp": {
+47 -17
View File
@@ -52,6 +52,9 @@ const LOGIN_URL_TIMEOUT_MS = 15_000;
const AUTH_POLL_INTERVAL_MS = 2_000; const AUTH_POLL_INTERVAL_MS = 2_000;
const AUTH_POLL_TIMEOUT_MS = 5 * 60_000; const AUTH_POLL_TIMEOUT_MS = 5 * 60_000;
const CURSOR_FORCE = process.env.PI_CURSOR_ACP_FORCE !== "false"; 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>([ const TOOL_NAME_ALIASES = new Map<string, string>([
["readtoolcall", "read"], ["readtoolcall", "read"],
@@ -336,7 +339,7 @@ async function execCapture(command: string, args: string[], stdin?: string): Pro
async function discoverModels(): Promise<DiscoveredModel[]> { async function discoverModels(): Promise<DiscoveredModel[]> {
try { try {
const result = await execCapture("cursor-agent", ["models"]); const result = await execCapture(CURSOR_AGENT_CMD, ["models"]);
const models = parseModels(result.stdout); const models = parseModels(result.stdout);
return models.length > 0 ? models : FALLBACK_MODELS; return models.length > 0 ? models : FALLBACK_MODELS;
} catch { } catch {
@@ -491,6 +494,13 @@ function renderToolContent(content: unknown): string {
} }
function classifyToolResult(content: unknown, isError: boolean | undefined): ToolLoopErrorClass { 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(); const text = renderToolContent(content).toLowerCase();
if ( if (
text.includes("missing required") text.includes("missing required")
@@ -1092,8 +1102,16 @@ function collectAssistantResponse(events: StreamJsonEvent[]): {
if (text) { if (text) {
if (isPartial) { if (isPartial) {
sawAssistantPartials = true; sawAssistantPartials = true;
assistantText += 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); assistantChunks.push(text);
assistantText = text;
}
// If text doesn't extend assistantText, skip (reset/duplicate).
} else if (!sawAssistantPartials) { } else if (!sawAssistantPartials) {
assistantText = text; assistantText = text;
} }
@@ -1102,8 +1120,14 @@ function collectAssistantResponse(events: StreamJsonEvent[]): {
if (thinking) { if (thinking) {
if (isPartial) { if (isPartial) {
sawThinkingPartials = true; sawThinkingPartials = true;
thinkingText += 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); thinkingChunks.push(thinking);
thinkingText = thinking;
}
} else if (!sawThinkingPartials) { } else if (!sawThinkingPartials) {
thinkingText = thinking; thinkingText = thinking;
} }
@@ -1143,7 +1167,9 @@ function getNovelStreamingDelta(previous: string, next: string): { delta: string
snapshot: next, 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( async function handleStreamingChatRequest(
@@ -1173,7 +1199,7 @@ async function handleStreamingChatRequest(
]; ];
if (CURSOR_FORCE) args.push("--force"); 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 stdoutBuffer = "";
let stdoutText = ""; let stdoutText = "";
let stderrText = ""; let stderrText = "";
@@ -1226,8 +1252,6 @@ async function handleStreamingChatRequest(
streamedAny = true; streamedAny = true;
sendSse(res, createChatCompletionChunk(model, { reasoning_content: delta }, null, responseMeta)); sendSse(res, createChatCompletionChunk(model, { reasoning_content: delta }, null, responseMeta));
} }
} else {
lastThinkingSnapshot = "";
} }
if (text) { if (text) {
const { delta, snapshot } = getNovelStreamingDelta(lastAssistantSnapshot, text); const { delta, snapshot } = getNovelStreamingDelta(lastAssistantSnapshot, text);
@@ -1236,15 +1260,21 @@ async function handleStreamingChatRequest(
streamedAny = true; streamedAny = true;
sendSse(res, createChatCompletionChunk(model, { content: delta }, null, responseMeta)); sendSse(res, createChatCompletionChunk(model, { content: delta }, null, responseMeta));
} }
} else {
lastAssistantSnapshot = "";
} }
}; };
child.on("error", (error) => { child.on("error", (error) => {
if (finished) return; if (finished) return;
finished = true; 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); endSse(res);
}); });
@@ -1327,10 +1357,10 @@ async function handleChatRequest(res: ServerResponse, workspace: string, body: a
model, model,
]; ];
if (CURSOR_FORCE) args.push("--force"); if (CURSOR_FORCE) args.push("--force");
result = await execCapture("cursor-agent", args, prompt); result = await execCapture(CURSOR_AGENT_CMD, args, prompt);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(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; return;
} }
@@ -1457,7 +1487,7 @@ async function refreshProvider(pi: ExtensionAPI, workspace: string) {
async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> { async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
return await new Promise((resolve, reject) => { 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 stdout = "";
let stderr = ""; let stderr = "";
let settled = false; let settled = false;
@@ -1500,7 +1530,7 @@ async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
child.on("close", async (code) => { child.on("close", async (code) => {
if (settled) return; if (settled) return;
if (code !== 0) { 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; return;
} }
@@ -1523,7 +1553,7 @@ async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
async function refreshCursorCredentials(credentials: OAuthCredentials): Promise<OAuthCredentials> { async function refreshCursorCredentials(credentials: OAuthCredentials): Promise<OAuthCredentials> {
if (!hasCursorAuth()) { 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 { return {
refresh: credentials.refresh || "cursor-auth", refresh: credentials.refresh || "cursor-auth",
@@ -1562,11 +1592,11 @@ export default function (pi: ExtensionAPI) {
pi.registerCommand("cursor-acp-status", { pi.registerCommand("cursor-acp-status", {
description: "Show Cursor ACP provider status", description: "Show Cursor ACP provider status",
handler: async (_args, ctx) => { 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 models = await discoverModels();
const status = [ const status = [
`provider: ${PROVIDER_ID}`, `provider: ${PROVIDER_ID}`,
`cursor-agent: ${version?.stdout.trim() || "not installed"}`, `cli (${CURSOR_AGENT_CMD}): ${version?.stdout.trim() || "not installed"}`,
`logged in: ${hasCursorAuth() ? "yes" : "no"}`, `logged in: ${hasCursorAuth() ? "yes" : "no"}`,
`proxy: ${runtime?.baseUrl ?? "not started"}`, `proxy: ${runtime?.baseUrl ?? "not started"}`,
`models: ${models.length}`, `models: ${models.length}`,