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:
@@ -10,6 +10,7 @@ import { SyncActions } from '@/components/views/git/SyncActions';
|
|||||||
import { PierreDiffViewer } from '@/components/views/PierreDiffViewer';
|
import { PierreDiffViewer } from '@/components/views/PierreDiffViewer';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||||
|
import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory';
|
||||||
import type { GitStatus } from '@/lib/api/types';
|
import type { GitStatus } from '@/lib/api/types';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { generateCommitMessage, stageGitFile, stageGitFiles, unstageGitFile, unstageGitFiles } from '@/lib/gitApi';
|
import { generateCommitMessage, stageGitFile, stageGitFiles, unstageGitFile, unstageGitFiles } from '@/lib/gitApi';
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
useIsGitRepo,
|
useIsGitRepo,
|
||||||
useGitLoadingStatus,
|
useGitLoadingStatus,
|
||||||
} from '@/stores/useGitStore';
|
} from '@/stores/useGitStore';
|
||||||
|
import { NestedRepoResolutionStates } from '@/components/views/git/NestedRepoResolutionStates';
|
||||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||||
|
|
||||||
type SyncAction = 'fetch' | 'pull' | 'push' | 'sync' | null;
|
type SyncAction = 'fetch' | 'pull' | 'push' | 'sync' | null;
|
||||||
@@ -56,12 +58,17 @@ type MobileChangesSurfaceProps = {
|
|||||||
export const MobileChangesSurface: React.FC<MobileChangesSurfaceProps> = ({ onClose, initialDiffPath, initialDiffStaged = false }) => {
|
export const MobileChangesSurface: React.FC<MobileChangesSurfaceProps> = ({ onClose, initialDiffPath, initialDiffStaged = false }) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { git } = useRuntimeAPIs();
|
const { git } = useRuntimeAPIs();
|
||||||
const currentDirectory = normalizePath(useEffectiveDirectory() ?? null);
|
const rootDirectory = normalizePath(useEffectiveDirectory() ?? null);
|
||||||
|
// When the root is not itself a repository, changes come from the resolved
|
||||||
|
// nested repository instead.
|
||||||
|
const { rootIsGitRepo, gitDirectory, nestedRepos } = useNestedGitDirectory(rootDirectory || null);
|
||||||
|
const currentDirectory = gitDirectory ?? rootDirectory;
|
||||||
const status = useGitStatus(currentDirectory || null);
|
const status = useGitStatus(currentDirectory || null);
|
||||||
const isGitRepo = useIsGitRepo(currentDirectory || null);
|
const isGitRepo = useIsGitRepo(currentDirectory || null);
|
||||||
const isLoadingStatus = useGitLoadingStatus(currentDirectory || null);
|
const isLoadingStatus = useGitLoadingStatus(currentDirectory || null);
|
||||||
const setActiveDirectory = useGitStore((state) => state.setActiveDirectory);
|
const setActiveDirectory = useGitStore((state) => state.setActiveDirectory);
|
||||||
const ensureAll = useGitStore((state) => state.ensureAll);
|
const ensureAll = useGitStore((state) => state.ensureAll);
|
||||||
|
const ensureNestedRepos = useGitStore((state) => state.ensureNestedRepos);
|
||||||
const fetchStatus = useGitStore((state) => state.fetchStatus);
|
const fetchStatus = useGitStore((state) => state.fetchStatus);
|
||||||
const fetchBranches = useGitStore((state) => state.fetchBranches);
|
const fetchBranches = useGitStore((state) => state.fetchBranches);
|
||||||
const prefetchDiffs = useGitStore((state) => state.prefetchDiffs);
|
const prefetchDiffs = useGitStore((state) => state.prefetchDiffs);
|
||||||
@@ -474,12 +481,22 @@ export const MobileChangesSurface: React.FC<MobileChangesSurfaceProps> = ({ onCl
|
|||||||
return renderListState(<MobileChangesState message={t('gitView.empty.selectSessionOrDirectory')} />);
|
return renderListState(<MobileChangesState message={t('gitView.empty.selectSessionOrDirectory')} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoadingStatus && isGitRepo === null) {
|
// Non-repo root: surface nested-repository resolution (discovering, failed,
|
||||||
return renderListState(<MobileChangesState loading message={t('gitView.loading.checkingRepository')} />);
|
// unsupported, none found, or settling on the auto-selected repository).
|
||||||
|
if (rootIsGitRepo === false || isGitRepo === false) {
|
||||||
|
return renderListState(
|
||||||
|
<NestedRepoResolutionStates
|
||||||
|
rootIsGitRepo={rootIsGitRepo}
|
||||||
|
nestedRepos={nestedRepos}
|
||||||
|
onRetryDiscovery={() => {
|
||||||
|
if (rootDirectory) void ensureNestedRepos(rootDirectory, { force: true });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGitRepo === false) {
|
if (isLoadingStatus && isGitRepo === null) {
|
||||||
return renderListState(<MobileChangesState icon message={t('gitView.empty.notGitRepository')} description={t('gitView.empty.notGitRepositoryDescription')} />);
|
return renderListState(<MobileChangesState loading message={t('gitView.loading.checkingRepository')} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.type === 'diff') {
|
if (route.type === 'diff') {
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ import {
|
|||||||
useIsGitRepo,
|
useIsGitRepo,
|
||||||
useGitLoadingStatus,
|
useGitLoadingStatus,
|
||||||
useGitLoadingLog,
|
useGitLoadingLog,
|
||||||
useEffectiveGitDirectory,
|
|
||||||
useNestedRepos,
|
|
||||||
useNestedRepoSelection,
|
|
||||||
} from '@/stores/useGitStore';
|
} from '@/stores/useGitStore';
|
||||||
|
import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory';
|
||||||
|
import { NestedRepoResolutionStates } from './git/NestedRepoResolutionStates';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
@@ -256,13 +255,14 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
|||||||
// The root the view is anchored to (session/worktree context stays keyed on
|
// The root the view is anchored to (session/worktree context stays keyed on
|
||||||
// it). When the root is not itself a repository and the user picked a nested
|
// it). When the root is not itself a repository and the user picked a nested
|
||||||
// one, `gitDirectory` is the effective repository all git data and actions
|
// one, `gitDirectory` is the effective repository all git data and actions
|
||||||
// operate on.
|
// operate on. The hook owns probing, discovery, auto-select, and
|
||||||
const rootIsGitRepo = useIsGitRepo(currentDirectory ?? null);
|
// stale-selection recovery; data fetching below keys off its result.
|
||||||
const gitDirectory = useEffectiveGitDirectory(currentDirectory ?? null);
|
const { rootIsGitRepo, gitDirectory, nestedRepos, nestedRepoSelection } = useNestedGitDirectory(
|
||||||
|
currentDirectory ?? null,
|
||||||
|
{ enabled: isActive },
|
||||||
|
);
|
||||||
const isGitRepo = useIsGitRepo(gitDirectory ?? null);
|
const isGitRepo = useIsGitRepo(gitDirectory ?? null);
|
||||||
const status = useGitStatus(gitDirectory ?? null);
|
const status = useGitStatus(gitDirectory ?? null);
|
||||||
const nestedRepos = useNestedRepos(currentDirectory ?? null);
|
|
||||||
const nestedRepoSelection = useNestedRepoSelection(currentDirectory ?? null);
|
|
||||||
|
|
||||||
// Authoritative session↔worktree attachment for repair action display
|
// Authoritative session↔worktree attachment for repair action display
|
||||||
const worktreeAttachment = useSessionWorktreeStore((s) =>
|
const worktreeAttachment = useSessionWorktreeStore((s) =>
|
||||||
@@ -298,7 +298,6 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
|||||||
bumpIndexRevision,
|
bumpIndexRevision,
|
||||||
ensureNestedRepos,
|
ensureNestedRepos,
|
||||||
selectNestedRepo,
|
selectNestedRepo,
|
||||||
clearNestedRepoSelection,
|
|
||||||
} = useGitStore(useShallow((state) => ({
|
} = useGitStore(useShallow((state) => ({
|
||||||
setActiveDirectory: state.setActiveDirectory,
|
setActiveDirectory: state.setActiveDirectory,
|
||||||
fetchAll: state.fetchAll,
|
fetchAll: state.fetchAll,
|
||||||
@@ -315,7 +314,6 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
|||||||
bumpIndexRevision: state.bumpIndexRevision,
|
bumpIndexRevision: state.bumpIndexRevision,
|
||||||
ensureNestedRepos: state.ensureNestedRepos,
|
ensureNestedRepos: state.ensureNestedRepos,
|
||||||
selectNestedRepo: state.selectNestedRepo,
|
selectNestedRepo: state.selectNestedRepo,
|
||||||
clearNestedRepoSelection: state.clearNestedRepoSelection,
|
|
||||||
})));
|
})));
|
||||||
const isMobile = useUIStore((state) => state.isMobile);
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
const openContextDiff = useUIStore((state) => state.openContextDiff);
|
const openContextDiff = useUIStore((state) => state.openContextDiff);
|
||||||
@@ -898,37 +896,6 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
|||||||
});
|
});
|
||||||
}, [isActive, clearDiffCache, gitDirectory, fetchStatus, git]);
|
}, [isActive, clearDiffCache, gitDirectory, fetchStatus, git]);
|
||||||
|
|
||||||
// Discover nested repositories once the root probe confirms it is not one.
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isActive) return;
|
|
||||||
if (!currentDirectory) return;
|
|
||||||
if (rootIsGitRepo !== false) return;
|
|
||||||
void ensureNestedRepos(currentDirectory);
|
|
||||||
}, [currentDirectory, ensureNestedRepos, isActive, rootIsGitRepo]);
|
|
||||||
|
|
||||||
// Auto-select the first nested repository so the tab opens straight into
|
|
||||||
// repository data; the header picker switches between repositories.
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isActive) return;
|
|
||||||
if (!currentDirectory) return;
|
|
||||||
if (rootIsGitRepo !== false) return;
|
|
||||||
if (!nestedRepos || nestedRepos.length === 0) return;
|
|
||||||
if (nestedRepoSelection) return;
|
|
||||||
selectNestedRepo(currentDirectory, nestedRepos[0]);
|
|
||||||
}, [currentDirectory, isActive, nestedRepos, nestedRepoSelection, rootIsGitRepo, selectNestedRepo]);
|
|
||||||
|
|
||||||
// A selected repository that is no longer a git repository is stale: drop
|
|
||||||
// the selection and re-scan so the picker reflects the current tree.
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isActive) return;
|
|
||||||
if (!currentDirectory) return;
|
|
||||||
if (!nestedRepoSelection) return;
|
|
||||||
if (gitDirectory === currentDirectory) return;
|
|
||||||
if (isGitRepo !== false) return;
|
|
||||||
clearNestedRepoSelection(currentDirectory);
|
|
||||||
void ensureNestedRepos(currentDirectory, { force: true });
|
|
||||||
}, [clearNestedRepoSelection, currentDirectory, ensureNestedRepos, gitDirectory, isActive, isGitRepo, nestedRepoSelection]);
|
|
||||||
|
|
||||||
const refreshStatusAndBranches = React.useCallback(
|
const refreshStatusAndBranches = React.useCallback(
|
||||||
async (showErrors = true) => {
|
async (showErrors = true) => {
|
||||||
if (!gitDirectory) return;
|
if (!gitDirectory) return;
|
||||||
@@ -2371,66 +2338,25 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nested repository discovery: while unknown or failed keep a loading
|
// Nested repository discovery states (discovering, failed, unsupported,
|
||||||
// state with the failure signal; the picker appears once repositories are
|
// none found, or settling on the auto-selected repository).
|
||||||
// found (a single repository is auto-selected by an effect above).
|
return (
|
||||||
if (nestedRepos === undefined || nestedRepos === null) {
|
<NestedRepoResolutionStates
|
||||||
return (
|
rootIsGitRepo={rootIsGitRepo}
|
||||||
<div className="flex h-full flex-col items-center justify-center px-4 text-center">
|
nestedRepos={nestedRepos}
|
||||||
<Icon name="loader-4" className="mb-3 size-6 animate-spin text-muted-foreground" />
|
onRetryDiscovery={() => {
|
||||||
<p className="typography-ui-label font-semibold text-foreground">
|
if (currentDirectory) {
|
||||||
{nestedRepos === null
|
void ensureNestedRepos(currentDirectory, { force: true });
|
||||||
? t('gitView.empty.discoverFailed')
|
}
|
||||||
: t('gitView.empty.discoveringRepositories')}
|
}}
|
||||||
</p>
|
emptyStateFooter={
|
||||||
{nestedRepos === null ? (
|
repairActions.includes('open-without-worktree-features') ? (
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="mt-3 gap-1.5"
|
|
||||||
onClick={() => {
|
|
||||||
if (currentDirectory) {
|
|
||||||
void ensureNestedRepos(currentDirectory, { force: true });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="refresh" className="size-4" />
|
|
||||||
{t('gitView.empty.retryDiscovery')}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nestedRepos.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col items-center justify-center px-4 text-center">
|
|
||||||
<Icon name="git-branch" className="mb-3 size-6 text-muted-foreground" />
|
|
||||||
<p className="typography-ui-label font-semibold text-foreground">
|
|
||||||
{t('gitView.empty.notGitRepository')}
|
|
||||||
</p>
|
|
||||||
<p className="typography-meta mt-1 text-muted-foreground">
|
|
||||||
{t('gitView.empty.notGitRepositoryDescription')}
|
|
||||||
</p>
|
|
||||||
{repairActions.includes('open-without-worktree-features') ? (
|
|
||||||
<p className="typography-meta mt-2 text-muted-foreground">
|
<p className="typography-meta mt-2 text-muted-foreground">
|
||||||
{t('gitView.empty.worktreeFeaturesUnavailable')}
|
{t('gitView.empty.worktreeFeaturesUnavailable')}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : undefined
|
||||||
</div>
|
}
|
||||||
);
|
/>
|
||||||
}
|
|
||||||
|
|
||||||
// Repositories were found and are about to be auto-selected (or the
|
|
||||||
// selected repository is still probing) — hold a brief loading state.
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col items-center justify-center px-4 text-center">
|
|
||||||
<Icon name="loader-4" className="mb-3 size-6 animate-spin text-muted-foreground" />
|
|
||||||
<p className="typography-ui-label font-semibold text-foreground">
|
|
||||||
{t('gitView.loading.checkingRepository')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2465,7 +2391,9 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
|||||||
onOpenPullRequest={
|
onOpenPullRequest={
|
||||||
gitDirectory ? () => openContextSurface(gitDirectory, 'pr') : undefined
|
gitDirectory ? () => openContextSurface(gitDirectory, 'pr') : undefined
|
||||||
}
|
}
|
||||||
repositoryOptions={gitDirectory !== currentDirectory ? (nestedRepos ?? undefined) : undefined}
|
repositoryOptions={
|
||||||
|
gitDirectory !== currentDirectory && Array.isArray(nestedRepos) ? nestedRepos : undefined
|
||||||
|
}
|
||||||
selectedRepository={gitDirectory !== currentDirectory ? gitDirectory : null}
|
selectedRepository={gitDirectory !== currentDirectory ? gitDirectory : null}
|
||||||
onSelectRepository={
|
onSelectRepository={
|
||||||
gitDirectory !== currentDirectory && currentDirectory
|
gitDirectory !== currentDirectory && currentDirectory
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import React from 'react';
|
|||||||
import { Icon } from '@/components/icon/Icon';
|
import { Icon } from '@/components/icon/Icon';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||||
|
import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory';
|
||||||
import { useDetectedWorktreeMetadata } from '@/hooks/useDetectedWorktreeRoot';
|
import { useDetectedWorktreeMetadata } from '@/hooks/useDetectedWorktreeRoot';
|
||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { useSessionWorktreeStore } from '@/sync/session-worktree-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 { useShallow } from 'zustand/react/shallow';
|
||||||
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
|
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
|
||||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||||
@@ -14,6 +15,7 @@ import { useI18n } from '@/lib/i18n';
|
|||||||
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { PullRequestSection } from './git/PullRequestSection';
|
import { PullRequestSection } from './git/PullRequestSection';
|
||||||
|
import { NestedRepoResolutionStates } from './git/NestedRepoResolutionStates';
|
||||||
import { deriveBaseBranch } from './git/baseBranch';
|
import { deriveBaseBranch } from './git/baseBranch';
|
||||||
|
|
||||||
const normalizePath = (value?: string | null): string =>
|
const normalizePath = (value?: string | null): string =>
|
||||||
@@ -36,9 +38,16 @@ export const PullRequestView: React.FC = () => {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { git } = useRuntimeAPIs();
|
const { git } = useRuntimeAPIs();
|
||||||
const currentDirectory = useEffectiveDirectory();
|
const currentDirectory = useEffectiveDirectory();
|
||||||
const status = useGitStatus(currentDirectory ?? null);
|
// When the root is not itself a repository, the pull-request workflow
|
||||||
const branches = useGitBranches(currentDirectory ?? null);
|
// operates on the resolved nested repository instead.
|
||||||
const { ensureAll } = useGitStore(useShallow((state) => ({ ensureAll: state.ensureAll })));
|
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 currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||||
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
|
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
|
||||||
@@ -89,11 +98,11 @@ export const PullRequestView: React.FC = () => {
|
|||||||
const worktreeMetadata = useDetectedWorktreeMetadata(currentDirectory, storeWorktreeMetadata, status?.current ?? undefined);
|
const worktreeMetadata = useDetectedWorktreeMetadata(currentDirectory, storeWorktreeMetadata, status?.current ?? undefined);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentDirectory || !git) {
|
if (!gitDirectory || !git) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void ensureAll(currentDirectory, git);
|
void ensureAll(gitDirectory, git);
|
||||||
}, [currentDirectory, ensureAll, git]);
|
}, [gitDirectory, ensureAll, git]);
|
||||||
|
|
||||||
const [rootBranchHint, setRootBranchHint] = React.useState<string | null>(null);
|
const [rootBranchHint, setRootBranchHint] = React.useState<string | null>(null);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -122,52 +131,52 @@ export const PullRequestView: React.FC = () => {
|
|||||||
}, [authoritativeProjectRoot, worktreeMetadata?.projectDirectory]);
|
}, [authoritativeProjectRoot, worktreeMetadata?.projectDirectory]);
|
||||||
|
|
||||||
const [remotes, setRemotes] = React.useState<GitRemote[]>(() =>
|
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>(() =>
|
const [remoteUrl, setRemoteUrl] = React.useState<string | null>(() =>
|
||||||
(currentDirectory ? remoteUrlCacheByDirectory.get(remoteCacheKey(currentDirectory)) : undefined) ?? null
|
(gitDirectory ? remoteUrlCacheByDirectory.get(remoteCacheKey(gitDirectory)) : undefined) ?? null
|
||||||
);
|
);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentDirectory || !git?.getRemotes) {
|
if (!gitDirectory || !git?.getRemotes) {
|
||||||
setRemotes([]);
|
setRemotes([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRemotes(remotesCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? []);
|
setRemotes(remotesCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? []);
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void git.getRemotes(currentDirectory)
|
void git.getRemotes(gitDirectory)
|
||||||
.then((remoteList) => {
|
.then((remoteList) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
remotesCacheByDirectory.set(remoteCacheKey(currentDirectory), remoteList ?? []);
|
remotesCacheByDirectory.set(remoteCacheKey(gitDirectory), remoteList ?? []);
|
||||||
setRemotes(remoteList ?? []);
|
setRemotes(remoteList ?? []);
|
||||||
})
|
})
|
||||||
.catch(() => { if (!cancelled) setRemotes(remotesCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? []); });
|
.catch(() => { if (!cancelled) setRemotes(remotesCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? []); });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [currentDirectory, git]);
|
}, [gitDirectory, git]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentDirectory || !git?.getRemoteUrl) {
|
if (!gitDirectory || !git?.getRemoteUrl) {
|
||||||
setRemoteUrl(null);
|
setRemoteUrl(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? null);
|
setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? null);
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void git.getRemoteUrl(currentDirectory)
|
void git.getRemoteUrl(gitDirectory)
|
||||||
.then((url) => {
|
.then((url) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
remoteUrlCacheByDirectory.set(remoteCacheKey(currentDirectory), url);
|
remoteUrlCacheByDirectory.set(remoteCacheKey(gitDirectory), url);
|
||||||
setRemoteUrl(url);
|
setRemoteUrl(url);
|
||||||
})
|
})
|
||||||
.catch(() => { if (!cancelled) setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(currentDirectory)) ?? null); });
|
.catch(() => { if (!cancelled) setRemoteUrl(remoteUrlCacheByDirectory.get(remoteCacheKey(gitDirectory)) ?? null); });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [currentDirectory, git]);
|
}, [gitDirectory, git]);
|
||||||
|
|
||||||
const localBranches = React.useMemo(() => {
|
const localBranches = React.useMemo(() => {
|
||||||
if (!branches?.all) return [];
|
if (!branches?.all) return [];
|
||||||
@@ -240,7 +249,31 @@ export const PullRequestView: React.FC = () => {
|
|||||||
worktreeMetadata?.createdFromBranch,
|
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 (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
<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" />
|
<Icon name="git-pull-request" className="h-12 w-12 text-muted-foreground/50" />
|
||||||
@@ -259,7 +292,7 @@ export const PullRequestView: React.FC = () => {
|
|||||||
preventOverscroll
|
preventOverscroll
|
||||||
>
|
>
|
||||||
<PullRequestSection
|
<PullRequestSection
|
||||||
directory={currentDirectory}
|
directory={gitDirectory ?? currentDirectory}
|
||||||
branch={currentBranch}
|
branch={currentBranch}
|
||||||
baseBranch={baseBranch}
|
baseBranch={baseBranch}
|
||||||
trackingBranch={status?.tracking ?? undefined}
|
trackingBranch={status?.tracking ?? undefined}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Icon } from '@/components/icon/Icon';
|
||||||
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import type { NestedRepoDiscovery } from '@/stores/useGitStore';
|
||||||
|
|
||||||
|
type NestedRepoResolutionStatesProps = {
|
||||||
|
/** Probe of the project root: `false` means nested resolution applies. */
|
||||||
|
rootIsGitRepo: boolean | null;
|
||||||
|
/** Discovery outcome for the root (`undefined` = not run yet). */
|
||||||
|
nestedRepos: NestedRepoDiscovery | undefined;
|
||||||
|
onRetryDiscovery: () => void;
|
||||||
|
/** Optional extra line under the not-a-repository description. */
|
||||||
|
emptyStateFooter?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared empty/loading states for git surfaces while nested-repository
|
||||||
|
* resolution is pending, failed, or impossible. Renders null once
|
||||||
|
* repositories are resolved so the consumer can proceed into its own content.
|
||||||
|
*
|
||||||
|
* A runtime without the discovery route (VS Code) reports "unsupported": the
|
||||||
|
* honest state there is the plain not-a-repository empty state, without a
|
||||||
|
* retry that can never succeed.
|
||||||
|
*/
|
||||||
|
export const NestedRepoResolutionStates: React.FC<NestedRepoResolutionStatesProps> = ({
|
||||||
|
rootIsGitRepo,
|
||||||
|
nestedRepos,
|
||||||
|
onRetryDiscovery,
|
||||||
|
emptyStateFooter,
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
if (rootIsGitRepo !== false) return null;
|
||||||
|
|
||||||
|
if (nestedRepos === undefined || nestedRepos === null) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||||
|
<Icon name="loader-4" className="mb-3 size-6 animate-spin text-muted-foreground" />
|
||||||
|
<p className="typography-ui-label font-semibold text-foreground">
|
||||||
|
{nestedRepos === null
|
||||||
|
? t('gitView.empty.discoverFailed')
|
||||||
|
: t('gitView.empty.discoveringRepositories')}
|
||||||
|
</p>
|
||||||
|
{nestedRepos === null ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-3 gap-1.5"
|
||||||
|
onClick={onRetryDiscovery}
|
||||||
|
>
|
||||||
|
<Icon name="refresh" className="size-4" />
|
||||||
|
{t('gitView.empty.retryDiscovery')}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nestedRepos === 'unsupported' || nestedRepos.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||||
|
<Icon name="git-branch" className="mb-3 size-6 text-muted-foreground" />
|
||||||
|
<p className="typography-ui-label font-semibold text-foreground">
|
||||||
|
{t('gitView.empty.notGitRepository')}
|
||||||
|
</p>
|
||||||
|
<p className="typography-meta mt-1 text-muted-foreground">
|
||||||
|
{t('gitView.empty.notGitRepositoryDescription')}
|
||||||
|
</p>
|
||||||
|
{emptyStateFooter}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repositories were found and one is about to be auto-selected (or the
|
||||||
|
// selected repository is still probing) — hold a brief loading state.
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||||
|
<Icon name="loader-4" className="mb-3 size-6 animate-spin text-muted-foreground" />
|
||||||
|
<p className="typography-ui-label font-semibold text-foreground">
|
||||||
|
{t('gitView.loading.checkingRepository')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,7 +19,7 @@ import { ModelSelector } from '@/components/sections/agents/ModelSelector';
|
|||||||
import { deriveBaseBranch, hasResolvableBaseBranch } from '@/components/views/git/baseBranch';
|
import { deriveBaseBranch, hasResolvableBaseBranch } from '@/components/views/git/baseBranch';
|
||||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { useGitBranches, useGitStatus, useGitStore } from '@/stores/useGitStore';
|
import { useGitBranches, useGitStatus, useGitStore, useIsGitRepo } from '@/stores/useGitStore';
|
||||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
import {
|
import {
|
||||||
getFreshestPrStatusForBranch,
|
getFreshestPrStatusForBranch,
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
useGitHubPrStatusStore,
|
useGitHubPrStatusStore,
|
||||||
} from '@/stores/useGitHubPrStatusStore';
|
} from '@/stores/useGitHubPrStatusStore';
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
|
import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useWalkthroughStore } from '@/stores/useWalkthroughStore';
|
import { useWalkthroughStore } from '@/stores/useWalkthroughStore';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -36,6 +37,7 @@ import { WalkthroughStages } from './WalkthroughStages';
|
|||||||
import { useWalkthroughStageProgress } from './useWalkthroughStageProgress';
|
import { useWalkthroughStageProgress } from './useWalkthroughStageProgress';
|
||||||
import { WalkthroughStream } from './WalkthroughStream';
|
import { WalkthroughStream } from './WalkthroughStream';
|
||||||
import { WalkthroughToc } from './WalkthroughToc';
|
import { WalkthroughToc } from './WalkthroughToc';
|
||||||
|
import { NestedRepoResolutionStates } from '@/components/views/git/NestedRepoResolutionStates';
|
||||||
|
|
||||||
interface WalkthroughViewProps {
|
interface WalkthroughViewProps {
|
||||||
directory: string;
|
directory: string;
|
||||||
@@ -73,11 +75,17 @@ const TOC_MAX_FRACTION = 0.5;
|
|||||||
// pickers, 32px action, 36px arrows) read as misalignment, not hierarchy.
|
// pickers, 32px action, 36px arrows) read as misalignment, not hierarchy.
|
||||||
const HEADER_COMPACT_WIDTH = 680;
|
const HEADER_COMPACT_WIDTH = 680;
|
||||||
|
|
||||||
export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
export const WalkthroughView = ({ directory: rootDirectory }: WalkthroughViewProps) => {
|
||||||
const { t, locale, locales, label } = useI18n();
|
const { t, locale, locales, label } = useI18n();
|
||||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [panelWidth, setPanelWidth] = useState(0);
|
const [panelWidth, setPanelWidth] = useState(0);
|
||||||
|
|
||||||
|
// The walkthrough documents one repository. When the root is not itself a
|
||||||
|
// repository, that is the resolved nested repository; everything below keys
|
||||||
|
// off `directory`.
|
||||||
|
const { rootIsGitRepo, gitDirectory, nestedRepos } = useNestedGitDirectory(rootDirectory || null);
|
||||||
|
const directory = gitDirectory ?? rootDirectory;
|
||||||
|
|
||||||
// Panel width, not viewport width: this surface is resizable independently of
|
// Panel width, not viewport width: this surface is resizable independently of
|
||||||
// the window.
|
// the window.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -484,6 +492,20 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
|||||||
[activeLanguage, directory, generate, generateDisabled, source]
|
[activeLanguage, directory, generate, generateDisabled, source]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isGitRepo = useIsGitRepo(gitDirectory || null);
|
||||||
|
const ensureNestedRepos = useGitStore((state) => state.ensureNestedRepos);
|
||||||
|
if (rootIsGitRepo === false || isGitRepo === false) {
|
||||||
|
return (
|
||||||
|
<NestedRepoResolutionStates
|
||||||
|
rootIsGitRepo={rootIsGitRepo}
|
||||||
|
nestedRepos={nestedRepos}
|
||||||
|
onRetryDiscovery={() => {
|
||||||
|
if (rootDirectory) void ensureNestedRepos(rootDirectory, { force: true });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={rootRef} className="flex h-full min-h-0 flex-col">
|
<div ref={rootRef} className="flex h-full min-h-0 flex-col">
|
||||||
<header className="flex shrink-0 flex-wrap items-center gap-2 border-b border-border/60 px-3 py-2">
|
<header className="flex shrink-0 flex-wrap items-center gap-2 border-b border-border/60 px-3 py-2">
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
|
import {
|
||||||
|
useEffectiveGitDirectory,
|
||||||
|
useGitStore,
|
||||||
|
useIsGitRepo,
|
||||||
|
useNestedRepoSelection,
|
||||||
|
useNestedRepos,
|
||||||
|
} from '@/stores/useGitStore';
|
||||||
|
|
||||||
|
type UseNestedGitDirectoryOptions = {
|
||||||
|
/** False defers all probing/discovery work while the surface is hidden. */
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the repository a git surface operates on when the project root may
|
||||||
|
* not itself be a git repository. Owns the full resolution flow: probing the
|
||||||
|
* root, discovering nested repositories, auto-selecting the first one, and
|
||||||
|
* dropping a selection whose repository disappeared.
|
||||||
|
*
|
||||||
|
* Consumers still fetch their own git data for the returned `gitDirectory`;
|
||||||
|
* this hook only owns who that directory is.
|
||||||
|
*/
|
||||||
|
export const useNestedGitDirectory = (
|
||||||
|
root: string | null,
|
||||||
|
options: UseNestedGitDirectoryOptions = {},
|
||||||
|
) => {
|
||||||
|
const { enabled = true } = options;
|
||||||
|
const { git } = useRuntimeAPIs();
|
||||||
|
|
||||||
|
const rootIsGitRepo = useIsGitRepo(root);
|
||||||
|
const gitDirectory = useEffectiveGitDirectory(root);
|
||||||
|
const nestedRepos = useNestedRepos(root);
|
||||||
|
const nestedRepoSelection = useNestedRepoSelection(root);
|
||||||
|
|
||||||
|
// Probe of the resolved repository, used to detect a stale selection. Null
|
||||||
|
// when there is nothing selected to probe.
|
||||||
|
const selectedIsGitRepo = useIsGitRepo(
|
||||||
|
gitDirectory && gitDirectory !== root ? gitDirectory : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { ensureStatus, ensureNestedRepos, selectNestedRepo, clearNestedRepoSelection } = useGitStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
ensureStatus: state.ensureStatus,
|
||||||
|
ensureNestedRepos: state.ensureNestedRepos,
|
||||||
|
selectNestedRepo: state.selectNestedRepo,
|
||||||
|
clearNestedRepoSelection: state.clearNestedRepoSelection,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Probe the root itself so nested-repo resolution never depends on some
|
||||||
|
// other surface (e.g. the sidebar badge) having probed it first.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enabled || !root) return;
|
||||||
|
if (rootIsGitRepo !== null) return;
|
||||||
|
void ensureStatus(root, git);
|
||||||
|
}, [enabled, ensureStatus, git, root, rootIsGitRepo]);
|
||||||
|
|
||||||
|
// Discover nested repositories once the root probe confirms it is not one.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enabled || !root) return;
|
||||||
|
if (rootIsGitRepo !== false) return;
|
||||||
|
void ensureNestedRepos(root);
|
||||||
|
}, [enabled, ensureNestedRepos, root, rootIsGitRepo]);
|
||||||
|
|
||||||
|
// Auto-select the first nested repository so the surface opens straight
|
||||||
|
// into repository data; a picker (where rendered) switches between them.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enabled || !root) return;
|
||||||
|
if (rootIsGitRepo !== false) return;
|
||||||
|
if (!nestedRepos || nestedRepos.length === 0) return;
|
||||||
|
if (nestedRepoSelection) return;
|
||||||
|
selectNestedRepo(root, nestedRepos[0]);
|
||||||
|
}, [enabled, nestedRepos, nestedRepoSelection, root, rootIsGitRepo, selectNestedRepo]);
|
||||||
|
|
||||||
|
// A selected repository that is no longer a git repository is stale: drop
|
||||||
|
// the selection and re-scan so resolution reflects the current tree.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enabled || !root || !nestedRepoSelection) return;
|
||||||
|
if (!gitDirectory || gitDirectory === root) return;
|
||||||
|
if (selectedIsGitRepo !== false) return;
|
||||||
|
clearNestedRepoSelection(root);
|
||||||
|
void ensureNestedRepos(root, { force: true });
|
||||||
|
}, [
|
||||||
|
clearNestedRepoSelection,
|
||||||
|
enabled,
|
||||||
|
ensureNestedRepos,
|
||||||
|
gitDirectory,
|
||||||
|
nestedRepoSelection,
|
||||||
|
root,
|
||||||
|
selectedIsGitRepo,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { rootIsGitRepo, gitDirectory, nestedRepos, nestedRepoSelection };
|
||||||
|
};
|
||||||
@@ -148,7 +148,7 @@ Important properties:
|
|||||||
- loading state is per-directory, not global
|
- loading state is per-directory, not global
|
||||||
- `ensureStatus()` and `ensureAll()` are the preferred entry points for consumers
|
- `ensureStatus()` and `ensureAll()` are the preferred entry points for consumers
|
||||||
- in-flight dedupe exists for status and `ensureAll()`
|
- in-flight dedupe exists for status and `ensureAll()`
|
||||||
- nested repository discovery (`nestedReposByRoot`, `nestedRepoSelection`, `ensureNestedRepos`) is per-root state for roots that are not themselves git repositories; discovery failure is a `null` marker (never a valid empty result), selections are persisted per runtime + root, and `useEffectiveGitDirectory(root)` resolves the directory the Git tab operates on (`root` when the root is a repository, the selected nested repository otherwise)
|
- nested repository discovery (`nestedReposByRoot`, `nestedRepoSelection`, `ensureNestedRepos`) is per-root state for roots that are not themselves git repositories; discovery failure is a `null` marker (never a valid empty result), a runtime without the discovery route (VS Code) commits an `'unsupported'` marker, and an in-flight discovery whose runtime switched is discarded at commit time instead of repopulating the cleared map. Selections are persisted per runtime + root, and `useEffectiveGitDirectory(root)` resolves the directory git surfaces operate on (`root` when the root is a repository, the selected nested repository otherwise). `hooks/useNestedGitDirectory.ts` owns the resolution flow (root probe, discovery, auto-select, stale-selection recovery) for every consuming surface, and `git/NestedRepoResolutionStates.tsx` renders the shared pending/failed/unsupported/empty states
|
||||||
- runtime reset replaces all live entries with that runtime's persisted branch seeds and invalidates old completions
|
- runtime reset replaces all live entries with that runtime's persisted branch seeds and invalidates old completions
|
||||||
- status, branches, log, identity, repository probes, and prefetch diffs commit through runtime and per-channel generations
|
- status, branches, log, identity, repository probes, and prefetch diffs commit through runtime and per-channel generations
|
||||||
- status mutations advance a revision so older refreshes cannot undo optimistic or confirmed index changes
|
- status mutations advance a revision so older refreshes cannot undo optimistic or confirmed index changes
|
||||||
|
|||||||
Reference in New Issue
Block a user