feat(git): support nested git repositories in the Git tab

When the project root is not itself a git repository, discover nested
repositories (depth- and visit-capped readdir walk via a new
/api/fs/git-dirs route), auto-select the first one, and show a repository
picker next to the branch dropdown to switch. Selections persist per
runtime and root; discovery failure is a distinct marker with a retry
action, never an empty success.
This commit is contained in:
jaygupta17
2026-08-08 19:08:42 +05:30
parent 4faf660a38
commit 9c9c28a552
20 changed files with 989 additions and 191 deletions
@@ -12,6 +12,12 @@ 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 type {
GitStatus,
GitIdentityProfile,
@@ -51,6 +57,13 @@ interface GitHeaderProps {
pullRequest?: GitHubPullRequest | null;
prChecks?: GitHubChecksSummary | null;
onOpenPullRequest?: () => void;
// Nested repository picker: shown when the Git tab operates on a repository
// nested inside a non-repository root. Options are absolute repository
// paths; `repositoryRoot` is the root those paths are relative to.
repositoryOptions?: string[];
selectedRepository?: string | null;
onSelectRepository?: (repository: string) => void;
repositoryRoot?: string;
}
const IDENTITY_ICON_MAP: Record<string, IconName> = {
@@ -258,12 +271,23 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
pullRequest,
prChecks,
onOpenPullRequest,
repositoryOptions,
selectedRepository,
onSelectRepository,
repositoryRoot,
}) => {
const { t } = useI18n();
if (!status) {
return null;
}
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">
{onOpenHistory || onOpenGraph || onOpenStashes || onOpenUpdateBranch ? (
@@ -410,7 +434,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
return (
<header className="@container/git-header px-3 py-2 bg-transparent">
<div className="flex items-center justify-between gap-2 min-w-0">
<div className="min-w-0 flex-1">
<div className="flex min-w-0 flex-1 items-center gap-1">
{isWorktreeMode ? (
<WorktreeBranchDisplay
currentBranch={status.current}
@@ -427,6 +451,34 @@ 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>
) : null}
</div>
<div className="flex shrink-0 items-center gap-1">
{identityControl}