fix(git): resolve blank nested-repo surfaces; add repository pickers

The resolution gate on the pull-request, walkthrough, and mobile changes
surfaces stayed on forever (rootIsGitRepo stays false on a non-repo
root) while NestedRepoResolutionStates exits once the selected
repository probes as a repository, so those surfaces rendered nothing.
The gate now shows resolution states only while the operating directory
has not proven to be a repository, matching GitView.

Extract GitHeader's repository switcher into git/NestedRepoPicker and
mount it in the diff toolbar, a new slim header in the pull-request
view, the walkthrough header, and the mobile changes header. The pick
is shared per root, so every surface follows.

The walkthrough tab mounts keep-alive and hidden; it now receives a
visible prop so discovery waits until the tab is actually opened. Add
component tests for the shared resolution states.
This commit is contained in:
jaygupta17
2026-08-25 20:47:02 +05:30
parent e26b55e067
commit 11a48958e8
9 changed files with 241 additions and 67 deletions
@@ -12,12 +12,7 @@ import type { IconName } from "@/components/icon/icons";
import { BranchSelector } from './BranchSelector';
import { WorktreeBranchDisplay } from './WorktreeBranchDisplay';
import { SyncActions } from './SyncActions';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '@/components/ui/select';
import { NestedRepoPicker } from './NestedRepoPicker';
import type {
GitStatus,
GitIdentityProfile,
@@ -282,11 +277,6 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
}
const repositoryOptionsForPicker = (repositoryOptions ?? []).filter(Boolean);
const repositoryRelativePath = (repository: string): string => {
const rootPrefix = `${repositoryRoot ?? ''}/`;
return repository.startsWith(rootPrefix) ? repository.slice(rootPrefix.length) : repository;
};
const repositoryLabel = selectedRepository ? repositoryRelativePath(selectedRepository) : '';
const managementButtons = (
<div className="flex items-center gap-1 shrink-0">
@@ -451,33 +441,13 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
remotes={remotes}
/>
)}
{repositoryOptionsForPicker.length > 0 ? (
<Select
value={selectedRepository ?? undefined}
onValueChange={(value) => {
if (value && onSelectRepository) {
onSelectRepository(value);
}
}}
>
<SelectTrigger
size="sm"
className="max-w-[13rem] gap-1.5 px-2 py-1"
aria-label={t('gitView.empty.selectRepositoryPlaceholder')}
>
<Icon name="folder-3" className="size-4 text-muted-foreground" />
<span className="min-w-0 truncate font-medium text-left">
{repositoryLabel}
</span>
</SelectTrigger>
<SelectContent align="start">
{repositoryOptionsForPicker.map((repository) => (
<SelectItem key={repository} value={repository}>
<span className="truncate">{repositoryRelativePath(repository)}</span>
</SelectItem>
))}
</SelectContent>
</Select>
{repositoryOptionsForPicker.length > 0 && onSelectRepository ? (
<NestedRepoPicker
repositories={repositoryOptionsForPicker}
selectedRepository={selectedRepository ?? null}
onSelectRepository={onSelectRepository}
repositoryRoot={repositoryRoot}
/>
) : null}
</div>
<div className="flex shrink-0 items-center gap-1">
@@ -0,0 +1,67 @@
import React from 'react';
import { Icon } from '@/components/icon/Icon';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '@/components/ui/select';
import { useI18n } from '@/lib/i18n';
type NestedRepoPickerProps = {
/** Discovered repository paths under the project root. */
repositories: string[];
/** Currently selected repository path (the operating directory). */
selectedRepository: string | null;
onSelectRepository: (repository: string) => void;
/** Root the repository paths are relative to for display labels. */
repositoryRoot?: string;
};
/**
* Repository switcher shown on git surfaces when a project root is not itself
* a git repository but nested repositories were discovered under it.
*/
export const NestedRepoPicker: React.FC<NestedRepoPickerProps> = ({
repositories,
selectedRepository,
onSelectRepository,
repositoryRoot,
}) => {
const { t } = useI18n();
const relativePath = (repository: string): string => {
const rootPrefix = `${repositoryRoot ?? ''}/`;
return repository.startsWith(rootPrefix) ? repository.slice(rootPrefix.length) : repository;
};
return (
<Select
value={selectedRepository ?? undefined}
onValueChange={(value) => {
if (value) {
onSelectRepository(value);
}
}}
>
<SelectTrigger
size="sm"
className="max-w-[13rem] gap-1.5 px-2 py-1"
aria-label={t('gitView.empty.selectRepositoryPlaceholder')}
>
<Icon name="folder-3" className="size-4 text-muted-foreground" />
<span className="min-w-0 truncate font-medium text-left">
{selectedRepository ? relativePath(selectedRepository) : ''}
</span>
</SelectTrigger>
<SelectContent align="start">
{repositories.map((repository) => (
<SelectItem key={repository} value={repository}>
<span className="truncate">{relativePath(repository)}</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
};
@@ -0,0 +1,69 @@
import React from 'react';
import { describe, expect, test } from 'bun:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { I18nProvider } from '@/lib/i18n';
import { NestedRepoResolutionStates } from './NestedRepoResolutionStates';
const render = (props: React.ComponentProps<typeof NestedRepoResolutionStates>): string =>
renderToStaticMarkup(
<I18nProvider>
<NestedRepoResolutionStates {...props} />
</I18nProvider>,
);
const baseProps = {
onRetryDiscovery: () => {},
};
describe('NestedRepoResolutionStates', () => {
test('renders nothing while the root has not probed as a non-repository', () => {
for (const rootIsGitRepo of [null, true] as const) {
const markup = render({ ...baseProps, rootIsGitRepo, resolvedIsGitRepo: null, nestedRepos: undefined });
expect(markup).toBe('');
}
});
test('renders nothing once the operating directory resolved as a repository', () => {
const markup = render({
...baseProps,
rootIsGitRepo: false,
resolvedIsGitRepo: true,
nestedRepos: ['/root/one'],
});
expect(markup).toBe('');
});
test('shows the discovering state before discovery has run', () => {
const markup = render({ ...baseProps, rootIsGitRepo: false, resolvedIsGitRepo: null, nestedRepos: undefined });
expect(markup).toContain('Looking for Git repositories...');
});
test('shows the failure state with a retry when discovery failed', () => {
const markup = render({ ...baseProps, rootIsGitRepo: false, resolvedIsGitRepo: null, nestedRepos: null });
expect(markup).toContain('Could not scan for Git repositories');
expect(markup).toContain('Retry');
});
test('shows the plain not-a-repository state with no retry when unsupported', () => {
const markup = render({ ...baseProps, rootIsGitRepo: false, resolvedIsGitRepo: null, nestedRepos: 'unsupported' });
expect(markup).toContain('This directory is not a Git repository');
expect(markup).not.toContain('Retry');
});
test('treats an empty discovery like the not-a-repository state', () => {
const markup = render({ ...baseProps, rootIsGitRepo: false, resolvedIsGitRepo: null, nestedRepos: [] });
expect(markup).toContain('This directory is not a Git repository');
});
test('holds a checking state while repositories are found but unresolved', () => {
const markup = render({
...baseProps,
rootIsGitRepo: false,
resolvedIsGitRepo: null,
nestedRepos: ['/root/one', '/root/two'],
});
expect(markup).toContain('Checking repository...');
});
});