diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1b90bc77a..dc40e57d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed the web process being capped at a ~4GiB heap regardless of how much memory the container has, which caused multi-second garbage collection pauses on larger deployments. [#1569](https://github.com/sourcebot-dev/sourcebot/pull/1569)
- Upgraded `@sentry/*` to `^10.70.0`, fixing memory leaks where spans retained request data indefinitely. [#1572](https://github.com/sourcebot-dev/sourcebot/pull/1572)
+- Fixed code search result links occasionally getting stuck during navigation and restored Cmd/Ctrl-click to open matches in preview. [#1574](https://github.com/sourcebot-dev/sourcebot/pull/1574)
## [5.1.6] - 2026-08-10
diff --git a/packages/web/src/app/(app)/components/pathHeader.tsx b/packages/web/src/app/(app)/components/pathHeader.tsx
index 924a35541..bd89253d1 100644
--- a/packages/web/src/app/(app)/components/pathHeader.tsx
+++ b/packages/web/src/app/(app)/components/pathHeader.tsx
@@ -230,6 +230,7 @@ export const PathHeader = ({
@
{hiddenSegments.map((segment) => (
)}
({
+ default: ({ children, href, prefetch, ...props }: React.AnchorHTMLAttributes & { prefetch?: boolean }) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock('@/app/(app)/components/lightweightCodeHighlighter', () => ({
+ LightweightCodeHighlighter: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+
+const file = {
+ fileName: {
+ text: 'src/index.ts',
+ matchRanges: [],
+ },
+ webUrl: '',
+ repository: 'github.com/sourcebot-dev/sourcebot',
+ repositoryId: 1,
+ language: 'TypeScript',
+ branches: ['main'],
+ chunks: [],
+};
+
+const match = {
+ content: 'const result = true;',
+ contentStart: {
+ byteOffset: 0,
+ lineNumber: 10,
+ column: 1,
+ },
+ matchRanges: [{
+ start: {
+ byteOffset: 6,
+ lineNumber: 10,
+ column: 7,
+ },
+ end: {
+ byteOffset: 12,
+ lineNumber: 10,
+ column: 13,
+ },
+ }],
+};
+
+afterEach(() => {
+ cleanup();
+});
+
+describe('FileMatch', () => {
+ it('disables prefetching and preserves ordinary link clicks', () => {
+ const onOpenPreview = vi.fn();
+ render();
+
+ const link = screen.getByRole('link');
+ expect(link.getAttribute('data-prefetch')).toBe('false');
+
+ fireEvent.click(link);
+ expect(onOpenPreview).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['Cmd', { metaKey: true }],
+ ['Ctrl', { ctrlKey: true }],
+ ])('opens the preview and cancels navigation for %s-click', (_modifier, eventInit) => {
+ const onOpenPreview = vi.fn();
+ render();
+
+ const link = screen.getByRole('link');
+ const clickEvent = createEvent.click(link, eventInit);
+ fireEvent(link, clickEvent);
+
+ expect(clickEvent.defaultPrevented).toBe(true);
+ expect(onOpenPreview).toHaveBeenCalledOnce();
+ });
+
+ it('supports modifier-plus-Enter for keyboard users', () => {
+ const onOpenPreview = vi.fn();
+ render();
+
+ const link = screen.getByRole('link');
+ const keyDownEvent = createEvent.keyDown(link, { key: 'Enter', metaKey: true });
+ fireEvent(link, keyDownEvent);
+
+ expect(keyDownEvent.defaultPrevented).toBe(true);
+ expect(onOpenPreview).toHaveBeenCalledOnce();
+ });
+});
diff --git a/packages/web/src/app/(app)/search/components/searchResultsPanel/fileMatch.tsx b/packages/web/src/app/(app)/search/components/searchResultsPanel/fileMatch.tsx
index eba49587f..a53ffb2c8 100644
--- a/packages/web/src/app/(app)/search/components/searchResultsPanel/fileMatch.tsx
+++ b/packages/web/src/app/(app)/search/components/searchResultsPanel/fileMatch.tsx
@@ -9,11 +9,13 @@ import { getBrowsePath } from "@/app/(app)/browse/hooks/utils";
interface FileMatchProps {
match: SearchResultChunk;
file: SearchResultFile;
+ onOpenPreview: () => void;
}
export const FileMatch = ({
match,
file,
+ onOpenPreview,
}: FileMatchProps) => {
// If it's just the title, don't show a code preview
if (match.matchRanges.length === 0) {
@@ -24,6 +26,7 @@ export const FileMatch = ({
{
+ if (!event.metaKey && !event.ctrlKey) {
+ return;
+ }
+
+ event.preventDefault();
+ onOpenPreview();
+ }}
+ onKeyDown={(event) => {
+ if (event.key !== 'Enter' || (!event.metaKey && !event.ctrlKey)) {
+ return;
+ }
+
+ event.preventDefault();
+ onOpenPreview();
+ }}
title="open file: click, open file preview: cmd/ctrl + click"
>
);
-}
\ No newline at end of file
+}
diff --git a/packages/web/src/app/(app)/search/components/searchResultsPanel/fileMatchContainer.tsx b/packages/web/src/app/(app)/search/components/searchResultsPanel/fileMatchContainer.tsx
index 1c5bf60dc..e134dd4e6 100644
--- a/packages/web/src/app/(app)/search/components/searchResultsPanel/fileMatchContainer.tsx
+++ b/packages/web/src/app/(app)/search/components/searchResultsPanel/fileMatchContainer.tsx
@@ -123,6 +123,12 @@ export const FileMatchContainer = ({
{
+ const matchIndex = matches.slice(0, index).reduce((acc, previousMatch) => {
+ return acc + previousMatch.matchRanges.length;
+ }, 0);
+ onOpenFilePreview(matchIndex);
+ }}
/>
{(index !== matches.length - 1 || isMoreContentButtonVisible) && (
@@ -153,4 +159,4 @@ export const FileMatchContainer = ({
)}
);
-}
\ No newline at end of file
+}