Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- [EE] Improved Ask Sourcebot prompt caching by splitting static and dynamic prompt sections and advancing cache breakpoints after every agent step instead of only after each message. [#1366](https://github.com/sourcebot-dev/sourcebot/pull/1366)
- Refactored Ask Sourcebot user message text extraction into a shared helper that robustly handles non-text message parts. [#1371](https://github.com/sourcebot-dev/sourcebot/pull/1371)
- [EE] Pinned Ask Sourcebot file citations to the commit they were sourced at so their content and line ranges stay aligned with the code as it was when the answer was generated. [#1397](https://github.com/sourcebot-dev/sourcebot/pull/1397)

### Added
- Added per-step token cost tracking and estimated tool call token usage to Ask Sourcebot chat history. [#1353](https://github.com/sourcebot-dev/sourcebot/pull/1353)
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export const getRepoInfoByName = async (repoName: string) => sew(() =>
externalWebUrl: repo.webUrl ?? undefined,
imageUrl: repo.imageUrl ?? undefined,
indexedAt: repo.indexedAt ?? undefined,
indexedCommitHash: repo.indexedCommitHash ?? undefined,
}
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,31 @@ const ReferencedFileSourceListItemContainerComponent = ({
}: ReferencedFileSourceListItemContainerProps) => {
const fileName = fileSource.path.split('/').pop() ?? fileSource.path;

// Prefer the pinned commit SHA so the file renders as it was when answered,
// with line ranges still aligned. Falls back to the symbolic ref.
const fetchRef = fileSource.commitSha ?? fileSource.revision;

const { data, isLoading, isError, error } = useQuery({
queryKey: ['fileSource', fileSource.path, fileSource.repo, fileSource.revision],
queryFn: () => unwrapServiceError(getFileSource({
path: fileSource.path,
repo: fileSource.repo,
ref: fileSource.revision,
})),
queryKey: ['fileSource', fileSource.path, fileSource.repo, fetchRef, fileSource.revision],
queryFn: async () => {
const pinned = await getFileSource({
path: fileSource.path,
repo: fileSource.repo,
ref: fetchRef,
});

// The pinned commit can disappear (e.g. a force-push + GC prunes it).
// Fall back once to the symbolic ref so the file still renders.
if (isServiceError(pinned) && fetchRef !== fileSource.revision) {
return unwrapServiceError(getFileSource({
path: fileSource.path,
repo: fileSource.repo,
ref: fileSource.revision,
}));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

return unwrapServiceError(Promise.resolve(pinned));
},
staleTime: Infinity,
});

Expand Down
5 changes: 3 additions & 2 deletions packages/web/src/features/chat/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,11 @@ export const convertLLMOutputToPortableMarkdown = (text: string, baseUrl: string
end: { lineNumber: parseInt(endLine || startLine) },
} : undefined;

// Construct full browse URL
// Prefer the pinned commit SHA so copied links resolve to the code
// as it was when answered; fall back to the symbolic ref.
const browsePath = getBrowsePath({
repoName: repo,
revisionName: source.revision,
revisionName: source.commitSha ?? source.revision,
path: fileName,
pathType: 'blob',
highlightRange,
Expand Down
10 changes: 10 additions & 0 deletions packages/web/src/features/git/getFileSourceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ export const getFileSourceForRepo = async (
return unexpectedError(errorMessage);
}

// Resolve the symbolic ref to a concrete commit SHA so callers can pin a
// citation to the exact code read. `^{commit}` peels annotated tags.
let commitSha: string | undefined;
try {
commitSha = (await git.raw(['rev-parse', `${gitRef}^{commit}`])).trim();
} catch {
// Leave unpinned if the ref can't be resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

let gitattributesContent: string | undefined;
try {
gitattributesContent = await git.raw(['show', `${gitRef}:.gitattributes`]);
Expand Down Expand Up @@ -97,6 +106,7 @@ export const getFileSourceForRepo = async (
repoExternalWebUrl: repo.webUrl ?? undefined,
webUrl,
externalWebUrl,
commitSha,
} satisfies FileSourceResponse;
});

Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/features/git/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const fileSourceResponseSchema = z.object({
repoExternalWebUrl: z.string().optional(),
webUrl: z.string(),
externalWebUrl: z.string().optional(),
// The concrete commit SHA that `ref` resolved to. Undefined if unresolvable.
commitSha: z.string().optional(),
Comment thread
whoisthey marked this conversation as resolved.
});

export const getDiffRequestSchema = z.object({
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/features/search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const repositoryInfoSchema = z.object({
name: z.string(),
displayName: z.string().optional(),
webUrl: z.string().optional(),
// The commit Zoekt last indexed; lets callers pin a result to that commit.
indexedCommitHash: z.string().optional(),
});
export type RepositoryInfo = z.infer<typeof repositoryInfoSchema>;

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/search/zoektSearcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ const transformZoektSearchResponse = async (response: ZoektGrpcSearchResponse, r
name: repo.name,
displayName: repo.displayName ?? undefined,
webUrl: repo.webUrl ?? undefined,
indexedCommitHash: repo.indexedCommitHash ?? undefined,
})),
stats,
}
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/features/tools/findSymbolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const findSymbolDefinitionsDefinition: ToolDefinition<
fileName: file.fileName,
repo: file.repository,
revision,
commitSha: repoInfoResult.indexedCommitHash,
})),
};

Expand Down Expand Up @@ -105,6 +106,7 @@ export const findSymbolDefinitionsDefinition: ToolDefinition<
path: file.fileName,
name: file.fileName.split('/').pop() ?? file.fileName,
revision: file.revision,
commitSha: file.commitSha,
}));

return {
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/features/tools/findSymbolReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type FindSymbolFile = {
fileName: string;
repo: string;
revision: string;
commitSha?: string;
};

export type FindSymbolReferencesMetadata = {
Expand Down Expand Up @@ -82,6 +83,7 @@ export const findSymbolReferencesDefinition: ToolDefinition<
fileName: file.fileName,
repo: file.repository,
revision,
commitSha: repoInfoResult.indexedCommitHash,
})),
};

Expand Down Expand Up @@ -115,6 +117,7 @@ export const findSymbolReferencesDefinition: ToolDefinition<
path: file.fileName,
name: file.fileName.split('/').pop() ?? file.fileName,
revision: file.revision,
commitSha: file.commitSha,
}));

return {
Expand Down
8 changes: 8 additions & 0 deletions packages/web/src/features/tools/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type GlobFile = {
name: string;
repo: string;
revision: string;
commitSha?: string;
};

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

// Matches reflect the commit Zoekt last indexed; pin each to it.
const indexedCommitShaByRepo = new Map(
response.repositoryInfo.map((info) => [info.name, info.indexedCommitHash]),
);

const files = response.files.map((file) => ({
path: file.fileName.text,
name: file.fileName.text.split('/').pop() ?? file.fileName.text,
repo: file.repository,
revision: ref ?? 'HEAD',
commitSha: indexedCommitShaByRepo.get(file.repository),
} satisfies GlobFile));

const repoInfoMap = Object.fromEntries(
Expand Down Expand Up @@ -190,6 +197,7 @@ export const globDefinition: ToolDefinition<'glob', typeof globShape, GlobMetada
path: file.path,
name: file.name,
revision: file.revision,
commitSha: file.commitSha,
}));

return {
Expand Down
8 changes: 8 additions & 0 deletions packages/web/src/features/tools/grep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type GrepFile = {
name: string;
repo: string;
revision: string;
commitSha?: string;
};

export type GrepRepoInfo = {
Expand Down Expand Up @@ -132,11 +133,17 @@ export const grepDefinition: ToolDefinition<'grep', typeof grepShape, GrepMetada
throw new Error(response.message);
}

// Matches reflect the commit Zoekt last indexed; pin each to it.
const indexedCommitShaByRepo = new Map(
response.repositoryInfo.map((info) => [info.name, info.indexedCommitHash]),
);

const files = response.files.map((file) => ({
path: file.fileName.text,
name: file.fileName.text.split('/').pop() ?? file.fileName.text,
repo: file.repository,
revision: ref ?? 'HEAD',
commitSha: indexedCommitShaByRepo.get(file.repository),
} satisfies GrepFile));

const repoInfoMap = Object.fromEntries(
Expand Down Expand Up @@ -241,6 +248,7 @@ export const grepDefinition: ToolDefinition<'grep', typeof grepShape, GrepMetada
path: file.path,
name: file.path.split('/').pop() ?? file.path,
revision: file.revision,
commitSha: file.commitSha,
}));

return {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/features/tools/readFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export const readFileDefinition: ToolDefinition<"read_file", typeof readFileShap
path: fileSource.path,
name: fileSource.path.split('/').pop() ?? fileSource.path,
revision: ref,
commitSha: fileSource.commitSha,
}],
};
},
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/features/tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const fileSourceSchema = z.object({
path: z.string(),
name: z.string(),
revision: z.string(),
// The concrete commit SHA the content was served at. Optional for
// backwards-compatibility with sources persisted before pinning existed.
commitSha: z.string().optional(),
});
export type FileSource = z.infer<typeof fileSourceSchema>;

Expand Down
Loading