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:
@@ -23,6 +23,7 @@ import {
|
||||
useGitLoadingStatus,
|
||||
} from '@/stores/useGitStore';
|
||||
import { NestedRepoResolutionStates } from '@/components/views/git/NestedRepoResolutionStates';
|
||||
import { NestedRepoPicker } from '@/components/views/git/NestedRepoPicker';
|
||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||
|
||||
type SyncAction = 'fetch' | 'pull' | 'push' | 'sync' | null;
|
||||
@@ -69,6 +70,7 @@ export const MobileChangesSurface: React.FC<MobileChangesSurfaceProps> = ({ onCl
|
||||
const setActiveDirectory = useGitStore((state) => state.setActiveDirectory);
|
||||
const ensureAll = useGitStore((state) => state.ensureAll);
|
||||
const ensureNestedRepos = useGitStore((state) => state.ensureNestedRepos);
|
||||
const selectNestedRepo = useGitStore((state) => state.selectNestedRepo);
|
||||
const fetchStatus = useGitStore((state) => state.fetchStatus);
|
||||
const fetchBranches = useGitStore((state) => state.fetchBranches);
|
||||
const prefetchDiffs = useGitStore((state) => state.prefetchDiffs);
|
||||
@@ -472,6 +474,16 @@ export const MobileChangesSurface: React.FC<MobileChangesSurfaceProps> = ({ onCl
|
||||
{status?.current || currentDirectory || ''}
|
||||
</p>
|
||||
</div>
|
||||
{rootIsGitRepo === false && Array.isArray(nestedRepos) && nestedRepos.length > 0 ? (
|
||||
<NestedRepoPicker
|
||||
repositories={nestedRepos}
|
||||
selectedRepository={gitDirectory ?? null}
|
||||
onSelectRepository={(repository) => {
|
||||
if (rootDirectory) selectNestedRepo(rootDirectory, repository);
|
||||
}}
|
||||
repositoryRoot={rootDirectory ?? undefined}
|
||||
/>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="min-h-0 flex-1">{state}</div>
|
||||
</div>
|
||||
@@ -481,9 +493,10 @@ export const MobileChangesSurface: React.FC<MobileChangesSurfaceProps> = ({ onCl
|
||||
return renderListState(<MobileChangesState message={t('gitView.empty.selectSessionOrDirectory')} />);
|
||||
}
|
||||
|
||||
// Non-repo root: surface nested-repository resolution (discovering, failed,
|
||||
// unsupported, none found, or settling on the auto-selected repository).
|
||||
if (rootIsGitRepo === false || isGitRepo === false) {
|
||||
// Non-repo root: surface nested-repository resolution while the operating
|
||||
// directory has not proven to be a repository (discovering, failed,
|
||||
// unsupported, none found, or settling on the auto-selected one).
|
||||
if (rootIsGitRepo === false && isGitRepo !== true) {
|
||||
return renderListState(
|
||||
<NestedRepoResolutionStates
|
||||
rootIsGitRepo={rootIsGitRepo}
|
||||
|
||||
@@ -1228,7 +1228,7 @@ export const ContextPanel: React.FC = () => {
|
||||
{hasWalkthroughTab ? (
|
||||
<div className={cn('absolute inset-0', activeTab?.mode === 'walkthrough' ? 'block' : 'hidden')}>
|
||||
<React.Suspense fallback={null}>
|
||||
<WalkthroughView directory={effectiveDirectory} />
|
||||
<WalkthroughView directory={effectiveDirectory} visible={activeTab?.mode === 'walkthrough'} />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory';
|
||||
import { NestedRepoPicker } from '@/components/views/git/NestedRepoPicker';
|
||||
import { useGitStore, useGitStatus, useIsGitRepo, useGitLoadingStatus } from '@/stores/useGitStore';
|
||||
import { useGitBaseBranchStore, gitBaseBranchEntryKey } from '@/stores/useGitBaseBranchStore';
|
||||
import { coerceDiffScope, branchRangeKey, isBranchScopeAvailable, isBranchScopeDefinitelyUnavailable, useRangeKeyedCache, useBoundedDirectoryRetry } from './branchDiffScope';
|
||||
@@ -1001,7 +1002,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
const rootDirectory = useEffectiveDirectory();
|
||||
// Diffs belong to the repository being diffed: when the root is not
|
||||
// itself a repository, operate on the resolved nested repository instead.
|
||||
const { gitDirectory: nestedGitDirectory } = useNestedGitDirectory(rootDirectory ?? null);
|
||||
const { rootIsGitRepo, gitDirectory: nestedGitDirectory, nestedRepos: nestedRepoOptions } = useNestedGitDirectory(rootDirectory ?? null);
|
||||
const effectiveDirectory = nestedGitDirectory ?? rootDirectory;
|
||||
const openContextSurface = useUIStore((state) => state.openContextSurface);
|
||||
const requestWalkthroughSource = useWalkthroughStore((state) => state.requestSource);
|
||||
@@ -1012,6 +1013,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
const isLoadingStatus = useGitLoadingStatus(effectiveDirectory ?? null);
|
||||
const setActiveDirectory = useGitStore((state) => state.setActiveDirectory);
|
||||
const ensureStatus = useGitStore((state) => state.ensureStatus);
|
||||
const selectNestedRepo = useGitStore((state) => state.selectNestedRepo);
|
||||
const fetchStatus = useGitStore((state) => state.fetchStatus);
|
||||
const fetchBranches = useGitStore((state) => state.fetchBranches);
|
||||
const clearDiffCache = useGitStore((state) => state.clearDiffCache);
|
||||
@@ -2044,6 +2046,16 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden bg-background">
|
||||
<div className="@container/diff-toolbar flex min-w-0 items-center gap-2 px-3 py-2 bg-background">
|
||||
{rootIsGitRepo === false && Array.isArray(nestedRepoOptions) && nestedRepoOptions.length > 0 ? (
|
||||
<NestedRepoPicker
|
||||
repositories={nestedRepoOptions}
|
||||
selectedRepository={nestedGitDirectory ?? null}
|
||||
onSelectRepository={(repository) => {
|
||||
if (rootDirectory) selectNestedRepo(rootDirectory, repository);
|
||||
}}
|
||||
repositoryRoot={rootDirectory ?? undefined}
|
||||
/>
|
||||
) : null}
|
||||
{!isMobile && (
|
||||
activeDiffScope === 'working' || activeDiffScope === 'staged' || activeDiffScope === 'turn' || activeDiffScope === 'branch' ? (
|
||||
<ChangeScopeSelector
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { PullRequestSection } from './git/PullRequestSection';
|
||||
import { NestedRepoResolutionStates } from './git/NestedRepoResolutionStates';
|
||||
import { NestedRepoPicker } from './git/NestedRepoPicker';
|
||||
import { deriveBaseBranch } from './git/baseBranch';
|
||||
|
||||
const normalizePath = (value?: string | null): string =>
|
||||
@@ -44,9 +45,10 @@ export const PullRequestView: React.FC = () => {
|
||||
const status = useGitStatus(gitDirectory ?? null);
|
||||
const branches = useGitBranches(gitDirectory ?? null);
|
||||
const isGitRepo = useIsGitRepo(gitDirectory ?? null);
|
||||
const { ensureAll, ensureNestedRepos } = useGitStore(useShallow((state) => ({
|
||||
const { ensureAll, ensureNestedRepos, selectNestedRepo } = useGitStore(useShallow((state) => ({
|
||||
ensureAll: state.ensureAll,
|
||||
ensureNestedRepos: state.ensureNestedRepos,
|
||||
selectNestedRepo: state.selectNestedRepo,
|
||||
})));
|
||||
|
||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||
@@ -259,9 +261,10 @@ export const PullRequestView: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Non-repo root: surface nested-repository resolution (discovering, failed,
|
||||
// unsupported, none found, or settling on the auto-selected repository).
|
||||
if (rootIsGitRepo === false || isGitRepo === false) {
|
||||
// Non-repo root: surface nested-repository resolution while the operating
|
||||
// directory has not proven to be a repository (discovering, failed,
|
||||
// unsupported, none found, or settling on the auto-selected one).
|
||||
if (rootIsGitRepo === false && isGitRepo !== true) {
|
||||
return (
|
||||
<NestedRepoResolutionStates
|
||||
rootIsGitRepo={rootIsGitRepo}
|
||||
@@ -284,22 +287,41 @@ export const PullRequestView: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Repository switcher for non-repo roots with discovered nested
|
||||
// repositories; the pick is shared per root across git surfaces.
|
||||
const showRepositoryPicker =
|
||||
rootIsGitRepo === false && Array.isArray(nestedRepos) && nestedRepos.length > 0;
|
||||
|
||||
return (
|
||||
<ScrollableOverlay
|
||||
as={ScrollShadow}
|
||||
outerClassName="h-full min-h-0"
|
||||
className="px-4 py-3"
|
||||
disableHorizontal
|
||||
preventOverscroll
|
||||
>
|
||||
<PullRequestSection
|
||||
directory={gitDirectory ?? currentDirectory}
|
||||
branch={currentBranch}
|
||||
baseBranch={baseBranch}
|
||||
trackingBranch={status?.tracking ?? undefined}
|
||||
remotes={remotes}
|
||||
remoteBranches={remoteBranches}
|
||||
/>
|
||||
</ScrollableOverlay>
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
{showRepositoryPicker ? (
|
||||
<div className="flex shrink-0 items-center border-b border-border/60 px-4 py-2">
|
||||
<NestedRepoPicker
|
||||
repositories={nestedRepos}
|
||||
selectedRepository={gitDirectory ?? null}
|
||||
onSelectRepository={(repository) => {
|
||||
if (currentDirectory) selectNestedRepo(currentDirectory, repository);
|
||||
}}
|
||||
repositoryRoot={currentDirectory ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<ScrollableOverlay
|
||||
as={ScrollShadow}
|
||||
outerClassName="h-full min-h-0 flex-1"
|
||||
className="px-4 py-3"
|
||||
disableHorizontal
|
||||
preventOverscroll
|
||||
>
|
||||
<PullRequestSection
|
||||
directory={gitDirectory ?? currentDirectory}
|
||||
branch={currentBranch}
|
||||
baseBranch={baseBranch}
|
||||
trackingBranch={status?.tracking ?? undefined}
|
||||
remotes={remotes}
|
||||
remoteBranches={remoteBranches}
|
||||
/>
|
||||
</ScrollableOverlay>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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...');
|
||||
});
|
||||
});
|
||||
@@ -38,9 +38,16 @@ import { useWalkthroughStageProgress } from './useWalkthroughStageProgress';
|
||||
import { WalkthroughStream } from './WalkthroughStream';
|
||||
import { WalkthroughToc } from './WalkthroughToc';
|
||||
import { NestedRepoResolutionStates } from '@/components/views/git/NestedRepoResolutionStates';
|
||||
import { NestedRepoPicker } from '@/components/views/git/NestedRepoPicker';
|
||||
|
||||
interface WalkthroughViewProps {
|
||||
directory: string;
|
||||
/**
|
||||
* The context panel keeps this view mounted but hidden via CSS, so work
|
||||
* that should only run for a visible consumer has to be told. Defaults to
|
||||
* true for mounts that have no visibility signal.
|
||||
*/
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
const SCOPES: WalkthroughWorkingTreeScope[] = ['all', 'staged', 'working'];
|
||||
@@ -75,7 +82,7 @@ const TOC_MAX_FRACTION = 0.5;
|
||||
// pickers, 32px action, 36px arrows) read as misalignment, not hierarchy.
|
||||
const HEADER_COMPACT_WIDTH = 680;
|
||||
|
||||
export const WalkthroughView = ({ directory: rootDirectory }: WalkthroughViewProps) => {
|
||||
export const WalkthroughView = ({ directory: rootDirectory, visible = true }: WalkthroughViewProps) => {
|
||||
const { t, locale, locales, label } = useI18n();
|
||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
const [panelWidth, setPanelWidth] = useState(0);
|
||||
@@ -83,7 +90,7 @@ export const WalkthroughView = ({ directory: rootDirectory }: WalkthroughViewPro
|
||||
// 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 { rootIsGitRepo, gitDirectory, nestedRepos } = useNestedGitDirectory(rootDirectory || null, { enabled: visible });
|
||||
const directory = gitDirectory ?? rootDirectory;
|
||||
|
||||
// Panel width, not viewport width: this surface is resizable independently of
|
||||
@@ -494,7 +501,11 @@ export const WalkthroughView = ({ directory: rootDirectory }: WalkthroughViewPro
|
||||
|
||||
const isGitRepo = useIsGitRepo(gitDirectory || null);
|
||||
const ensureNestedRepos = useGitStore((state) => state.ensureNestedRepos);
|
||||
if (rootIsGitRepo === false || isGitRepo === false) {
|
||||
const selectNestedRepo = useGitStore((state) => state.selectNestedRepo);
|
||||
// Non-repo root: surface nested-repository resolution while the operating
|
||||
// directory has not proven to be a repository (discovering, failed,
|
||||
// unsupported, none found, or settling on the auto-selected one).
|
||||
if (rootIsGitRepo === false && isGitRepo !== true) {
|
||||
return (
|
||||
<NestedRepoResolutionStates
|
||||
rootIsGitRepo={rootIsGitRepo}
|
||||
@@ -510,6 +521,16 @@ export const WalkthroughView = ({ directory: rootDirectory }: WalkthroughViewPro
|
||||
return (
|
||||
<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">
|
||||
{rootIsGitRepo === false && Array.isArray(nestedRepos) && nestedRepos.length > 0 ? (
|
||||
<NestedRepoPicker
|
||||
repositories={nestedRepos}
|
||||
selectedRepository={gitDirectory ?? null}
|
||||
onSelectRepository={(repository) => {
|
||||
if (rootDirectory) selectNestedRepo(rootDirectory, repository);
|
||||
}}
|
||||
repositoryRoot={rootDirectory ?? undefined}
|
||||
/>
|
||||
) : null}
|
||||
<DropdownMenu open={sourceMenuOpen} onOpenChange={setSourceMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
|
||||
@@ -148,7 +148,7 @@ Important properties:
|
||||
- loading state is per-directory, not global
|
||||
- `ensureStatus()` and `ensureAll()` are the preferred entry points for consumers
|
||||
- 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), 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
|
||||
- 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 (Git tab, diff view, pull-request view, walkthrough view, mobile changes), 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
|
||||
- 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
|
||||
|
||||
Reference in New Issue
Block a user