= ({
}
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 = (
@@ -451,33 +441,13 @@ export const GitHeader: React.FC = ({
remotes={remotes}
/>
)}
- {repositoryOptionsForPicker.length > 0 ? (
-
+ {repositoryOptionsForPicker.length > 0 && onSelectRepository ? (
+
) : null}
diff --git a/packages/ui/src/components/views/git/NestedRepoPicker.tsx b/packages/ui/src/components/views/git/NestedRepoPicker.tsx
new file mode 100644
index 00000000..69b009f4
--- /dev/null
+++ b/packages/ui/src/components/views/git/NestedRepoPicker.tsx
@@ -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 = ({
+ 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 (
+
+ );
+};
diff --git a/packages/ui/src/components/views/git/NestedRepoResolutionStates.test.tsx b/packages/ui/src/components/views/git/NestedRepoResolutionStates.test.tsx
new file mode 100644
index 00000000..8e685852
--- /dev/null
+++ b/packages/ui/src/components/views/git/NestedRepoResolutionStates.test.tsx
@@ -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): string =>
+ renderToStaticMarkup(
+
+
+ ,
+ );
+
+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...');
+ });
+});
diff --git a/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx b/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx
index c085fdaa..a504eb34 100644
--- a/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx
+++ b/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx
@@ -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(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 (
+ {rootIsGitRepo === false && Array.isArray(nestedRepos) && nestedRepos.length > 0 ? (
+ {
+ if (rootDirectory) selectNestedRepo(rootDirectory, repository);
+ }}
+ repositoryRoot={rootDirectory ?? undefined}
+ />
+ ) : null}