From c798d25c59ce8bb79925671db1655e805416703d Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 3 Feb 2026 11:22:04 -0800 Subject: [PATCH 1/3] fix(web): Convert file references to full browse URLs in portable markdown Change convertLLMOutputToPortableMarkdown to embed full browse URLs instead of relative file paths, so users can click links without needing an IDE open. Co-Authored-By: Claude Opus 4.5 --- .../app/api/(server)/chat/blocking/route.ts | 8 +++---- .../chat/components/chatThread/answerCard.tsx | 3 ++- packages/web/src/features/chat/utils.ts | 24 ++++++++++++++++--- 3 files changed, 27 insertions(+), 8 deletions(-) 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(); } From a10b9327b919f22f86991ad6ff23f1e651c5ff0f Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 3 Feb 2026 11:23:00 -0800 Subject: [PATCH 2/3] docs: Update changelog for file reference URL fix Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e8db874..b85c6da83 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. ## [4.10.24] - 2026-02-03 From c430beae61a8812bb337f78b4802e1d6cbdd1538 Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 3 Feb 2026 11:23:52 -0800 Subject: [PATCH 3/3] docs: Add PR link to changelog entry Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b85c6da83..893a427e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +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. +- 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