mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-20 11:03:08 +00:00
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:
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
Reference in New Issue
Block a user