Skip to content

Commit 9c56827

Browse files
fix(web): Convert file references to full browse URLs in portable markdown (#847)
* 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 <noreply@anthropic.com> * docs: Update changelog for file reference URL fix Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: Add PR link to changelog entry Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 880c8e0 commit 9c56827

File tree

4 files changed

+28
-8
lines changed

4 files changed

+28
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111
- Fixed issue where opening GitLab file links would result in a 404. [#846](https://github.com/sourcebot-dev/sourcebot/pull/846)
12+
- 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)
1213

1314
## [4.10.24] - 2026-02-03
1415

packages/web/src/app/api/(server)/chat/blocking/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
182182
: undefined;
183183
const answerText = answerPart?.text ?? '';
184184

185-
// Convert to portable markdown (replaces @file: references with markdown links)
186-
const portableAnswer = convertLLMOutputToPortableMarkdown(answerText);
187-
188-
// Build the chat URL
185+
// Build the base URL and chat URL
189186
const baseUrl = env.AUTH_URL;
187+
188+
// Convert to portable markdown (replaces @file: references with markdown links)
189+
const portableAnswer = convertLLMOutputToPortableMarkdown(answerText, baseUrl);
190190
const chatUrl = `${baseUrl}/${org.domain}/chat/${chat.id}`;
191191

192192
logger.debug(`Completed blocking agent for chat ${chat.id}`, {

packages/web/src/features/chat/components/chatThread/answerCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ const AnswerCardComponent = forwardRef<HTMLDivElement, AnswerCardProps>(({
5151
);
5252

5353
const onCopyAnswer = useCallback(() => {
54-
const markdownText = convertLLMOutputToPortableMarkdown(answerText);
54+
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
55+
const markdownText = convertLLMOutputToPortableMarkdown(answerText, baseUrl);
5556
navigator.clipboard.writeText(markdownText);
5657
toast({
5758
description: "✅ Copied to clipboard",

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { CreateUIMessage, TextUIPart, UIMessagePart } from "ai";
22
import { Descendant, Editor, Point, Range, Transforms } from "slate";
33
import { ANSWER_TAG, FILE_REFERENCE_PREFIX, FILE_REFERENCE_REGEX } from "./constants";
4+
import { getBrowsePath, BrowseHighlightRange } from "@/app/[domain]/browse/hooks/utils";
5+
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
46
import {
57
CustomEditor,
68
CustomText,
@@ -243,10 +245,10 @@ export const createFileReference = ({ repo, path, startLine, endLine }: { repo:
243245
* Markdown format. Practically, this means converting references into Markdown
244246
* links and removing the answer tag.
245247
*/
246-
export const convertLLMOutputToPortableMarkdown = (text: string): string => {
248+
export const convertLLMOutputToPortableMarkdown = (text: string, baseUrl: string): string => {
247249
return text
248250
.replace(ANSWER_TAG, '')
249-
.replace(FILE_REFERENCE_REGEX, (_, _repo, fileName, startLine, endLine) => {
251+
.replace(FILE_REFERENCE_REGEX, (_, repo, fileName, startLine, endLine) => {
250252
const displayName = fileName.split('/').pop() || fileName;
251253

252254
let linkText = displayName;
@@ -258,7 +260,23 @@ export const convertLLMOutputToPortableMarkdown = (text: string): string => {
258260
}
259261
}
260262

261-
return `[${linkText}](${fileName})`;
263+
// Construct highlight range for line numbers
264+
const highlightRange: BrowseHighlightRange | undefined = startLine ? {
265+
start: { lineNumber: parseInt(startLine) },
266+
end: { lineNumber: parseInt(endLine || startLine) },
267+
} : undefined;
268+
269+
// Construct full browse URL
270+
const browsePath = getBrowsePath({
271+
repoName: repo,
272+
path: fileName,
273+
pathType: 'blob',
274+
domain: SINGLE_TENANT_ORG_DOMAIN,
275+
highlightRange,
276+
});
277+
278+
const fullUrl = `${baseUrl}${browsePath}`;
279+
return `[${linkText}](${fullUrl})`;
262280
})
263281
.trim();
264282
}

0 commit comments

Comments
 (0)