Skip to content

fix(cursor): Add native image support for Cursor - #1228

Draft
yansigit wants to merge 10 commits into
lidge-jun:devfrom
yansigit:audit/cursor-dev
Draft

fix(cursor): Add native image support for Cursor #1228
yansigit wants to merge 10 commits into
lidge-jun:devfrom
yansigit:audit/cursor-dev

Conversation

@yansigit

@yansigit yansigit commented Aug 7, 2026

Copy link
Copy Markdown

Summary

  • Wire native Cursor vision through SelectedImage (blobIdWithData, JPEG soft-cap with edge shrink, view_image → SelectedImage promotion + MCP image omit).
  • Treat trailing non-image Desktop multi-agent developer injections as transparent for vision/tool-continuation so Codex Desktop no longer drops SelectedImage after view_image and hallucinates.
  • For cursor/grok-4.5, map Codex effort none/minimal to wire medium and send live-discovery cursor-grok-4.5-{low,medium,high} ids (Fast still uses parameterized grok-4.5).

Verification

  • bun test tests/cursor-images.test.ts tests/cursor-blob.test.ts tests/cursor-effort-suffix.test.ts (110 pass)
  • bun run typecheck
  • Rebased onto latest origin/dev
  • Headless Codex smoke with collab enabled (view_image on Grumpy): exact captions I HATE WHEN / PEOPLE USE ME TO ILLUSTRATE A POINT (no geometry/Vite hallucinations)
  • Workspace proxy restarted via bun run src/cli/index.ts ensure before smoke

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Made with Cursor

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • New Features

    • Added native image support for Cursor conversations, including uploads, data URLs, HTTPS images, and image-only messages.
    • Preserved and replayed images across tool results and conversation history.
    • Added automatic vision-sidecar handling for models without native image support.
    • Improved Cursor Grok 4.5 effort-level and model routing behavior.
  • Documentation

    • Updated Cursor guidance for vision support and reasoning tiers.
  • Bug Fixes

    • Improved handling of unsupported, invalid, oversized, or stale images with safe omission.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 69c7ec33-d45c-464a-b9e1-b820255a0e6b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds Cursor vision support for image resolution, selected-image protobuf encoding, tool-result promotion, native and sidecar model metadata, and request replay. It also updates Grok 4.5 effort-tier wire IDs and continuation handling.

Changes

Cursor model capabilities and Grok mapping

Layer / File(s) Summary
Model capabilities and Grok wire mapping
src/adapters/cursor/discovery.ts, src/adapters/cursor/effort-map.ts, src/providers/registry.ts, src/types.ts, docs-site/src/content/docs/reference/..., tests/...
Adds curated no-vision metadata, wildcard matching, native modality metadata, stale configuration reconciliation, and cursor--prefixed Grok 4.5 effort IDs.
Continuation handling regression coverage
src/adapters/cursor.ts, tests/cursor-adapter.test.ts
Recognizes developer suffixes after tool results and verifies error, retry, and conversation-ID behavior.

Cursor vision request flow

Layer / File(s) Summary
Image resolution and promotion
src/adapters/cursor/images.ts, tests/cursor-images.test.ts, tests/cursor-vision-wire-harness.test.ts
Adds image validation, fetching, optimization, blob storage, selected-image construction, active-turn resolution, and view_image result promotion.
Request construction and replay integration
src/adapters/cursor/live-transport.ts, src/adapters/cursor/request-builder.ts, src/adapters/cursor/protobuf-request.ts, src/adapters/cursor/types.ts, tests/cursor-blob.test.ts, tests/cursor-request-builder.test.ts
Prepares raw messages, preserves image-only turns, encodes MCP image content, promotes selected images, omits promoted replay images, and always emits the mcp_tools wrapper.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant LiveTransport
  participant CursorImages
  participant ProtobufRequest
  participant Cursor
  Client->>LiveTransport: submit raw messages
  LiveTransport->>CursorImages: prepare and resolve images
  CursorImages-->>LiveTransport: prepared messages and selected images
  LiveTransport->>ProtobufRequest: build Cursor request
  ProtobufRequest->>Cursor: send selected context and MCP image content
  Cursor-->>ProtobufRequest: return continuation or tool result
Loading

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun, wibias

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the PR's primary change: adding native image support for Cursor.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch audit/cursor-dev
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bug Something isn't working label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ⬜ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

@github-actions
github-actions Bot marked this pull request as draft August 7, 2026 17:23
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as ready for review August 7, 2026 17:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/adapters/cursor/images.ts`:
- Around line 271-305: Update resolveCursorImages so an image whose prepared
data exceeds MAX_CURSOR_IMAGE_BYTES is omitted with continue instead of throwing
CursorImageError. Preserve the existing behavior for other validation failures
and continue processing subsequent images, aligning this path with
prepareCursorImageDataUrl’s omission policy.
- Around line 459-475: Update the JPEG marker scan in the dimension-detection
function to recognize standalone markers 0xD0–0xD9 and 0x01 before reading a
length, advancing past them without consuming length bytes. Preserve the
existing SOF0/SOF2 dimension extraction and malformed-length handling for
markers that carry a length.
- Around line 564-593: Restrict isTransparentCursorVisionSuffix to developer
messages containing the known Codex Desktop <multi_agent_mode> guidance block,
rather than any image-free developer message. Preserve intentional developer
instructions, while keeping stripTrailingTransparentDeveloperMessages and
cursorIsTrailingToolResultContinuation behavior unchanged for the recognized
guidance suffix.

In `@src/adapters/cursor/live-transport.ts`:
- Around line 573-579: Eliminate duplicate image preparation across both sites:
in src/adapters/cursor/live-transport.ts lines 573-579, make
resolveActiveCursorImages reuse the output of prepareCursorRawMessages and
enforce one deadline for the complete image phase; in
src/adapters/cursor/images.ts lines 664-709, update prepareCursorContentParts to
bypass parts already marked image/jpeg and within the soft cap, preventing
repeated processing of accumulated view_image history.

In `@src/adapters/cursor/protobuf-request.ts`:
- Around line 364-418: Export the existing decodeDataUrl helper from images.ts,
then replace the inline data-URL parsing and Buffer.from logic in
toolResultContentItems with a call to decodeDataUrl inside the existing try
block. Preserve the current omission behavior by returning no image item when
decoding fails or the shared size limits reject the payload.
- Around line 534-538: Update the flush function to skip pending external-model
MCP tool calls that have no result, while preserving image-path pairing and
existing handling for answered calls. Do not emit result-less external entries
through toolCallStep; only append valid completed calls to current.steps.

In `@tests/cursor-blob.test.ts`:
- Around line 838-1196: Add focused regression coverage for the live-transport
image phase, targeting the wiring around prepareCursorRawMessages,
resolveActiveCursorImages, and preparedRequest in the live transport flow.
Exercise the production path end to end so the resolver receives the prepared
rawMessages and produces selectedImages, rather than injecting selectedImages
directly through encodeCursorRunRequest; place the test with the existing cursor
image or live-transport tests.

In `@tests/cursor-request-builder.test.ts`:
- Around line 223-254: Add a focused regression test alongside the existing
createCursorRequest tests for a conversation containing an assistant message
whose content consists only of a toolCall part. Assert that this assistant
message converts to undefined and is omitted from request.messages, and verify
the resulting final message role remains the expected user role for
protobuf-request lastRole handling.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a4299e02-aacb-425b-bb01-2dfefc5c30fb

📥 Commits

Reviewing files that changed from the base of the PR and between 2a9656d and a76c091.

⛔ Files ignored due to path filters (1)
  • tests/helpers/cursor-grumpy-fixture.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • docs-site/src/content/docs/reference/adapters.md
  • docs-site/src/content/docs/reference/configuration/providers.md
  • src/adapters/cursor/discovery.ts
  • src/adapters/cursor/effort-map.ts
  • src/adapters/cursor/images.ts
  • src/adapters/cursor/live-transport.ts
  • src/adapters/cursor/protobuf-request.ts
  • src/adapters/cursor/request-builder.ts
  • src/adapters/cursor/types.ts
  • src/providers/registry.ts
  • src/types.ts
  • tests/catalog-vision-sidecar-modalities.test.ts
  • tests/cursor-blob.test.ts
  • tests/cursor-discovery.test.ts
  • tests/cursor-effort-suffix.test.ts
  • tests/cursor-images.test.ts
  • tests/cursor-request-builder.test.ts
  • tests/cursor-static-catalog.test.ts
  • tests/cursor-vision-wire-harness.test.ts
  • tests/model-in-list.test.ts
  • tests/oauth-provider-reconcile.test.ts
  • tests/provider-registry-parity.test.ts

Comment thread src/adapters/cursor/images.ts
Comment on lines +459 to +475
// JPEG: scan for SOF0/SOF2 marker with dimensions
if (data.byteLength >= 4 && data[0] === 0xff && data[1] === 0xd8) {
let offset = 2;
while (offset + 8 < data.byteLength) {
if (data[offset] !== 0xff) break;
const marker = data[offset + 1]!;
const length = (data[offset + 2]! << 8) | data[offset + 3]!;
if (marker === 0xc0 || marker === 0xc2) {
const height = (data[offset + 5]! << 8) | data[offset + 6]!;
const width = (data[offset + 7]! << 8) | data[offset + 8]!;
if (width > 0 && height > 0) return { width, height };
break;
}
if (length < 2) break;
offset += 2 + length;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard the JPEG marker scan against standalone markers.

The scan at lines 460-475 assumes every marker after SOI carries a two-byte length. Standalone markers (0xD0-0xD9, 0x01) carry no length. If one appears before SOF0/SOF2, the loop reads the next two bytes as a length and jumps to a wrong offset, so the function returns undefined or, in a crafted file, a wrong width/height. A wrong dimension is written into SelectedImage.dimension by buildSelectedImages at line 497.

The function is documented as best-effort, so the fallback is safe, but the fix is one condition.

🐛 Proposed fix for standalone JPEG markers
     let offset = 2;
     while (offset + 8 < data.byteLength) {
       if (data[offset] !== 0xff) break;
       const marker = data[offset + 1]!;
+      // Standalone markers (RSTn, SOI, EOI, TEM) carry no length payload.
+      if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd9)) {
+        offset += 2;
+        continue;
+      }
       const length = (data[offset + 2]! << 8) | data[offset + 3]!;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// JPEG: scan for SOF0/SOF2 marker with dimensions
if (data.byteLength >= 4 && data[0] === 0xff && data[1] === 0xd8) {
let offset = 2;
while (offset + 8 < data.byteLength) {
if (data[offset] !== 0xff) break;
const marker = data[offset + 1]!;
const length = (data[offset + 2]! << 8) | data[offset + 3]!;
if (marker === 0xc0 || marker === 0xc2) {
const height = (data[offset + 5]! << 8) | data[offset + 6]!;
const width = (data[offset + 7]! << 8) | data[offset + 8]!;
if (width > 0 && height > 0) return { width, height };
break;
}
if (length < 2) break;
offset += 2 + length;
}
}
// JPEG: scan for SOF0/SOF2 marker with dimensions
if (data.byteLength >= 4 && data[0] === 0xff && data[1] === 0xd8) {
let offset = 2;
while (offset + 8 < data.byteLength) {
if (data[offset] !== 0xff) break;
const marker = data[offset + 1]!;
// Standalone markers (RSTn, SOI, EOI, TEM) carry no length payload.
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd9)) {
offset += 2;
continue;
}
const length = (data[offset + 2]! << 8) | data[offset + 3]!;
if (marker === 0xc0 || marker === 0xc2) {
const height = (data[offset + 5]! << 8) | data[offset + 6]!;
const width = (data[offset + 7]! << 8) | data[offset + 8]!;
if (width > 0 && height > 0) return { width, height };
break;
}
if (length < 2) break;
offset += 2 + length;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/cursor/images.ts` around lines 459 - 475, Update the JPEG marker
scan in the dimension-detection function to recognize standalone markers
0xD0–0xD9 and 0x01 before reading a length, advancing past them without
consuming length bytes. Preserve the existing SOF0/SOF2 dimension extraction and
malformed-length handling for markers that carry a length.

Comment thread src/adapters/cursor/images.ts
Comment on lines +573 to +579
// JPEG soft-cap rewrite for attach + view_image tool-result data URLs before encode.
const rawMessages = await prepareCursorRawMessages(request.rawMessages);
const selectedImages = await resolveActiveCursorImages(rawMessages, signal);
const preparedRequest = { ...request, rawMessages, selectedImages };
// Build the payload once. The estimate is only worth deriving when there is no
// carry-forward to fall back on — with a carry present it would never be used (#373).
const prepared = prepareCursorRunRequest(request, {
const prepared = prepareCursorRunRequest(preparedRequest, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Redundant image preparation on the request path. Both sites run the same expensive prep without memoization: prepareCursorImageDataUrl performs a validating Bun.Image decode plus a JPEG quality ladder, and nothing records that a given payload was already prepared. The result is repeated work per turn, serially, before the HTTP/2 stream opens. One memoized preparation step, keyed by the source data URL, removes both costs.

  • src/adapters/cursor/live-transport.ts#L573-L579: reuse the output of prepareCursorRawMessages so resolveActiveCursorImages does not decode and re-encode the active images a second time, and bound the whole image phase with a single deadline instead of relying on the per-image IMAGE_FETCH_TIMEOUT_MS.
  • src/adapters/cursor/images.ts#L664-L709: in prepareCursorContentParts, skip parts that are already image/jpeg and at or under the soft cap, so accumulated view_image history is not re-prepared on every subsequent turn.
📍 Affects 2 files
  • src/adapters/cursor/live-transport.ts#L573-L579 (this comment)
  • src/adapters/cursor/images.ts#L664-L709
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/cursor/live-transport.ts` around lines 573 - 579, Eliminate
duplicate image preparation across both sites: in
src/adapters/cursor/live-transport.ts lines 573-579, make
resolveActiveCursorImages reuse the output of prepareCursorRawMessages and
enforce one deadline for the complete image phase; in
src/adapters/cursor/images.ts lines 664-709, update prepareCursorContentParts to
bypass parts already marked image/jpeg and within the soft cap, preventing
repeated processing of accumulated view_image history.

Comment on lines +364 to +418
function toolResultContentItems(
content: OcxToolResultMessage["content"],
options?: { omitImages?: boolean },
) {
if (typeof content === "string") {
return [create(McpToolResultContentItemSchema, {
content: { case: "text", value: create(McpTextContentSchema, { text: content }) },
})];
}

const items = content.flatMap(part => {
if (part.type === "text" && part.text.length > 0) {
return [create(McpToolResultContentItemSchema, {
content: { case: "text", value: create(McpTextContentSchema, { text: part.text }) },
})];
}
if (part.type === "image" && typeof part.imageUrl === "string" && part.imageUrl.length > 0) {
if (options?.omitImages) {
return [create(McpToolResultContentItemSchema, {
content: { case: "text", value: create(McpTextContentSchema, { text: CURSOR_VISION_MCP_IMAGE_OMITTED }) },
})];
}
try {
if (!part.imageUrl.toLowerCase().startsWith("data:")) return [];
const comma = part.imageUrl.indexOf(",");
if (comma < 0) return [];
const header = part.imageUrl.slice(5, comma);
const payload = part.imageUrl.slice(comma + 1).replace(/\s/g, "");
if (!/;base64/i.test(header) || payload.length === 0) return [];
const mimeType = (header.split(";")[0] || "").trim().toLowerCase() || "image/png";
if (!mimeType.startsWith("image/")) return [];
const data = Buffer.from(payload, "base64");
if (data.byteLength === 0) return [];
return [create(McpToolResultContentItemSchema, {
content: {
case: "image",
value: create(McpImageContentSchema, {
data,
mimeType,
}),
},
})];
} catch {
return [];
}
}
return [];
});

return items.length > 0
? items
: [create(McpToolResultContentItemSchema, {
content: { case: "text", value: create(McpTextContentSchema, { text: "" }) },
})];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reuse decodeDataUrl here; the inline parser has no size guard and duplicates the resolver.

Lines 386-396 re-implement the data-URL parsing that decodeDataUrl already performs in src/adapters/cursor/images.ts lines 121-157: same slice(5, comma) header split, same ;base64 test, same whitespace strip, same MIME defaulting. The copy omits the two limits the original enforces, MAX_CURSOR_IMAGE_DECODE_BYTES and the estimated-decoded-bytes precheck.

The failure mode is concrete. Buffer.from(payload, "base64") at line 395 materializes an unbounded buffer, the bytes land in the ConversationStep blob, and storeCursorBlob then throws CursorBlobAdmissionError when admission rejects the oversized entry. prepareCursorRunRequest propagates that, so the whole turn fails instead of degrading to a text-only tool result. prepareCursorRawMessages does cap this path, but only for callers that route through src/adapters/cursor/live-transport.ts; encodeCursorRunRequest has no such guarantee.

Export decodeDataUrl from images.ts and call it inside the try, so the limits and the omission behavior stay in one place.

♻️ Proposed reuse of the shared decoder
       try {
         if (!part.imageUrl.toLowerCase().startsWith("data:")) return [];
-        const comma = part.imageUrl.indexOf(",");
-        if (comma < 0) return [];
-        const header = part.imageUrl.slice(5, comma);
-        const payload = part.imageUrl.slice(comma + 1).replace(/\s/g, "");
-        if (!/;base64/i.test(header) || payload.length === 0) return [];
-        const mimeType = (header.split(";")[0] || "").trim().toLowerCase() || "image/png";
-        if (!mimeType.startsWith("image/")) return [];
-        const data = Buffer.from(payload, "base64");
-        if (data.byteLength === 0) return [];
+        // Shared decoder enforces MAX_CURSOR_IMAGE_DECODE_BYTES and MIME validation.
+        const { data, mimeType } = decodeCursorImageDataUrl(part.imageUrl);
+        if (data.byteLength === 0) return [];
         return [create(McpToolResultContentItemSchema, {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/cursor/protobuf-request.ts` around lines 364 - 418, Export the
existing decodeDataUrl helper from images.ts, then replace the inline data-URL
parsing and Buffer.from logic in toolResultContentItems with a call to
decodeDataUrl inside the existing try block. Preserve the current omission
behavior by returning no image item when decoding fails or the shared size
limits reject the payload.

Comment on lines 534 to +538
const flush = () => {
if (!current) return;
for (const part of pendingToolCalls.values()) current.steps.push(toolCallStep(part, requestScope));
for (const part of pendingToolCalls.values()) {
current.steps.push(toolCallStep(part, requestScope, undefined, options));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Look for coverage of unanswered tool calls on external Cursor models.
set -euo pipefail

rg -n -C8 'pendingToolCalls|invalid_argument' --type=ts -g '!src/adapters/cursor/gen/**'
rg -n -C10 'parallel|two tool calls|call_b' tests/cursor-blob.test.ts

Repository: lidge-jun/opencodex

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg 'src/adapters/cursor|tests/cursor|protobuf-request' || true

echo
echo "Target file outline/size:"
wc -l src/adapters/cursor/protobuf-request.ts 2>/dev/null || true
ast-grep outline src/adapters/cursor/protobuf-request.ts 2>/dev/null | sed -n '1,220p' || true

echo
echo "Relevant source lines:"
sed -n '480,620p' src/adapters/cursor/protobuf-request.ts

echo
echo "Search relevant terms repository-wide:"
rg -n -C6 --glob '!src/adapters/cursor/gen/**' --glob '!node_modules/**' --glob '!dist/**' \
  'pendingToolCalls|native mcpToolCall|mcpToolCall|invalid_argument|toolResult|toolCall|externalModel|toolCallStep' \
  .

Repository: lidge-jun/opencodex

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Target file lines 480-620:"
sed -n '480,620p' src/adapters/cursor/protobuf-request.ts

echo
echo "Target file lines 510-560 with only matching nearby identifiers:"
rg -n -C4 'externalModel|pendingToolCalls|pendingToolCalls|toolResult|toolCallStep|invalid_argument|current??.*steps|flush|message' src/adapters/cursor/protobuf-request.ts

echo
echo "Imports and type definitions in target file:"
sed -n '1,80p' src/adapters/cursor/protobuf-request.ts

echo
echo "Tool result image helper / toolCallStep definitions:"
rg -n -C10 'function toolCallStep|toolCallStep|toolResultImage|invalid_argument|mcpToolCall' src/adapters/cursor/protobuf-request.ts src/adapters/cursor tests -g '*.ts'

Repository: lidge-jun/opencodex

Length of output: 50376


Do not flush result-less external-model MCP tool calls.

In src/adapters/cursor/protobuf-request.ts, external-tool-call parts enter pendingToolCalls at line 558, and flush() at line 536-537 emits every remaining entry as a native toolCall step with no result. External workers replay only assistant text; these dangling mcpToolCall steps can hydrate blobs, reach step completion without a result, and reject the turn with invalid_argument. Keep the image-path pairing, but drop unanswered external calls at flush time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/cursor/protobuf-request.ts` around lines 534 - 538, Update the
flush function to skip pending external-model MCP tool calls that have no
result, while preserving image-path pairing and existing handling for answered
calls. Do not emit result-less external entries through toolCallStep; only
append valid completed calls to current.steps.

Comment thread tests/cursor-blob.test.ts
Comment thread tests/cursor-request-builder.test.ts
@yansigit yansigit changed the title fix(cursor): SelectedImage vision and Desktop view_image promote fix(cursor): Add native image support for Cursor Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@github-actions[bot] The readiness gate is complete. I will review pull request #1228.

⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a76c091700

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/adapters/cursor/live-transport.ts Outdated
// JPEG soft-cap rewrite for attach + view_image tool-result data URLs before encode.
const rawMessages = await prepareCursorRawMessages(request.rawMessages);
const selectedImages = await resolveActiveCursorImages(rawMessages, signal);
const preparedRequest = { ...request, rawMessages, selectedImages };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild Cursor messages after image prep

When prepareCursorRawMessages() converts an active user/developer image part into text (for example an unsupported or corrupt image), only rawMessages is replaced here while request.messages still contains the pre-rewrite text that deliberately dropped image parts. activePromptText() then reads the stale message first, so the omission marker is lost; for an image-only turn this even falls through to resumeAction with no user text or SelectedImage. Recompute/sync the prepared messages from the rewritten raw messages before encoding.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

return {
parts: kept.map(entry => entry.part),
omittedOlder: Math.max(0, entries.length - kept.length),
promotedCallIds: new Set(kept.map(entry => entry.callId)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Omit only promoted images from mixed tool-result calls

When a single trailing tool result contains more than MAX_CURSOR_IMAGES images, kept contains only the newest images but this set records just the call id. The protobuf builder later treats that call id as fully promoted and replaces every image in that tool result with the SelectedImage marker, so older overflow images are neither promoted nor kept on MCP as intended. Track the promoted image parts/indices instead of the whole call id.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Comment on lines +588 to +592
export function cursorIsTrailingToolResultContinuation(
messages: readonly OcxMessage[] | undefined,
): boolean {
if (!messages?.length) return false;
return stripTrailingTransparentDeveloperMessages(messages).at(-1)?.role === "toolResult";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the continuation helper for retry gating

For view_image continuations followed by Desktop's trailing multi-agent developer message, this helper now correctly reports a tool-result continuation after stripping that suffix, but createCursorAdapter still uses the raw last-message role for its invalid-argument retry guard (src/adapters/cursor.ts:114). In that Desktop shape the guard is false, so a pre-output Cursor invalid_argument can be replayed in a fresh conversation even though tool-result resumes are explicitly excluded from retry; use this helper there too.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Comment on lines +397 to +401
return [create(McpToolResultContentItemSchema, {
content: {
case: "image",
value: create(McpImageContentSchema, {
data,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep old tool-result images out of replay turns

When a later user turn follows an earlier view_image, that prior tool result is no longer the active image turn, but this branch still decodes the historical data URL into McpImageContent while building retained conversation turns. The root replay budget only accounts for text because contentToText drops images, so several retained screenshots can silently blow the Cursor request/hydration payload even though SelectedImage is active-turn-only; gate image MCP content to the trailing active tool-result block or budget these bytes.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

}
const resolved = url.toLowerCase().startsWith("data:")
? decodeDataUrl(url)
: await fetchHttpsImageBytes(url, signal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Omit stale HTTPS images instead of aborting

If the active Cursor turn contains an HTTPS image URL that fails DNS, times out, returns a non-image content type, or otherwise cannot be fetched, this await propagates CursorImageError through resolveActiveCursorImages() and the live transport aborts the whole request before Cursor sees the text. Data URLs in the same path are safely converted to omission text when they cannot be prepared, so remote image failures should degrade the same way rather than fail an otherwise valid turn.

AGENTS.md reference: src/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

@yansigit

yansigit commented Aug 7, 2026

Copy link
Copy Markdown
Author

Addressed the high-signal automated review items:

  • Transparent developer suffix now requires <multi_agent_mode>…</multi_agent_mode> (other developer nudges stay opaque).
  • invalid_argument retry gating uses cursorIsTrailingToolResultContinuation so Desktop multi-agent trailing developers do not enable a fresh-conversation replay mid tool resume.
  • Oversized prepared images are omitted instead of throwing in resolveCursorImages.
  • Live transport rebuilds messages from prepared rawMessages so omission text is visible to activePromptText.

Verification: focused unit suite (157 pass) + typecheck; collab-on headless attach + view_image smokes both returned exact Grumpy captions.

@github-actions
github-actions Bot marked this pull request as draft August 7, 2026 17:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-site/src/content/docs/reference/configuration/providers.md`:
- Around line 275-278: Update the Vision section in the provider configuration
documentation to explicitly include the curated GLM entries from noVisionModels
as vision-sidecar users, or clarify that auto and composer-* are examples rather
than an exhaustive list. Keep the documented model behavior aligned with the
actual noVisionModels configuration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8e25a38a-1fb3-4959-80ba-33269f6ad9c0

📥 Commits

Reviewing files that changed from the base of the PR and between a76c091 and 47bbffb.

📒 Files selected for processing (8)
  • docs-site/src/content/docs/reference/configuration/providers.md
  • src/adapters/cursor.ts
  • src/adapters/cursor/images.ts
  • src/adapters/cursor/live-transport.ts
  • src/adapters/cursor/request-builder.ts
  • tests/cursor-adapter.test.ts
  • tests/cursor-images.test.ts
  • tests/cursor-request-builder.test.ts

Comment on lines +275 to +278
the curated `noVisionModels` list and use the vision describe sidecar instead. Trailing
`<multi_agent_mode>` developer injections (Codex Desktop collab guidance after `view_image`)
are transparent for SelectedImage promotion so the continuation still carries the image and
promote nudge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include GLM in the vision-sidecar description.

Line 282 identifies GLM as part of the curated noVisionModels set. Models in this set use the vision describe sidecar. The Vision section names only auto and composer-*, which can imply that GLM uses native SelectedImage vision.

Include the curated GLM entries, or state that auto and composer-* are examples.

Proposed fix
- the curated `noVisionModels` list and use the vision describe sidecar instead.
+ the curated `noVisionModels` list and use the vision describe sidecar instead. This also includes the curated GLM entries.

As per path instructions, user-facing documentation must stay synchronized with actual behavior.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
the curated `noVisionModels` list and use the vision describe sidecar instead. Trailing
`<multi_agent_mode>` developer injections (Codex Desktop collab guidance after `view_image`)
are transparent for SelectedImage promotion so the continuation still carries the image and
promote nudge.
the curated `noVisionModels` list and use the vision describe sidecar instead. This also includes the curated GLM entries. Trailing
`<multi_agent_mode>` developer injections (Codex Desktop collab guidance after `view_image`)
are transparent for SelectedImage promotion so the continuation still carries the image and
promote nudge.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/reference/configuration/providers.md` around lines
275 - 278, Update the Vision section in the provider configuration documentation
to explicitly include the curated GLM entries from noVisionModels as
vision-sidecar users, or clarify that auto and composer-* are examples rather
than an exhaustive list. Keep the documented model behavior aligned with the
actual noVisionModels configuration.

Source: Path instructions

@yansigit

yansigit commented Aug 7, 2026

Copy link
Copy Markdown
Author

Also landed the low-risk optional review cleanups:

  • JPEG dimension sniff skips standalone markers (0xD0–0xD9, 0x01) before SOF.
  • MCP tool-result image encode reuses decodeCursorImageDataUrl (decode-byte ceiling).
  • Live-transport prepare → resolve → SelectedImage wiring test (tool-call-only assistant omit was already covered).

Verification: focused unit suite + typecheck green.

yansigit and others added 10 commits August 7, 2026 12:01
Resolve active-turn images before prepare/encode, attach selectedContext on
the active UserMessage, omit image placeholders from text, and remove Cursor
from noVisionModels so native vision replaces the sidecar path.
Add focused unit tests for images.ts caps, data URL validation, HTTPS/SSRF
rejection, and active-turn selection. Extend cursor-blob coverage to assert
history/root UserMessages omit selectedContext while the active turn keeps
inline SelectedImage data.
Assert Cursor is absent from noVisionModels, advertises image through
modelInputModalities, keeps catalog hints sidecar-free, and skips the
vision sidecar when requests carry images.
Keep image-only user/developer turns in request-builder as empty-string
active messages, and choose userMessageAction when selectedImages are
present even if prompt text is empty. Add regression tests for first-turn
and follow-up image-only encodes.
External Cursor models flattened tool results to text via contentToText(),
which dropped image parts from view_image continuations. Route image-bearing
tool results through the native mcpToolCall/McpImageContent path while
keeping text-only external replay flattened.

Adds grok-4.5 wire regression coverage and a reusable vision harness test.
Prep before the 1 MiB hard cap, promote consecutive view_image tool
results with nudge text and MCP dedupe, fail-closed on undecodable
MIME, and restore curated Auto/Composer sidecar noVisionModels.

Co-authored-by: Cursor <cursoragent@cursor.com>
Treat trailing non-image developer injections as transparent for SelectedImage
resolution and tool-continuation gates so Codex Desktop collab guidance no
longer drops vision after view_image. Also enforce the JPEG soft-cap with edge
shrink and map Grok none/minimal to medium with the cursor- wire prefix.

Co-authored-by: Cursor <cursoragent@cursor.com>
Scope transparent developer suffixes to multi_agent_mode, reuse that
continuation helper for invalid_argument retry gating, omit oversized
prepared images instead of failing the turn, and rebuild text messages
after JPEG prep so omission markers reach activePromptText.

Co-authored-by: Cursor <cursoragent@cursor.com>
Skip standalone JPEG markers before SOF so SelectedImage dimensions stay
correct, reuse decodeCursorImageDataUrl for MCP tool-result encode size
limits, and cover the live-transport prepare→resolve→SelectedImage wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not merge this yet. The overall SelectedImage/SSRF approach looks reasonable, but there are several correctness/availability blockers in the current head.

Merge blockers

  1. HTTPS image failures can abort or erase the active user turn.
    prepareCursorRawMessages() only rewrites data: images. Remote images are resolved later. DNS failures, timeouts, non-image responses, etc. currently propagate and abort the whole request. Worse, if an HTTPS image fetch succeeds but prepareCursorImageForWire() later omits it, an image-only user turn can end up with selectedImages=[] and empty text, causing protobuf construction to select resumeAction. Cursor then receives no new user input. Remote failures should degrade consistently to an omission marker/text-only turn, never silently become resume.

  2. Historical view_image pixels are replayed after the active image turn.
    conversationTurns() still serializes old image-bearing tool results as McpImageContent. Those bytes are outside the normal root replay text budget, so accumulated screenshots can materially inflate the request/hydration payload and keep getting retransmitted to Cursor after they are no longer needed. Gate MCP image bytes to the active trailing tool-result block, or explicitly budget/omit historical image bytes.

  3. Result-less external-model MCP tool calls are emitted during replay.
    External assistant tool calls go into pendingToolCalls, but flush() emits every remaining entry through toolCallStep() even without a result. This conflicts with the surrounding external-model handling that intentionally avoids native MCP tool-call replay because those structures can cause invalid_argument. Unanswered/interrupted calls should be dropped for external models; completed image-call/result pairs still need to be preserved.

  4. Promotion overflow is tracked at call-ID granularity instead of image granularity.
    extractTrailingToolResultImagePromotion() keeps the newest MAX_CURSOR_IMAGES entries but returns only promotedCallIds. If one tool result contains more than 12 images, that call ID is marked promoted and protobuf omits all images from that result. The older overflow images are therefore neither promoted nor retained on MCP, despite the stated behavior. Track the promoted image parts/indices rather than the entire call ID.

  5. Image preprocessing is duplicated and has no aggregate deadline.
    The live path runs prepareCursorRawMessages() and then resolveActiveCursorImages(), causing active data: images to be decoded/re-encoded again. Historical data URLs are also prepared again on subsequent turns. HTTPS processing is serial with a per-image timeout, so 12 stalled images can delay stream creation for a very long time. Reuse prepared results and bound the entire image phase with one deadline/budget.

Security/resource hardening

  • The SSRF design itself looks good: HTTPS-only, destination checks, public DNS resolution, and pinned connection/SNI behavior. I did not find an obvious auth bypass, path traversal, command injection, or network-pivot issue in this diff.
  • The 16 MiB encoded-byte ceiling does not bound decoded pixel memory. prepareCursorImageForWire() forces a full Bun.Image decode before resize/dimension limiting, so a highly compressed image with huge dimensions can still create a decompression/memory-DoS path. Add max dimensions / total-pixel limits before expensive processing where possible.
  • Buffer.from(..., "base64") is lenient, so malformed base64 is not reliably rejected by the current try/catch. Also, accepting any undecodable <=64-byte payload for PNG/JPEG/GIF/WebP is a production validation bypass added for unit-test stubs. Tests should use real minimal images instead; production should stay fail-closed.

Required regression coverage

  • image-only HTTPS unsupported/corrupt image must produce a user action with an omission marker, never resumeAction
  • failed HTTPS image fetch plus valid text must continue text-only
  • one tool result with >12 images must promote exactly 12 and preserve overflow correctly
  • external assistant tool call without a result must not produce a result-less historical mcpToolCall
  • prior view_image followed by a new user turn must not replay historical McpImageContent
  • malformed base64 must be rejected strictly; remove the <=64-byte production bypass
  • add pixel/dimension limits and a bounded aggregate image-processing deadline

Some existing bot comments are already stale on this head (JPEG standalone-marker handling, shared capped data-URL decoder use, message rebuild after prep, retry continuation helper), so those should not be fixed twice.

Please address the blockers above and get the PR checks green before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants