Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Added commit history viewer to code browser. [#1150](https://github.com/sourcebot-dev/sourcebot/pull/1150)
- Added commit diff viewer to code browser. [#1154](https://github.com/sourcebot-dev/sourcebot/pull/1154)
- Added `/api/commits/authors` to the public API to allow fetching a list of authors for a given path and revision. [#1150](https://github.com/sourcebot-dev/sourcebot/pull/1150)
- Added optional `path` query parameter to the `/api/diff` endpoint and `get_diff` MCP tool to restrict diffs to changes touching a specific file. [#1154](https://github.com/sourcebot-dev/sourcebot/pull/1154)

### Fixed
- Bumped `postcss` to `8.5.10`. [#1155](https://github.com/sourcebot-dev/sourcebot/pull/1155)
Expand Down
10 changes: 10 additions & 0 deletions docs/api-reference/sourcebot-public.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,16 @@
"description": "The head git ref (branch, tag, or commit SHA) to diff to.",
"name": "head",
"in": "query"
},
{
"schema": {
"type": "string",
"description": "Restrict the diff to changes touching this file path. Omit to diff all changes between the two refs."
},
"required": false,
"description": "Restrict the diff to changes touching this file path. Omit to diff all changes between the two refs.",
"name": "path",
"in": "query"
}
],
"responses": {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ const options = {

DEBUG_WRITE_CHAT_MESSAGES_TO_FILE: booleanSchema.default('false'),
DEBUG_ENABLE_REACT_SCAN: booleanSchema.default('false'),
DEBUG_ENABLE_REACT_GRAB: booleanSchema.default('false'),

LANGFUSE_SECRET_KEY: z.string().optional(),

Expand Down
1 change: 1 addition & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@codemirror/language": "^6.0.0",
"@codemirror/language-data": "^6.5.1",
"@codemirror/legacy-modes": "^6.4.2",
"@codemirror/merge": "^6.12.1",
"@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.33.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
import { getRepoInfoByName } from "@/actions";
import { PathHeader } from "@/app/(app)/components/pathHeader";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn, getCodeHostInfoForRepo, isServiceError, truncateSha } from "@/lib/utils";
import { X } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { getBrowsePath } from "../../../hooks/utils";
import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
import { getFileSource } from '@/features/git';

interface CodePreviewPanelProps {
path: string;
repoName: string;
revisionName?: string;
// When set, the file's content is fetched at this ref while the
// surrounding browse context (path header) stays at `revisionName`.
previewRef?: string;
}

export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePreviewPanelProps) => {
export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRef }: CodePreviewPanelProps) => {
const contentRef = previewRef ?? revisionName;

const [fileSourceResponse, repoInfoResponse] = await Promise.all([
getFileSource({
path,
repo: repoName,
ref: revisionName,
ref: contentRef,
}, { source: 'sourcebot-web-client' }),
getRepoInfoByName(repoName),
]);
Expand Down Expand Up @@ -53,7 +63,7 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePre
displayName: repoInfoResponse.displayName,
externalWebUrl: repoInfoResponse.externalWebUrl,
}}
revisionName={revisionName}
revisionName={contentRef}
/>

{fileWebUrl && (
Expand All @@ -74,12 +84,54 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePre
)}
</div>
<Separator />
{previewRef && (
<div className="flex flex-row items-center justify-between gap-2 px-4 py-2 border-b shrink-0">
<span className="text-sm">
Previewing file at revision{" "}
<Link
href={getBrowsePath({
repoName,
revisionName,
path: '',
pathType: 'commit',
commitSha: previewRef,
})}
className="font-mono text-link hover:underline"
>
{truncateSha(previewRef)}
</Link>
</span>
<Tooltip key={previewRef}>
<TooltipTrigger>
<Button
asChild
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground"
>
<Link
href={getBrowsePath({
repoName,
revisionName,
path,
pathType: 'blob',
})}
aria-label="Close preview"
>
<X className="h-4 w-4" />
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Close preview</TooltipContent>
</Tooltip>
</div>
)}
<PureCodePreviewPanel
source={fileSourceResponse.source}
language={fileSourceResponse.language}
repoName={repoName}
path={path}
revisionName={revisionName ?? 'HEAD'}
revisionName={contentRef ?? 'HEAD'}
/>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
import { search } from "@codemirror/search";
import CodeMirror, { EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, ViewUpdate } from "@uiw/react-codemirror";
import { useEffect, useMemo, useState } from "react";
import { EditorContextMenu } from "../../../components/editorContextMenu";
import { BrowseHighlightRange, HIGHLIGHT_RANGE_QUERY_PARAM } from "../../hooks/utils";
import { EditorContextMenu } from "@/app/(app)/components/editorContextMenu";
import { BrowseHighlightRange, HIGHLIGHT_RANGE_QUERY_PARAM } from "@/app/(app)/browse/hooks/utils";
import { rangeHighlightingExtension } from "./rangeHighlightingExtension";

interface PureCodePreviewPanelProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { StateField, Range } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView } from "@codemirror/view";
import { BrowseHighlightRange } from "../../hooks/utils";
import { BrowseHighlightRange } from "@/app/(app)/browse/hooks/utils";

const markDecoration = Decoration.mark({
class: "searchMatch-selected",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client';

import { CopyIconButton } from "@/app/(app)/components/copyIconButton";
import { useToast } from "@/components/hooks/use-toast";
import Link from "next/link";
import { Fragment, useCallback } from "react";
import { getBrowsePath } from "../../../hooks/utils";

interface CommitHashLineProps {
repoName: string;
commitHash: string;
parents: string[];
}

export const CommitHashLine = ({ repoName, commitHash, parents }: CommitHashLineProps) => {
const { toast } = useToast();

const onCopyHash = useCallback(() => {
navigator.clipboard.writeText(commitHash).then(() => {
toast({ description: "✅ Copied commit SHA to clipboard" });
});
return true;
}, [commitHash, toast]);

return (
<div className="text-xs font-mono text-muted-foreground flex flex-row items-center gap-1">
{parents.length > 0 && (
<>
<span>
{parents.length} parent{parents.length > 1 ? 's' : ''}
</span>
{parents.map((parent, i) => (
<Fragment key={parent}>
{i > 0 && <span>+</span>}
<Link
href={getBrowsePath({
repoName,
path: '',
pathType: 'commit',
commitSha: parent,
})}
className="underline hover:text-foreground"

@softlight softlight Bot Jun 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before/after suggested changes

Parent commit links stay underlined at rest

In the commit details line, the parent commit links are always underlined, while every other commit and revision link in the file browser only underlines when you hover. Matching the hover-only style would make these links feel consistent and a little less busy in the dense metadata row.

Prompt to fix with AI
This is a comment left during a design review.

Comment:
**Parent commit links stay underlined at rest**
In the commit details line, the parent commit links are always underlined, while every other commit and revision link in the file browser only underlines when you hover. Matching the hover-only style would make these links feel consistent and a little less busy in the dense metadata row.

Issue:
commitHashLine.tsx L42: parent links use `className="underline hover:text-foreground"` — underline is always on and the only hover affordance is a color change.

Suggested fix:
Change the parent link className in commitHashLine.tsx from `underline hover:text-foreground` to `hover:underline` (optionally keeping the muted color), matching the historyRow and pathHeader SHA links.

Screenshots:
- Before: https://drive.orianna.ai/ea54dfe382fef731164751c0557c09da.png
- After: https://drive.orianna.ai/3201e26f79e7cbccfe50d320b0ee8b94.png
Use these screenshot URLs as visual evidence if your environment can open remote images.

How can I resolve this? Keep the fix scoped to the PR-touched UI and preserve existing design-system patterns.
Developer output
  • Crop source: goal crop locator
  • Before crop: x1=290, y1=95, x2=720, y2=175
  • After crop: x1=290, y1=95, x2=720, y2=175

Gridded crops

Before

Before gridded detail

After

After gridded detail

Full gridded screenshots

Before

Before full gridded screenshot

After

After full gridded screenshot

title={parent}
>
{parent.slice(0, 7)}
</Link>
</Fragment>
))}
</>
)}
<span>commit {commitHash.slice(0, 7)}</span>
<CopyIconButton onCopy={onCopyHash} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import { CommitBody, CommitBodyToggle } from "@/app/(app)/browse/components/commitParts";
import { useState } from "react";

interface CommitMessageProps {
subject: string;
body: string;
}

export const CommitMessage = ({ subject, body }: CommitMessageProps) => {
const [isBodyExpanded, setIsBodyExpanded] = useState(false);
const hasBody = body.trim().length > 0;

return (
<>
<div className="flex flex-row items-center gap-2">
<h1 className="text-lg font-semibold">{subject}</h1>
{hasBody && (
<CommitBodyToggle
pressed={isBodyExpanded}
onPressedChange={setIsBodyExpanded}
/>
)}
</div>
{hasBody && isBodyExpanded && (
<CommitBody body={body} className="rounded max-h-[40vh] overflow-y-auto" />

@softlight softlight Bot Jun 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before/after suggested changes

Expanded commit description hugs the title with no breathing room

When you expand the commit description on the commit page, the description panel butts directly against the commit title with no separation, while the author and commit lines below it have clear spacing. A small gap above the description matches the surrounding rhythm and makes the expanded state feel balanced rather than crowded.

Prompt to fix with AI
This is a comment left during a design review.

Comment:
**Expanded commit description hugs the title with no breathing room**
When you expand the commit description on the commit page, the description panel butts directly against the commit title with no separation, while the author and commit lines below it have clear spacing. A small gap above the description matches the surrounding rhythm and makes the expanded state feel balanced rather than crowded.

Issue:
When the commit description is expanded, the rounded, tinted CommitBody panel sits flush against the bottom of the commit subject (h1) with 0px separation, while the author row and hash row below it are separated by the header's gap-2 (8px). The expanded body is nested inside the subject row, so it bypasses the header's vertical gap, producing an uneven rhythm: the description card touches the title above but has space below it.

Suggested fix:
Add `mt-2` to the expanded CommitBody className in commitMessage.tsx (`"mt-2 rounded max-h-[40vh] overflow-y-auto"`) so the description panel is separated from the subject by the same 8px the header already uses between its other stacked rows. This is scoped to the commit page; the commit-history-row usage passes its own className and is unaffected.

Screenshots:
- Before: https://drive.orianna.ai/5a5da0ebe9e8c273cab2e15869124637.png
- After: https://drive.orianna.ai/9b9a505e15f63f8f6806f86d059e6fb2.png
Use these screenshot URLs as visual evidence if your environment can open remote images.

How can I resolve this? Keep the fix scoped to the PR-touched UI and preserve existing design-system patterns.
Developer output
  • Crop source: goal crop locator
  • Before crop: x1=295, y1=48, x2=915, y2=300
  • After crop: x1=295, y1=48, x2=915, y2=300

Gridded crops

Before

Before gridded detail

After

After gridded detail

Full gridded screenshots

Before

Before full gridded screenshot

After

After full gridded screenshot

)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { FileDiff } from "@/features/git";

const TOTAL_SQUARES = 5;

// Count `+`/`-` lines across all hunks in a file.
export const computeChangeCounts = (file: FileDiff) => {
let additions = 0;
let deletions = 0;
for (const hunk of file.hunks) {
for (const raw of hunk.body.split('\n')) {
if (raw.startsWith('+')) {
additions++;
} else if (raw.startsWith('-')) {
deletions++;
}
}
}
return { additions, deletions };
};

// Sum line-level change counts across multiple files.
export const computeTotalChangeCounts = (files: FileDiff[]) => {
let additions = 0;
let deletions = 0;
for (const file of files) {
const counts = computeChangeCounts(file);
additions += counts.additions;
deletions += counts.deletions;
}
return { additions, deletions };
};

// Map a total change count to a number of filled squares (0–5) using a
// log-ish scale so tiny diffs still show one square and huge diffs cap out.
// Mirrors GitHub's diffstat indicator behavior.
const filledSquaresForTotal = (total: number): number => {
if (total === 0) {
return 0;
}
if (total < 5) {
return 1;
}
if (total < 10) {
return 2;
}
if (total < 30) {
return 3;
}
if (total < 100) {
return 4;
}
return 5;
};

interface DiffStatProps {
additions: number;
deletions: number;
}

export const DiffStat = ({ additions, deletions }: DiffStatProps) => {
const total = additions + deletions;

// Skip rendering when there are no line-level changes (e.g. pure renames).
if (total === 0) {
return null;
}

const filled = filledSquaresForTotal(total);
const greenCount = Math.round((filled * additions) / total);

@softlight softlight Bot Jun 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before/after suggested changes

Change dots can hide that a file also had deletions

On a file with one line added and one line removed, the little change-indicator squares show only the 'added' color, so at a glance the file looks purely additive even though its own +/- count clearly shows a removed line. The squares should agree with the count beside them.

Prompt to fix with AI
This is a comment left during a design review.

Comment:
**Change dots can hide that a file also had deletions**
On a file with one line added and one line removed, the little change-indicator squares show only the 'added' color, so at a glance the file looks purely additive even though its own +/- count clearly shows a removed line. The squares should agree with the count beside them.

Issue:
DiffStat computes greenCount = Math.round((filled * additions) / total) and redCount = filled - greenCount. For a file with 1 addition and 1 deletion, filledSquaresForTotal(2) = 1 and Math.round(1*1/2) = 1, so greenCount = 1, redCount = 0, and the single filled square renders green (bg-green-500) with no red square, even though the same row prints '+1 -1'.

Suggested fix:
Allocate at least one square to each non-zero side before distributing the remainder (e.g. start greenCount/redCount at 1 when additions/deletions > 0 and filled permits, then split the leftover proportionally), so a file with deletions never shows an all-green single-square indicator.

Screenshots:
- Before: https://drive.orianna.ai/30c9f189aa320beeda74aed1c7405a05.png
- After: https://drive.orianna.ai/451d723bf71f0ed0729ceae48bf645be.png
Use these screenshot URLs as visual evidence if your environment can open remote images.

How can I resolve this? Keep the fix scoped to the PR-touched UI and preserve existing design-system patterns.
Developer output
  • Crop source: goal crop locator
  • Before crop: x1=1180, y1=196, x2=1440, y2=256
  • After crop: x1=1180, y1=196, x2=1440, y2=256

Gridded crops

Before

Before gridded detail

After

After gridded detail

Full gridded screenshots

Before

Before full gridded screenshot

After

After full gridded screenshot

const redCount = filled - greenCount;
const emptyCount = TOTAL_SQUARES - filled;

return (
<div
className="flex flex-row items-center gap-2 text-xs flex-shrink-0 font-mono"
title={`${additions} additions, ${deletions} deletions`}
>
{additions > 0 && (
<span className="text-green-700 dark:text-green-400">+{additions}</span>
)}
{deletions > 0 && (
<span className="text-red-700 dark:text-red-400">-{deletions}</span>
)}
<div className="flex flex-row gap-px">
{Array.from({ length: greenCount }).map((_, i) => (
<span key={`g-${i}`} className="w-2 h-2 bg-green-500 dark:bg-green-400 rounded-[1px]" />
))}
{Array.from({ length: redCount }).map((_, i) => (
<span key={`r-${i}`} className="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-[1px]" />
))}
{Array.from({ length: emptyCount }).map((_, i) => (
<span key={`e-${i}`} className="w-2 h-2 bg-border rounded-[1px]" />
))}
</div>
</div>
);
};
Loading