Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GetDiffMetadata>) => {
const fileCount = metadata.files.length;
Expand All @@ -10,16 +18,14 @@ export const GetDiffToolComponent = ({ metadata }: ToolResult<GetDiffMetadata>)
<div className="flex items-center gap-2 select-none cursor-default text-sm text-muted-foreground">
<span className="flex-shrink-0">Compared</span>
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded text-foreground">
{metadata.base}
{truncateSha(metadata.base)}
</span>
<span className="flex-shrink-0">to</span>
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded text-foreground">
{metadata.head}
{truncateSha(metadata.head)}
</span>
<span className="flex-shrink-0">in</span>
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded text-foreground truncate">
{metadata.repo}
</span>
<RepoBadge repo={metadata.repoInfo} />
<span className="flex-1" />
<span className="text-xs flex-shrink-0">
{fileCount} changed {fileCount === 1 ? 'file' : 'files'}
Expand Down
234 changes: 234 additions & 0 deletions packages/web/src/features/tools/getDiff.test.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Comment thread
brendan-kellam marked this conversation as resolved.
Outdated

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);
});
});
56 changes: 55 additions & 1 deletion packages/web/src/features/tools/getDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Comment thread
brendan-kellam marked this conversation as resolved.
Outdated

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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

export const getDiffDefinition: ToolDefinition<'get_diff', typeof getDiffRequestSchema.shape, GetDiffMetadata> = {
name: 'get_diff',
title: 'Get diff',
Expand All @@ -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,
},
Expand Down
Loading