diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e8db874..893a427e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed issue where opening GitLab file links would result in a 404. [#846](https://github.com/sourcebot-dev/sourcebot/pull/846) +- Fixed issue where file references in copied chat answers were relative paths instead of full browse URLs. [#847](https://github.com/sourcebot-dev/sourcebot/pull/847) ## [4.10.24] - 2026-02-03 diff --git a/packages/web/src/app/api/(server)/chat/blocking/route.ts b/packages/web/src/app/api/(server)/chat/blocking/route.ts index ec114d394..7ef4939f9 100644 --- a/packages/web/src/app/api/(server)/chat/blocking/route.ts +++ b/packages/web/src/app/api/(server)/chat/blocking/route.ts @@ -182,11 +182,11 @@ export const POST = apiHandler(async (request: NextRequest) => { : undefined; const answerText = answerPart?.text ?? ''; - // Convert to portable markdown (replaces @file: references with markdown links) - const portableAnswer = convertLLMOutputToPortableMarkdown(answerText); - - // Build the chat URL + // Build the base URL and chat URL const baseUrl = env.AUTH_URL; + + // Convert to portable markdown (replaces @file: references with markdown links) + const portableAnswer = convertLLMOutputToPortableMarkdown(answerText, baseUrl); const chatUrl = `${baseUrl}/${org.domain}/chat/${chat.id}`; logger.debug(`Completed blocking agent for chat ${chat.id}`, { diff --git a/packages/web/src/features/chat/components/chatThread/answerCard.tsx b/packages/web/src/features/chat/components/chatThread/answerCard.tsx index d420ce5e1..f4402ebfa 100644 --- a/packages/web/src/features/chat/components/chatThread/answerCard.tsx +++ b/packages/web/src/features/chat/components/chatThread/answerCard.tsx @@ -51,7 +51,8 @@ const AnswerCardComponent = forwardRef(({ ); const onCopyAnswer = useCallback(() => { - const markdownText = convertLLMOutputToPortableMarkdown(answerText); + const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; + const markdownText = convertLLMOutputToPortableMarkdown(answerText, baseUrl); navigator.clipboard.writeText(markdownText); toast({ description: "✅ Copied to clipboard", diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index afc00ac80..ca412618e 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -1,6 +1,8 @@ import { CreateUIMessage, TextUIPart, UIMessagePart } from "ai"; import { Descendant, Editor, Point, Range, Transforms } from "slate"; import { ANSWER_TAG, FILE_REFERENCE_PREFIX, FILE_REFERENCE_REGEX } from "./constants"; +import { getBrowsePath, BrowseHighlightRange } from "@/app/[domain]/browse/hooks/utils"; +import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { CustomEditor, CustomText, @@ -243,10 +245,10 @@ 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): string => { +export const convertLLMOutputToPortableMarkdown = (text: string, baseUrl: string): string => { return text .replace(ANSWER_TAG, '') - .replace(FILE_REFERENCE_REGEX, (_, _repo, fileName, startLine, endLine) => { + .replace(FILE_REFERENCE_REGEX, (_, repo, fileName, startLine, endLine) => { const displayName = fileName.split('/').pop() || fileName; let linkText = displayName; @@ -258,7 +260,23 @@ export const convertLLMOutputToPortableMarkdown = (text: string): string => { } } - return `[${linkText}](${fileName})`; + // Construct highlight range for line numbers + const highlightRange: BrowseHighlightRange | undefined = startLine ? { + start: { lineNumber: parseInt(startLine) }, + end: { lineNumber: parseInt(endLine || startLine) }, + } : undefined; + + // Construct full browse URL + const browsePath = getBrowsePath({ + repoName: repo, + path: fileName, + pathType: 'blob', + domain: SINGLE_TENANT_ORG_DOMAIN, + highlightRange, + }); + + const fullUrl = `${baseUrl}${browsePath}`; + return `[${linkText}](${fullUrl})`; }) .trim(); }