fix: renderer crash on empty pipelines, Cursor tool-schema rejection, dead SDK update - #648
fix: renderer crash on empty pipelines, Cursor tool-schema rejection, dead SDK update#648playtix-brain wants to merge 2 commits into
Conversation
- chat: guard empty shell pipelines in parseHeadFileView. A blank or pipe-only command made splitShellPipeline return [], so parts[parts.length - 1] handed undefined to splitShellWords and the renderer died with "e is not iterable". - crossagents: drop the root-level oneOf from spawn_agent/wait_for_agent. Cursor's backend rejects union-typed tool schemas and fails the whole turn with "NonRetriableError: Provider Error", for every model. - cursor: allow updating a globally installed @cursor/sdk. The update command only accepted global-npm/global-pnpm, which the discovery only reports from the deferred npm root -g probe; a Node prefix of its own resolves as global-inferred, so the action was dead and the agent updater refreshed the CLI instead. Each fix ships a regression test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The global keydown listener forwards every event to eventToKeybinding, and
synthetic or stand-in events can arrive without `key`. normalizeKeyPart called
`part.toLowerCase()` unguarded, so one such event threw inside a listener that
has no error boundary and replaced the whole app shell with the crash screen
("Cannot read properties of undefined (reading 'toLowerCase')").
Normalizing an absent key now yields undefined, which simply matches no
keybinding. Adds a regression test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SDSLeon
left a comment
There was a problem hiding this comment.
Requesting changes for one blocking protocol-correctness issue. Removing the root-level unions is appropriate for Cursor compatibility, but the either/or contract must still be enforced at runtime so malformed model calls cannot silently select the wrong operation.
| { type: "object", required: ["prompt"] }, | ||
| { type: "object", required: ["tasks"] }, | ||
| ], | ||
| // No root-level union here: Cursor's backend rejects tool schemas that carry |
There was a problem hiding this comment.
The root-level oneOf removal fixes Cursor compatibility, but the new comment says the request parser still enforces the choice and it currently does not.
- With both
promptandtasks,spawnAgenttakes thetasksbranch and silently discardsprompt. - With both
run_idandrun_ids, dispatch waits only forrun_idsand silently ignoresrun_id.
Because the published schema now permits both shapes, a model-generated call can spawn or wait on the wrong work. Please keep the schema union-free, add explicit exactly-one validation before branch selection for both pairs, and add regression tests that pass both conflicting fields, assert an isError result, and verify that no spawn/wait manager method was called.
Three independent fixes, each with a regression test. All reproduced on 1.6.5/1.6.6 before fixing.
1. Renderer crash:
TypeError: e is not iterableparseHeadFileViewindexedparts[parts.length - 1]without guarding an empty pipeline. A blank or pipe-only command (bash -c '',cd x &&,|) makessplitShellPipelinereturn[], sosplitShellWords(undefined)iteratesundefinedand takes down the whole renderer — the React error boundary replaces the app shell. It is reached from ordinary tool-call rows, so one odd command from an agent kills the UI.2. Cursor turns fail with
NonRetriableError: Provider Errorspawn_agentandwait_for_agentdeclaredoneOfat the root of theirinputSchema. Cursor's backend rejects root-level unions and fails the entire turn, for every model (reproduced with Fable 5, Sonnet 5 and Kimi K3). Verified outside the app withcursor-agent acpplus a minimal HTTP MCP server mirroring these tools: withoneOfthe turn errors, without it the same prompt answers normally. The prompt/tasks and run_id/run_ids choices stay documented in the tool descriptions and are already enforced by the request parser.3. Cursor SDK update action was dead
cursorSdkUpdateCommand/canUpdateCursorSdkonly acceptedglobal-npm/global-pnpm, whichsdkPackageDiscoveryreports only from the deferrednpm root -gprobe — and that probe never runs once a filesystem candidate matched. A Node prefix of its own (~/.local, nvm, fnm, volta, Homebrew) resolves asglobal-inferred, so no command was produced and the click fell through to the agent updater, refreshing the CLI while@cursor/sdkstayed on its old version. The update banner then never cleared.Tests: 177 passing across the touched modules;
typecheckclean on the changed files.🤖 Generated with Claude Code