diff --git a/CHANGELOG.md b/CHANGELOG.md index d487b5b8c..ec0c850f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Upgraded `brace-expansion` to `^1.1.17`/`^2.1.3`/`^5.0.8`. [#1527](https://github.com/sourcebot-dev/sourcebot/pull/1527) +- The browse file-tree panel now shows a clear "This repository is empty." (root) or "This path has no contents in this revision." (sub-path) message when the folder contents are empty, and a dedicated "Repository not found." message for 404s, instead of the generic "Error loading tree preview" or a blank scroll area. The sub-path message is intentionally ambiguous because `git ls-tree` returns empty output for both empty directories and paths that don't exist in the tree, so we can't tell the cases apart at this layer. [#1530](https://github.com/sourcebot-dev/sourcebot/pull/1530) ## [5.1.5] - 2026-07-31 diff --git a/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.test.tsx b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.test.tsx new file mode 100644 index 000000000..32c449eef --- /dev/null +++ b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.test.tsx @@ -0,0 +1,46 @@ +import { describe, expect, test, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { SidebarProvider } from '@/components/ui/sidebar'; + +// Stub the file-tree item component so the test only exercises the +// empty-state branch. The real component pulls in `usePathname` and +// a router context that's heavy to mock for an empty-state assertion. +vi.mock('@/app/(app)/browse/components/fileTreeItemComponent', () => ({ + FileTreeItemComponent: () =>
  • , +})); + +vi.mock('next/navigation', () => ({ + usePathname: () => '/browse/github.com/foo/bar@main/-/tree/src', +})); + +vi.mock('@/hooks/use-mobile', () => ({ + useIsMobile: () => false, +})); + +const { PureTreePreviewPanel } = await import('./pureTreePreviewPanel'); + +const wrap = (ui: React.ReactNode) => ( + + + +); + +describe('PureTreePreviewPanel empty state (issue #1530)', () => { + test('renders an honest empty-state message when the items array is empty', () => { + // The empty-state message is rendered inside the same + // ScrollArea as the items, so a real "directory" sub-path + // (path != '') still shows the path header above the panel + // and the empty-state copy below. The PureTree component + // doesn't know about the path; the caller decides whether + // to render the full panel (path != '') or the full-panel + // "This repository is empty" copy (path == ''). + // + // We can't tell from `[]` alone whether the directory is + // empty or the path doesn't exist in this revision + // (git ls-tree returns empty output for both cases), so the + // message is intentionally ambiguous. Bugbot finding on PR + // #1531. + render(wrap()); + expect(screen.getByText('This path has no contents in this revision.')).toBeTruthy(); + }); +}); diff --git a/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.tsx b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.tsx index 3f7c0f473..32d426885 100644 --- a/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/pureTreePreviewPanel.tsx @@ -20,22 +20,34 @@ export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => { className="flex flex-col p-0.5" ref={scrollAreaRef} > - {items.map((item) => ( - - ))} + {items.length === 0 ? ( + // We can't tell from `[]` alone whether the directory + // is empty or the path doesn't exist in this revision + // (git ls-tree returns empty output for both cases). The + // message is intentionally ambiguous — the user can + // verify the path by looking at the repo on the code + // host. Bugbot finding on PR #1531. +
    + This path has no contents in this revision. +
    + ) : ( + items.map((item) => ( + + )) + )} ) } \ No newline at end of file diff --git a/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanel.tsx b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanel.tsx index 400c488f5..662a509cd 100644 --- a/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanel.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanel.tsx @@ -12,6 +12,14 @@ export const TreePreviewPanel = async ({ path, repoName, revisionName }: TreePre const repoInfoResponse = await getRepoInfoByName(repoName); if (isServiceError(repoInfoResponse)) { + // A 404 means the user hit a typo in a repo name (or a config + // that's no longer indexed). A different status code is an + // actual system error. Showing the same generic message for + // both confuses users — the typo case looks like a sync + // problem. See issue #1530. + if (repoInfoResponse.errorCode === 'NOT_FOUND') { + return
    Repository not found.
    ; + } return
    Error loading tree preview
    } diff --git a/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanelClient.test.tsx b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanelClient.test.tsx new file mode 100644 index 000000000..5f3d5131c --- /dev/null +++ b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanelClient.test.tsx @@ -0,0 +1,89 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; + +// Stub the folder-contents client and the path header so the test +// only exercises the empty-state branches in treePreviewPanelClient. +// The folder-contents client makes a real network call; the path +// header pulls in router context. Both are irrelevant for the +// empty-state assertions. +vi.mock('@/app/api/(client)/client', () => ({ + getFolderContents: vi.fn(), +})); + +vi.mock('@/app/(app)/components/pathHeader', () => ({ + PathHeader: () =>
    , +})); + +vi.mock('@/components/ui/separator', () => ({ + Separator: () =>
    , +})); + +vi.mock('@/components/ui/skeleton', () => ({ + Skeleton: () =>
    , +})); + +vi.mock('next/navigation', () => ({ + usePathname: () => '/browse/github.com/foo/bar@main/-/tree/src', +})); + +vi.mock('@tanstack/react-query', () => ({ + useQuery: vi.fn(), +})); + +vi.mock('react-hotkeys-hook', () => ({ + useHotkeys: vi.fn(), +})); + +const mockQueryState = vi.hoisted(() => ({ + current: { data: undefined as unknown, isPending: false, isError: false }, +})); + +vi.mock('@tanstack/react-query', () => ({ + useQuery: () => mockQueryState.current, +})); + +const { TreePreviewPanelClient } = await import('./treePreviewPanelClient'); + +const baseRepo = { name: 'github.com/foo/bar', codeHostType: 'github' as const }; + +const renderClient = (opts: { path: string; revisionName?: string }) => { + return render( + + ); +}; + +describe('TreePreviewPanelClient empty-state branches (issue #1530)', () => { + afterEach(() => { + mockQueryState.current = { data: undefined, isPending: false, isError: false }; + }); + + test('renders the "This repository is empty." message at the root when the response is []', () => { + // The root-path case takes the full panel (no PathHeader, no + // Separator). The "directory" copy from PureTreePreviewPanel + // is NOT used here — the root case is more emphatic and + // accurate. + mockQueryState.current = { data: [], isPending: false, isError: false }; + renderClient({ path: '' }); + expect(screen.getByText('This repository is empty.')).toBeTruthy(); + expect(screen.queryByTestId('path-header')).toBeNull(); + expect(screen.queryByTestId('separator')).toBeNull(); + }); + + test('renders the "Error loading tree preview" message on a non-404 service error', () => { + // The folder-contents client returns the service error + // object for any non-NOT_FOUND error. Bugbot finding on + // PR #1531: the non-404 case keeps the existing copy. + mockQueryState.current = { + data: { statusCode: 500, errorCode: 'UNEXPECTED_ERROR', message: 'boom' }, + isPending: false, + isError: false, + }; + renderClient({ path: 'src' }); + expect(screen.getByText('Error loading tree preview')).toBeTruthy(); + }); +}); diff --git a/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanelClient.tsx b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanelClient.tsx index afb3f888b..99937ab6e 100644 --- a/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanelClient.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel/treePreviewPanelClient.tsx @@ -39,6 +39,22 @@ export const TreePreviewPanelClient = ({ path, repoName, revisionName, repo }: T return
    Error loading tree preview
    } + // Distinguish "the repository has no files" from "this sub-directory + // has no files" at the root path. The latter is handled inside + // `PureTreePreviewPanel` with a "This directory is empty." copy; + // the former is a stronger message and avoids rendering a path + // header for a non-existent root. See issue #1530. + if (folderContentsResponse.length === 0 && path === '') { + return ( +
    +

    This repository is empty.

    +

    + Once a commit lands on the default branch, its files will appear here. +

    +
    + ); + } + return ( <>