fix(ui): reconcile git state after worktree changes
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
useGitLoadingLog,
|
||||
} from '@/stores/useGitStore';
|
||||
import { useNestedGitDirectory } from '@/hooks/useNestedGitDirectory';
|
||||
import { useWorktreeBootstrapPending } from '@/hooks/useWorktreeBootstrapPending';
|
||||
import { NestedRepoResolutionStates } from './git/NestedRepoResolutionStates';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
||||
@@ -203,8 +204,14 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
const { t } = useI18n();
|
||||
const { git } = useRuntimeAPIs();
|
||||
const currentDirectory = useEffectiveDirectory();
|
||||
const [worktreeBootstrapStatus, setWorktreeBootstrapStatus] = React.useState<'pending' | 'ready' | 'failed' | null>(null);
|
||||
const [isWaitingForGitRefreshAfterBootstrap, setIsWaitingForGitRefreshAfterBootstrap] = React.useState(false);
|
||||
const [worktreeBootstrapSnapshot, setWorktreeBootstrapSnapshot] = React.useState<{
|
||||
directory: string;
|
||||
status: 'pending' | 'ready' | 'failed' | null;
|
||||
} | null>(null);
|
||||
const [postBootstrapRefresh, setPostBootstrapRefresh] = React.useState<{
|
||||
directory: string;
|
||||
status: 'refreshing' | 'failed';
|
||||
} | null>(null);
|
||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||
const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft);
|
||||
const setDraftBootstrapPendingDirectory = useSessionUIStore((s) => s.setDraftBootstrapPendingDirectory);
|
||||
@@ -329,7 +336,6 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
});
|
||||
const navigateToDiff = useUIStore((state) => state.navigateToDiff);
|
||||
|
||||
const previousBootstrapStatusRef = React.useRef<'pending' | 'ready' | 'failed' | null>(null);
|
||||
const gitReconcileTimeoutRef = React.useRef<number | null>(null);
|
||||
const gitMutationFlushTimeoutRef = React.useRef<number | null>(null);
|
||||
const flushQueuedGitMutationsRef = React.useRef<(() => void) | null>(null);
|
||||
@@ -438,11 +444,13 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
React.useEffect(() => {
|
||||
if (!isActive) return;
|
||||
if (!currentDirectory) {
|
||||
setWorktreeBootstrapStatus(null);
|
||||
setIsWaitingForGitRefreshAfterBootstrap(false);
|
||||
setWorktreeBootstrapSnapshot(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const bootstrapDirectory = normalizePath(currentDirectory) ?? currentDirectory;
|
||||
setWorktreeBootstrapSnapshot({ directory: bootstrapDirectory, status: null });
|
||||
|
||||
let cancelled = false;
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
@@ -452,7 +460,7 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setWorktreeBootstrapStatus(next.status);
|
||||
setWorktreeBootstrapSnapshot({ directory: bootstrapDirectory, status: next.status });
|
||||
if (next.status === 'pending') {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
void poll();
|
||||
@@ -460,7 +468,7 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setWorktreeBootstrapStatus(null);
|
||||
setWorktreeBootstrapSnapshot({ directory: bootstrapDirectory, status: null });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -475,37 +483,84 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
};
|
||||
}, [isActive, currentDirectory]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const previous = previousBootstrapStatusRef.current;
|
||||
previousBootstrapStatusRef.current = worktreeBootstrapStatus;
|
||||
|
||||
if (!currentDirectory || !git) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (previous === 'pending' && worktreeBootstrapStatus === 'ready') {
|
||||
setIsWaitingForGitRefreshAfterBootstrap(true);
|
||||
void fetchAll(currentDirectory, git).finally(() => {
|
||||
window.setTimeout(() => {
|
||||
setIsWaitingForGitRefreshAfterBootstrap(false);
|
||||
}, 1200);
|
||||
});
|
||||
}
|
||||
|
||||
if (worktreeBootstrapStatus === 'failed') {
|
||||
setDraftBootstrapPendingDirectory(null);
|
||||
setIsWaitingForGitRefreshAfterBootstrap(false);
|
||||
}
|
||||
}, [currentDirectory, fetchAll, git, setDraftBootstrapPendingDirectory, worktreeBootstrapStatus]);
|
||||
|
||||
const normalizedDraftBootstrapPendingDirectory = normalizePath(newSessionDraft?.bootstrapPendingDirectory ?? null);
|
||||
const isDraftBootstrapPendingForCurrentDirectory = Boolean(
|
||||
currentDirectory && normalizedDraftBootstrapPendingDirectory && normalizedDraftBootstrapPendingDirectory === normalizePath(currentDirectory)
|
||||
);
|
||||
const sharedWorktreeBootstrapPending = useWorktreeBootstrapPending(currentDirectory ?? null);
|
||||
const normalizedCurrentBootstrapDirectory = normalizePath(currentDirectory);
|
||||
const observedWorktreeBootstrapStatus = worktreeBootstrapSnapshot?.directory === normalizedCurrentBootstrapDirectory
|
||||
? worktreeBootstrapSnapshot.status
|
||||
: null;
|
||||
const isPendingWorktreeSetup = Boolean(
|
||||
currentDirectory && (worktreeBootstrapStatus === 'pending' || isDraftBootstrapPendingForCurrentDirectory)
|
||||
currentDirectory
|
||||
&& (
|
||||
sharedWorktreeBootstrapPending
|
||||
|| observedWorktreeBootstrapStatus === 'pending'
|
||||
|| (isDraftBootstrapPendingForCurrentDirectory && newSessionDraft?.pendingWorktreeRequestId)
|
||||
)
|
||||
);
|
||||
const shouldHideNotGitState = isPendingWorktreeSetup || isWaitingForGitRefreshAfterBootstrap;
|
||||
const isPostBootstrapRefreshForCurrentDirectory = Boolean(
|
||||
normalizedCurrentBootstrapDirectory
|
||||
&& postBootstrapRefresh?.directory === normalizedCurrentBootstrapDirectory
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!normalizedCurrentBootstrapDirectory) return;
|
||||
|
||||
if (observedWorktreeBootstrapStatus === 'failed') {
|
||||
setDraftBootstrapPendingDirectory(null);
|
||||
setPostBootstrapRefresh((current) => (
|
||||
current?.directory === normalizedCurrentBootstrapDirectory ? null : current
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPendingWorktreeSetup) {
|
||||
setPostBootstrapRefresh((current) => (
|
||||
current?.directory === normalizedCurrentBootstrapDirectory && current.status === 'refreshing'
|
||||
? current
|
||||
: { directory: normalizedCurrentBootstrapDirectory, status: 'refreshing' }
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
postBootstrapRefresh?.directory !== normalizedCurrentBootstrapDirectory
|
||||
|| postBootstrapRefresh.status !== 'refreshing'
|
||||
|| !gitDirectory
|
||||
|| !git
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void fetchStatus(gitDirectory, git, {
|
||||
force: true,
|
||||
silent: true,
|
||||
throwOnError: true,
|
||||
}).then(() => {
|
||||
if (cancelled) return;
|
||||
setPostBootstrapRefresh((current) => (
|
||||
current?.directory === normalizedCurrentBootstrapDirectory ? null : current
|
||||
));
|
||||
}).catch(() => {
|
||||
if (cancelled) return;
|
||||
setPostBootstrapRefresh((current) => (
|
||||
current?.directory === normalizedCurrentBootstrapDirectory
|
||||
? { ...current, status: 'failed' }
|
||||
: current
|
||||
));
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [fetchStatus, git, gitDirectory, isPendingWorktreeSetup, normalizedCurrentBootstrapDirectory, observedWorktreeBootstrapStatus, postBootstrapRefresh, setDraftBootstrapPendingDirectory]);
|
||||
|
||||
const shouldHideGitState = isPendingWorktreeSetup || isPostBootstrapRefreshForCurrentDirectory;
|
||||
const postBootstrapRefreshFailed = isPostBootstrapRefreshForCurrentDirectory
|
||||
&& postBootstrapRefresh?.status === 'failed';
|
||||
|
||||
const initialSnapshot = React.useMemo(() => {
|
||||
if (!gitDirectory) return null;
|
||||
@@ -2336,6 +2391,42 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldHideGitState) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||
{!postBootstrapRefreshFailed ? (
|
||||
<Icon name="loader-4" className="mb-3 size-6 animate-spin text-muted-foreground" />
|
||||
) : null}
|
||||
<p className="typography-ui-label font-semibold text-foreground">
|
||||
{postBootstrapRefreshFailed
|
||||
? t('gitView.toast.refreshRepositoryFailed')
|
||||
: t('gitView.empty.worktreeSetupInProgress')}
|
||||
</p>
|
||||
{!postBootstrapRefreshFailed ? (
|
||||
<p className="typography-meta mt-1 text-muted-foreground">
|
||||
{t('gitView.empty.worktreeSetupDescription')}
|
||||
</p>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
if (!normalizedCurrentBootstrapDirectory) return;
|
||||
setPostBootstrapRefresh({
|
||||
directory: normalizedCurrentBootstrapDirectory,
|
||||
status: 'refreshing',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('gitView.empty.retryDiscovery')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isGitRepo === null || (isGitRepo === true && !status)) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
@@ -2348,20 +2439,6 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
}
|
||||
|
||||
if (isGitRepo === false) {
|
||||
if (shouldHideNotGitState) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||
<Icon name="loader-4" className="mb-3 size-6 animate-spin text-muted-foreground" />
|
||||
<p className="typography-ui-label font-semibold text-foreground">
|
||||
{t('gitView.empty.worktreeSetupInProgress')}
|
||||
</p>
|
||||
<p className="typography-meta mt-1 text-muted-foreground">
|
||||
{t('gitView.empty.worktreeSetupDescription')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Nested repository discovery states (discovering, failed, unsupported,
|
||||
// none found, or settling on the auto-selected repository).
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user