mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
Fix concurrency limits and remote branch fetching issues (#788)
* Changes from fix/bug-fixes * feat: Refactor worktree iteration and improve error logging across services * feat: Extract URL/port patterns to module level and fix abort condition * fix: Improve IPv6 loopback handling, select component layout, and terminal UI * feat: Add thinking level defaults and adjust list row padding * Update apps/ui/src/store/app-store.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: Add worktree-aware terminal creation and split options, fix npm security issues from audit * feat: Add tracked remote detection to pull dialog flow * feat: Add merge state tracking to git operations * feat: Improve merge detection and add post-merge action preferences * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: Pass merge detection info to stash reapplication and handle merge state consistently * fix: Call onPulled callback in merge handlers and add validation checks --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -157,8 +157,40 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
// Check if we can start a new task based on concurrency limit
|
||||
const canStartNewTask = runningAutoTasks.length < maxConcurrency;
|
||||
|
||||
// Ref to prevent refreshStatus from overwriting optimistic state during start/stop
|
||||
// Ref to prevent refreshStatus and WebSocket handlers from overwriting optimistic state
|
||||
// during start/stop transitions.
|
||||
const isTransitioningRef = useRef(false);
|
||||
// Tracks specifically a restart-for-concurrency transition. When true, the
|
||||
// auto_mode_started WebSocket handler will clear isTransitioningRef, ensuring
|
||||
// delayed auto_mode_stopped events that arrive after the HTTP calls complete
|
||||
// (but before the WebSocket events) are still suppressed.
|
||||
const isRestartTransitionRef = useRef(false);
|
||||
// Safety timeout ID to clear the transition flag if the auto_mode_started event never arrives
|
||||
const restartSafetyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Use refs for mutable state in refreshStatus to avoid unstable callback identity.
|
||||
// This prevents the useEffect that calls refreshStatus on mount from re-firing
|
||||
// every time isAutoModeRunning or runningAutoTasks changes, which was a source of
|
||||
// flickering as refreshStatus would race with WebSocket events and optimistic updates.
|
||||
const isAutoModeRunningRef = useRef(isAutoModeRunning);
|
||||
const runningAutoTasksRef = useRef(runningAutoTasks);
|
||||
useEffect(() => {
|
||||
isAutoModeRunningRef.current = isAutoModeRunning;
|
||||
}, [isAutoModeRunning]);
|
||||
useEffect(() => {
|
||||
runningAutoTasksRef.current = runningAutoTasks;
|
||||
}, [runningAutoTasks]);
|
||||
|
||||
// Clean up safety timeout on unmount to prevent timer leaks and misleading log warnings
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (restartSafetyTimeoutRef.current) {
|
||||
clearTimeout(restartSafetyTimeoutRef.current);
|
||||
restartSafetyTimeoutRef.current = null;
|
||||
}
|
||||
isRestartTransitionRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
@@ -175,20 +207,25 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
if (result.success && result.isAutoLoopRunning !== undefined) {
|
||||
const backendIsRunning = result.isAutoLoopRunning;
|
||||
const backendRunningFeatures = result.runningFeatures ?? [];
|
||||
// Read latest state from refs to avoid stale closure values
|
||||
const currentIsRunning = isAutoModeRunningRef.current;
|
||||
const currentRunningTasks = runningAutoTasksRef.current;
|
||||
const needsSync =
|
||||
backendIsRunning !== isAutoModeRunning ||
|
||||
backendIsRunning !== currentIsRunning ||
|
||||
// Also sync when backend has runningFeatures we're missing (handles missed WebSocket events)
|
||||
(backendIsRunning &&
|
||||
Array.isArray(backendRunningFeatures) &&
|
||||
backendRunningFeatures.length > 0 &&
|
||||
!arraysEqual(backendRunningFeatures, runningAutoTasks)) ||
|
||||
!arraysEqual(backendRunningFeatures, currentRunningTasks)) ||
|
||||
// Also sync when UI has stale running tasks but backend has none
|
||||
// (handles server restart where features were reconciled to backlog/ready)
|
||||
(!backendIsRunning && runningAutoTasks.length > 0 && backendRunningFeatures.length === 0);
|
||||
(!backendIsRunning &&
|
||||
currentRunningTasks.length > 0 &&
|
||||
backendRunningFeatures.length === 0);
|
||||
|
||||
if (needsSync) {
|
||||
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
|
||||
if (backendIsRunning !== isAutoModeRunning) {
|
||||
if (backendIsRunning !== currentIsRunning) {
|
||||
logger.info(
|
||||
`[AutoMode] Syncing UI state with backend for ${worktreeDesc} in ${currentProject.path}: ${backendIsRunning ? 'ON' : 'OFF'}`
|
||||
);
|
||||
@@ -206,7 +243,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
} catch (error) {
|
||||
logger.error('Error syncing auto mode state with backend:', error);
|
||||
}
|
||||
}, [branchName, currentProject, isAutoModeRunning, runningAutoTasks, setAutoModeRunning]);
|
||||
}, [branchName, currentProject, setAutoModeRunning]);
|
||||
|
||||
// On mount, query backend for current auto loop status and sync UI state.
|
||||
// This handles cases where the backend is still running after a page refresh.
|
||||
@@ -281,8 +318,23 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
'maxConcurrency' in event && typeof event.maxConcurrency === 'number'
|
||||
? event.maxConcurrency
|
||||
: getMaxConcurrencyForWorktree(eventProjectId, eventBranchName);
|
||||
// Always apply start events even during transitions - this confirms the optimistic state
|
||||
setAutoModeRunning(eventProjectId, eventBranchName, true, eventMaxConcurrency);
|
||||
}
|
||||
// If we were in a restart transition (concurrency change), the arrival of
|
||||
// auto_mode_started confirms the restart is complete. Clear the transition
|
||||
// flags so future auto_mode_stopped events are processed normally.
|
||||
// Only clear transition refs when the event is for this hook's worktree,
|
||||
// to avoid events for worktree B incorrectly affecting worktree A's state.
|
||||
if (isRestartTransitionRef.current && eventBranchName === branchName) {
|
||||
logger.debug(`[AutoMode] Restart transition complete for ${worktreeDesc}`);
|
||||
isTransitioningRef.current = false;
|
||||
isRestartTransitionRef.current = false;
|
||||
if (restartSafetyTimeoutRef.current) {
|
||||
clearTimeout(restartSafetyTimeoutRef.current);
|
||||
restartSafetyTimeoutRef.current = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -307,12 +359,23 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
break;
|
||||
|
||||
case 'auto_mode_stopped':
|
||||
// Backend stopped auto loop - update UI state
|
||||
// Backend stopped auto loop - update UI state.
|
||||
// Skip during transitions (e.g., restartWithConcurrency) to avoid flickering the toggle
|
||||
// off between stop and start. The transition handler will set the correct final state.
|
||||
// Only suppress (and only apply transition guard) when the event is for this hook's
|
||||
// worktree, to avoid worktree B's stop events being incorrectly suppressed by
|
||||
// worktree A's transition state.
|
||||
{
|
||||
const worktreeDesc = eventBranchName ? `worktree ${eventBranchName}` : 'main worktree';
|
||||
logger.info(`[AutoMode] Backend stopped auto loop for ${worktreeDesc}`);
|
||||
if (eventProjectId) {
|
||||
setAutoModeRunning(eventProjectId, eventBranchName, false);
|
||||
if (eventBranchName === branchName && isTransitioningRef.current) {
|
||||
logger.info(
|
||||
`[AutoMode] Backend stopped auto loop for ${worktreeDesc} (ignored during transition)`
|
||||
);
|
||||
} else {
|
||||
logger.info(`[AutoMode] Backend stopped auto loop for ${worktreeDesc}`);
|
||||
if (eventProjectId) {
|
||||
setAutoModeRunning(eventProjectId, eventBranchName, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -574,6 +637,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
return unsubscribe;
|
||||
}, [
|
||||
projectId,
|
||||
branchName,
|
||||
addRunningTask,
|
||||
removeRunningTask,
|
||||
addAutoModeActivity,
|
||||
@@ -582,7 +646,6 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
setAutoModeRunning,
|
||||
currentProject?.path,
|
||||
getMaxConcurrencyForWorktree,
|
||||
setMaxConcurrencyForWorktree,
|
||||
isPrimaryWorktreeBranch,
|
||||
]);
|
||||
|
||||
@@ -624,8 +687,10 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
}
|
||||
|
||||
logger.debug(`[AutoMode] Started successfully for ${worktreeDesc}`);
|
||||
// Sync with backend after success (gets runningFeatures if events were delayed)
|
||||
queueMicrotask(() => void refreshStatus());
|
||||
// Sync with backend after a short delay to get runningFeatures if events were delayed.
|
||||
// The delay ensures the backend has fully processed the start before we poll status,
|
||||
// avoiding a race where status returns stale data and briefly flickers the toggle.
|
||||
setTimeout(() => void refreshStatus(), 500);
|
||||
} catch (error) {
|
||||
// Revert UI state on error
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, false);
|
||||
@@ -635,7 +700,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
} finally {
|
||||
isTransitioningRef.current = false;
|
||||
}
|
||||
}, [currentProject, branchName, setAutoModeRunning, getMaxConcurrencyForWorktree]);
|
||||
}, [currentProject, branchName, setAutoModeRunning, getMaxConcurrencyForWorktree, refreshStatus]);
|
||||
|
||||
// Stop auto mode - calls backend to stop the auto loop for this worktree
|
||||
const stop = useCallback(async () => {
|
||||
@@ -672,8 +737,8 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
// NOTE: Running tasks will continue until natural completion.
|
||||
// The backend stops picking up new features but doesn't abort running ones.
|
||||
logger.info(`Stopped ${worktreeDesc} - running tasks will continue`);
|
||||
// Sync with backend after success
|
||||
queueMicrotask(() => void refreshStatus());
|
||||
// Sync with backend after a short delay to confirm stopped state
|
||||
setTimeout(() => void refreshStatus(), 500);
|
||||
} catch (error) {
|
||||
// Revert UI state on error
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, true);
|
||||
@@ -683,7 +748,95 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
} finally {
|
||||
isTransitioningRef.current = false;
|
||||
}
|
||||
}, [currentProject, branchName, setAutoModeRunning]);
|
||||
}, [currentProject, branchName, setAutoModeRunning, refreshStatus]);
|
||||
|
||||
// Restart auto mode with new concurrency without flickering the toggle.
|
||||
// Unlike stop() + start(), this keeps isRunning=true throughout the transition
|
||||
// so the toggle switch never visually turns off.
|
||||
//
|
||||
// IMPORTANT: isTransitioningRef is NOT cleared in the finally block here.
|
||||
// Instead, it stays true until the auto_mode_started WebSocket event arrives,
|
||||
// which confirms the backend restart is complete. This prevents a race condition
|
||||
// where a delayed auto_mode_stopped WebSocket event (sent by the backend during
|
||||
// stop()) arrives after the HTTP calls complete but before the WebSocket events,
|
||||
// which would briefly set isRunning=false and cause a visible toggle flicker.
|
||||
// A safety timeout ensures the flag is cleared even if the event never arrives.
|
||||
const restartWithConcurrency = useCallback(async () => {
|
||||
if (!currentProject) {
|
||||
logger.error('No project selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any previous safety timeout
|
||||
if (restartSafetyTimeoutRef.current) {
|
||||
clearTimeout(restartSafetyTimeoutRef.current);
|
||||
restartSafetyTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
isTransitioningRef.current = true;
|
||||
isRestartTransitionRef.current = true;
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.stop || !api?.autoMode?.start) {
|
||||
throw new Error('Auto mode API not available');
|
||||
}
|
||||
|
||||
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
|
||||
logger.info(
|
||||
`[AutoMode] Restarting with new concurrency for ${worktreeDesc} in ${currentProject.path}`
|
||||
);
|
||||
|
||||
// Stop backend without updating UI state (keep isRunning=true)
|
||||
const stopResult = await api.autoMode.stop(currentProject.path, branchName);
|
||||
|
||||
if (!stopResult.success) {
|
||||
logger.error('Failed to stop auto mode during restart:', stopResult.error);
|
||||
// Don't throw - try to start anyway since the goal is to update concurrency
|
||||
}
|
||||
|
||||
// Start backend with the new concurrency (UI state stays isRunning=true)
|
||||
const currentMaxConcurrency = getMaxConcurrencyForWorktree(currentProject.id, branchName);
|
||||
const startResult = await api.autoMode.start(
|
||||
currentProject.path,
|
||||
branchName,
|
||||
currentMaxConcurrency
|
||||
);
|
||||
|
||||
if (!startResult.success) {
|
||||
// If start fails, we need to revert UI state since we're actually stopped now
|
||||
isTransitioningRef.current = false;
|
||||
isRestartTransitionRef.current = false;
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, false);
|
||||
setAutoModeRunning(currentProject.id, branchName, false);
|
||||
logger.error('Failed to restart auto mode with new concurrency:', startResult.error);
|
||||
throw new Error(startResult.error || 'Failed to restart auto mode');
|
||||
}
|
||||
|
||||
logger.debug(`[AutoMode] Restarted successfully for ${worktreeDesc}`);
|
||||
|
||||
// Don't clear isTransitioningRef here - let the auto_mode_started WebSocket
|
||||
// event handler clear it. Set a safety timeout in case the event never arrives.
|
||||
restartSafetyTimeoutRef.current = setTimeout(() => {
|
||||
if (isRestartTransitionRef.current) {
|
||||
logger.warn('[AutoMode] Restart transition safety timeout - clearing transition flag');
|
||||
isTransitioningRef.current = false;
|
||||
isRestartTransitionRef.current = false;
|
||||
restartSafetyTimeoutRef.current = null;
|
||||
}
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
// On error, clear the transition flags immediately
|
||||
isTransitioningRef.current = false;
|
||||
isRestartTransitionRef.current = false;
|
||||
// Revert UI state since the backend may be stopped after a partial restart
|
||||
if (currentProject) {
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, false);
|
||||
setAutoModeRunning(currentProject.id, branchName, false);
|
||||
}
|
||||
logger.error('Error restarting auto mode:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [currentProject, branchName, setAutoModeRunning, getMaxConcurrencyForWorktree]);
|
||||
|
||||
// Stop a specific feature
|
||||
const stopFeature = useCallback(
|
||||
@@ -731,6 +884,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
start,
|
||||
stop,
|
||||
stopFeature,
|
||||
restartWithConcurrency,
|
||||
refreshStatus,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user