diff --git a/src/renderer/src/components/sidebar/AddRepoCreateStep.tsx b/src/renderer/src/components/sidebar/AddRepoCreateStep.tsx index 9c994fe5e4c..685bd04d3b3 100644 --- a/src/renderer/src/components/sidebar/AddRepoCreateStep.tsx +++ b/src/renderer/src/components/sidebar/AddRepoCreateStep.tsx @@ -1,14 +1,9 @@ -/** - * Step for AddRepoDialog (orca#763). - * - * Split from AddRepoDialog and AddRepoSteps to keep both under the 400-line - * oxlint limit, following the same pattern as useRemoteRepo. - */ - +// Step for AddRepoDialog (orca#763), split out so create-project state stays scoped. import React, { useCallback, useRef, useState } from 'react' import { toast } from 'sonner' import { Folder, GitBranch, Home, Pencil } from 'lucide-react' import { useAppStore } from '@/store' +import { useMountedRef } from '@/hooks/useMountedRef' import { DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -33,6 +28,7 @@ export function useCreateRepo( const [createKind, setCreateKind] = useState('git') const [createError, setCreateError] = useState(null) const [isCreating, setIsCreating] = useState(false) + const mountedRef = useMountedRef() // Why: monotonic ID so stale create callbacks can detect they were superseded // when the user clicks Back or closes the dialog mid-create. Mirrors the @@ -57,11 +53,11 @@ export function useCreateRepo( } const gen = createGenRef.current const dir = await window.api.repos.pickDirectory() - if (dir && gen === createGenRef.current) { + if (dir && gen === createGenRef.current && mountedRef.current) { setCreateParent(dir) setCreateError(null) } - }, []) + }, [mountedRef]) const handleCreate = useCallback(async () => { const name = createName.trim() @@ -73,8 +69,7 @@ export function useCreateRepo( setIsCreating(true) setCreateError(null) try { - const settings = useAppStore.getState().settings - const target = getActiveRuntimeTarget(settings) + const target = getActiveRuntimeTarget(useAppStore.getState().settings) const result = target.kind === 'environment' ? await callRuntimeRpc<{ repo: Repo } | { error: string }>( @@ -94,7 +89,7 @@ export function useCreateRepo( }) // Why: if the user closed the dialog or clicked Back mid-create, // createGenRef was bumped by resetCreateState. Ignore stale results. - if (gen !== createGenRef.current) { + if (gen !== createGenRef.current || !mountedRef.current) { return } if ('error' in result) { @@ -133,7 +128,7 @@ export function useCreateRepo( setAddedRepo(repo) setExistingWorkspaceSource?.('create_project') await fetchWorktrees(repo.id) - if (gen !== createGenRef.current) { + if (gen !== createGenRef.current || !mountedRef.current) { return } setStep('setup') @@ -141,7 +136,7 @@ export function useCreateRepo( // Why: folder repos skip the Git setup step, so activate the synthetic // root workspace before closing. Matches addNonGitFolder's behavior. await fetchWorktrees(repo.id) - if (gen !== createGenRef.current) { + if (gen !== createGenRef.current || !mountedRef.current) { return } const folderWorktree = useAppStore.getState().worktreesByRepo[repo.id]?.[0] @@ -151,15 +146,14 @@ export function useCreateRepo( closeModal() } } catch (err) { - if (gen !== createGenRef.current) { + if (gen !== createGenRef.current || !mountedRef.current) { return } - const message = err instanceof Error ? err.message : String(err) - setCreateError(message) + setCreateError(err instanceof Error ? err.message : String(err)) } finally { // Why: only clear the loading state if this invocation is still current; // a superseded create must not flip the flag back off for a new flow. - if (gen === createGenRef.current) { + if (gen === createGenRef.current && mountedRef.current) { setIsCreating(false) } } @@ -168,6 +162,7 @@ export function useCreateRepo( createParent, createKind, fetchWorktrees, + mountedRef, setStep, setAddedRepo, closeModal, @@ -327,8 +322,7 @@ export function CreateStep({ }) }, [cancelRadioFocusFrame, createKind, onKindChange]) - const trimmedName = createName.trim() - const canSubmit = trimmedName.length > 0 && createParent.trim().length > 0 && !isCreating + const canSubmit = createName.trim().length > 0 && createParent.trim().length > 0 && !isCreating return ( <>