From b281b931f5ceff213a4a8f7156f99ee6df1d58dc Mon Sep 17 00:00:00 2001 From: SSU-WEI HUANG Date: Wed, 29 Jul 2026 10:02:46 +0800 Subject: [PATCH] fix(web): prevent duplicate session creation (#1222) --- web/src/components/NewSession/index.test.tsx | 33 +++++++++++++++++++- web/src/components/NewSession/index.tsx | 13 ++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/web/src/components/NewSession/index.test.tsx b/web/src/components/NewSession/index.test.tsx index 08283b5f..c7685fed 100644 --- a/web/src/components/NewSession/index.test.tsx +++ b/web/src/components/NewSession/index.test.tsx @@ -13,6 +13,7 @@ const mocks = vi.hoisted(() => ({ spawnSession: vi.fn(), onSuccess: vi.fn(), notification: vi.fn(), + checkPathsExists: vi.fn(), codexModelsLoading: false, directoryExists: undefined as boolean | undefined })) @@ -44,7 +45,7 @@ vi.mock('@/hooks/useRecentPaths', () => ({ vi.mock('@/hooks/useMachinePathsExists', () => ({ useMachinePathsExists: () => ({ pathExistence: { 'C:\\repo': mocks.directoryExists }, - checkPathsExists: async () => ({ 'C:\\repo': mocks.directoryExists }) + checkPathsExists: mocks.checkPathsExists }) })) vi.mock('@/hooks/useDirectorySuggestions', () => ({ @@ -152,6 +153,8 @@ describe('NewSession launch preferences', () => { mocks.spawnSession.mockReset() mocks.onSuccess.mockReset() mocks.notification.mockReset() + mocks.checkPathsExists.mockReset() + mocks.checkPathsExists.mockImplementation(async () => ({ 'C:\\repo': mocks.directoryExists })) mocks.codexModelsLoading = false mocks.directoryExists = true savePreferredAgent('codex') @@ -276,6 +279,34 @@ describe('NewSession launch preferences', () => { }) }) + it('spawns only once when Create is activated twice during directory validation', async () => { + let finishDirectoryCheck!: (result: Record) => void + mocks.checkPathsExists.mockReturnValue(new Promise((resolve) => { + finishDirectoryCheck = resolve + })) + mocks.spawnSession.mockResolvedValue({ type: 'success', sessionId: 'session-1' }) + + render( + {}} + /> + ) + + const create = screen.getByTestId('create') + fireEvent.click(create) + fireEvent.click(create) + finishDirectoryCheck({ 'C:\\repo': true }) + + await waitFor(() => expect(mocks.onSuccess).toHaveBeenCalledWith('session-1')) + expect(mocks.checkPathsExists).toHaveBeenCalledTimes(1) + expect(mocks.spawnSession).toHaveBeenCalledTimes(1) + }) + it('does not save changed launch settings when creation fails', async () => { mocks.spawnSession.mockResolvedValue({ type: 'error', message: 'spawn failed' }) diff --git a/web/src/components/NewSession/index.tsx b/web/src/components/NewSession/index.tsx index 7fde36b2..8d4d8c91 100644 --- a/web/src/components/NewSession/index.tsx +++ b/web/src/components/NewSession/index.tsx @@ -147,7 +147,9 @@ export function NewSession(props: { const [codexImportError, setCodexImportError] = useState(null) const [isImportingCodexSession, setIsImportingCodexSession] = useState(false) const [isCodexImportDialogOpen, setIsCodexImportDialogOpen] = useState(false) - const isFormDisabled = Boolean(isPending || props.isLoading || isImportingCodexSession) + const [isCreating, setIsCreating] = useState(false) + const createInFlightRef = useRef(false) + const isFormDisabled = Boolean(isCreating || isPending || props.isLoading || isImportingCodexSession) const worktreeInputRef = useRef(null) const preserveRestoredDraftRef = useRef(false) @@ -877,8 +879,10 @@ export function NewSession(props: { }, [suggestions, selectedIndex, moveUp, moveDown, clearSuggestions, handleSuggestionSelect]) async function handleCreate() { - if (!machineId || !trimmedDirectory) return + if (!machineId || !trimmedDirectory || createInFlightRef.current) return + createInFlightRef.current = true + setIsCreating(true) setError(null) try { const existsResult = await checkPathsExists([trimmedDirectory]) @@ -998,6 +1002,9 @@ export function NewSession(props: { setIsImportingCodexSession(false) haptic.notification('error') setError(e instanceof Error ? e.message : 'Failed to create session') + } finally { + createInFlightRef.current = false + setIsCreating(false) } } @@ -1217,7 +1224,7 @@ export function NewSession(props: { ) : null}