feat(git): resolve nested repositories in the other git surfaces

Extract GitView's resolution flow into hooks/useNestedGitDirectory (root
probe, discovery, auto-select, stale-selection recovery) so the flow no
longer depends on SessionSidebar probing the root first, and reuse it in
the pull-request view, walkthrough view, and mobile changes surface —
all three now operate on the selected nested repository instead of
dead-ending on a non-repo root. Shared pending/failed/unsupported/empty
states live in git/NestedRepoResolutionStates; desktop changes inherits
the behavior through GitView. Selection stays shared per root, so the
picker's pick carries across surfaces.
This commit is contained in:
jaygupta17
2026-08-25 19:15:53 +05:30
parent 5e3f9d1ba2
commit cda273d69f
7 changed files with 315 additions and 130 deletions
@@ -2,10 +2,11 @@ import React from 'react';
import { Icon } from '@/components/icon/Icon';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory';
import { useDetectedWorktreeMetadata } from '@/hooks/useDetectedWorktreeRoot';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionWorktreeStore } from '@/sync/session-worktree-store';
import { useGitStatus, useGitBranches, useGitStore } from '@/stores/useGitStore';
import { useGitStatus, useGitBranches, useGitStore, useIsGitRepo } from '@/stores/useGitStore';
import { useShallow } from 'zustand/react/shallow';
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
import { getRuntimeKey } from '@/lib/runtime-switch';
@@ -14,6 +15,7 @@ import { useI18n } from '@/lib/i18n';
import { ScrollShadow } from '@/components/ui/ScrollShadow';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { PullRequestSection } from './git/PullRequestSection';
import { NestedRepoResolutionStates } from './git/NestedRepoResolutionStates';
import { deriveBaseBranch } from './git/baseBranch';
const normalizePath = (value?: string | null): string =>
@@ -36,9 +38,16 @@ export const PullRequestView: React.FC = () => {
const { t } = useI18n();
const { git } = useRuntimeAPIs();
const currentDirectory = useEffectiveDirectory();
const status = useGitStatus(currentDirectory ?? null);
const branches = useGitBranches(currentDirectory ?? null);
const { ensureAll } = useGitStore(useShallow((state) => ({ ensureAll: state.ensureAll })));
// When the root is not itself a repository, the pull-request workflow
// operates on the resolved nested repository instead.
const { rootIsGitRepo, gitDirectory, nestedRepos } = useNestedGitDirectory(currentDirectory ?? null);
const status = useGitStatus(gitDirectory ?? null);
const branches = useGitBranches(gitDirectory ?? null);
const isGitRepo = useIsGitRepo(gitDirectory ?? null);
const { ensureAll, ensureNestedRepos } = useGitStore(useShallow((state) => ({
ensureAll: state.ensureAll,
ensureNestedRepos: state.ensureNestedRepos,
})));
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
@@ -89,11 +98,11 @@ export const PullRequestView: React.FC = () => {
const worktreeMetadata = useDetectedWorktreeMetadata(currentDirectory, storeWorktreeMetadata, status?.current ?? undefined);
React.useEffect(() => {
if (!currentDirectory || !git) {
if (!gitDirectory || !git) {
return;
}
void ensureAll(currentDirectory, git);
}, [currentDirectory, ensureAll, git]);
void ensureAll(gitDirectory, git);
}, [gitDirectory, ensureAll, git]);
const [rootBranchHint, setRootBranchHint] = React.useState<string | null>(null);
React.useEffect(() => {
@@ -122,52 +131,52 @@ export const PullRequestView: React.FC = () => {
}, [authoritativeProjectRoot, worktreeMetadata?.projectDirectory]);
const [remotes, setRemotes] = React.useState<GitRemote[]>(() =>
(currentDirectory ? remotesCacheByDirectory.get(remoteCacheKey(currentDirectory)) : undefined) ?? []
(gitDirectory ? remotesCacheByDirectory.get(remoteCacheKey(gitDirectory)) : undefined) ?? []
);
const [remoteUrl, setRemoteUrl] = React.useState<string | null>(() =>
(currentDirectory ? remoteUrlCacheByDirectory.get(remoteCacheKey(currentDirectory)) : undefined) ?? null
(gitDirectory ? remoteUrlCacheByDirectory.get(remoteCacheKey(gitDirectory)) : undefined) ?? null
);
React.useEffect(() => {
if (!currentDirectory || !git?.getRemotes) {
if (!gitDirectory || !git?.getRemotes) {
setRemotes([]);
return;
}
setRemotes(remotesCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? []);
setRemotes(remotesCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? []);
let cancelled = false;
void git.getRemotes(currentDirectory)
void git.getRemotes(gitDirectory)
.then((remoteList) => {
if (cancelled) return;
remotesCacheByDirectory.set(remoteCacheKey(currentDirectory), remoteList ?? []);
remotesCacheByDirectory.set(remoteCacheKey(gitDirectory), remoteList ?? []);
setRemotes(remoteList ?? []);
})
.catch(() => { if (!cancelled) setRemotes(remotesCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? []); });
.catch(() => { if (!cancelled) setRemotes(remotesCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? []); });
return () => {
cancelled = true;
};
}, [currentDirectory, git]);
}, [gitDirectory, git]);
React.useEffect(() => {
if (!currentDirectory || !git?.getRemoteUrl) {
if (!gitDirectory || !git?.getRemoteUrl) {
setRemoteUrl(null);
return;
}
setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? null);
setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? null);
let cancelled = false;
void git.getRemoteUrl(currentDirectory)
void git.getRemoteUrl(gitDirectory)
.then((url) => {
if (cancelled) return;
remoteUrlCacheByDirectory.set(remoteCacheKey(currentDirectory), url);
remoteUrlCacheByDirectory.set(remoteCacheKey(gitDirectory), url);
setRemoteUrl(url);
})
.catch(() => { if (!cancelled) setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? null); });
.catch(() => { if (!cancelled) setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? null); });
return () => {
cancelled = true;
};
}, [currentDirectory, git]);
}, [gitDirectory, git]);
const localBranches = React.useMemo(() => {
if (!branches?.all) return [];
@@ -240,7 +249,31 @@ export const PullRequestView: React.FC = () => {
worktreeMetadata?.createdFromBranch,
]);
if (!currentDirectory || !currentBranch) {
if (!currentDirectory) {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
<Icon name="git-pull-request" className="h-12 w-12 text-muted-foreground/50" />
<div className="typography-ui-header text-foreground">{t('gitView.pullRequest.title')}</div>
<div className="max-w-sm typography-micro text-muted-foreground">{t('gitView.pullRequest.createHint')}</div>
</div>
);
}
// Non-repo root: surface nested-repository resolution (discovering, failed,
// unsupported, none found, or settling on the auto-selected repository).
if (rootIsGitRepo === false || isGitRepo === false) {
return (
<NestedRepoResolutionStates
rootIsGitRepo={rootIsGitRepo}
nestedRepos={nestedRepos}
onRetryDiscovery={() => {
void ensureNestedRepos(currentDirectory, { force: true });
}}
/>
);
}
if (!currentBranch) {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
<Icon name="git-pull-request" className="h-12 w-12 text-muted-foreground/50" />
@@ -259,7 +292,7 @@ export const PullRequestView: React.FC = () => {
preventOverscroll
>
<PullRequestSection
directory={currentDirectory}
directory={gitDirectory ?? currentDirectory}
branch={currentBranch}
baseBranch={baseBranch}
trackingBranch={status?.tracking ?? undefined}