From bc08c186a2c6e2cde294b4014e90c0ea7363fcf8 Mon Sep 17 00:00:00 2001 From: Arham Wani Date: Fri, 31 Jul 2026 11:33:18 +0530 Subject: [PATCH] fix(server): fall back to a Cursor todo's title when its content is blank `extractTodosAsPlan` derives each plan step with `todo.content?.trim() ?? todo.title?.trim() ?? ""`. Because `??` only falls back on null/undefined, a todo whose `content` is present but empty or whitespace (`""` / `" "`) keeps the empty string and never falls back to the `title`, so a real step is dropped by the `if (step === "") return []` guard below. Both `content` and `title` are optional in `CursorTodo`, so a blank-content-with-title todo is a valid payload. Use `||` so a blank content falls back to the title, matching the evident intent (and the trailing `|| ""` default). A missing/blank content with no title still yields "" and is dropped as before. Added a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../provider/acp/CursorAcpExtension.test.ts | 19 +++++++++++++++++++ .../src/provider/acp/CursorAcpExtension.ts | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/server/src/provider/acp/CursorAcpExtension.test.ts b/apps/server/src/provider/acp/CursorAcpExtension.test.ts index 0a6adb75fc4..ba070c91295 100644 --- a/apps/server/src/provider/acp/CursorAcpExtension.test.ts +++ b/apps/server/src/provider/acp/CursorAcpExtension.test.ts @@ -107,6 +107,25 @@ describe("CursorAcpExtension", () => { }); }); + it("falls back to the title when content is present but blank", () => { + expect( + extractTodosAsPlan({ + toolCallId: "todos-2", + todos: [ + { id: "1", content: "", title: "Titled step", status: "pending" }, + { id: "2", content: " ", title: "Whitespace content", status: "in_progress" }, + { id: "3", content: "", title: "", status: "pending" }, + ], + merge: true, + }), + ).toEqual({ + plan: [ + { step: "Titled step", status: "pending" }, + { step: "Whitespace content", status: "inProgress" }, + ], + }); + }); + it("decodes Cursor list_available_models responses with per-model config options", () => { const decoded = CursorListAvailableModelsResponse.make({ models: [ diff --git a/apps/server/src/provider/acp/CursorAcpExtension.ts b/apps/server/src/provider/acp/CursorAcpExtension.ts index 2e131a61608..05fc53f4a6b 100644 --- a/apps/server/src/provider/acp/CursorAcpExtension.ts +++ b/apps/server/src/provider/acp/CursorAcpExtension.ts @@ -94,7 +94,10 @@ export function extractTodosAsPlan(params: typeof CursorUpdateTodosRequest.Type) }>; } { const plan = params.todos.flatMap((todo) => { - const step = todo.content?.trim() ?? todo.title?.trim() ?? ""; + // Fall back to the title when content is missing OR blank. `??` only + // covers a missing content, so a present-but-empty content ("" or + // whitespace) would shadow a real title and drop the step below. + const step = todo.content?.trim() || todo.title?.trim() || ""; if (step === "") { return []; }