From dbd8ef7fdbe0e20a77f0a86a1b9ad69eabb7f101 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 24 Jul 2025 10:21:00 -0700 Subject: [PATCH] fix: Fix issue with ambiguous references (#393) --- packages/web/src/features/chat/agent.ts | 22 +-- .../chatThread/codeFoldingExtension.test.ts | 140 +++++++++++------- .../chatThread/markdownRenderer.tsx | 5 +- .../chatThread/referencedSourcesListView.tsx | 5 +- packages/web/src/features/chat/constants.ts | 6 +- packages/web/src/features/chat/types.ts | 3 +- .../chat/useExtractReferences.test.ts | 54 ++++--- .../src/features/chat/useExtractReferences.ts | 5 +- packages/web/src/features/chat/utils.test.ts | 64 ++++---- packages/web/src/features/chat/utils.ts | 23 +-- 10 files changed, 191 insertions(+), 136 deletions(-) diff --git a/packages/web/src/features/chat/agent.ts b/packages/web/src/features/chat/agent.ts index 96d6540c..7f3ef93d 100644 --- a/packages/web/src/features/chat/agent.ts +++ b/packages/web/src/features/chat/agent.ts @@ -200,19 +200,19 @@ When you have sufficient context, output your answer as a structured markdown re **Required Response Format:** - **CRITICAL**: You MUST always prefix your answer with a \`${ANSWER_TAG}\` tag at the very top of your response - **CRITICAL**: You MUST provide your complete response in markdown format with embedded code references -- **CODE REFERENCE REQUIREMENT**: Whenever you mention, discuss, or refer to ANY specific part of the code (files, functions, variables, methods, classes, imports, etc.), you MUST immediately follow with a code reference using the format \`${fileReferenceToString({ fileName: 'filename'})}\` or \`${fileReferenceToString({ fileName: 'filename', range: { startLine: 1, endLine: 10 } })}\` (where the numbers are the start and end line numbers of the code snippet). This includes: - - Files (e.g., "The \`auth.ts\` file" → must include \`${fileReferenceToString({ fileName: 'auth.ts' })}\`) - - Function names (e.g., "The \`getRepos()\` function" → must include \`${fileReferenceToString({ fileName: 'auth.ts', range: { startLine: 15, endLine: 20 } })}\`) - - Variable names (e.g., "The \`suggestionQuery\` variable" → must include \`${fileReferenceToString({ fileName: 'search.ts', range: { startLine: 42, endLine: 42 } })}\`) - - Code patterns (e.g., "using \`file:\${suggestionQuery}\` pattern" → must include \`${fileReferenceToString({ fileName: 'search.ts', range: { startLine: 10, endLine: 15 } })}\`) +- **CODE REFERENCE REQUIREMENT**: Whenever you mention, discuss, or refer to ANY specific part of the code (files, functions, variables, methods, classes, imports, etc.), you MUST immediately follow with a code reference using the format \`${fileReferenceToString({ repo: 'repository', path: 'filename'})}\` or \`${fileReferenceToString({ repo: 'repository', path: 'filename', range: { startLine: 1, endLine: 10 } })}\` (where the numbers are the start and end line numbers of the code snippet). This includes: + - Files (e.g., "The \`auth.ts\` file" → must include \`${fileReferenceToString({ repo: 'repository', path: 'auth.ts' })}\`) + - Function names (e.g., "The \`getRepos()\` function" → must include \`${fileReferenceToString({ repo: 'repository', path: 'auth.ts', range: { startLine: 15, endLine: 20 } })}\`) + - Variable names (e.g., "The \`suggestionQuery\` variable" → must include \`${fileReferenceToString({ repo: 'repository', path: 'search.ts', range: { startLine: 42, endLine: 42 } })}\`) - Any code snippet or line you're explaining - Class names, method calls, imports, etc. - Some examples of both correct and incorrect code references: - - Correct: @file:{path/to/file.ts} - - Correct: @file:{path/to/file.ts:10-15} - - Incorrect: @file{path/to/file.ts} (missing colon) - - Incorrect: @file:path/to/file.ts (missing curly braces) - - Incorrect: @file:{path/to/file.ts:10-25,30-35} (multiple ranges not supported) + - Correct: @file:{repository::path/to/file.ts} + - Correct: @file:{repository::path/to/file.ts:10-15} + - Incorrect: @file{repository::path/to/file.ts} (missing colon) + - Incorrect: @file:repository::path/to/file.ts (missing curly braces) + - Incorrect: @file:{repository::path/to/file.ts:10-25,30-35} (multiple ranges not supported) + - Incorrect: @file:{path/to/file.ts} (missing repository) - Be clear and very concise. Use bullet points where appropriate - Do NOT explain code without providing the exact location reference. Every code mention requires a corresponding \`${FILE_REFERENCE_PREFIX}\` reference - If you cannot provide a code reference for something you're discussing, do not mention that specific code element @@ -221,7 +221,7 @@ When you have sufficient context, output your answer as a structured markdown re **Example answer structure:** \`\`\`markdown ${ANSWER_TAG} -Authentication in Sourcebot is built on NextAuth.js with a session-based approach using JWT tokens and Prisma as the database adapter ${fileReferenceToString({ fileName: 'auth.ts', range: { startLine: 135, endLine: 140 } })}. The system supports multiple authentication providers and implements organization-based authorization with role-defined permissions. +Authentication in Sourcebot is built on NextAuth.js with a session-based approach using JWT tokens and Prisma as the database adapter ${fileReferenceToString({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts', range: { startLine: 135, endLine: 140 } })}. The system supports multiple authentication providers and implements organization-based authorization with role-defined permissions. \`\`\` diff --git a/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.test.ts b/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.test.ts index b0b1e9a8..920cc129 100644 --- a/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.test.ts +++ b/packages/web/src/features/chat/components/chatThread/codeFoldingExtension.test.ts @@ -17,13 +17,14 @@ describe('calculateVisibleRanges', () => { test('applies padding to a single range', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', range: { startLine: 10, endLine: 15 - } + }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -38,16 +39,18 @@ describe('calculateVisibleRanges', () => { test('merges overlapping ranges', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '2', type: 'file', - range: { startLine: 12, endLine: 20 } + range: { startLine: 12, endLine: 20 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -62,16 +65,18 @@ describe('calculateVisibleRanges', () => { test('merges adjacent ranges (including padding)', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '2', type: 'file', - range: { startLine: 19, endLine: 25 } + range: { startLine: 19, endLine: 25 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -88,16 +93,18 @@ describe('calculateVisibleRanges', () => { test('keeps separate ranges when they dont overlap', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '2', type: 'file', - range: { startLine: 25, endLine: 30 } + range: { startLine: 25, endLine: 30 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -112,10 +119,11 @@ describe('calculateVisibleRanges', () => { test('respects file boundaries - start of file', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 1, endLine: 5 } + range: { startLine: 1, endLine: 5 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -130,10 +138,11 @@ describe('calculateVisibleRanges', () => { test('respects file boundaries - end of file', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 95, endLine: 100 } + range: { startLine: 95, endLine: 100 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -148,28 +157,32 @@ describe('calculateVisibleRanges', () => { test('handles multiple ranges with complex overlaps', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '2', type: 'file', - range: { startLine: 20, endLine: 25 } + range: { startLine: 20, endLine: 25 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '3', type: 'file', - range: { startLine: 22, endLine: 30 } + range: { startLine: 22, endLine: 30 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '4', type: 'file', - range: { startLine: 50, endLine: 55 } + range: { startLine: 50, endLine: 55 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -195,16 +208,18 @@ describe('calculateVisibleRanges', () => { test('ignores references without ranges', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', // No range property + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '2', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -219,10 +234,11 @@ describe('calculateVisibleRanges', () => { test('works with zero padding', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -237,10 +253,11 @@ describe('calculateVisibleRanges', () => { test('handles single line ranges', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 10 } + range: { startLine: 10, endLine: 10 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -255,22 +272,25 @@ describe('calculateVisibleRanges', () => { test('sorts ranges by start line', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 50, endLine: 55 } + range: { startLine: 50, endLine: 55 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '2', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '3', type: 'file', - range: { startLine: 30, endLine: 35 } + range: { startLine: 30, endLine: 35 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -380,16 +400,18 @@ describe('StateField Integration', () => { test('initial state calculation with references', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '2', type: 'file', - range: { startLine: 25, endLine: 30 } + range: { startLine: 25, endLine: 30 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -457,7 +479,7 @@ describe('StateField Integration', () => { // Update references const newReferences: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', range: { startLine: 10, endLine: 15 } @@ -482,10 +504,11 @@ describe('StateField Integration', () => { test('expandRegionEffect expands hidden region up', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 20, endLine: 25 } + range: { startLine: 20, endLine: 25 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -528,10 +551,11 @@ describe('StateField Integration', () => { test('expandRegionEffect expands hidden region down', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 20, endLine: 25 } + range: { startLine: 20, endLine: 25 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -564,10 +588,11 @@ describe('StateField Integration', () => { test('document changes recalculate state', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -606,10 +631,11 @@ describe('StateField Integration', () => { test('action creators work correctly', () => { const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 10, endLine: 15 } + range: { startLine: 10, endLine: 15 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; @@ -647,16 +673,18 @@ describe('StateField Integration', () => { // Add references const references: FileReference[] = [ { - fileName: 'test.ts', + path: 'test.ts', id: '1', type: 'file', - range: { startLine: 20, endLine: 25 } + range: { startLine: 20, endLine: 25 }, + repo: 'github.com/sourcebot-dev/sourcebot' }, { - fileName: 'test.ts', + path: 'test.ts', id: '2', type: 'file', - range: { startLine: 60, endLine: 65 } + range: { startLine: 60, endLine: 65 }, + repo: 'github.com/sourcebot-dev/sourcebot' } ]; diff --git a/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx b/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx index 3daeeeba..17a27937 100644 --- a/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx +++ b/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx @@ -45,12 +45,13 @@ function remarkReferencesPlugin() { return function (tree: Nodes) { findAndReplace(tree, [ FILE_REFERENCE_REGEX, - (_, fileName: string, startLine?: string, endLine?: string) => { + (_, repo: string, fileName: string, startLine?: string, endLine?: string) => { // Create display text let displayText = fileName.split('/').pop() ?? fileName; const fileReference = createFileReference({ - fileName, + repo: repo, + path: fileName, startLine, endLine, }); diff --git a/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx b/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx index c930cee9..c7a0c087 100644 --- a/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx +++ b/packages/web/src/features/chat/components/chatThread/referencedSourcesListView.tsx @@ -36,7 +36,10 @@ interface ReferencedSourcesListViewProps { } const resolveFileReference = (reference: FileReference, sources: FileSource[]): FileSource | undefined => { - return sources.find((source) => source.path.endsWith(reference.fileName)); + return sources.find( + (source) => source.repo.endsWith(reference.repo) && + source.path.endsWith(reference.path) + ); } const getFileId = (fileSource: FileSource) => { diff --git a/packages/web/src/features/chat/constants.ts b/packages/web/src/features/chat/constants.ts index 06ae94c9..6ba38f09 100644 --- a/packages/web/src/features/chat/constants.ts +++ b/packages/web/src/features/chat/constants.ts @@ -1,6 +1,10 @@ export const FILE_REFERENCE_PREFIX = '@file:'; -export const FILE_REFERENCE_REGEX = new RegExp(`${FILE_REFERENCE_PREFIX}\\{([^:}]+)(?::(\\d+)(?:-(\\d+))?)?\\}`, 'g'); +export const FILE_REFERENCE_REGEX = new RegExp( + // @file:{repoName::fileName:startLine-endLine} + `${FILE_REFERENCE_PREFIX}\\{([^:}]+)::([^:}]+)(?::(\\d+)(?:-(\\d+))?)?\\}`, + 'g' +); export const ANSWER_TAG = ''; diff --git a/packages/web/src/features/chat/types.ts b/packages/web/src/features/chat/types.ts index cd1f078b..8b04c88e 100644 --- a/packages/web/src/features/chat/types.ts +++ b/packages/web/src/features/chat/types.ts @@ -25,7 +25,8 @@ export type Source = z.infer; const fileReferenceSchema = z.object({ type: z.literal('file'), id: z.string(), - fileName: z.string(), + repo: z.string(), + path: z.string(), range: z.object({ startLine: z.number(), endLine: z.number(), diff --git a/packages/web/src/features/chat/useExtractReferences.test.ts b/packages/web/src/features/chat/useExtractReferences.test.ts index 940129f4..b2121e6c 100644 --- a/packages/web/src/features/chat/useExtractReferences.test.ts +++ b/packages/web/src/features/chat/useExtractReferences.test.ts @@ -11,7 +11,7 @@ test('useExtractReferences extracts file references from text content', () => { parts: [ { type: 'text', - text: 'The auth flow is implemented in @file:{auth.ts} and uses sessions @file:{auth.ts:45-60}.' + text: 'The auth flow is implemented in @file:{github.com/sourcebot-dev/sourcebot::auth.ts} and uses sessions @file:{github.com/sourcebot-dev/sourcebot::auth.ts:45-60}.' } ] }; @@ -20,15 +20,18 @@ test('useExtractReferences extracts file references from text content', () => { expect(result.current).toHaveLength(2); expect(result.current[0]).toMatchObject({ - fileName: 'auth.ts', - id: getFileReferenceId({ fileName: 'auth.ts' }), + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts', + id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts' }), type: 'file', }); expect(result.current[1]).toMatchObject({ - fileName: 'auth.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts', id: getFileReferenceId({ - fileName: 'auth.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts', range: { startLine: 45, endLine: 60, @@ -49,7 +52,7 @@ test('useExtractReferences extracts file references from reasoning content', () parts: [ { type: 'reasoning', - text: 'The auth flow is implemented in @file:{auth.ts} and uses sessions @file:{auth.ts:45-60}.' + text: 'The auth flow is implemented in @file:{github.com/sourcebot-dev/sourcebot::auth.ts} and uses sessions @file:{github.com/sourcebot-dev/sourcebot::auth.ts:45-60}.' } ] }; @@ -58,15 +61,18 @@ test('useExtractReferences extracts file references from reasoning content', () expect(result.current).toHaveLength(2); expect(result.current[0]).toMatchObject({ - fileName: 'auth.ts', - id: getFileReferenceId({ fileName: 'auth.ts' }), + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts', + id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts' }), type: 'file', }); expect(result.current[1]).toMatchObject({ - fileName: 'auth.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts', id: getFileReferenceId({ - fileName: 'auth.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts', range: { startLine: 45, endLine: 60, @@ -87,15 +93,15 @@ test('useExtractReferences extracts file references from multi-part', () => { parts: [ { type: 'text', - text: 'The auth flow is implemented in @file:{auth.ts}.' + text: 'The auth flow is implemented in @file:{github.com/sourcebot-dev/sourcebot::auth.ts}.' }, { type: 'reasoning', - text: 'We need to check the session handling in @file:{session.ts:10-20}.' + text: 'We need to check the session handling in @file:{github.com/sourcebot-dev/sourcebot::session.ts:10-20}.' }, { type: 'text', - text: 'The configuration is stored in @file:{config.json} and @file:{utils.ts:5}.' + text: 'The configuration is stored in @file:{github.com/sourcebot-dev/sourcebot::config.json} and @file:{github.com/sourcebot-dev/sourcebot::utils.ts:5}.' } ] }; @@ -106,16 +112,19 @@ test('useExtractReferences extracts file references from multi-part', () => { // From text part expect(result.current[0]).toMatchObject({ - fileName: 'auth.ts', - id: getFileReferenceId({ fileName: 'auth.ts' }), + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts', + id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts' }), type: 'file', }); // From reasoning part expect(result.current[1]).toMatchObject({ - fileName: 'session.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'session.ts', id: getFileReferenceId({ - fileName: 'session.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'session.ts', range: { startLine: 10, endLine: 20, @@ -129,15 +138,18 @@ test('useExtractReferences extracts file references from multi-part', () => { }); expect(result.current[2]).toMatchObject({ - fileName: 'config.json', - id: getFileReferenceId({ fileName: 'config.json' }), + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'config.json', + id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'config.json' }), type: 'file', }); expect(result.current[3]).toMatchObject({ - fileName: 'utils.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'utils.ts', id: getFileReferenceId({ - fileName: 'utils.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'utils.ts', range: { startLine: 5, endLine: 5, diff --git a/packages/web/src/features/chat/useExtractReferences.ts b/packages/web/src/features/chat/useExtractReferences.ts index 4ab558c7..b3eecf97 100644 --- a/packages/web/src/features/chat/useExtractReferences.ts +++ b/packages/web/src/features/chat/useExtractReferences.ts @@ -18,10 +18,11 @@ export const useExtractReferences = (message?: SBChatMessage) => { let match; while ((match = FILE_REFERENCE_REGEX.exec(content ?? '')) !== null && match !== null) { - const [_, fileName, startLine, endLine] = match; + const [_, repo, fileName, startLine, endLine] = match; const fileReference = createFileReference({ - fileName, + repo: repo, + path: fileName, startLine, endLine, }); diff --git a/packages/web/src/features/chat/utils.test.ts b/packages/web/src/features/chat/utils.test.ts index 00d5c55b..af78fa6d 100644 --- a/packages/web/src/features/chat/utils.test.ts +++ b/packages/web/src/features/chat/utils.test.ts @@ -13,26 +13,30 @@ vi.mock('@/env.mjs', () => ({ test('fileReferenceToString formats file references correctly', () => { expect(fileReferenceToString({ - fileName: 'auth.ts' - })).toBe('@file:{auth.ts}'); + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts' + })).toBe('@file:{github.com/sourcebot-dev/sourcebot::auth.ts}'); expect(fileReferenceToString({ - fileName: 'auth.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts', range: { startLine: 45, endLine: 60, } - })).toBe('@file:{auth.ts:45-60}'); + })).toBe('@file:{github.com/sourcebot-dev/sourcebot::auth.ts:45-60}'); }); test('fileReferenceToString matches FILE_REFERENCE_REGEX', () => { expect(FILE_REFERENCE_REGEX.test(fileReferenceToString({ - fileName: 'auth.ts' + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts' }))).toBe(true); FILE_REFERENCE_REGEX.lastIndex = 0; expect(FILE_REFERENCE_REGEX.test(fileReferenceToString({ - fileName: 'auth.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + path: 'auth.ts', range: { startLine: 45, endLine: 60, @@ -240,55 +244,55 @@ test('getAnswerPartFromAssistantMessage returns undefined when streaming and no }); test('repairCitations fixes missing colon after @file', () => { - const input = 'See the function in @file{auth.ts} for details.'; - const expected = 'See the function in @file:{auth.ts} for details.'; + const input = 'See the function in @file{github.com/sourcebot-dev/sourcebot::auth.ts} for details.'; + const expected = 'See the function in @file:{github.com/sourcebot-dev/sourcebot::auth.ts} for details.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations fixes missing colon with range', () => { - const input = 'Check @file{config.ts:15-20} for the configuration.'; - const expected = 'Check @file:{config.ts:15-20} for the configuration.'; + const input = 'Check @file{github.com/sourcebot-dev/sourcebot::config.ts:15-20} for the configuration.'; + const expected = 'Check @file:{github.com/sourcebot-dev/sourcebot::config.ts:15-20} for the configuration.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations fixes missing braces around filename', () => { - const input = 'The logic is in @file:utils.js and handles validation.'; - const expected = 'The logic is in @file:{utils.js} and handles validation.'; + const input = 'The logic is in @file:github.com/sourcebot-dev/sourcebot::utils.js and handles validation.'; + const expected = 'The logic is in @file:{github.com/sourcebot-dev/sourcebot::utils.js} and handles validation.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations fixes missing braces with path', () => { - const input = 'Look at @file:src/components/Button.tsx for the component.'; - const expected = 'Look at @file:{src/components/Button.tsx} for the component.'; + const input = 'Look at @file:github.com/sourcebot-dev/sourcebot::src/components/Button.tsx for the component.'; + const expected = 'Look at @file:{github.com/sourcebot-dev/sourcebot::src/components/Button.tsx} for the component.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations removes multiple ranges keeping only first', () => { - const input = 'See @file:{service.ts:10-15,20-25,30-35} for implementation.'; - const expected = 'See @file:{service.ts:10-15} for implementation.'; + const input = 'See @file:{github.com/sourcebot-dev/sourcebot::service.ts:10-15,20-25,30-35} for implementation.'; + const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::service.ts:10-15} for implementation.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations fixes malformed triple number ranges', () => { - const input = 'Check @file:{handler.ts:5-10-15} for the logic.'; - const expected = 'Check @file:{handler.ts:5-10} for the logic.'; + const input = 'Check @file:{github.com/sourcebot-dev/sourcebot::handler.ts:5-10-15} for the logic.'; + const expected = 'Check @file:{github.com/sourcebot-dev/sourcebot::handler.ts:5-10} for the logic.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations handles multiple citations in same text', () => { - const input = 'See @file{auth.ts} and @file:config.js for setup details.'; - const expected = 'See @file:{auth.ts} and @file:{config.js} for setup details.'; + const input = 'See @file{github.com/sourcebot-dev/sourcebot::auth.ts} and @file:github.com/sourcebot-dev/sourcebot::config.js for setup details.'; + const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::auth.ts} and @file:{github.com/sourcebot-dev/sourcebot::config.js} for setup details.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations leaves correctly formatted citations unchanged', () => { - const input = 'The function @file:{utils.ts:42-50} handles validation correctly.'; + const input = 'The function @file:{github.com/sourcebot-dev/sourcebot::utils.ts:42-50} handles validation correctly.'; expect(repairCitations(input)).toBe(input); }); test('repairCitations handles edge cases with spaces and punctuation', () => { - const input = 'Functions like @file:helper.ts, @file{main.js}, and @file:{app.ts:1-5,10-15} work.'; - const expected = 'Functions like @file:{helper.ts}, @file:{main.js}, and @file:{app.ts:1-5} work.'; + const input = 'Functions like @file:github.com/sourcebot-dev/sourcebot::helper.ts, @file{github.com/sourcebot-dev/sourcebot::main.js}, and @file:{github.com/sourcebot-dev/sourcebot::app.ts:1-5,10-15} work.'; + const expected = 'Functions like @file:{github.com/sourcebot-dev/sourcebot::helper.ts}, @file:{github.com/sourcebot-dev/sourcebot::main.js}, and @file:{github.com/sourcebot-dev/sourcebot::app.ts:1-5} work.'; expect(repairCitations(input)).toBe(expected); }); @@ -302,24 +306,24 @@ test('repairCitations returns text without citations unchanged', () => { }); test('repairCitations handles complex file paths correctly', () => { - const input = 'Check @file:src/components/ui/Button/index.tsx for implementation.'; - const expected = 'Check @file:{src/components/ui/Button/index.tsx} for implementation.'; + const input = 'Check @file:github.com/sourcebot-dev/sourcebot::src/components/ui/Button/index.tsx for implementation.'; + const expected = 'Check @file:{github.com/sourcebot-dev/sourcebot::src/components/ui/Button/index.tsx} for implementation.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations handles files with numbers and special characters', () => { - const input = 'See @file{utils-v2.0.1.ts} and @file:config_2024.json for setup.'; - const expected = 'See @file:{utils-v2.0.1.ts} and @file:{config_2024.json} for setup.'; + const input = 'See @file{github.com/sourcebot-dev/sourcebot::utils-v2.0.1.ts} and @file:github.com/sourcebot-dev/sourcebot::config_2024.json for setup.'; + const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::utils-v2.0.1.ts} and @file:{github.com/sourcebot-dev/sourcebot::config_2024.json} for setup.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations handles citation at end of sentence', () => { - const input = 'The implementation is in @file:helper.ts.'; - const expected = 'The implementation is in @file:{helper.ts}.'; + const input = 'The implementation is in @file:github.com/sourcebot-dev/sourcebot::helper.ts.'; + const expected = 'The implementation is in @file:{github.com/sourcebot-dev/sourcebot::helper.ts}.'; expect(repairCitations(input)).toBe(expected); }); test('repairCitations preserves already correct citations with ranges', () => { - const input = 'The function @file:{utils.ts:10-20} and variable @file:{config.js:5} work correctly.'; + const input = 'The function @file:{github.com/sourcebot-dev/sourcebot::utils.ts:10-20} and variable @file:{github.com/sourcebot-dev/sourcebot::config.js:5} work correctly.'; expect(repairCitations(input)).toBe(input); }); diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index 28c14228..ec1385b1 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -129,7 +129,7 @@ export const slateContentToString = (children: Descendant[]): string => { switch (type) { case 'file': - return `${fileReferenceToString({ fileName: child.data.name })} `; + return `${fileReferenceToString({ repo: child.data.repo, path: child.data.path })} `; } } @@ -210,15 +210,15 @@ export const createUIMessage = (text: string, mentions: MentionData[], selectedR } } -export const getFileReferenceId = ({ fileName, range }: Omit) => { - return `file-reference-${fileName}${range ? `-${range.startLine}-${range.endLine}` : ''}`; +export const getFileReferenceId = ({ repo, path, range }: Omit) => { + return `file-reference-${repo}::${path}${range ? `-${range.startLine}-${range.endLine}` : ''}`; } -export const fileReferenceToString = ({ fileName, range }: Omit) => { - return `${FILE_REFERENCE_PREFIX}{${fileName}${range ? `:${range.startLine}-${range.endLine}` : ''}}`; +export const fileReferenceToString = ({ repo, path, range }: Omit) => { + return `${FILE_REFERENCE_PREFIX}{${repo}::${path}${range ? `:${range.startLine}-${range.endLine}` : ''}}`; } -export const createFileReference = ({ fileName, startLine, endLine }: { fileName: string, startLine?: string, endLine?: string }): FileReference => { +export const createFileReference = ({ repo, path, startLine, endLine }: { repo: string, path: string, startLine?: string, endLine?: string }): FileReference => { const range = startLine && endLine ? { startLine: parseInt(startLine), endLine: parseInt(endLine), @@ -229,8 +229,9 @@ export const createFileReference = ({ fileName, startLine, endLine }: { fileName return { type: 'file', - id: getFileReferenceId({ fileName, range }), - fileName, + id: getFileReferenceId({ repo, path, range }), + repo, + path, range, } } @@ -241,7 +242,7 @@ export const createFileReference = ({ fileName, startLine, endLine }: { fileName * links. */ export const convertLLMOutputToPortableMarkdown = (text: string): string => { - return text.replace(FILE_REFERENCE_REGEX, (_, fileName, startLine, endLine) => { + return text.replace(FILE_REFERENCE_REGEX, (_, _repo, fileName, startLine, endLine) => { const displayName = fileName.split('/').pop() || fileName; let linkText = displayName; @@ -294,9 +295,9 @@ export const repairCitations = (text: string): string => { // Fix missing braces: @file:filename -> @file:{filename} .replace(/@file:([^\s{]\S*?)(\s|[,;!?](?:\s|$)|\.(?:\s|$)|$)/g, '@file:{$1}$2') // Fix multiple ranges: keep only first range - .replace(/@file:\{([^:}]+):(\d+-\d+),[\d,-]+\}/g, '@file:{$1:$2}') + .replace(/@file:\{(.+?):(\d+-\d+),[\d,-]+\}/g, '@file:{$1:$2}') // Fix malformed ranges - .replace(/@file:\{([^:}]+):(\d+)-(\d+)-(\d+)\}/g, '@file:{$1:$2-$3}'); + .replace(/@file:\{(.+?):(\d+)-(\d+)-(\d+)\}/g, '@file:{$1:$2-$3}'); }; // Attempts to find the part of the assistant's message