fix: Fix issue with ambiguous references (#393)

This commit is contained in:
Brendan Kellam 2025-07-24 10:21:00 -07:00 committed by GitHub
parent da8d49f8d9
commit dbd8ef7fdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 191 additions and 136 deletions

View file

@ -200,19 +200,19 @@ When you have sufficient context, output your answer as a structured markdown re
**Required Response Format:** **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 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 - **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: - **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({ fileName: 'auth.ts' })}\`) - 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({ fileName: 'auth.ts', range: { startLine: 15, endLine: 20 } })}\`) - 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({ fileName: 'search.ts', range: { startLine: 42, endLine: 42 } })}\`) - Variable names (e.g., "The \`suggestionQuery\` variable" must include \`${fileReferenceToString({ repo: 'repository', path: '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 } })}\`)
- Any code snippet or line you're explaining - Any code snippet or line you're explaining
- Class names, method calls, imports, etc. - Class names, method calls, imports, etc.
- Some examples of both correct and incorrect code references: - Some examples of both correct and incorrect code references:
- Correct: @file:{path/to/file.ts} - Correct: @file:{repository::path/to/file.ts}
- Correct: @file:{path/to/file.ts:10-15} - Correct: @file:{repository::path/to/file.ts:10-15}
- Incorrect: @file{path/to/file.ts} (missing colon) - Incorrect: @file{repository::path/to/file.ts} (missing colon)
- Incorrect: @file:path/to/file.ts (missing curly braces) - Incorrect: @file:repository::path/to/file.ts (missing curly braces)
- Incorrect: @file:{path/to/file.ts:10-25,30-35} (multiple ranges not supported) - 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 - 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 - 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 - 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:** **Example answer structure:**
\`\`\`markdown \`\`\`markdown
${ANSWER_TAG} ${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.
\`\`\` \`\`\`
</answer_instructions> </answer_instructions>

View file

@ -17,13 +17,14 @@ describe('calculateVisibleRanges', () => {
test('applies padding to a single range', () => { test('applies padding to a single range', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', type: 'file',
range: { range: {
startLine: 10, startLine: 10,
endLine: 15 endLine: 15
} },
repo: 'github.com/sourcebot-dev/sourcebot'
} }
]; ];
@ -38,16 +39,18 @@ describe('calculateVisibleRanges', () => {
test('merges overlapping ranges', () => { test('merges overlapping ranges', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', id: '2',
type: 'file', 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)', () => { test('merges adjacent ranges (including padding)', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', id: '2',
type: 'file', 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', () => { test('keeps separate ranges when they dont overlap', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', id: '2',
type: 'file', 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', () => { test('respects file boundaries - start of file', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', () => { test('respects file boundaries - end of file', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', () => { test('handles multiple ranges with complex overlaps', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', id: '2',
type: 'file', 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', id: '3',
type: 'file', 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', id: '4',
type: 'file', 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', () => { test('ignores references without ranges', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', type: 'file',
// No range property // No range property
repo: 'github.com/sourcebot-dev/sourcebot'
}, },
{ {
fileName: 'test.ts', path: 'test.ts',
id: '2', id: '2',
type: 'file', 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', () => { test('works with zero padding', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', () => { test('handles single line ranges', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', () => { test('sorts ranges by start line', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', id: '2',
type: 'file', 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', id: '3',
type: 'file', 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', () => { test('initial state calculation with references', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', id: '2',
type: 'file', 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 // Update references
const newReferences: FileReference[] = [ const newReferences: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', type: 'file',
range: { startLine: 10, endLine: 15 } range: { startLine: 10, endLine: 15 }
@ -482,10 +504,11 @@ describe('StateField Integration', () => {
test('expandRegionEffect expands hidden region up', () => { test('expandRegionEffect expands hidden region up', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', () => { test('expandRegionEffect expands hidden region down', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', () => { test('document changes recalculate state', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', () => { test('action creators work correctly', () => {
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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 // Add references
const references: FileReference[] = [ const references: FileReference[] = [
{ {
fileName: 'test.ts', path: 'test.ts',
id: '1', id: '1',
type: 'file', 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', id: '2',
type: 'file', type: 'file',
range: { startLine: 60, endLine: 65 } range: { startLine: 60, endLine: 65 },
repo: 'github.com/sourcebot-dev/sourcebot'
} }
]; ];

View file

@ -45,12 +45,13 @@ function remarkReferencesPlugin() {
return function (tree: Nodes) { return function (tree: Nodes) {
findAndReplace(tree, [ findAndReplace(tree, [
FILE_REFERENCE_REGEX, FILE_REFERENCE_REGEX,
(_, fileName: string, startLine?: string, endLine?: string) => { (_, repo: string, fileName: string, startLine?: string, endLine?: string) => {
// Create display text // Create display text
let displayText = fileName.split('/').pop() ?? fileName; let displayText = fileName.split('/').pop() ?? fileName;
const fileReference = createFileReference({ const fileReference = createFileReference({
fileName, repo: repo,
path: fileName,
startLine, startLine,
endLine, endLine,
}); });

View file

@ -36,7 +36,10 @@ interface ReferencedSourcesListViewProps {
} }
const resolveFileReference = (reference: FileReference, sources: FileSource[]): FileSource | undefined => { 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) => { const getFileId = (fileSource: FileSource) => {

View file

@ -1,6 +1,10 @@
export const FILE_REFERENCE_PREFIX = '@file:'; 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 = '<!--answer-->'; export const ANSWER_TAG = '<!--answer-->';

View file

@ -25,7 +25,8 @@ export type Source = z.infer<typeof sourceSchema>;
const fileReferenceSchema = z.object({ const fileReferenceSchema = z.object({
type: z.literal('file'), type: z.literal('file'),
id: z.string(), id: z.string(),
fileName: z.string(), repo: z.string(),
path: z.string(),
range: z.object({ range: z.object({
startLine: z.number(), startLine: z.number(),
endLine: z.number(), endLine: z.number(),

View file

@ -11,7 +11,7 @@ test('useExtractReferences extracts file references from text content', () => {
parts: [ parts: [
{ {
type: 'text', 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).toHaveLength(2);
expect(result.current[0]).toMatchObject({ expect(result.current[0]).toMatchObject({
fileName: 'auth.ts', repo: 'github.com/sourcebot-dev/sourcebot',
id: getFileReferenceId({ fileName: 'auth.ts' }), path: 'auth.ts',
id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts' }),
type: 'file', type: 'file',
}); });
expect(result.current[1]).toMatchObject({ expect(result.current[1]).toMatchObject({
fileName: 'auth.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'auth.ts',
id: getFileReferenceId({ id: getFileReferenceId({
fileName: 'auth.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'auth.ts',
range: { range: {
startLine: 45, startLine: 45,
endLine: 60, endLine: 60,
@ -49,7 +52,7 @@ test('useExtractReferences extracts file references from reasoning content', ()
parts: [ parts: [
{ {
type: 'reasoning', 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).toHaveLength(2);
expect(result.current[0]).toMatchObject({ expect(result.current[0]).toMatchObject({
fileName: 'auth.ts', repo: 'github.com/sourcebot-dev/sourcebot',
id: getFileReferenceId({ fileName: 'auth.ts' }), path: 'auth.ts',
id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts' }),
type: 'file', type: 'file',
}); });
expect(result.current[1]).toMatchObject({ expect(result.current[1]).toMatchObject({
fileName: 'auth.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'auth.ts',
id: getFileReferenceId({ id: getFileReferenceId({
fileName: 'auth.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'auth.ts',
range: { range: {
startLine: 45, startLine: 45,
endLine: 60, endLine: 60,
@ -87,15 +93,15 @@ test('useExtractReferences extracts file references from multi-part', () => {
parts: [ parts: [
{ {
type: 'text', 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', 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', 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 // From text part
expect(result.current[0]).toMatchObject({ expect(result.current[0]).toMatchObject({
fileName: 'auth.ts', repo: 'github.com/sourcebot-dev/sourcebot',
id: getFileReferenceId({ fileName: 'auth.ts' }), path: 'auth.ts',
id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'auth.ts' }),
type: 'file', type: 'file',
}); });
// From reasoning part // From reasoning part
expect(result.current[1]).toMatchObject({ expect(result.current[1]).toMatchObject({
fileName: 'session.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'session.ts',
id: getFileReferenceId({ id: getFileReferenceId({
fileName: 'session.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'session.ts',
range: { range: {
startLine: 10, startLine: 10,
endLine: 20, endLine: 20,
@ -129,15 +138,18 @@ test('useExtractReferences extracts file references from multi-part', () => {
}); });
expect(result.current[2]).toMatchObject({ expect(result.current[2]).toMatchObject({
fileName: 'config.json', repo: 'github.com/sourcebot-dev/sourcebot',
id: getFileReferenceId({ fileName: 'config.json' }), path: 'config.json',
id: getFileReferenceId({ repo: 'github.com/sourcebot-dev/sourcebot', path: 'config.json' }),
type: 'file', type: 'file',
}); });
expect(result.current[3]).toMatchObject({ expect(result.current[3]).toMatchObject({
fileName: 'utils.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'utils.ts',
id: getFileReferenceId({ id: getFileReferenceId({
fileName: 'utils.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'utils.ts',
range: { range: {
startLine: 5, startLine: 5,
endLine: 5, endLine: 5,

View file

@ -18,10 +18,11 @@ export const useExtractReferences = (message?: SBChatMessage) => {
let match; let match;
while ((match = FILE_REFERENCE_REGEX.exec(content ?? '')) !== null && match !== null) { while ((match = FILE_REFERENCE_REGEX.exec(content ?? '')) !== null && match !== null) {
const [_, fileName, startLine, endLine] = match; const [_, repo, fileName, startLine, endLine] = match;
const fileReference = createFileReference({ const fileReference = createFileReference({
fileName, repo: repo,
path: fileName,
startLine, startLine,
endLine, endLine,
}); });

View file

@ -13,26 +13,30 @@ vi.mock('@/env.mjs', () => ({
test('fileReferenceToString formats file references correctly', () => { test('fileReferenceToString formats file references correctly', () => {
expect(fileReferenceToString({ expect(fileReferenceToString({
fileName: 'auth.ts' repo: 'github.com/sourcebot-dev/sourcebot',
})).toBe('@file:{auth.ts}'); path: 'auth.ts'
})).toBe('@file:{github.com/sourcebot-dev/sourcebot::auth.ts}');
expect(fileReferenceToString({ expect(fileReferenceToString({
fileName: 'auth.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'auth.ts',
range: { range: {
startLine: 45, startLine: 45,
endLine: 60, 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', () => { test('fileReferenceToString matches FILE_REFERENCE_REGEX', () => {
expect(FILE_REFERENCE_REGEX.test(fileReferenceToString({ expect(FILE_REFERENCE_REGEX.test(fileReferenceToString({
fileName: 'auth.ts' repo: 'github.com/sourcebot-dev/sourcebot',
path: 'auth.ts'
}))).toBe(true); }))).toBe(true);
FILE_REFERENCE_REGEX.lastIndex = 0; FILE_REFERENCE_REGEX.lastIndex = 0;
expect(FILE_REFERENCE_REGEX.test(fileReferenceToString({ expect(FILE_REFERENCE_REGEX.test(fileReferenceToString({
fileName: 'auth.ts', repo: 'github.com/sourcebot-dev/sourcebot',
path: 'auth.ts',
range: { range: {
startLine: 45, startLine: 45,
endLine: 60, endLine: 60,
@ -240,55 +244,55 @@ test('getAnswerPartFromAssistantMessage returns undefined when streaming and no
}); });
test('repairCitations fixes missing colon after @file', () => { test('repairCitations fixes missing colon after @file', () => {
const input = '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:{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); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations fixes missing colon with range', () => { test('repairCitations fixes missing colon with range', () => {
const input = '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:{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); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations fixes missing braces around filename', () => { test('repairCitations fixes missing braces around filename', () => {
const input = '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:{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); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations fixes missing braces with path', () => { test('repairCitations fixes missing braces with path', () => {
const input = '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:{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); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations removes multiple ranges keeping only first', () => { test('repairCitations removes multiple ranges keeping only first', () => {
const input = 'See @file:{service.ts:10-15,20-25,30-35} 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:{service.ts:10-15} for implementation.'; const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::service.ts:10-15} for implementation.';
expect(repairCitations(input)).toBe(expected); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations fixes malformed triple number ranges', () => { test('repairCitations fixes malformed triple number ranges', () => {
const input = 'Check @file:{handler.ts:5-10-15} for the logic.'; const input = 'Check @file:{github.com/sourcebot-dev/sourcebot::handler.ts:5-10-15} for the logic.';
const expected = 'Check @file:{handler.ts:5-10} for the logic.'; const expected = 'Check @file:{github.com/sourcebot-dev/sourcebot::handler.ts:5-10} for the logic.';
expect(repairCitations(input)).toBe(expected); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations handles multiple citations in same text', () => { test('repairCitations handles multiple citations in same text', () => {
const input = '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:{auth.ts} and @file:{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); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations leaves correctly formatted citations unchanged', () => { 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); expect(repairCitations(input)).toBe(input);
}); });
test('repairCitations handles edge cases with spaces and punctuation', () => { 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 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:{helper.ts}, @file:{main.js}, and @file:{app.ts:1-5} 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); expect(repairCitations(input)).toBe(expected);
}); });
@ -302,24 +306,24 @@ test('repairCitations returns text without citations unchanged', () => {
}); });
test('repairCitations handles complex file paths correctly', () => { test('repairCitations handles complex file paths correctly', () => {
const input = '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:{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); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations handles files with numbers and special characters', () => { 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 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:{utils-v2.0.1.ts} and @file:{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); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations handles citation at end of sentence', () => { test('repairCitations handles citation at end of sentence', () => {
const input = '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:{helper.ts}.'; const expected = 'The implementation is in @file:{github.com/sourcebot-dev/sourcebot::helper.ts}.';
expect(repairCitations(input)).toBe(expected); expect(repairCitations(input)).toBe(expected);
}); });
test('repairCitations preserves already correct citations with ranges', () => { 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); expect(repairCitations(input)).toBe(input);
}); });

View file

@ -129,7 +129,7 @@ export const slateContentToString = (children: Descendant[]): string => {
switch (type) { switch (type) {
case 'file': 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<FileReference, 'type' | 'id'>) => { export const getFileReferenceId = ({ repo, path, range }: Omit<FileReference, 'type' | 'id'>) => {
return `file-reference-${fileName}${range ? `-${range.startLine}-${range.endLine}` : ''}`; return `file-reference-${repo}::${path}${range ? `-${range.startLine}-${range.endLine}` : ''}`;
} }
export const fileReferenceToString = ({ fileName, range }: Omit<FileReference, 'type' | 'id'>) => { export const fileReferenceToString = ({ repo, path, range }: Omit<FileReference, 'type' | 'id'>) => {
return `${FILE_REFERENCE_PREFIX}{${fileName}${range ? `:${range.startLine}-${range.endLine}` : ''}}`; 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 ? { const range = startLine && endLine ? {
startLine: parseInt(startLine), startLine: parseInt(startLine),
endLine: parseInt(endLine), endLine: parseInt(endLine),
@ -229,8 +229,9 @@ export const createFileReference = ({ fileName, startLine, endLine }: { fileName
return { return {
type: 'file', type: 'file',
id: getFileReferenceId({ fileName, range }), id: getFileReferenceId({ repo, path, range }),
fileName, repo,
path,
range, range,
} }
} }
@ -241,7 +242,7 @@ export const createFileReference = ({ fileName, startLine, endLine }: { fileName
* links. * links.
*/ */
export const convertLLMOutputToPortableMarkdown = (text: string): string => { 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; const displayName = fileName.split('/').pop() || fileName;
let linkText = displayName; let linkText = displayName;
@ -294,9 +295,9 @@ export const repairCitations = (text: string): string => {
// Fix missing braces: @file:filename -> @file:{filename} // Fix missing braces: @file:filename -> @file:{filename}
.replace(/@file:([^\s{]\S*?)(\s|[,;!?](?:\s|$)|\.(?:\s|$)|$)/g, '@file:{$1}$2') .replace(/@file:([^\s{]\S*?)(\s|[,;!?](?:\s|$)|\.(?:\s|$)|$)/g, '@file:{$1}$2')
// Fix multiple ranges: keep only first range // 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 // 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 // Attempts to find the part of the assistant's message