From 24260132312cc59467bb941e1ce5227111a6f633 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 17:48:58 +0000 Subject: [PATCH 1/4] feat: improve diff tool display and token efficiency - Truncate full commit SHAs to 7 characters for compact display - Use RepoBadge component for consistent repository rendering - Convert JSON output to git-diff format for better token efficiency Co-authored-by: Brendan Kellam --- .../chatThread/tools/getDiffToolComponent.tsx | 16 ++++-- packages/web/src/features/tools/getDiff.ts | 56 ++++++++++++++++++- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx b/packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx index b25fb6656..7259970cd 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx @@ -2,6 +2,14 @@ import { Separator } from '@/components/ui/separator'; import { GetDiffMetadata, ToolResult } from '@/features/tools'; +import { RepoBadge } from './repoBadge'; + +function truncateSha(ref: string): string { + if (/^[0-9a-f]{40}$/i.test(ref)) { + return ref.substring(0, 7); + } + return ref; +} export const GetDiffToolComponent = ({ metadata }: ToolResult) => { const fileCount = metadata.files.length; @@ -10,16 +18,14 @@ export const GetDiffToolComponent = ({ metadata }: ToolResult)
Compared - {metadata.base} + {truncateSha(metadata.base)} to - {metadata.head} + {truncateSha(metadata.head)} in - - {metadata.repo} - + {fileCount} changed {fileCount === 1 ? 'file' : 'files'} diff --git a/packages/web/src/features/tools/getDiff.ts b/packages/web/src/features/tools/getDiff.ts index 803a9429d..847a8c5ff 100644 --- a/packages/web/src/features/tools/getDiff.ts +++ b/packages/web/src/features/tools/getDiff.ts @@ -4,13 +4,54 @@ import { isServiceError } from '@/lib/utils'; import description from './getDiff.txt'; import { logger } from './logger'; import { ToolDefinition } from './types'; +import { CodeHostType } from '@sourcebot/db'; +import { getRepoInfoByName } from '@/actions'; + +export type GetDiffRepoInfo = { + name: string; + displayName: string; + codeHostType: CodeHostType; +}; export type GetDiffMetadata = GetDiffResult & { repo: string; + repoInfo: GetDiffRepoInfo; base: string; head: string; }; +function formatDiffAsGitDiff(result: GetDiffResult): string { + let output = ''; + + for (const file of result.files) { + const oldPath = file.oldPath ?? '/dev/null'; + const newPath = file.newPath ?? '/dev/null'; + + output += `--- a/${oldPath}\n`; + output += `+++ b/${newPath}\n`; + + for (const hunk of file.hunks) { + const oldStart = hunk.oldRange.start; + const oldLines = hunk.oldRange.lines; + const newStart = hunk.newRange.start; + const newLines = hunk.newRange.lines; + + output += `@@ -${oldStart},${oldLines} +${newStart},${newLines} @@`; + if (hunk.heading) { + output += ` ${hunk.heading}`; + } + output += '\n'; + + output += hunk.body; + if (!hunk.body.endsWith('\n')) { + output += '\n'; + } + } + } + + return output; +} + export const getDiffDefinition: ToolDefinition<'get_diff', typeof getDiffRequestSchema.shape, GetDiffMetadata> = { name: 'get_diff', title: 'Get diff', @@ -27,11 +68,24 @@ export const getDiffDefinition: ToolDefinition<'get_diff', typeof getDiffRequest throw new Error(response.message); } + const repoInfoResult = await getRepoInfoByName(repo); + if (isServiceError(repoInfoResult) || !repoInfoResult) { + throw new Error(`Repository "${repo}" not found.`); + } + const repoInfo: GetDiffRepoInfo = { + name: repoInfoResult.name, + displayName: repoInfoResult.displayName ?? repoInfoResult.name, + codeHostType: repoInfoResult.codeHostType, + }; + + const gitDiffOutput = formatDiffAsGitDiff(response); + return { - output: JSON.stringify(response), + output: gitDiffOutput, metadata: { ...response, repo, + repoInfo, base, head, }, From f66c17e4c482e5eca9f4058675864bffd762ce3a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 17:53:20 +0000 Subject: [PATCH 2/4] test: add unit tests for getDiff git-diff formatting Co-authored-by: Brendan Kellam --- .../web/src/features/tools/getDiff.test.ts | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 packages/web/src/features/tools/getDiff.test.ts diff --git a/packages/web/src/features/tools/getDiff.test.ts b/packages/web/src/features/tools/getDiff.test.ts new file mode 100644 index 000000000..fc44f14ab --- /dev/null +++ b/packages/web/src/features/tools/getDiff.test.ts @@ -0,0 +1,234 @@ +import { describe, it, expect } from 'vitest'; +import { GetDiffResult } from '@/features/git'; + +// Extract the formatting function for testing +function formatDiffAsGitDiff(result: GetDiffResult): string { + let output = ''; + + for (const file of result.files) { + const oldPath = file.oldPath ?? '/dev/null'; + const newPath = file.newPath ?? '/dev/null'; + + output += `--- a/${oldPath}\n`; + output += `+++ b/${newPath}\n`; + + for (const hunk of file.hunks) { + const oldStart = hunk.oldRange.start; + const oldLines = hunk.oldRange.lines; + const newStart = hunk.newRange.start; + const newLines = hunk.newRange.lines; + + output += `@@ -${oldStart},${oldLines} +${newStart},${newLines} @@`; + if (hunk.heading) { + output += ` ${hunk.heading}`; + } + output += '\n'; + + output += hunk.body; + if (!hunk.body.endsWith('\n')) { + output += '\n'; + } + } + } + + return output; +} + +describe('formatDiffAsGitDiff', () => { + it('should format a simple file change correctly', () => { + const input: GetDiffResult = { + files: [ + { + oldPath: 'file.txt', + newPath: 'file.txt', + hunks: [ + { + oldRange: { start: 1, lines: 3 }, + newRange: { start: 1, lines: 4 }, + heading: undefined, + body: ' context line\n-removed line\n+added line\n context line', + }, + ], + }, + ], + }; + + const expected = `--- a/file.txt ++++ b/file.txt +@@ -1,3 +1,4 @@ + context line +-removed line ++added line + context line +`; + + expect(formatDiffAsGitDiff(input)).toBe(expected); + }); + + it('should handle file deletion', () => { + const input: GetDiffResult = { + files: [ + { + oldPath: 'deleted.txt', + newPath: null, + hunks: [ + { + oldRange: { start: 1, lines: 2 }, + newRange: { start: 0, lines: 0 }, + heading: undefined, + body: '-line 1\n-line 2', + }, + ], + }, + ], + }; + + const expected = `--- a/deleted.txt ++++ b//dev/null +@@ -1,2 +0,0 @@ +-line 1 +-line 2 +`; + + expect(formatDiffAsGitDiff(input)).toBe(expected); + }); + + it('should handle file addition', () => { + const input: GetDiffResult = { + files: [ + { + oldPath: null, + newPath: 'new.txt', + hunks: [ + { + oldRange: { start: 0, lines: 0 }, + newRange: { start: 1, lines: 2 }, + heading: undefined, + body: '+line 1\n+line 2', + }, + ], + }, + ], + }; + + const expected = `--- a//dev/null ++++ b/new.txt +@@ -0,0 +1,2 @@ ++line 1 ++line 2 +`; + + expect(formatDiffAsGitDiff(input)).toBe(expected); + }); + + it('should include hunk heading when present', () => { + const input: GetDiffResult = { + files: [ + { + oldPath: 'code.ts', + newPath: 'code.ts', + hunks: [ + { + oldRange: { start: 10, lines: 5 }, + newRange: { start: 10, lines: 6 }, + heading: 'function myFunction()', + body: ' function myFunction() {\n+ console.log("new line");\n return true;\n }', + }, + ], + }, + ], + }; + + const expected = `--- a/code.ts ++++ b/code.ts +@@ -10,5 +10,6 @@ function myFunction() + function myFunction() { ++ console.log("new line"); + return true; + } +`; + + expect(formatDiffAsGitDiff(input)).toBe(expected); + }); + + it('should handle multiple files', () => { + const input: GetDiffResult = { + files: [ + { + oldPath: 'file1.txt', + newPath: 'file1.txt', + hunks: [ + { + oldRange: { start: 1, lines: 1 }, + newRange: { start: 1, lines: 2 }, + heading: undefined, + body: ' old\n+new', + }, + ], + }, + { + oldPath: 'file2.txt', + newPath: 'file2.txt', + hunks: [ + { + oldRange: { start: 1, lines: 1 }, + newRange: { start: 1, lines: 1 }, + heading: undefined, + body: ' unchanged', + }, + ], + }, + ], + }; + + const expected = `--- a/file1.txt ++++ b/file1.txt +@@ -1,1 +1,2 @@ + old ++new +--- a/file2.txt ++++ b/file2.txt +@@ -1,1 +1,1 @@ + unchanged +`; + + expect(formatDiffAsGitDiff(input)).toBe(expected); + }); + + it('should handle multiple hunks in a single file', () => { + const input: GetDiffResult = { + files: [ + { + oldPath: 'file.txt', + newPath: 'file.txt', + hunks: [ + { + oldRange: { start: 1, lines: 2 }, + newRange: { start: 1, lines: 2 }, + heading: undefined, + body: ' line 1\n+line 2', + }, + { + oldRange: { start: 10, lines: 2 }, + newRange: { start: 11, lines: 2 }, + heading: undefined, + body: '-line 10\n line 11', + }, + ], + }, + ], + }; + + const expected = `--- a/file.txt ++++ b/file.txt +@@ -1,2 +1,2 @@ + line 1 ++line 2 +@@ -10,2 +11,2 @@ +-line 10 + line 11 +`; + + expect(formatDiffAsGitDiff(input)).toBe(expected); + }); +}); From 8d1cb5fbfbec8dbf8537d09fa646d57d1ae18358 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 18:08:35 +0000 Subject: [PATCH 3/4] docs: update CHANGELOG for diff tool improvements Co-authored-by: Brendan Kellam --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aad57323..2b97ef9da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Improved diff tool UI to truncate full commit SHAs to 7 characters, use RepoBadge component for repository display, and output diffs in git-diff format instead of JSON for better token efficiency. [#1146](https://github.com/sourcebot-dev/sourcebot/pull/1146) + ## [4.16.14] - 2026-04-21 ### Added From e5f89fba1a082b785b5fdc4d089b565b9619ba5c Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 23 Apr 2026 11:54:43 -0700 Subject: [PATCH 4/4] feedback --- .../chatThread/tools/getDiffToolComponent.tsx | 12 ++++-- packages/web/src/features/tools/getDiff.ts | 33 +-------------- .../tools/{getDiff.test.ts => utils.test.ts} | 41 +++---------------- packages/web/src/features/tools/utils.ts | 33 +++++++++++++++ 4 files changed, 48 insertions(+), 71 deletions(-) rename packages/web/src/features/tools/{getDiff.test.ts => utils.test.ts} (83%) create mode 100644 packages/web/src/features/tools/utils.ts diff --git a/packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx b/packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx index 7259970cd..e52225cf0 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/getDiffToolComponent.tsx @@ -2,11 +2,13 @@ import { Separator } from '@/components/ui/separator'; import { GetDiffMetadata, ToolResult } from '@/features/tools'; +import { GitCommitHorizontalIcon } from 'lucide-react'; import { RepoBadge } from './repoBadge'; function truncateSha(ref: string): string { - if (/^[0-9a-f]{40}$/i.test(ref)) { - return ref.substring(0, 7); + const match = ref.match(/^([0-9a-f]{40})(.*)$/i); + if (match) { + return match[1].substring(0, 7) + match[2]; } return ref; } @@ -17,11 +19,13 @@ export const GetDiffToolComponent = ({ metadata }: ToolResult) return (
Compared - + + {truncateSha(metadata.base)} to - + + {truncateSha(metadata.head)} in diff --git a/packages/web/src/features/tools/getDiff.ts b/packages/web/src/features/tools/getDiff.ts index 847a8c5ff..a19cb668a 100644 --- a/packages/web/src/features/tools/getDiff.ts +++ b/packages/web/src/features/tools/getDiff.ts @@ -2,6 +2,7 @@ import { getDiff, GetDiffResult } from '@/features/git'; import { getDiffRequestSchema } from '@/features/git/schemas'; import { isServiceError } from '@/lib/utils'; import description from './getDiff.txt'; +import { formatDiffAsGitDiff } from './utils'; import { logger } from './logger'; import { ToolDefinition } from './types'; import { CodeHostType } from '@sourcebot/db'; @@ -20,38 +21,6 @@ export type GetDiffMetadata = GetDiffResult & { head: string; }; -function formatDiffAsGitDiff(result: GetDiffResult): string { - let output = ''; - - for (const file of result.files) { - const oldPath = file.oldPath ?? '/dev/null'; - const newPath = file.newPath ?? '/dev/null'; - - output += `--- a/${oldPath}\n`; - output += `+++ b/${newPath}\n`; - - for (const hunk of file.hunks) { - const oldStart = hunk.oldRange.start; - const oldLines = hunk.oldRange.lines; - const newStart = hunk.newRange.start; - const newLines = hunk.newRange.lines; - - output += `@@ -${oldStart},${oldLines} +${newStart},${newLines} @@`; - if (hunk.heading) { - output += ` ${hunk.heading}`; - } - output += '\n'; - - output += hunk.body; - if (!hunk.body.endsWith('\n')) { - output += '\n'; - } - } - } - - return output; -} - export const getDiffDefinition: ToolDefinition<'get_diff', typeof getDiffRequestSchema.shape, GetDiffMetadata> = { name: 'get_diff', title: 'Get diff', diff --git a/packages/web/src/features/tools/getDiff.test.ts b/packages/web/src/features/tools/utils.test.ts similarity index 83% rename from packages/web/src/features/tools/getDiff.test.ts rename to packages/web/src/features/tools/utils.test.ts index fc44f14ab..56bc0796d 100644 --- a/packages/web/src/features/tools/getDiff.test.ts +++ b/packages/web/src/features/tools/utils.test.ts @@ -1,38 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { GetDiffResult } from '@/features/git'; +import type { z } from 'zod'; +import type { getDiffResponseSchema } from '@/features/git/schemas'; +import { formatDiffAsGitDiff } from './utils'; -// Extract the formatting function for testing -function formatDiffAsGitDiff(result: GetDiffResult): string { - let output = ''; - - for (const file of result.files) { - const oldPath = file.oldPath ?? '/dev/null'; - const newPath = file.newPath ?? '/dev/null'; - - output += `--- a/${oldPath}\n`; - output += `+++ b/${newPath}\n`; - - for (const hunk of file.hunks) { - const oldStart = hunk.oldRange.start; - const oldLines = hunk.oldRange.lines; - const newStart = hunk.newRange.start; - const newLines = hunk.newRange.lines; - - output += `@@ -${oldStart},${oldLines} +${newStart},${newLines} @@`; - if (hunk.heading) { - output += ` ${hunk.heading}`; - } - output += '\n'; - - output += hunk.body; - if (!hunk.body.endsWith('\n')) { - output += '\n'; - } - } - } - - return output; -} +type GetDiffResult = z.infer; describe('formatDiffAsGitDiff', () => { it('should format a simple file change correctly', () => { @@ -84,7 +55,7 @@ describe('formatDiffAsGitDiff', () => { }; const expected = `--- a/deleted.txt -+++ b//dev/null ++++ /dev/null @@ -1,2 +0,0 @@ -line 1 -line 2 @@ -111,7 +82,7 @@ describe('formatDiffAsGitDiff', () => { ], }; - const expected = `--- a//dev/null + const expected = `--- /dev/null +++ b/new.txt @@ -0,0 +1,2 @@ +line 1 diff --git a/packages/web/src/features/tools/utils.ts b/packages/web/src/features/tools/utils.ts new file mode 100644 index 000000000..0a704dc93 --- /dev/null +++ b/packages/web/src/features/tools/utils.ts @@ -0,0 +1,33 @@ +import type { z } from 'zod'; +import type { getDiffResponseSchema } from '@/features/git/schemas'; + +type DiffResult = z.infer; + +export function formatDiffAsGitDiff(result: DiffResult): string { + let output = ''; + + for (const file of result.files) { + output += file.oldPath ? `--- a/${file.oldPath}\n` : `--- /dev/null\n`; + output += file.newPath ? `+++ b/${file.newPath}\n` : `+++ /dev/null\n`; + + for (const hunk of file.hunks) { + const oldStart = hunk.oldRange.start; + const oldLines = hunk.oldRange.lines; + const newStart = hunk.newRange.start; + const newLines = hunk.newRange.lines; + + output += `@@ -${oldStart},${oldLines} +${newStart},${newLines} @@`; + if (hunk.heading) { + output += ` ${hunk.heading}`; + } + output += '\n'; + + output += hunk.body; + if (!hunk.body.endsWith('\n')) { + output += '\n'; + } + } + } + + return output; +}