Fix deleting worktree crash and improve UX (#798)

* Changes from fix/deleting-worktree

* fix: Improve worktree deletion safety and branch cleanup logic

* fix: Improve error handling and async operations across auto-mode and worktree services

* Update apps/server/src/routes/auto-mode/routes/analyze-project.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
gsxdsm
2026-02-22 00:58:00 -08:00
committed by GitHub
parent 1d732916f1
commit 2f071a1ba3
15 changed files with 300 additions and 110 deletions

View File

@@ -8,12 +8,21 @@ import { getHttpApiClient } from '@/lib/http-api-client';
* before the user opens feature dialogs.
*/
export function useCursorStatusInit() {
const { setCursorCliStatus, cursorCliStatus } = useSetupStore();
// Use individual selectors instead of bare useSetupStore() to prevent
// re-rendering on every setup store mutation during initialization.
const setCursorCliStatus = useSetupStore((s) => s.setCursorCliStatus);
const initialized = useRef(false);
useEffect(() => {
// Only initialize once per session
if (initialized.current || cursorCliStatus !== null) {
if (initialized.current) {
return;
}
// Check current status at call time rather than via dependency to avoid
// re-renders when other setup store fields change during initialization.
const currentStatus = useSetupStore.getState().cursorCliStatus;
if (currentStatus !== null) {
initialized.current = true;
return;
}
initialized.current = true;
@@ -42,5 +51,5 @@ export function useCursorStatusInit() {
};
initCursorStatus();
}, [setCursorCliStatus, cursorCliStatus]);
}, [setCursorCliStatus]);
}

View File

@@ -17,17 +17,16 @@ const logger = createLogger('ProviderAuthInit');
* without needing to visit the settings page first.
*/
export function useProviderAuthInit() {
const {
setClaudeAuthStatus,
setCodexAuthStatus,
setZaiAuthStatus,
setGeminiCliStatus,
setGeminiAuthStatus,
claudeAuthStatus,
codexAuthStatus,
zaiAuthStatus,
geminiAuthStatus,
} = useSetupStore();
// IMPORTANT: Use individual selectors instead of bare useSetupStore() to prevent
// re-rendering on every setup store mutation. The bare call subscribes to the ENTIRE
// store, which during initialization causes cascading re-renders as multiple status
// setters fire in rapid succession. With enough rapid mutations, React hits the
// maximum update depth limit (error #185).
const setClaudeAuthStatus = useSetupStore((s) => s.setClaudeAuthStatus);
const setCodexAuthStatus = useSetupStore((s) => s.setCodexAuthStatus);
const setZaiAuthStatus = useSetupStore((s) => s.setZaiAuthStatus);
const setGeminiCliStatus = useSetupStore((s) => s.setGeminiCliStatus);
const setGeminiAuthStatus = useSetupStore((s) => s.setGeminiAuthStatus);
const initialized = useRef(false);
const refreshStatuses = useCallback(async () => {
@@ -219,5 +218,9 @@ export function useProviderAuthInit() {
// Always call refreshStatuses() to background re-validate on app restart,
// even when statuses are pre-populated from persisted storage (cache case).
void refreshStatuses();
}, [refreshStatuses, claudeAuthStatus, codexAuthStatus, zaiAuthStatus, geminiAuthStatus]);
// Only depend on the callback ref. The status values were previously included
// but they are outputs of refreshStatuses(), not inputs — including them caused
// cascading re-renders during initialization that triggered React error #185
// (maximum update depth exceeded) on first run.
}, [refreshStatuses]);
}

View File

@@ -26,6 +26,7 @@ import { useEffect, useState, useRef } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { getHttpApiClient, waitForApiKeyInit } from '@/lib/http-api-client';
import { getItem, setItem } from '@/lib/storage';
import { sanitizeWorktreeByProject } from '@/lib/settings-utils';
import { useAppStore, THEME_STORAGE_KEY } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import {
@@ -794,7 +795,14 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
projectHistory: settings.projectHistory ?? [],
projectHistoryIndex: settings.projectHistoryIndex ?? -1,
lastSelectedSessionByProject: settings.lastSelectedSessionByProject ?? {},
currentWorktreeByProject: settings.currentWorktreeByProject ?? {},
// Sanitize currentWorktreeByProject: only restore entries where path is null
// (main branch). Non-null paths point to worktree directories that may have
// been deleted while the app was closed. Restoring a stale path causes the
// board to render an invalid worktree selection, triggering a crash loop
// (error boundary reloads → restores same bad path → crash again).
// The use-worktrees validation effect will re-discover valid worktrees
// from the server once they load.
currentWorktreeByProject: sanitizeWorktreeByProject(settings.currentWorktreeByProject),
// UI State
worktreePanelCollapsed: settings.worktreePanelCollapsed ?? false,
lastProjectDir: settings.lastProjectDir ?? '',

View File

@@ -19,6 +19,7 @@ import { useAppStore, type ThemeMode, THEME_STORAGE_KEY } from '@/store/app-stor
import { useSetupStore } from '@/store/setup-store';
import { useAuthStore } from '@/store/auth-store';
import { waitForMigrationComplete, resetMigrationState } from './use-settings-migration';
import { sanitizeWorktreeByProject } from '@/lib/settings-utils';
import {
DEFAULT_OPENCODE_MODEL,
DEFAULT_GEMINI_MODEL,
@@ -584,6 +585,15 @@ export async function forceSyncSettingsToServer(): Promise<boolean> {
updates[field] = setupState[field as keyof typeof setupState];
}
// Update localStorage cache immediately so a page reload before the
// server response arrives still sees the latest state (e.g. after
// deleting a worktree, the stale worktree path won't survive in cache).
try {
setItem('automaker-settings-cache', JSON.stringify(updates));
} catch (storageError) {
logger.warn('Failed to update localStorage cache during force sync:', storageError);
}
const result = await api.settings.updateGlobal(updates);
return result.success;
} catch (error) {
@@ -796,8 +806,11 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
projectHistory: serverSettings.projectHistory,
projectHistoryIndex: serverSettings.projectHistoryIndex,
lastSelectedSessionByProject: serverSettings.lastSelectedSessionByProject,
currentWorktreeByProject:
serverSettings.currentWorktreeByProject ?? currentAppState.currentWorktreeByProject,
// Sanitize: only restore entries with path === null (main branch).
// Non-null paths may reference deleted worktrees, causing crash loops.
currentWorktreeByProject: sanitizeWorktreeByProject(
serverSettings.currentWorktreeByProject ?? currentAppState.currentWorktreeByProject
),
// UI State (previously in localStorage)
worktreePanelCollapsed: serverSettings.worktreePanelCollapsed ?? false,
lastProjectDir: serverSettings.lastProjectDir ?? '',