diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ace9904..7c156846a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `hono` to `^4.12.18` to address CVE-2026-44455, CVE-2026-44456, CVE-2026-44457, CVE-2026-44458. [#1186](https://github.com/sourcebot-dev/sourcebot/pull/1186) - Upgraded `ip-address` to `^10.2.0` to address CVE-2026-42338. [#1189](https://github.com/sourcebot-dev/sourcebot/pull/1189) - Upgraded `fast-xml-builder` to `^1.2.0` to address CVE-2026-44664, CVE-2026-44665. [#1184](https://github.com/sourcebot-dev/sourcebot/pull/1184) +- Fixed file citations from the `get_diff` tool not being reliably citable in chat answers. [#1205](https://github.com/sourcebot-dev/sourcebot/pull/1205) ### Changed - Reduced the log verbosity of the worker by changing various log messages from info to debug. [#1179](https://github.com/sourcebot-dev/sourcebot/pull/1179) diff --git a/packages/web/src/features/chat/components/chatThread/answerCard.tsx b/packages/web/src/features/chat/components/chatThread/answerCard.tsx index 3f0a62a0d..8775c69be 100644 --- a/packages/web/src/features/chat/components/chatThread/answerCard.tsx +++ b/packages/web/src/features/chat/components/chatThread/answerCard.tsx @@ -18,12 +18,14 @@ import useCaptureEvent from "@/hooks/useCaptureEvent"; import { LangfuseWeb } from "langfuse"; import { env } from "@sourcebot/shared/client"; import isEqual from "fast-deep-equal/react"; +import { FileSource } from "../../types"; interface AnswerCardProps { answerText: string; messageId: string; chatId: string; traceId?: string; + sources: FileSource[]; } const langfuseWeb = env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY ? new LangfuseWeb({ @@ -36,6 +38,7 @@ const AnswerCardComponent = forwardRef(({ messageId, chatId, traceId, + sources, }, forwardedRef) => { const markdownRendererRef = useRef(null); // eslint-disable-next-line react-hooks/refs -- ref.current is passed to a custom hook, not used directly in render output @@ -53,14 +56,14 @@ const AnswerCardComponent = forwardRef(({ const onCopyAnswer = useCallback(() => { const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; - const markdownText = convertLLMOutputToPortableMarkdown(answerText, baseUrl); + const markdownText = convertLLMOutputToPortableMarkdown(answerText, baseUrl, sources); navigator.clipboard.writeText(markdownText); toast({ description: "✅ Copied to clipboard", }); captureEvent('wa_chat_copy_answer_pressed', { chatId }); return true; - }, [answerText, chatId, captureEvent, toast]); + }, [answerText, sources, chatId, captureEvent, toast]); const onFeedback = useCallback(async (feedbackType: 'like' | 'dislike') => { setIsSubmittingFeedback(true); diff --git a/packages/web/src/features/chat/components/chatThread/chatThreadListItem.tsx b/packages/web/src/features/chat/components/chatThread/chatThreadListItem.tsx index f84abe97b..0cbd4b264 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThreadListItem.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThreadListItem.tsx @@ -371,6 +371,7 @@ const ChatThreadListItemComponent = forwardRef ) : !isStreaming && (

Error: No answer response was provided

diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index b4f80bec2..38dd784fd 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -244,13 +244,23 @@ export const createFileReference = ({ repo, path, startLine, endLine }: { repo: * Markdown format. Practically, this means converting references into Markdown * links and removing the answer tag. */ -export const convertLLMOutputToPortableMarkdown = (text: string, baseUrl: string): string => { +export const convertLLMOutputToPortableMarkdown = (text: string, baseUrl: string, sources: FileSource[]): string => { return text .replace(ANSWER_TAG, '') .replace(FILE_REFERENCE_REGEX, (_, repo, fileName, startLine, endLine) => { - const displayName = fileName.split('/').pop() || fileName; + const reference = createFileReference({ + repo, + path: fileName, + startLine, + endLine + }); + + const source = tryResolveFileReference(reference, sources); + if (!source) { + return fileName; + } - let linkText = displayName; + let linkText = source.name; if (startLine) { if (endLine && startLine !== endLine) { linkText += `:${startLine}-${endLine}`; @@ -268,6 +278,7 @@ export const convertLLMOutputToPortableMarkdown = (text: string, baseUrl: string // Construct full browse URL const browsePath = getBrowsePath({ repoName: repo, + revisionName: source.revision, path: fileName, pathType: 'blob', highlightRange, diff --git a/packages/web/src/features/mcp/askCodebase.ts b/packages/web/src/features/mcp/askCodebase.ts index 6451e680f..810010bcc 100644 --- a/packages/web/src/features/mcp/askCodebase.ts +++ b/packages/web/src/features/mcp/askCodebase.ts @@ -196,8 +196,15 @@ export const askCodebase = (params: AskCodebaseParams): Promise + message.parts + .filter((part) => part.type === 'data-source') + .map((part) => part.data) + .filter((source) => source.type === 'file') + ); + const baseUrl = env.AUTH_URL; - const portableAnswer = convertLLMOutputToPortableMarkdown(answerText, baseUrl); + const portableAnswer = convertLLMOutputToPortableMarkdown(answerText, baseUrl, fileSources); const chatUrl = `${baseUrl}/chat/${chat.id}`; logger.debug(`Completed blocking agent for chat ${chat.id}`, { chatId: chat.id }); diff --git a/packages/web/src/features/tools/getDiff.ts b/packages/web/src/features/tools/getDiff.ts index f3a3cf77e..ae1048e54 100644 --- a/packages/web/src/features/tools/getDiff.ts +++ b/packages/web/src/features/tools/getDiff.ts @@ -4,7 +4,7 @@ import { isServiceError } from '@/lib/utils'; import description from './getDiff.txt'; import { formatDiffAsGitDiff } from './utils'; import { logger } from './logger'; -import { ToolDefinition } from './types'; +import { Source, ToolDefinition } from './types'; import { CodeHostType } from '@sourcebot/db'; import { getRepoInfoByName } from '@/actions'; @@ -49,6 +49,48 @@ export const getDiffDefinition: ToolDefinition<'get_diff', typeof getDiffRequest const gitDiffOutput = formatDiffAsGitDiff(response); + const sources: Source[] = response.files.flatMap((file) => { + // Deleted: only the base side exists. + if (file.newPath === null) { + return file.oldPath ? [{ + type: 'file' as const, + repo: repoInfo.name, + path: file.oldPath, + name: file.oldPath.split('/').pop() ?? file.oldPath, + revision: base, + }] : []; + } + + // Renamed: emit both sides since they have distinct paths. + if (file.oldPath && file.oldPath !== file.newPath) { + return [ + { + type: 'file' as const, + repo: repoInfo.name, + path: file.oldPath, + name: file.oldPath.split('/').pop() ?? file.oldPath, + revision: base, + }, + { + type: 'file' as const, + repo: repoInfo.name, + path: file.newPath, + name: file.newPath.split('/').pop() ?? file.newPath, + revision: head, + }, + ]; + } + + // Added or modified: only the head side is citable. + return [{ + type: 'file' as const, + repo: repoInfo.name, + path: file.newPath, + name: file.newPath.split('/').pop() ?? file.newPath, + revision: head, + }]; + }); + return { output: gitDiffOutput, metadata: { @@ -58,6 +100,7 @@ export const getDiffDefinition: ToolDefinition<'get_diff', typeof getDiffRequest base, head, }, + sources, }; }, };