From 74f3951c3d86c7463ee85ecb558577810f7e6e28 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 12 Aug 2026 15:37:33 -0700 Subject: [PATCH 1/2] fix(web): prevent stalled search result navigation --- .../src/app/(app)/components/pathHeader.tsx | 4 + .../searchResultsPanel/fileMatch.test.tsx | 93 +++++++++++++++++++ .../searchResultsPanel/fileMatch.tsx | 21 ++++- .../searchResultsPanel/fileMatchContainer.tsx | 8 +- 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 packages/web/src/app/(app)/search/components/searchResultsPanel/fileMatch.test.tsx 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 +} From fa0343c7b86b557268a598693bc71578d55b74af Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 12 Aug 2026 15:38:10 -0700 Subject: [PATCH 2/2] chore: update changelog for #1574 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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