[Bug]: Cursor Origin single-line review threads lose their line anchor #35895
Send to my agentSummary#
Single-line review comments from Cursor Origin can lose their line location when T3 Code loads the review activity. The adapter treats Origin's zero end-line sentinel as the comment's location, then returns no line. An isolated reproduction through the provider confirms the failure while a range comment retains its location; single-line comments should retain their start line.
Before submitting#
- Related issues: no duplicate found in open and closed issues and pull requests for Cursor Origin, cursor-origin, line anchors, or review threads.
- Removed credentials, tokens, private paths, and sensitive data.
Area#
apps/server
Observed behavior#
Calling the Origin provider's getChangeRequestActivity with a documented single-line comment thread (startLine: 12, endLine: 0) returns reviewThreads[0].line: null. A range thread (startLine: 12, endLine: 14) returns line 14. This was reproduced through the unmodified provider with an injected OriginCli that decodes fixture responses through the production schemas; it makes no network request to a forge and executes no Origin CLI command.
Reproduction#
- Check out the commit in Version.
- Save the script below as
repro.shand runsh repro.sh /path/to/checkout. - Observe the successful range control and the missing single-line anchor.
The recorded run reproduced the failure deterministically with the two fixed inputs. Prerequisites are Node.js with TypeScript stripping, npm, and registry access for the repository's pinned Effect packages. The script creates its inputs and dependencies under mktemp -d, isolates HOME/XDG/T3 state, loads the actual checkout modules through a package-export resolver, and removes its scratch directory without changing the checkout. Exit 0 means expected behavior, exit 1 with RESULT: defect reproduced means the missing anchor, and exit 2 means an inconclusive setup or failed control.
Reproduction script: repro.sh
#!/bin/sh
set -u
repo=$(cd "$1" && pwd) || { echo 'RESULT: inconclusive'; exit 2; }
scratch=$(mktemp -d) || { echo 'RESULT: inconclusive'; exit 2; }
trap 'rm -rf "$scratch"' EXIT HUP INT TERM
export HOME="$scratch/home" TMPDIR="$scratch/tmp"
export XDG_CONFIG_HOME="$scratch/config" XDG_DATA_HOME="$scratch/data" XDG_STATE_HOME="$scratch/state" T3CODE_HOME="$scratch/t3"
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
mkdir -p "$HOME" "$TMPDIR" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$T3CODE_HOME"
node --version
npm --version
if ! npm install --prefix "$scratch" --ignore-scripts --no-audit --no-fund --no-package-lock --save-exact effect@4.1.3 @effect/platform-node@4.1.2 >"$scratch/install.log" 2>&1; then
cat "$scratch/install.log"
echo 'RESULT: inconclusive'
exit 2
fi
node -e 'for (const n of ["effect", "@effect/platform-node"]) console.log(n + " " + require(process.argv[1] + "/node_modules/" + n + "/package.json").version)' "$scratch"
cat >"$scratch/probe.mjs" <<'JS'
import { registerHooks, createRequire } from 'node:module';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const repo = resolve(process.argv[2]);
const runtime = import.meta.url;
registerHooks({ resolve(specifier, context, nextResolve) {
if (specifier.startsWith('@t3tools/')) {
const [name, ...rest] = specifier.slice('@t3tools/'.length).split('/');
const folder = `${repo}/packages/${name}`;
const manifest = JSON.parse(readFileSync(`${folder}/package.json`, 'utf8'));
const key = rest.length ? `./${rest.join('/')}` : '.';
const target = manifest.exports[key];
return nextResolve(pathToFileURL(resolve(folder, typeof target === 'string' ? target : target.import)).href, context);
}
if (specifier === 'effect' || specifier.startsWith('effect/') || specifier.startsWith('@effect/')) return nextResolve(specifier, {...context, parentURL: runtime});
return nextResolve(specifier, context);
}});
const Effect = await import('effect/Effect');
const Schema = await import('effect/Schema');
const { make } = await import(pathToFileURL(`${repo}/packages/source-control-cursor-origin/src/server/OriginPullRequestProvider.ts`).href);
const { OriginCli } = await import(pathToFileURL(`${repo}/packages/source-control-cursor-origin/src/server/OriginCli.ts`).href);
const now = '2026-01-01T00:00:00.000Z';
const comments = [
{id:'fixture-single', thread:{id:'thread-single',path:'file.ts',side:'right',startLine:12,endLine:0},body:'single',createdAt:now},
{id:'fixture-range', thread:{id:'thread-range',path:'file.ts',side:'right',startLine:12,endLine:14},body:'range',createdAt:now},
];
const cli = OriginCli.of({
api: (input, schema) => Schema.decodeUnknownEffect(schema)(input.path.includes('/comments?') ? {comments} : input.path.includes('/reviews?') ? {reviews:[]} : input.path.includes('/commits?') ? {commits:[]} : {number:1,title:'Fixture',webUrl:'https://cursor.com/codebase/fixture/repo/pull/1',head:{ref:'feature',sha:'a'.repeat(40)},base:{ref:'main',sha:'b'.repeat(40)}}),
execute: () => Effect.succeed('{}'), viewer: () => Effect.succeed('fixture'), personalNamespace:()=>Effect.succeed('fixture'),resolveRepository:()=>Effect.succeed('fixture/repo'),
});
await Effect.runPromise(Effect.scoped(Effect.gen(function*(){
const provider = yield* make;
const result = yield* provider.getChangeRequestActivity({cwd:process.cwd(),repository:'fixture/repo',number:1});
const single = result.reviewThreads.find(x => x.id === 'thread-single');
const range = result.reviewThreads.find(x => x.id === 'thread-range');
console.log(`range control: expected 14, actual ${range?.line}`);
console.log(`single-line anchor: expected 12, actual ${single?.line}`);
if (range?.line !== 14 || !single) { console.log('RESULT: inconclusive'); process.exitCode = 2; }
else if (single.line === 12) { console.log('RESULT: expected behavior'); process.exitCode = 0; }
else { console.log('RESULT: defect reproduced'); process.exitCode = 1; }
}).pipe(Effect.provideService(OriginCli, cli))));
JS
node "$scratch/probe.mjs" "$repo"
status=$?
if [ "$status" -ne 0 ] && [ "$status" -ne 1 ]; then
echo 'RESULT: inconclusive'
exit 2
fi
exit "$status"
Expected behavior#
A single-line thread with startLine: 12, endLine: 0 retains line 12 in the provider result, while a true file-level thread with no positive start line may have line: null. Cursor's Origin OpenAPI document, CommentThread.startLine and CommentThread.endLine, defines the start as the anchor's first line and zero end as a single-line or no-range sentinel. The fetched OpenAPI 3.1.0 / v1alpha1 artifact has SHA-256 1e53a3e45bc1f52cb0b22ff3b2e57cc41849b4471c85725d75e59a68ef6aa610.
Impact and scope#
Impact: P2 - A valid inline review thread loses its location in the shared provider result, so clients cannot place that thread on its original line from that result.
The comment body and file path remain available. The demonstrated input is a single-line thread whose response includes zero endLine; omitted endLine and positive range endpoints do not take this failing path. NOT_ENOUGH_INFO: live-server frequency was not measured; a real response capture would establish how frequently the service includes this default field. The Origin website is a possible way to inspect the anchor, but that workaround was not tested.
Version#
T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93.
Runtime and platform#
Darwin arm64; Node.js v26.12.1; npm 11.20.0 in the isolated script; Effect 4.1.3; @effect/platform-node 4.1.2. No installed Origin CLI or account is required by this fixture.
Interface#
Library: OriginPullRequestProvider.getChangeRequestActivity, used by the shared pull-request activity service.
Earliest failure stage#
Core processing
Relevant output or evidence#
Actual output from the script above, exit 1:
v26.12.1
11.20.0
effect 4.1.3
@effect/platform-node 4.1.2
range control: expected 14, actual 14
single-line anchor: expected 12, actual null
RESULT: defect reproduced
The packages/source-control-cursor-origin/src/server/OriginPullRequestProvider.ts#L130-L147 source selects endLine ?? startLine, so a zero end line wins and is then converted to null. The script contains only synthetic repository, comment and thread identities and no account credentials or private paths.
tanstack-source added labelsbug
github-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:denounced
Sign in to join this conversation.