Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -36,6 +38,7 @@ const AnswerCardComponent = forwardRef<HTMLDivElement, AnswerCardProps>(({
messageId,
chatId,
traceId,
sources,
}, forwardedRef) => {
const markdownRendererRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line react-hooks/refs -- ref.current is passed to a custom hook, not used directly in render output
Expand All @@ -53,14 +56,14 @@ const AnswerCardComponent = forwardRef<HTMLDivElement, AnswerCardProps>(({

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
chatId={chatId}
messageId={assistantMessage.id}
traceId={assistantMessage.metadata?.traceId}
sources={referencedFileSources}
/>
) : !isStreaming && (
<p className="text-destructive">Error: No answer response was provided</p>
Expand Down
17 changes: 14 additions & 3 deletions packages/web/src/features/chat/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand All @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion packages/web/src/features/mcp/askCodebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,15 @@ export const askCodebase = (params: AskCodebaseParams): Promise<AskCodebaseResul
: undefined;
const answerText = answerPart?.text ?? '';

const fileSources = finalMessages.flatMap((message) =>
message.parts
.filter((part) => part.type === 'data-source')
.map((part) => part.data)
.filter((source) => source.type === 'file')
);
Comment thread
brendan-kellam marked this conversation as resolved.

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 });
Expand Down
45 changes: 44 additions & 1 deletion packages/web/src/features/tools/getDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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: {
Expand All @@ -58,6 +100,7 @@ export const getDiffDefinition: ToolDefinition<'get_diff', typeof getDiffRequest
base,
head,
},
sources,
};
},
};
Loading