diff --git a/packages/ui/src/apps/MobileChangesSurface.tsx b/packages/ui/src/apps/MobileChangesSurface.tsx index b8e98b59..463886b3 100644 --- a/packages/ui/src/apps/MobileChangesSurface.tsx +++ b/packages/ui/src/apps/MobileChangesSurface.tsx @@ -10,6 +10,7 @@ import { SyncActions } from '@/components/views/git/SyncActions'; import { PierreDiffViewer } from '@/components/views/PierreDiffViewer'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory'; +import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory'; import type { GitStatus } from '@/lib/api/types'; import { useI18n } from '@/lib/i18n'; import { generateCommitMessage, stageGitFile, stageGitFiles, unstageGitFile, unstageGitFiles } from '@/lib/gitApi'; @@ -21,6 +22,8 @@ import { useIsGitRepo, 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; @@ -56,12 +59,18 @@ type MobileChangesSurfaceProps = { export const MobileChangesSurface: React.FC = ({ onClose, initialDiffPath, initialDiffStaged = false }) => { const { t } = useI18n(); 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 isGitRepo = useIsGitRepo(currentDirectory || null); const isLoadingStatus = useGitLoadingStatus(currentDirectory || null); 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); @@ -465,6 +474,16 @@ export const MobileChangesSurface: React.FC = ({ onCl {status?.current || currentDirectory || ''}

+ {rootIsGitRepo === false && Array.isArray(nestedRepos) && nestedRepos.length > 0 ? ( + { + if (rootDirectory) selectNestedRepo(rootDirectory, repository); + }} + repositoryRoot={rootDirectory ?? undefined} + /> + ) : null}
{state}
@@ -474,12 +493,24 @@ export const MobileChangesSurface: React.FC = ({ onCl return renderListState(); } - if (isLoadingStatus && isGitRepo === null) { - return renderListState(); + // 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( + { + if (rootDirectory) void ensureNestedRepos(rootDirectory, { force: true }); + }} + /> + ); } - if (isGitRepo === false) { - return renderListState(); + if (isLoadingStatus && isGitRepo === null) { + return renderListState(); } if (route.type === 'diff') { diff --git a/packages/ui/src/components/layout/ContextPanel.tsx b/packages/ui/src/components/layout/ContextPanel.tsx index 5917547a..3e86e28b 100644 --- a/packages/ui/src/components/layout/ContextPanel.tsx +++ b/packages/ui/src/components/layout/ContextPanel.tsx @@ -1291,7 +1291,7 @@ export const ContextPanel: React.FC = () => { {hasWalkthroughTab ? (
- +
) : null} diff --git a/packages/ui/src/components/views/DiffView.tsx b/packages/ui/src/components/views/DiffView.tsx index 90e370a4..b6f47eb2 100644 --- a/packages/ui/src/components/views/DiffView.tsx +++ b/packages/ui/src/components/views/DiffView.tsx @@ -2,6 +2,8 @@ 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'; @@ -997,7 +999,11 @@ export const DiffView: React.FC = ({ }) => { const { t } = useI18n(); const { git, files } = useRuntimeAPIs(); - const effectiveDirectory = useEffectiveDirectory(); + 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 { rootIsGitRepo, gitDirectory: nestedGitDirectory, nestedRepos: nestedRepoOptions } = useNestedGitDirectory(rootDirectory ?? null); + const effectiveDirectory = nestedGitDirectory ?? rootDirectory; const openContextSurface = useUIStore((state) => state.openContextSurface); const requestWalkthroughSource = useWalkthroughStore((state) => state.requestSource); const { screenWidth, isMobile } = useDeviceInfo(); @@ -1007,6 +1013,7 @@ export const DiffView: React.FC = ({ 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); @@ -1038,7 +1045,7 @@ export const DiffView: React.FC = ({ const setDiffWrapLines = useUIStore((state) => state.setDiffWrapLines); const openContextFileAtLine = useUIStore((state) => state.openContextFileAtLine); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); - const sessionMessages = useSessionMessages(currentSessionId ?? '', effectiveDirectory ?? undefined); + const sessionMessages = useSessionMessages(currentSessionId ?? '', rootDirectory ?? undefined); const diffWrapLines = diffWrapLinesStore; const forcedStaged = activeDiffScope === 'staged' ? true : activeDiffScope === 'working' ? false : null; const activeDiffStaged = forcedStaged ?? displayFileStaged; @@ -1645,7 +1652,7 @@ export const DiffView: React.FC = ({ const handleStartReviewFlow = React.useCallback(async (execution: ReviewFlowExecution) => { if (!currentSessionId) return; - const directory = useSessionUIStore.getState().getDirectoryForSession(currentSessionId) || effectiveDirectory || ''; + const directory = useSessionUIStore.getState().getDirectoryForSession(currentSessionId) || rootDirectory || ''; if (!directory) { toast.error(t('diffView.reviewDialog.toast.noSessionDirectory')); return; @@ -1671,7 +1678,7 @@ export const DiffView: React.FC = ({ } finally { setReviewFlowSubmitting(false); } - }, [currentSessionId, effectiveDirectory, t]); + }, [currentSessionId, rootDirectory, t]); const scrollToFile = React.useCallback((path: string): boolean => { const node = fileSectionRefs.current.get(path); @@ -2070,6 +2077,16 @@ export const DiffView: React.FC = ({ return (
+ {rootIsGitRepo === false && Array.isArray(nestedRepoOptions) && nestedRepoOptions.length > 0 ? ( + { + if (rootDirectory) selectNestedRepo(rootDirectory, repository); + }} + repositoryRoot={rootDirectory ?? undefined} + /> + ) : null} {!isMobile && ( activeDiffScope === 'working' || activeDiffScope === 'staged' || activeDiffScope === 'turn' || activeDiffScope === 'branch' ? ( = ({ isActive }) => { loadDefaultGitIdentityId: s.loadDefaultGitIdentityId, }))); - const isGitRepo = useIsGitRepo(currentDirectory ?? null); - const status = useGitStatus(currentDirectory ?? null); + // 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 + // one, `gitDirectory` is the effective repository all git data and actions + // operate on. The hook owns probing, discovery, auto-select, and + // stale-selection recovery; data fetching below keys off its result. + const { rootIsGitRepo, gitDirectory, nestedRepos } = useNestedGitDirectory( + currentDirectory ?? null, + { enabled: isActive }, + ); + const isGitRepo = useIsGitRepo(gitDirectory ?? null); + const status = useGitStatus(gitDirectory ?? null); // Authoritative session↔worktree attachment for repair action display const worktreeAttachment = useSessionWorktreeStore((s) => @@ -266,11 +277,11 @@ export const GitView: React.FC = ({ isActive }) => { : undefined; const worktreeMetadata = useDetectedWorktreeMetadata(currentDirectory, storeWorktreeMetadata, status?.current ?? undefined); - const branches = useGitBranches(currentDirectory ?? null); - const log = useGitLog(currentDirectory ?? null); - const currentIdentity = useGitIdentity(currentDirectory ?? null); - const isLoading = useGitLoadingStatus(currentDirectory ?? null); - const isLogLoading = useGitLoadingLog(currentDirectory ?? null); + const branches = useGitBranches(gitDirectory ?? null); + const log = useGitLog(gitDirectory ?? null); + const currentIdentity = useGitIdentity(gitDirectory ?? null); + const isLoading = useGitLoadingStatus(gitDirectory ?? null); + const isLogLoading = useGitLoadingLog(gitDirectory ?? null); const { setActiveDirectory, fetchAll, @@ -285,6 +296,8 @@ export const GitView: React.FC = ({ isActive }) => { moveStatusPathsOptimistically, restoreStatus, bumpIndexRevision, + ensureNestedRepos, + selectNestedRepo, } = useGitStore(useShallow((state) => ({ setActiveDirectory: state.setActiveDirectory, fetchAll: state.fetchAll, @@ -299,6 +312,8 @@ export const GitView: React.FC = ({ isActive }) => { moveStatusPathsOptimistically: state.moveStatusPathsOptimistically, restoreStatus: state.restoreStatus, bumpIndexRevision: state.bumpIndexRevision, + ensureNestedRepos: state.ensureNestedRepos, + selectNestedRepo: state.selectNestedRepo, }))); const isMobile = useUIStore((state) => state.isMobile); const openContextDiff = useUIStore((state) => state.openContextDiff); @@ -306,10 +321,10 @@ export const GitView: React.FC = ({ isActive }) => { const prStatusBranch = status?.current ?? null; const prChipStatus = useGitHubPrStatusStore((state) => { - if (!currentDirectory || !prStatusBranch) { + if (!gitDirectory || !prStatusBranch) { return null; } - return getFreshestPrStatusForBranch(state.entries, currentDirectory, prStatusBranch); + return getFreshestPrStatusForBranch(state.entries, gitDirectory, prStatusBranch); }); const navigateToDiff = useUIStore((state) => state.navigateToDiff); @@ -333,12 +348,12 @@ export const GitView: React.FC = ({ isActive }) => { clearScheduledGitReconcile(); gitReconcileTimeoutRef.current = window.setTimeout(() => { gitReconcileTimeoutRef.current = null; - if (normalizePath(directory) !== normalizePath(currentDirectory)) { + if (normalizePath(directory) !== normalizePath(gitDirectory)) { return; } void fetchStatus(directory, git, { silent: true }); }, GIT_RECONCILE_DELAY_MS); - }, [clearScheduledGitReconcile, currentDirectory, fetchStatus, git]); + }, [clearScheduledGitReconcile, gitDirectory, fetchStatus, git]); React.useEffect(() => clearScheduledGitReconcile, [clearScheduledGitReconcile]); @@ -492,9 +507,9 @@ export const GitView: React.FC = ({ isActive }) => { const shouldHideNotGitState = isPendingWorktreeSetup || isWaitingForGitRefreshAfterBootstrap; const initialSnapshot = React.useMemo(() => { - if (!currentDirectory) return null; - return gitViewSnapshots.get(currentDirectory) ?? null; - }, [currentDirectory]); + if (!gitDirectory) return null; + return gitViewSnapshots.get(gitDirectory) ?? null; + }, [gitDirectory]); const settingsGitmojiEnabled = useConfigStore((state) => state.settingsGitmojiEnabled); const [rootBranchHint, setRootBranchHint] = React.useState(null); @@ -664,7 +679,7 @@ export const GitView: React.FC = ({ isActive }) => { // Restore conflict state from localStorage on mount React.useEffect(() => { - if (!conflictStorageKey || typeof window === 'undefined' || !currentDirectory) return; + if (!conflictStorageKey || typeof window === 'undefined' || !gitDirectory) return; const raw = window.localStorage.getItem(conflictStorageKey); if (!raw) return; @@ -676,8 +691,8 @@ export const GitView: React.FC = ({ isActive }) => { operation: 'merge' | 'rebase'; }; - // Validate the stored state matches current directory - if (parsed.directory !== currentDirectory) { + // Validate the stored state matches the effective repository + if (parsed.directory !== gitDirectory) { window.localStorage.removeItem(conflictStorageKey); return; } @@ -689,7 +704,7 @@ export const GitView: React.FC = ({ isActive }) => { } catch { window.localStorage.removeItem(conflictStorageKey); } - }, [conflictStorageKey, currentDirectory]); + }, [conflictStorageKey, gitDirectory]); const [stashDialogOpen, setStashDialogOpen] = React.useState(false); const [stashDialogOperation, setStashDialogOperation] = React.useState<'merge' | 'rebase'>('merge'); const [stashDialogBranch, setStashDialogBranch] = React.useState(''); @@ -725,7 +740,7 @@ export const GitView: React.FC = ({ isActive }) => { }, [loadingCommitHashes]); React.useEffect(() => { - if (!currentDirectory || !git) return; + if (!gitDirectory || !git) return; // Find hashes that are expanded but not yet loaded or loading const hashesToLoad = Array.from(expandedCommitHashes).filter( @@ -748,7 +763,7 @@ export const GitView: React.FC = ({ isActive }) => { void Promise.all( hashesToLoad.map((hash) => git - .getCommitFiles(currentDirectory, hash) + .getCommitFiles(gitDirectory, hash) .then((response) => ({ hash, files: response.files })) .catch((error) => { console.error('Failed to fetch commit files:', error); @@ -792,16 +807,26 @@ export const GitView: React.FC = ({ isActive }) => { return next; }); }; - }, [expandedCommitHashes, currentDirectory, git]); + }, [expandedCommitHashes, gitDirectory, git]); + + // Restore the per-repository draft when the effective repository changes + // (e.g. the user picks a different nested repository from the picker), + // mirroring the fresh-mount behavior of a directory switch. + React.useEffect(() => { + if (!gitDirectory) return; + const snapshot = gitViewSnapshots.get(gitDirectory) ?? null; + setCommitMessage(snapshot?.commitMessage ?? ''); + setGeneratedHighlights(snapshot?.generatedHighlights ?? []); + }, [gitDirectory]); React.useEffect(() => { - if (!currentDirectory) return; - rememberSnapshot(currentDirectory, { - directory: currentDirectory, + if (!gitDirectory) return; + rememberSnapshot(gitDirectory, { + directory: gitDirectory, commitMessage, generatedHighlights, }); - }, [commitMessage, currentDirectory, generatedHighlights]); + }, [commitMessage, gitDirectory, generatedHighlights]); React.useEffect(() => { if (!isActive) return; @@ -812,25 +837,25 @@ export const GitView: React.FC = ({ isActive }) => { React.useEffect(() => { if (!isActive) return; - if (!currentDirectory || !git?.getRemoteUrl) { + if (!gitDirectory || !git?.getRemoteUrl || isGitRepo !== true) { setRemoteUrl(null); return; } let cancelled = false; git - .getRemoteUrl(currentDirectory) + .getRemoteUrl(gitDirectory) .then((url) => { if (!cancelled) setRemoteUrl(url); }) .catch(() => { if (!cancelled) setRemoteUrl(null); }); return () => { cancelled = true; }; - }, [isActive, currentDirectory, git]); + }, [isActive, gitDirectory, git, isGitRepo]); const refreshRemotes = React.useCallback(async () => { - if (!currentDirectory || !git?.getRemotes) { + if (!gitDirectory || !git?.getRemotes || isGitRepo !== true) { setRemotes([]); return; } try { - const remoteList = await git.getRemotes(currentDirectory); + const remoteList = await git.getRemotes(gitDirectory); if (mountedRef.current) { setRemotes(remoteList); } @@ -839,7 +864,7 @@ export const GitView: React.FC = ({ isActive }) => { setRemotes([]); } } - }, [currentDirectory, git]); + }, [gitDirectory, git, isGitRepo]); React.useEffect(() => { if (!isActive) return; @@ -848,37 +873,37 @@ export const GitView: React.FC = ({ isActive }) => { React.useEffect(() => { if (!isActive) return; - if (currentDirectory) { + if (currentDirectory && gitDirectory) { setActiveDirectory(currentDirectory); - void ensureAll(currentDirectory, git); + void ensureAll(gitDirectory, git); } - }, [isActive, currentDirectory, setActiveDirectory, ensureAll, git]); + }, [isActive, currentDirectory, gitDirectory, setActiveDirectory, ensureAll, git]); React.useEffect(() => { if (!isActive) return; - if (!currentDirectory) { + if (!gitDirectory) { return; } return sessionEvents.onGitRefreshHint((hint) => { - if (normalizePath(hint.directory) !== normalizePath(currentDirectory)) { + if (normalizePath(hint.directory) !== normalizePath(gitDirectory)) { return; } if (hint.paths?.length) { - clearDiffCache(currentDirectory, hint.paths); + clearDiffCache(gitDirectory, hint.paths); } - void fetchStatus(currentDirectory, git, { silent: true }); + void fetchStatus(gitDirectory, git, { silent: true }); }); - }, [isActive, clearDiffCache, currentDirectory, fetchStatus, git]); + }, [isActive, clearDiffCache, gitDirectory, fetchStatus, git]); const refreshStatusAndBranches = React.useCallback( async (showErrors = true) => { - if (!currentDirectory) return; + if (!gitDirectory) return; try { await Promise.all([ - fetchStatus(currentDirectory, git), - fetchBranches(currentDirectory, git), + fetchStatus(gitDirectory, git), + fetchBranches(gitDirectory, git), ]); } catch (err) { if (showErrors) { @@ -888,42 +913,42 @@ export const GitView: React.FC = ({ isActive }) => { } } }, - [currentDirectory, git, fetchStatus, fetchBranches, t] + [gitDirectory, git, fetchStatus, fetchBranches, t] ); const refreshLog = React.useCallback(async () => { - if (!currentDirectory) return; - await fetchLog(currentDirectory, git, logMaxCountLocal); - }, [currentDirectory, git, fetchLog, logMaxCountLocal]); + if (!gitDirectory) return; + await fetchLog(gitDirectory, git, logMaxCountLocal); + }, [gitDirectory, git, fetchLog, logMaxCountLocal]); const refreshIdentity = React.useCallback(async () => { - if (!currentDirectory) return; - await fetchIdentity(currentDirectory, git); - }, [currentDirectory, git, fetchIdentity]); + if (!gitDirectory) return; + await fetchIdentity(gitDirectory, git); + }, [gitDirectory, git, fetchIdentity]); React.useEffect(() => { if (!isActive) return; - if (!currentDirectory) return; + if (!gitDirectory) return; if (!git?.hasLocalIdentity) return; if (isGitRepo !== true) return; const defaultId = typeof defaultGitIdentityId === 'string' ? defaultGitIdentityId.trim() : ''; if (!defaultId || defaultId === 'global') return; - const previousAttempt = autoAppliedDefaultRef.current.get(currentDirectory); + const previousAttempt = autoAppliedDefaultRef.current.get(gitDirectory); if (previousAttempt === defaultId) return; let cancelled = false; const run = async () => { try { - const hasLocal = await git.hasLocalIdentity?.(currentDirectory); + const hasLocal = await git.hasLocalIdentity?.(gitDirectory); if (cancelled) return; if (hasLocal === true) return; beginIdentityApply(); - await git.setGitIdentity(currentDirectory, defaultId); - autoAppliedDefaultRef.current.set(currentDirectory, defaultId); + await git.setGitIdentity(gitDirectory, defaultId); + autoAppliedDefaultRef.current.set(gitDirectory, defaultId); await refreshIdentity(); } catch (error) { console.warn('Failed to auto-apply default git identity:', error); @@ -939,7 +964,7 @@ export const GitView: React.FC = ({ isActive }) => { return () => { cancelled = true; }; - }, [isActive, beginIdentityApply, currentDirectory, defaultGitIdentityId, endIdentityApply, git, isGitRepo, refreshIdentity]); + }, [isActive, beginIdentityApply, gitDirectory, defaultGitIdentityId, endIdentityApply, git, isGitRepo, refreshIdentity]); const changeEntries = React.useMemo(() => { if (!status) return []; @@ -960,7 +985,7 @@ export const GitView: React.FC = ({ isActive }) => { ); React.useEffect(() => { - if (!currentDirectory || changeEntries.length === 0) { + if (!gitDirectory || changeEntries.length === 0) { return; } @@ -984,13 +1009,13 @@ export const GitView: React.FC = ({ isActive }) => { } const timeoutId = window.setTimeout(() => { - void prefetchDiffs(currentDirectory, git, orderedPaths, { maxFiles: GIT_DIFF_PRIORITY_PREFETCH_LIMIT }); + void prefetchDiffs(gitDirectory, git, orderedPaths, { maxFiles: GIT_DIFF_PRIORITY_PREFETCH_LIMIT }); }, 120); return () => { window.clearTimeout(timeoutId); }; - }, [changeEntries, currentDirectory, git, prefetchDiffs, stagedChangeEntries, visibleChangePaths]); + }, [changeEntries, gitDirectory, git, prefetchDiffs, stagedChangeEntries, visibleChangePaths]); const getPushedRemoteName = (result?: Awaited>) => { return result?.pushed[0]?.remote @@ -1001,7 +1026,7 @@ export const GitView: React.FC = ({ isActive }) => { }; const handleSyncAction = async (action: Exclude, remote?: GitRemote) => { - if (!currentDirectory) return; + if (!gitDirectory) return; setSyncAction(action); try { @@ -1021,20 +1046,20 @@ export const GitView: React.FC = ({ isActive }) => { if (!remote) { throw new Error('No remote available for fetch'); } - await git.gitFetch(currentDirectory, { remote: remote.name }); + await git.gitFetch(gitDirectory, { remote: remote.name }); toast.success(t('gitView.toast.fetchedFromRemote', { name: remote.name })); } else if (action === 'pull') { if (!remote) { throw new Error('No remote available for pull'); } - const result = await git.gitPull(currentDirectory, getPullOptions(remote)); + const result = await git.gitPull(gitDirectory, getPullOptions(remote)); toast.success( result.files.length === 1 ? t('gitView.toast.pulledFilesSingle', { count: result.files.length, name: remote.name }) : t('gitView.toast.pulledFilesPlural', { count: result.files.length, name: remote.name }) ); } else if (action === 'push') { - const result = await git.gitPush(currentDirectory); + const result = await git.gitPush(gitDirectory); toast.success(t('gitView.toast.pushedToUpstream', { name: getPushedRemoteName(result) })); } else if (action === 'sync') { if (!remote) { @@ -1042,21 +1067,21 @@ export const GitView: React.FC = ({ isActive }) => { } let pulledFileCount = 0; let pushedChanges = false; - await git.gitFetch(currentDirectory, { remote: remote.name }); - const afterFetch = await git.getGitStatus(currentDirectory); + await git.gitFetch(gitDirectory, { remote: remote.name }); + const afterFetch = await git.getGitStatus(gitDirectory); if ((afterFetch.behind ?? 0) > 0) { if ((afterFetch.files?.length ?? 0) > 0) { toast.error(t('gitView.toast.commitOrStashBeforeSync')); return; } - const pullResult = await git.gitPull(currentDirectory, getPullOptions(remote)); + const pullResult = await git.gitPull(gitDirectory, getPullOptions(remote)); pulledFileCount = pullResult.files.length; } - const afterPull = await git.getGitStatus(currentDirectory); + const afterPull = await git.getGitStatus(gitDirectory); if ((afterPull.ahead ?? 0) > 0) { - await git.gitPush(currentDirectory); + await git.gitPush(gitDirectory); pushedChanges = true; } if (pulledFileCount > 0 && pushedChanges) { @@ -1092,7 +1117,7 @@ export const GitView: React.FC = ({ isActive }) => { }; const handleRemoveRemote = React.useCallback(async (remote: GitRemote) => { - if (!currentDirectory) return; + if (!gitDirectory) return; const remoteName = remote.name.trim(); if (!remoteName) { @@ -1106,7 +1131,7 @@ export const GitView: React.FC = ({ isActive }) => { setRemovingRemoteName(remoteName); try { - await git.removeRemote(currentDirectory, { remote: remoteName }); + await git.removeRemote(gitDirectory, { remote: remoteName }); toast.success(t('gitView.toast.removedRemote', { name: remoteName })); await Promise.all([ refreshStatusAndBranches(false), @@ -1118,10 +1143,10 @@ export const GitView: React.FC = ({ isActive }) => { } finally { setRemovingRemoteName(null); } - }, [currentDirectory, git, refreshRemotes, refreshStatusAndBranches, t]); + }, [gitDirectory, git, refreshRemotes, refreshStatusAndBranches, t]); const handleCommit = async (options: { pushAfter?: boolean } = {}) => { - if (!currentDirectory) return; + if (!gitDirectory) return; if (!commitMessage.trim()) { toast.error(t('gitView.toast.enterCommitMessage')); return; @@ -1137,11 +1162,11 @@ export const GitView: React.FC = ({ isActive }) => { setCommitAction(action); try { - await git.createGitCommit(currentDirectory, commitMessage.trim(), { + await git.createGitCommit(gitDirectory, commitMessage.trim(), { files: filesToCommit, stageFiles: [], }); - bumpIndexRevision(currentDirectory); + bumpIndexRevision(gitDirectory); toast.success(t('gitView.toast.commitCreated')); setCommitMessage(''); clearGeneratedHighlights(); @@ -1161,21 +1186,21 @@ export const GitView: React.FC = ({ isActive }) => { ? status.tracking.slice(trackingPrefix.length) : undefined; - await git.gitFetch(currentDirectory, { remote: remote.name }); - const afterFetch = await git.getGitStatus(currentDirectory); + await git.gitFetch(gitDirectory, { remote: remote.name }); + const afterFetch = await git.getGitStatus(gitDirectory); if ((afterFetch.behind ?? 0) > 0) { if ((afterFetch.files?.length ?? 0) > 0) { toast.error(t('gitView.toast.commitOrStashBeforeSync')); await refreshStatusAndBranches(false); return; } - await git.gitPull(currentDirectory, { remote: remote.name, branch: trackedBranch, rebase: true }); + await git.gitPull(gitDirectory, { remote: remote.name, branch: trackedBranch, rebase: true }); } - const afterPull = await git.getGitStatus(currentDirectory); + const afterPull = await git.getGitStatus(gitDirectory); let result: Awaited> | undefined; if ((afterPull.ahead ?? 0) > 0) { - result = await git.gitPush(currentDirectory); + result = await git.gitPush(gitDirectory); } toast.success(t('gitView.toast.pushedToUpstream', { name: getPushedRemoteName(result) })); triggerFireworks(); @@ -1198,7 +1223,7 @@ export const GitView: React.FC = ({ isActive }) => { }; const handleGenerateCommitMessage = React.useCallback(async () => { - if (!currentDirectory) return; + if (!gitDirectory) return; const selectedFilePaths = stagedChangeEntries.map((file) => file.path).sort(); if (selectedFilePaths.length === 0) { toast.error(t('gitView.toast.stageFileToDescribe')); @@ -1206,13 +1231,13 @@ export const GitView: React.FC = ({ isActive }) => { } console.error('[git-generation][browser] generate button clicked', { - directory: currentDirectory, + directory: gitDirectory, selectedFiles: selectedFilePaths.length, }); setIsGeneratingMessage(true); try { - const { message } = await generateSessionCommitMessage(currentDirectory, selectedFilePaths); + const { message } = await generateSessionCommitMessage(gitDirectory, selectedFilePaths); const subject = message.subject?.trim() ?? ''; const highlights = Array.isArray(message.highlights) ? message.highlights : []; @@ -1243,7 +1268,7 @@ export const GitView: React.FC = ({ isActive }) => { } finally { setIsGeneratingMessage(false); } - }, [currentDirectory, stagedChangeEntries, settingsGitmojiEnabled, gitmojiEmojis, scrollActionPanelToBottom, t]); + }, [gitDirectory, stagedChangeEntries, settingsGitmojiEnabled, gitmojiEmojis, scrollActionPanelToBottom, t]); const formatBlockingReason = (reason: ReturnType[number]): string => { if (reason.reason === 'attention') { @@ -1256,7 +1281,7 @@ export const GitView: React.FC = ({ isActive }) => { }; const handleCreateBranch = async (branchName: string, remote?: GitRemote) => { - if (!currentDirectory || !status) return; + if (!gitDirectory || !status) return; const blockingReasons = getMutationBlockingReasons(worktreeAttachment); if (blockingReasons.length > 0) { @@ -1268,15 +1293,15 @@ export const GitView: React.FC = ({ isActive }) => { const remoteName = remote?.name ?? 'origin'; try { - await git.createBranch(currentDirectory, branchName, checkoutBase ?? 'HEAD'); + await git.createBranch(gitDirectory, branchName, checkoutBase ?? 'HEAD'); toast.success(t('gitView.toast.createdBranch', { name: branchName })); // Checkout the new branch and stay on it - await git.checkoutBranch(currentDirectory, branchName); + await git.checkoutBranch(gitDirectory, branchName); let pushSucceeded = false; try { - await git.gitPush(currentDirectory, { + await git.gitPush(gitDirectory, { remote: remoteName, branch: branchName, options: ['--set-upstream'], @@ -1310,7 +1335,7 @@ export const GitView: React.FC = ({ isActive }) => { }; const handleRenameBranch = async (oldName: string, newName: string) => { - if (!currentDirectory) return; + if (!gitDirectory) return; const blockingReasons = getMutationBlockingReasons(worktreeAttachment); if (blockingReasons.length > 0) { @@ -1319,7 +1344,7 @@ export const GitView: React.FC = ({ isActive }) => { } try { - await git.renameBranch(currentDirectory, oldName, newName); + await git.renameBranch(gitDirectory, oldName, newName); toast.success(t('gitView.toast.renamedBranch', { oldName, newName })); await refreshStatusAndBranches(); await refreshLog(); @@ -1331,7 +1356,7 @@ export const GitView: React.FC = ({ isActive }) => { }; const handleCheckoutBranch = async (branch: string) => { - if (!currentDirectory) return; + if (!gitDirectory) return; // Block mutation if worktree is in an attention-required state const blockingReasons = getMutationBlockingReasons(worktreeAttachment); @@ -1349,7 +1374,7 @@ export const GitView: React.FC = ({ isActive }) => { try { // Picking a remote-tracking branch checks out the local branch that // tracks it, so report the branch the repository actually landed on. - const result = await git.checkoutBranch(currentDirectory, normalized); + const result = await git.checkoutBranch(gitDirectory, normalized); toast.success(t('gitView.toast.checkedOut', { name: result?.branch || normalized })); await refreshStatusAndBranches(); await refreshLog(); @@ -1361,11 +1386,11 @@ export const GitView: React.FC = ({ isActive }) => { }; const handleApplyIdentity = async (profile: GitIdentityProfile) => { - if (!currentDirectory) return; + if (!gitDirectory) return; beginIdentityApply(); try { - await git.setGitIdentity(currentDirectory, profile.id); + await git.setGitIdentity(gitDirectory, profile.id); toast.success(t('gitView.toast.appliedIdentity', { name: profile.name })); await refreshIdentity(); } catch (err) { @@ -1548,7 +1573,7 @@ export const GitView: React.FC = ({ isActive }) => { : null; React.useEffect(() => { - if (!currentDirectory || !git || !log?.all?.length || !currentBranch || !baseBranch || currentBranch === baseBranch) { + if (!gitDirectory || !git || !log?.all?.length || !currentBranch || !baseBranch || currentBranch === baseBranch) { setHistoryBranchDivider(null); return; } @@ -1557,7 +1582,7 @@ export const GitView: React.FC = ({ isActive }) => { const resolveBranchDivider = async () => { try { - const branchOnlyLog = await git.getGitLog(currentDirectory, { + const branchOnlyLog = await git.getGitLog(gitDirectory, { from: baseBranch, to: 'HEAD', maxCount: logMaxCountLocal, @@ -1610,21 +1635,21 @@ export const GitView: React.FC = ({ isActive }) => { return () => { cancelled = true; }; - }, [baseBranch, currentBranch, currentDirectory, git, log, logMaxCountLocal]); + }, [baseBranch, currentBranch, gitDirectory, git, log, logMaxCountLocal]); // Clear graph log when directory changes React.useEffect(() => { setGraphLog(null); - }, [currentDirectory]); + }, [gitDirectory]); React.useEffect(() => { - if (gitLogDialogMode !== 'graph' || !currentDirectory) { + if (gitLogDialogMode !== 'graph' || !gitDirectory) { if (gitLogDialogMode !== 'graph') setGraphLog(null); return; } let cancelled = false; setGraphLogLoading(true); - git.getGitLog(currentDirectory, { maxCount: graphLogMaxCount, all: true }) + git.getGitLog(gitDirectory, { maxCount: graphLogMaxCount, all: true }) .then((result) => { if (!cancelled) setGraphLog(result); }) @@ -1635,33 +1660,33 @@ export const GitView: React.FC = ({ isActive }) => { if (!cancelled) setGraphLogLoading(false); }); return () => { cancelled = true; }; - }, [gitLogDialogMode, currentDirectory, graphLogMaxCount, graphLogRefreshToken, git]); + }, [gitLogDialogMode, gitDirectory, graphLogMaxCount, graphLogRefreshToken, git]); // Keep these sections stable in layout; individual cards render placeholders when unavailable. const moveChangePaths = React.useCallback((paths: string[], direction: GitIndexMutationDirection) => { - if (!currentDirectory || paths.length === 0) return; + if (!gitDirectory || paths.length === 0) return; const uniquePaths = Array.from(new Set(paths)); setMovingChangePaths((previous) => { const next = new Set(previous); uniquePaths.forEach((path) => next.add(path)); return next; }); - const previousStatus = moveStatusPathsOptimistically(currentDirectory, uniquePaths, direction); + const previousStatus = moveStatusPathsOptimistically(gitDirectory, uniquePaths, direction); gitIndexMutationQueue.enqueue({ - directory: currentDirectory, + directory: gitDirectory, direction, paths: new Set(uniquePaths), - rollback: () => restoreStatus(currentDirectory, previousStatus), + rollback: () => restoreStatus(gitDirectory, previousStatus), }); scheduleGitMutationFlush(); - }, [currentDirectory, gitIndexMutationQueue, moveStatusPathsOptimistically, restoreStatus, scheduleGitMutationFlush]); + }, [gitDirectory, gitIndexMutationQueue, moveStatusPathsOptimistically, restoreStatus, scheduleGitMutationFlush]); const handleRevertFile = React.useCallback( async (filePath: string) => { - if (!currentDirectory) return; + if (!gitDirectory) return; setRevertingPaths((previous) => { const next = new Set(previous); @@ -1670,7 +1695,7 @@ export const GitView: React.FC = ({ isActive }) => { }); try { - await git.revertGitFile(currentDirectory, filePath, { scope: 'working' }); + await git.revertGitFile(gitDirectory, filePath, { scope: 'working' }); toast.success(t('gitView.toast.revertedFile', { path: filePath })); await refreshStatusAndBranches(false); } catch (err) { @@ -1684,12 +1709,12 @@ export const GitView: React.FC = ({ isActive }) => { }); } }, - [currentDirectory, refreshStatusAndBranches, git, t] + [gitDirectory, refreshStatusAndBranches, git, t] ); const handleRevertPaths = React.useCallback( async (paths: string[], setGlobalReverting: boolean, scope: 'all' | 'working' = 'all') => { - if (!currentDirectory || paths.length === 0) { + if (!gitDirectory || paths.length === 0) { return; } @@ -1715,7 +1740,7 @@ export const GitView: React.FC = ({ isActive }) => { try { await Promise.all(uniquePaths.map(async (filePath) => { try { - await git.revertGitFile(currentDirectory, filePath, { scope }); + await git.revertGitFile(gitDirectory, filePath, { scope }); } catch (err) { failed.push({ path: filePath, @@ -1725,7 +1750,7 @@ export const GitView: React.FC = ({ isActive }) => { })); if (touchesStagedIndex && failed.length < uniquePaths.length) { - bumpIndexRevision(currentDirectory); + bumpIndexRevision(gitDirectory); } await refreshStatusAndBranches(false); @@ -1757,7 +1782,7 @@ export const GitView: React.FC = ({ isActive }) => { } } }, - [bumpIndexRevision, currentDirectory, git, isRevertingAll, refreshStatusAndBranches, revertingPaths, stagedChangeEntries, t] + [bumpIndexRevision, gitDirectory, git, isRevertingAll, refreshStatusAndBranches, revertingPaths, stagedChangeEntries, t] ); const handleRevertAll = React.useCallback( @@ -1775,12 +1800,12 @@ export const GitView: React.FC = ({ isActive }) => { ); const handleViewChangeDiff = React.useCallback((path: string, staged: boolean) => { - if (currentDirectory && !isMobile) { - openContextDiff(currentDirectory, path, staged); + if (gitDirectory && !isMobile) { + openContextDiff(gitDirectory, path, staged); return; } navigateToDiff(path, staged); - }, [currentDirectory, isMobile, navigateToDiff, openContextDiff]); + }, [gitDirectory, isMobile, navigateToDiff, openContextDiff]); const openStashes = React.useCallback(() => setIsStashesDialogOpen(true), []); @@ -1928,7 +1953,7 @@ export const GitView: React.FC = ({ isActive }) => { const handleMerge = React.useCallback( async (branch: string) => { - if (!currentDirectory) return; + if (!gitDirectory) return; setBranchOperation('merge'); resetOperationLogs(); @@ -1939,19 +1964,19 @@ export const GitView: React.FC = ({ isActive }) => { try { if (target.remote && target.remoteBranch) { addOperationLog(`Fetching ${target.remote}/${target.remoteBranch}...`, 'running'); - await git.gitFetch(currentDirectory, { remote: target.remote, branch: target.remoteBranch }); + await git.gitFetch(gitDirectory, { remote: target.remote, branch: target.remoteBranch }); updateLastLog('done', `Fetched ${target.remote}/${target.remoteBranch}`); } addOperationLog(`Merging ${target.branch} into ${currentBranch}...`, 'running'); - const result = await git.merge(currentDirectory, { branch: target.branch }); + const result = await git.merge(gitDirectory, { branch: target.branch }); if (result.conflict) { updateLastLog('error', `Merge conflicts detected`); setConflictFiles(result.conflictFiles ?? []); setConflictOperation('merge'); setConflictDialogOpen(true); - persistConflictState(currentDirectory, result.conflictFiles ?? [], 'merge'); + persistConflictState(gitDirectory, result.conflictFiles ?? [], 'merge'); } else { updateLastLog('done', `Merged ${target.branch} into ${currentBranch}`); clearConflictState(); @@ -1973,12 +1998,12 @@ export const GitView: React.FC = ({ isActive }) => { } // Note: branchOperation is cleared when dialog closes via handleOperationComplete }, - [currentDirectory, git, status, resolveIntegrationTarget, refreshStatusAndBranches, refreshLog, isUncommittedChangesError, persistConflictState, clearConflictState, addOperationLog, updateLastLog, resetOperationLogs] + [gitDirectory, git, status, resolveIntegrationTarget, refreshStatusAndBranches, refreshLog, isUncommittedChangesError, persistConflictState, clearConflictState, addOperationLog, updateLastLog, resetOperationLogs] ); const handleRebase = React.useCallback( async (branch: string) => { - if (!currentDirectory) return; + if (!gitDirectory) return; setBranchOperation('rebase'); resetOperationLogs(); @@ -1989,19 +2014,19 @@ export const GitView: React.FC = ({ isActive }) => { try { if (target.remote && target.remoteBranch) { addOperationLog(`Fetching ${target.remote}/${target.remoteBranch}...`, 'running'); - await git.gitFetch(currentDirectory, { remote: target.remote, branch: target.remoteBranch }); + await git.gitFetch(gitDirectory, { remote: target.remote, branch: target.remoteBranch }); updateLastLog('done', `Fetched ${target.remote}/${target.remoteBranch}`); } addOperationLog(`Rebasing ${currentBranch} onto ${target.branch}...`, 'running'); - const result = await git.rebase(currentDirectory, { onto: target.branch }); + const result = await git.rebase(gitDirectory, { onto: target.branch }); if (result.conflict) { updateLastLog('error', `Rebase conflicts detected`); setConflictFiles(result.conflictFiles ?? []); setConflictOperation('rebase'); setConflictDialogOpen(true); - persistConflictState(currentDirectory, result.conflictFiles ?? [], 'rebase'); + persistConflictState(gitDirectory, result.conflictFiles ?? [], 'rebase'); } else { updateLastLog('done', `Rebased ${currentBranch} onto ${target.branch}`); clearConflictState(); @@ -2023,18 +2048,18 @@ export const GitView: React.FC = ({ isActive }) => { } // Note: branchOperation is cleared when dialog closes via handleOperationComplete }, - [currentDirectory, git, status, resolveIntegrationTarget, refreshStatusAndBranches, refreshLog, isUncommittedChangesError, persistConflictState, clearConflictState, addOperationLog, updateLastLog, resetOperationLogs] + [gitDirectory, git, status, resolveIntegrationTarget, refreshStatusAndBranches, refreshLog, isUncommittedChangesError, persistConflictState, clearConflictState, addOperationLog, updateLastLog, resetOperationLogs] ); const handleAbortConflict = React.useCallback(async () => { - if (!currentDirectory) return; + if (!gitDirectory) return; try { if (conflictOperation === 'merge') { - await git.abortMerge(currentDirectory); + await git.abortMerge(gitDirectory); toast.success(t('gitView.toast.mergeAborted')); } else { - await git.abortRebase(currentDirectory); + await git.abortRebase(gitDirectory); toast.success(t('gitView.toast.rebaseAborted')); } clearConflictState(); @@ -2044,7 +2069,7 @@ export const GitView: React.FC = ({ isActive }) => { const message = err instanceof Error ? err.message : `Failed to abort ${conflictOperation}`; toast.error(message); } - }, [currentDirectory, git, conflictOperation, refreshStatusAndBranches, refreshLog, clearConflictState, t]); + }, [gitDirectory, git, conflictOperation, refreshStatusAndBranches, refreshLog, clearConflictState, t]); // Count unresolved conflicts (files with 'U' status) const conflictCount = React.useMemo(() => { @@ -2057,19 +2082,19 @@ export const GitView: React.FC = ({ isActive }) => { }, [status?.files]); const handleContinueOperation = React.useCallback(async () => { - if (!currentDirectory) return; + if (!gitDirectory) return; try { const isMerge = !!status?.mergeInProgress?.head; const isRebase = !!(status?.rebaseInProgress?.headName || status?.rebaseInProgress?.onto); if (isMerge) { - const result = await git.continueMerge(currentDirectory); + const result = await git.continueMerge(gitDirectory); if (result.conflict) { setConflictFiles(result.conflictFiles ?? []); setConflictOperation('merge'); setConflictDialogOpen(true); - persistConflictState(currentDirectory, result.conflictFiles ?? [], 'merge'); + persistConflictState(gitDirectory, result.conflictFiles ?? [], 'merge'); toast.error(t('gitView.toast.mergeConflictsDetected')); } else { clearConflictState(); @@ -2078,12 +2103,12 @@ export const GitView: React.FC = ({ isActive }) => { await refreshLog(); } } else if (isRebase) { - const result = await git.continueRebase(currentDirectory); + const result = await git.continueRebase(gitDirectory); if (result.conflict) { setConflictFiles(result.conflictFiles ?? []); setConflictOperation('rebase'); setConflictDialogOpen(true); - persistConflictState(currentDirectory, result.conflictFiles ?? [], 'rebase'); + persistConflictState(gitDirectory, result.conflictFiles ?? [], 'rebase'); toast.error(t('gitView.toast.rebaseConflictsDetected')); } else { clearConflictState(); @@ -2096,18 +2121,18 @@ export const GitView: React.FC = ({ isActive }) => { const message = err instanceof Error ? err.message : t('gitView.toast.continueOperationFailed'); toast.error(message); } - }, [currentDirectory, git, status, refreshStatusAndBranches, refreshLog, persistConflictState, clearConflictState, t]); + }, [gitDirectory, git, status, refreshStatusAndBranches, refreshLog, persistConflictState, clearConflictState, t]); const handleAbortOperation = React.useCallback(async () => { - if (!currentDirectory) return; + if (!gitDirectory) return; try { const isMerge = !!status?.mergeInProgress?.head; if (isMerge) { - await git.abortMerge(currentDirectory); + await git.abortMerge(gitDirectory); toast.success(t('gitView.toast.mergeAborted')); } else { - await git.abortRebase(currentDirectory); + await git.abortRebase(gitDirectory); toast.success(t('gitView.toast.rebaseAborted')); } clearConflictState(); @@ -2117,10 +2142,10 @@ export const GitView: React.FC = ({ isActive }) => { const message = err instanceof Error ? err.message : t('gitView.toast.abortOperationFailed'); toast.error(message); } - }, [currentDirectory, git, status, refreshStatusAndBranches, refreshLog, clearConflictState, t]); + }, [gitDirectory, git, status, refreshStatusAndBranches, refreshLog, clearConflictState, t]); const handleResolveWithAIFromBanner = React.useCallback(() => { - if (!currentDirectory) return; + if (!gitDirectory) return; // Determine operation type from status const isMerge = !!status?.mergeInProgress?.head; @@ -2137,11 +2162,11 @@ export const GitView: React.FC = ({ isActive }) => { } setConflictOperation(operation); setConflictDialogOpen(true); - }, [currentDirectory, status]); + }, [gitDirectory, status]); const handleStashAndRetry = React.useCallback( async (restoreAfter: boolean) => { - if (!currentDirectory) return; + if (!gitDirectory) return; const currentBranch = status?.current; const operation = stashDialogOperation; @@ -2150,12 +2175,12 @@ export const GitView: React.FC = ({ isActive }) => { // Stash changes try { - await git.stash(currentDirectory, { + await git.stash(gitDirectory, { message: `Auto-stash before ${operation} with ${branch}`, includeUntracked: true, }); if (hadStagedChanges) { - bumpIndexRevision(currentDirectory); + bumpIndexRevision(gitDirectory); } } catch (stashErr) { const msg = stashErr instanceof Error ? stashErr.message : 'Failed to stash changes'; @@ -2169,7 +2194,7 @@ export const GitView: React.FC = ({ isActive }) => { try { // Perform the operation if (operation === 'merge') { - const result = await git.merge(currentDirectory, { branch }); + const result = await git.merge(gitDirectory, { branch }); if (result.conflict) { hasConflict = true; setConflictFiles(result.conflictFiles ?? []); @@ -2180,7 +2205,7 @@ export const GitView: React.FC = ({ isActive }) => { toast.success(t('gitView.toast.mergedIntoBranch', { branch, currentBranch: currentBranch || '' })); } } else { - const result = await git.rebase(currentDirectory, { onto: branch }); + const result = await git.rebase(gitDirectory, { onto: branch }); if (result.conflict) { hasConflict = true; setConflictFiles(result.conflictFiles ?? []); @@ -2195,8 +2220,8 @@ export const GitView: React.FC = ({ isActive }) => { // Restore stashed changes if requested and operation succeeded if (restoreAfter && operationSucceeded) { try { - await git.stashPop(currentDirectory); - bumpIndexRevision(currentDirectory); + await git.stashPop(gitDirectory); + bumpIndexRevision(gitDirectory); toast.success(t('gitView.toast.stashedRestored')); } catch (popErr) { const popMessage = popErr instanceof Error ? popErr.message : t('gitView.toast.restoreStashFailed'); @@ -2212,8 +2237,8 @@ export const GitView: React.FC = ({ isActive }) => { // If the operation failed (not due to conflicts), try to restore stash if (restoreAfter) { try { - await git.stashPop(currentDirectory); - bumpIndexRevision(currentDirectory); + await git.stashPop(gitDirectory); + bumpIndexRevision(gitDirectory); } catch { // Ignore stash pop errors in this case } @@ -2221,18 +2246,18 @@ export const GitView: React.FC = ({ isActive }) => { throw err; } }, - [bumpIndexRevision, currentDirectory, git, status, stashDialogOperation, stashDialogBranch, refreshStatusAndBranches, refreshLog, t] + [bumpIndexRevision, gitDirectory, git, status, stashDialogOperation, stashDialogBranch, refreshStatusAndBranches, refreshLog, t] ); const handleLogMaxCountChange = React.useCallback( (count: number) => { setLogMaxCountLocal(count); - if (currentDirectory) { - setLogMaxCount(currentDirectory, count); - fetchLog(currentDirectory, git, count); + if (gitDirectory) { + setLogMaxCount(gitDirectory, count); + fetchLog(gitDirectory, git, count); } }, - [currentDirectory, fetchLog, git, setLogMaxCount] + [gitDirectory, fetchLog, git, setLogMaxCount] ); const handleGraphLogMaxCountChange = React.useCallback((count: number) => { @@ -2241,12 +2266,12 @@ export const GitView: React.FC = ({ isActive }) => { const handleGraphActionSuccess = React.useCallback(() => { setGitLogDialogMode(null); - if (currentDirectory) { - fetchStatus(currentDirectory, git); - fetchBranches(currentDirectory, git); - fetchLog(currentDirectory, git, logMaxCountLocal); + if (gitDirectory) { + fetchStatus(gitDirectory, git); + fetchBranches(gitDirectory, git); + fetchLog(gitDirectory, git, logMaxCountLocal); } - }, [currentDirectory, fetchStatus, fetchBranches, fetchLog, logMaxCountLocal, git]); + }, [gitDirectory, fetchStatus, fetchBranches, fetchLog, logMaxCountLocal, git]); const handleGraphConflict = React.useCallback((result: { conflict: boolean; @@ -2263,10 +2288,10 @@ export const GitView: React.FC = ({ isActive }) => { files: result.conflictFiles?.join(', ') ?? 'unknown files', }), }); - if (currentDirectory) { - fetchStatus(currentDirectory, git); - fetchBranches(currentDirectory, git); - fetchLog(currentDirectory, git, logMaxCountLocal); + if (gitDirectory) { + fetchStatus(gitDirectory, git); + fetchBranches(gitDirectory, git); + fetchLog(gitDirectory, git, logMaxCountLocal); } return; } @@ -2274,10 +2299,10 @@ export const GitView: React.FC = ({ isActive }) => { setConflictFiles(result.conflictFiles ?? []); setConflictOperation(result.operation); setConflictDialogOpen(true); - if (currentDirectory) { - persistConflictState(currentDirectory, result.conflictFiles ?? [], result.operation); + if (gitDirectory) { + persistConflictState(gitDirectory, result.conflictFiles ?? [], result.operation); } - }, [t, setConflictFiles, setConflictOperation, setConflictDialogOpen, persistConflictState, currentDirectory, fetchStatus, fetchBranches, fetchLog, logMaxCountLocal, git]); + }, [t, setConflictFiles, setConflictOperation, setConflictDialogOpen, persistConflictState, gitDirectory, fetchStatus, fetchBranches, fetchLog, logMaxCountLocal, git]); if (!currentDirectory) { return ( @@ -2315,21 +2340,26 @@ export const GitView: React.FC = ({ isActive }) => { ); } + // Nested repository discovery states (discovering, failed, unsupported, + // none found, or settling on the auto-selected repository). return ( -
- -

- {t('gitView.empty.notGitRepository')} -

-

- {t('gitView.empty.notGitRepositoryDescription')} -

- {repairActions.includes('open-without-worktree-features') ? ( -

- {t('gitView.empty.worktreeFeaturesUnavailable')} -

- ) : null} -
+ { + if (currentDirectory) { + void ensureNestedRepos(currentDirectory, { force: true }); + } + }} + emptyStateFooter={ + repairActions.includes('open-without-worktree-features') ? ( +

+ {t('gitView.empty.worktreeFeaturesUnavailable')} +

+ ) : undefined + } + /> ); } @@ -2362,8 +2392,18 @@ export const GitView: React.FC = ({ isActive }) => { pullRequest={prChipStatus?.pr ?? null} prChecks={prChipStatus?.checks ?? null} onOpenPullRequest={ - currentDirectory ? () => openContextSurface(currentDirectory, 'pr') : undefined + gitDirectory ? () => openContextSurface(gitDirectory, 'pr') : undefined } + repositoryOptions={ + gitDirectory !== currentDirectory && Array.isArray(nestedRepos) ? nestedRepos : undefined + } + selectedRepository={gitDirectory !== currentDirectory ? gitDirectory : null} + onSelectRepository={ + gitDirectory !== currentDirectory && currentDirectory + ? (repository) => selectNestedRepo(currentDirectory, repository) + : undefined + } + repositoryRoot={gitDirectory !== currentDirectory ? currentDirectory : undefined} /> {/* In-progress operation banner */} @@ -2496,10 +2536,10 @@ export const GitView: React.FC = ({ isActive }) => { refreshKey={integrateRefreshKey} showHeader={false} onRefresh={() => { - if (!currentDirectory) return; - fetchStatus(currentDirectory, git); - fetchBranches(currentDirectory, git); - fetchLog(currentDirectory, git, logMaxCountLocal); + if (!gitDirectory) return; + fetchStatus(gitDirectory, git); + fetchBranches(gitDirectory, git); + fetchLog(gitDirectory, git, logMaxCountLocal); }} /> ) : null} @@ -2523,8 +2563,8 @@ export const GitView: React.FC = ({ isActive }) => { setGraphLogRefreshToken((token) => token + 1); return; } - if (!currentDirectory) return; - void fetchLog(currentDirectory, git, logMaxCountLocal); + if (!gitDirectory) return; + void fetchLog(gitDirectory, git, logMaxCountLocal); }} disabled={gitLogDialogMode === 'graph' ? graphLogLoading : isLogLoading} title={t('gitView.history.refresh')} @@ -2556,7 +2596,7 @@ export const GitView: React.FC = ({ isActive }) => { commitFilesMap={commitFilesMap} loadingCommitHashes={loadingCommitHashes} onCopyHash={handleCopyCommitHash} - directory={currentDirectory ?? undefined} + directory={gitDirectory ?? undefined} showHeader={false} contentMaxHeightClassName="h-full max-h-none" branchDivider={gitLogDialogMode === 'graph' ? null : historyBranchDivider} @@ -2570,13 +2610,13 @@ export const GitView: React.FC = ({ isActive }) => { 0} hasStagedChanges={stagedChangeEntries.length > 0} uncommittedFileCount={status?.files?.length ?? 0} onChanged={async (change) => { - if (currentDirectory && change?.affectsIndex) { - bumpIndexRevision(currentDirectory); + if (gitDirectory && change?.affectsIndex) { + bumpIndexRevision(gitDirectory); } await refreshStatusAndBranches(false); await refreshLog(); @@ -2614,12 +2654,12 @@ export const GitView: React.FC = ({ isActive }) => { - {currentDirectory && ( + {gitDirectory && ( @@ -36,9 +39,17 @@ 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, selectNestedRepo } = useGitStore(useShallow((state) => ({ + ensureAll: state.ensureAll, + ensureNestedRepos: state.ensureNestedRepos, + selectNestedRepo: state.selectNestedRepo, + }))); const currentSessionId = useSessionUIStore((s) => s.currentSessionId); const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft); @@ -89,11 +100,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(null); React.useEffect(() => { @@ -122,52 +133,52 @@ export const PullRequestView: React.FC = () => { }, [authoritativeProjectRoot, worktreeMetadata?.projectDirectory]); const [remotes, setRemotes] = React.useState(() => - (currentDirectory ? remotesCacheByDirectory.get(remoteCacheKey(currentDirectory)) : undefined) ?? [] + (gitDirectory ? remotesCacheByDirectory.get(remoteCacheKey(gitDirectory)) : undefined) ?? [] ); const [remoteUrl, setRemoteUrl] = React.useState(() => - (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 +251,7 @@ export const PullRequestView: React.FC = () => { worktreeMetadata?.createdFromBranch, ]); - if (!currentDirectory || !currentBranch) { + if (!currentDirectory) { return (
@@ -250,22 +261,67 @@ export const PullRequestView: React.FC = () => { ); } - return ( - - { + void ensureNestedRepos(currentDirectory, { force: true }); + }} /> - + ); + } + + if (!currentBranch) { + return ( +
+ +
{t('gitView.pullRequest.title')}
+
{t('gitView.pullRequest.createHint')}
+
+ ); + } + + // 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 ( +
+ {showRepositoryPicker ? ( +
+ { + if (currentDirectory) selectNestedRepo(currentDirectory, repository); + }} + repositoryRoot={currentDirectory ?? undefined} + /> +
+ ) : null} + + + +
); }; diff --git a/packages/ui/src/components/views/git/GitHeader.tsx b/packages/ui/src/components/views/git/GitHeader.tsx index c96a8a59..8324f7a3 100644 --- a/packages/ui/src/components/views/git/GitHeader.tsx +++ b/packages/ui/src/components/views/git/GitHeader.tsx @@ -12,6 +12,7 @@ import type { IconName } from "@/components/icon/icons"; import { BranchSelector } from './BranchSelector'; import { WorktreeBranchDisplay } from './WorktreeBranchDisplay'; import { SyncActions } from './SyncActions'; +import { NestedRepoPicker } from './NestedRepoPicker'; import type { GitStatus, GitIdentityProfile, @@ -51,6 +52,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 = { @@ -258,12 +266,18 @@ export const GitHeader: React.FC = ({ pullRequest, prChecks, onOpenPullRequest, + repositoryOptions, + selectedRepository, + onSelectRepository, + repositoryRoot, }) => { const { t } = useI18n(); if (!status) { return null; } + const repositoryOptionsForPicker = (repositoryOptions ?? []).filter(Boolean); + const managementButtons = (
{onOpenHistory || onOpenGraph || onOpenStashes || onOpenUpdateBranch ? ( @@ -410,7 +424,7 @@ export const GitHeader: React.FC = ({ return (
-
+
{isWorktreeMode ? ( = ({ remotes={remotes} /> )} + {repositoryOptionsForPicker.length > 0 && onSelectRepository ? ( + + ) : null}
{identityControl} 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/git/NestedRepoResolutionStates.tsx b/packages/ui/src/components/views/git/NestedRepoResolutionStates.tsx new file mode 100644 index 00000000..3e9e1e6d --- /dev/null +++ b/packages/ui/src/components/views/git/NestedRepoResolutionStates.tsx @@ -0,0 +1,96 @@ +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; + /** + * Probe of the directory the consumer operates on (root or selected nested + * repository). `true` means resolution succeeded and the consumer should + * render its own content. + */ + resolvedIsGitRepo: 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 resolution + * has finished — either the root is a repository or the operating directory + * probed as one — 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 = ({ + rootIsGitRepo, + resolvedIsGitRepo, + nestedRepos, + onRetryDiscovery, + emptyStateFooter, +}) => { + const { t } = useI18n(); + + if (rootIsGitRepo !== false) return null; + if (resolvedIsGitRepo === true) return null; + + if (nestedRepos === undefined || nestedRepos === null) { + return ( +
+ +

+ {nestedRepos === null + ? t('gitView.empty.discoverFailed') + : t('gitView.empty.discoveringRepositories')} +

+ {nestedRepos === null ? ( + + ) : null} +
+ ); + } + + if (nestedRepos === 'unsupported' || nestedRepos.length === 0) { + return ( +
+ +

+ {t('gitView.empty.notGitRepository')} +

+

+ {t('gitView.empty.notGitRepositoryDescription')} +

+ {emptyStateFooter} +
+ ); + } + + // Repositories were found and one is about to be auto-selected (or the + // selected repository is still probing) — hold a brief loading state. + return ( +
+ +

+ {t('gitView.loading.checkingRepository')} +

+
+ ); +}; diff --git a/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx b/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx index 51b20387..a504eb34 100644 --- a/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx +++ b/packages/ui/src/components/views/walkthrough/WalkthroughView.tsx @@ -19,7 +19,7 @@ import { ModelSelector } from '@/components/sections/agents/ModelSelector'; import { deriveBaseBranch, hasResolvableBaseBranch } from '@/components/views/git/baseBranch'; import { runtimeFetch } from '@/lib/runtime-fetch'; 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 { getFreshestPrStatusForBranch, @@ -27,6 +27,7 @@ import { useGitHubPrStatusStore, } from '@/stores/useGitHubPrStatusStore'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; +import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory'; import { useUIStore } from '@/stores/useUIStore'; import { useWalkthroughStore } from '@/stores/useWalkthroughStore'; import { cn } from '@/lib/utils'; @@ -36,9 +37,17 @@ import { WalkthroughStages } from './WalkthroughStages'; 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']; @@ -73,11 +82,17 @@ 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 }: WalkthroughViewProps) => { +export const WalkthroughView = ({ directory: rootDirectory, visible = true }: WalkthroughViewProps) => { const { t, locale, locales, label } = useI18n(); const rootRef = useRef(null); 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, { enabled: visible }); + const directory = gitDirectory ?? rootDirectory; + // Panel width, not viewport width: this surface is resizable independently of // the window. useEffect(() => { @@ -484,9 +499,38 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => { [activeLanguage, directory, generate, generateDisabled, source] ); + const isGitRepo = useIsGitRepo(gitDirectory || null); + const ensureNestedRepos = useGitStore((state) => state.ensureNestedRepos); + 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 ( + { + if (rootDirectory) void ensureNestedRepos(rootDirectory, { force: true }); + }} + /> + ); + } + return (
+ {rootIsGitRepo === false && Array.isArray(nestedRepos) && nestedRepos.length > 0 ? ( + { + if (rootDirectory) selectNestedRepo(rootDirectory, repository); + }} + repositoryRoot={rootDirectory ?? undefined} + /> + ) : null}