Skip to content

Commit 55d30e1

Browse files
feat(web): pin Ask citations to a resolved commit SHA
Resolve and persist the concrete commit SHA each file citation was sourced at (git rev-parse for read_file; the repo's indexed commit for grep, glob, and symbol search), and fetch the evidence panel + build copied links at that SHA. This keeps a citation's content and line ranges aligned with the code as it was when the answer was generated, instead of drifting against a moving HEAD. commitSha is optional, so pre-pinning chats fall back to the symbolic ref. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f7f0fef commit 55d30e1

13 files changed

Lines changed: 50 additions & 4 deletions

File tree

packages/web/src/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ export const getRepoInfoByName = async (repoName: string) => sew(() =>
372372
externalWebUrl: repo.webUrl ?? undefined,
373373
imageUrl: repo.imageUrl ?? undefined,
374374
indexedAt: repo.indexedAt ?? undefined,
375+
indexedCommitHash: repo.indexedCommitHash ?? undefined,
375376
}
376377
}));
377378

packages/web/src/ee/features/chat/components/chatThread/referencedFileSourceListItemContainer.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,16 @@ const ReferencedFileSourceListItemContainerComponent = ({
3838
}: ReferencedFileSourceListItemContainerProps) => {
3939
const fileName = fileSource.path.split('/').pop() ?? fileSource.path;
4040

41+
// Prefer the pinned commit SHA so the file renders as it was when answered,
42+
// with line ranges still aligned. Falls back to the symbolic ref.
43+
const fetchRef = fileSource.commitSha ?? fileSource.revision;
44+
4145
const { data, isLoading, isError, error } = useQuery({
42-
queryKey: ['fileSource', fileSource.path, fileSource.repo, fileSource.revision],
46+
queryKey: ['fileSource', fileSource.path, fileSource.repo, fetchRef],
4347
queryFn: () => unwrapServiceError(getFileSource({
4448
path: fileSource.path,
4549
repo: fileSource.repo,
46-
ref: fileSource.revision,
50+
ref: fetchRef,
4751
})),
4852
staleTime: Infinity,
4953
});

packages/web/src/features/chat/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,11 @@ export const convertLLMOutputToPortableMarkdown = (text: string, baseUrl: string
287287
end: { lineNumber: parseInt(endLine || startLine) },
288288
} : undefined;
289289

290-
// Construct full browse URL
290+
// Prefer the pinned commit SHA so copied links resolve to the code
291+
// as it was when answered; fall back to the symbolic ref.
291292
const browsePath = getBrowsePath({
292293
repoName: repo,
293-
revisionName: source.revision,
294+
revisionName: source.commitSha ?? source.revision,
294295
path: fileName,
295296
pathType: 'blob',
296297
highlightRange,

packages/web/src/features/git/getFileSourceApi.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ export const getFileSourceForRepo = async (
6161
return unexpectedError(errorMessage);
6262
}
6363

64+
// Resolve the symbolic ref to a concrete commit SHA so callers can pin a
65+
// citation to the exact code read. `^{commit}` peels annotated tags.
66+
let commitSha: string | undefined;
67+
try {
68+
commitSha = (await git.raw(['rev-parse', `${gitRef}^{commit}`])).trim();
69+
} catch {
70+
// Leave unpinned if the ref can't be resolved.
71+
}
72+
6473
let gitattributesContent: string | undefined;
6574
try {
6675
gitattributesContent = await git.raw(['show', `${gitRef}:.gitattributes`]);
@@ -97,6 +106,7 @@ export const getFileSourceForRepo = async (
97106
repoExternalWebUrl: repo.webUrl ?? undefined,
98107
webUrl,
99108
externalWebUrl,
109+
commitSha,
100110
} satisfies FileSourceResponse;
101111
});
102112

packages/web/src/features/git/schemas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const fileSourceResponseSchema = z.object({
3535
repoExternalWebUrl: z.string().optional(),
3636
webUrl: z.string(),
3737
externalWebUrl: z.string().optional(),
38+
// The concrete commit SHA that `ref` resolved to. Undefined if unresolvable.
39+
commitSha: z.string().optional(),
3840
});
3941

4042
export const getDiffRequestSchema = z.object({

packages/web/src/features/search/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const repositoryInfoSchema = z.object({
2727
name: z.string(),
2828
displayName: z.string().optional(),
2929
webUrl: z.string().optional(),
30+
// The commit Zoekt last indexed; lets callers pin a result to that commit.
31+
indexedCommitHash: z.string().optional(),
3032
});
3133
export type RepositoryInfo = z.infer<typeof repositoryInfoSchema>;
3234

packages/web/src/features/search/zoektSearcher.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ const transformZoektSearchResponse = async (response: ZoektGrpcSearchResponse, r
511511
name: repo.name,
512512
displayName: repo.displayName ?? undefined,
513513
webUrl: repo.webUrl ?? undefined,
514+
indexedCommitHash: repo.indexedCommitHash ?? undefined,
514515
})),
515516
stats,
516517
}

packages/web/src/features/tools/findSymbolDefinitions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const findSymbolDefinitionsDefinition: ToolDefinition<
7272
fileName: file.fileName,
7373
repo: file.repository,
7474
revision,
75+
commitSha: repoInfoResult.indexedCommitHash,
7576
})),
7677
};
7778

@@ -105,6 +106,7 @@ export const findSymbolDefinitionsDefinition: ToolDefinition<
105106
path: file.fileName,
106107
name: file.fileName.split('/').pop() ?? file.fileName,
107108
revision: file.revision,
109+
commitSha: file.commitSha,
108110
}));
109111

110112
return {

packages/web/src/features/tools/findSymbolReferences.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type FindSymbolFile = {
2525
fileName: string;
2626
repo: string;
2727
revision: string;
28+
commitSha?: string;
2829
};
2930

3031
export type FindSymbolReferencesMetadata = {
@@ -82,6 +83,7 @@ export const findSymbolReferencesDefinition: ToolDefinition<
8283
fileName: file.fileName,
8384
repo: file.repository,
8485
revision,
86+
commitSha: repoInfoResult.indexedCommitHash,
8587
})),
8688
};
8789

@@ -115,6 +117,7 @@ export const findSymbolReferencesDefinition: ToolDefinition<
115117
path: file.fileName,
116118
name: file.fileName.split('/').pop() ?? file.fileName,
117119
revision: file.revision,
120+
commitSha: file.commitSha,
118121
}));
119122

120123
return {

packages/web/src/features/tools/glob.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export type GlobFile = {
4444
name: string;
4545
repo: string;
4646
revision: string;
47+
commitSha?: string;
4748
};
4849

4950
export type GlobRepoInfo = {
@@ -113,11 +114,17 @@ export const globDefinition: ToolDefinition<'glob', typeof globShape, GlobMetada
113114
throw new Error(response.message);
114115
}
115116

117+
// Matches reflect the commit Zoekt last indexed; pin each to it.
118+
const indexedCommitShaByRepo = new Map(
119+
response.repositoryInfo.map((info) => [info.name, info.indexedCommitHash]),
120+
);
121+
116122
const files = response.files.map((file) => ({
117123
path: file.fileName.text,
118124
name: file.fileName.text.split('/').pop() ?? file.fileName.text,
119125
repo: file.repository,
120126
revision: ref ?? 'HEAD',
127+
commitSha: indexedCommitShaByRepo.get(file.repository),
121128
} satisfies GlobFile));
122129

123130
const repoInfoMap = Object.fromEntries(
@@ -190,6 +197,7 @@ export const globDefinition: ToolDefinition<'glob', typeof globShape, GlobMetada
190197
path: file.path,
191198
name: file.name,
192199
revision: file.revision,
200+
commitSha: file.commitSha,
193201
}));
194202

195203
return {

0 commit comments

Comments
 (0)