mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
feat(auto-mode): implement facade caching and enhance error handling
- Added caching for facades in AutoModeServiceCompat to persist auto loop state across API calls. - Improved error handling in BoardView for starting and stopping auto mode, with user-friendly toast notifications. - Updated WorktreePanel to manage auto mode state and concurrency limits more effectively. - Enhanced useAutoMode hook to prevent state overwrites during transitions and synchronize UI with backend status. This update optimizes performance and user experience in the auto mode feature.
This commit is contained in:
@@ -22,6 +22,7 @@ import type { FacadeOptions, AutoModeStatus, RunningAgentInfo } from './types.js
|
||||
export class AutoModeServiceCompat {
|
||||
private readonly globalService: GlobalAutoModeService;
|
||||
private readonly facadeOptions: FacadeOptions;
|
||||
private readonly facadeCache = new Map<string, AutoModeServiceFacade>();
|
||||
|
||||
constructor(
|
||||
events: EventEmitter,
|
||||
@@ -47,10 +48,17 @@ export class AutoModeServiceCompat {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a facade for a specific project
|
||||
* Get or create a facade for a specific project.
|
||||
* Facades are cached by project path so that auto loop state
|
||||
* (stored in the facade's AutoLoopCoordinator) persists across API calls.
|
||||
*/
|
||||
createFacade(projectPath: string): AutoModeServiceFacade {
|
||||
return AutoModeServiceFacade.create(projectPath, this.facadeOptions);
|
||||
let facade = this.facadeCache.get(projectPath);
|
||||
if (!facade) {
|
||||
facade = AutoModeServiceFacade.create(projectPath, this.facadeOptions);
|
||||
this.facadeCache.set(projectPath, facade);
|
||||
}
|
||||
return facade;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
@@ -1362,10 +1362,16 @@ export function BoardView() {
|
||||
if (enabled) {
|
||||
autoMode.start().catch((error) => {
|
||||
logger.error('[AutoMode] Failed to start:', error);
|
||||
toast.error('Failed to start auto mode', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
});
|
||||
} else {
|
||||
autoMode.stop().catch((error) => {
|
||||
logger.error('[AutoMode] Failed to stop:', error);
|
||||
toast.error('Failed to stop auto mode', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -112,6 +112,8 @@ export function WorktreePanel({
|
||||
// Use separate selectors to avoid creating new object references on each render
|
||||
const autoModeByWorktree = useAppStore((state) => state.autoModeByWorktree);
|
||||
const currentProject = useAppStore((state) => state.currentProject);
|
||||
const setAutoModeRunning = useAppStore((state) => state.setAutoModeRunning);
|
||||
const getMaxConcurrencyForWorktree = useAppStore((state) => state.getMaxConcurrencyForWorktree);
|
||||
|
||||
// Helper to generate worktree key for auto-mode (inlined to avoid selector issues)
|
||||
const getAutoModeWorktreeKey = useCallback(
|
||||
@@ -137,8 +139,6 @@ export function WorktreePanel({
|
||||
async (worktree: WorktreeInfo) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
// Import the useAutoMode to get start/stop functions
|
||||
// Since useAutoMode is a hook, we'll use the API client directly
|
||||
const api = getHttpApiClient();
|
||||
const branchName = worktree.isMain ? null : worktree.branch;
|
||||
const isRunning = isAutoModeRunningForWorktree(worktree);
|
||||
@@ -147,14 +147,17 @@ export function WorktreePanel({
|
||||
if (isRunning) {
|
||||
const result = await api.autoMode.stop(projectPath, branchName);
|
||||
if (result.success) {
|
||||
setAutoModeRunning(currentProject.id, branchName, false);
|
||||
const desc = branchName ? `worktree ${branchName}` : 'main branch';
|
||||
toast.success(`Auto Mode stopped for ${desc}`);
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to stop Auto Mode');
|
||||
}
|
||||
} else {
|
||||
const result = await api.autoMode.start(projectPath, branchName);
|
||||
const maxConcurrency = getMaxConcurrencyForWorktree(currentProject.id, branchName);
|
||||
const result = await api.autoMode.start(projectPath, branchName, maxConcurrency);
|
||||
if (result.success) {
|
||||
setAutoModeRunning(currentProject.id, branchName, true, maxConcurrency);
|
||||
const desc = branchName ? `worktree ${branchName}` : 'main branch';
|
||||
toast.success(`Auto Mode started for ${desc}`);
|
||||
} else {
|
||||
@@ -166,7 +169,13 @@ export function WorktreePanel({
|
||||
console.error('Auto mode toggle error:', error);
|
||||
}
|
||||
},
|
||||
[currentProject, projectPath, isAutoModeRunningForWorktree]
|
||||
[
|
||||
currentProject,
|
||||
projectPath,
|
||||
isAutoModeRunningForWorktree,
|
||||
setAutoModeRunning,
|
||||
getMaxConcurrencyForWorktree,
|
||||
]
|
||||
);
|
||||
|
||||
// Check if init script exists for the project using React Query
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { DEFAULT_MAX_CONCURRENCY } from '@automaker/types';
|
||||
@@ -11,6 +11,12 @@ import { getGlobalEventsRecent } from '@/hooks/use-event-recency';
|
||||
const logger = createLogger('AutoMode');
|
||||
|
||||
const AUTO_MODE_SESSION_KEY = 'automaker:autoModeRunningByWorktreeKey';
|
||||
|
||||
function arraysEqual(a: string[], b: string[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
const set = new Set(b);
|
||||
return a.every((id) => set.has(id));
|
||||
}
|
||||
const AUTO_MODE_POLLING_INTERVAL = 30000;
|
||||
|
||||
/**
|
||||
@@ -142,9 +148,16 @@ 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
|
||||
const isTransitioningRef = useRef(false);
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
// Skip sync when user is in the middle of start/stop - avoids race where
|
||||
// refreshStatus runs before the API call completes and overwrites optimistic state
|
||||
if (isTransitioningRef.current) return;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.status) return;
|
||||
@@ -152,18 +165,28 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
const result = await api.autoMode.status(currentProject.path, branchName);
|
||||
if (result.success && result.isAutoLoopRunning !== undefined) {
|
||||
const backendIsRunning = result.isAutoLoopRunning;
|
||||
const backendRunningFeatures = result.runningFeatures ?? [];
|
||||
const needsSync =
|
||||
backendIsRunning !== isAutoModeRunning ||
|
||||
// Also sync when backend has runningFeatures we're missing (handles missed WebSocket events)
|
||||
(backendIsRunning &&
|
||||
Array.isArray(backendRunningFeatures) &&
|
||||
backendRunningFeatures.length > 0 &&
|
||||
!arraysEqual(backendRunningFeatures, runningAutoTasks));
|
||||
|
||||
if (backendIsRunning !== isAutoModeRunning) {
|
||||
if (needsSync) {
|
||||
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
|
||||
if (backendIsRunning !== isAutoModeRunning) {
|
||||
logger.info(
|
||||
`[AutoMode] Syncing UI state with backend for ${worktreeDesc} in ${currentProject.path}: ${backendIsRunning ? 'ON' : 'OFF'}`
|
||||
);
|
||||
}
|
||||
setAutoModeRunning(
|
||||
currentProject.id,
|
||||
branchName,
|
||||
backendIsRunning,
|
||||
result.maxConcurrency,
|
||||
result.runningFeatures
|
||||
backendRunningFeatures
|
||||
);
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, backendIsRunning);
|
||||
}
|
||||
@@ -171,7 +194,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
} catch (error) {
|
||||
logger.error('Error syncing auto mode state with backend:', error);
|
||||
}
|
||||
}, [branchName, currentProject, isAutoModeRunning, setAutoModeRunning]);
|
||||
}, [branchName, currentProject, isAutoModeRunning, runningAutoTasks, 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.
|
||||
@@ -558,6 +581,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
isTransitioningRef.current = true;
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.start) {
|
||||
@@ -588,14 +612,18 @@ 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());
|
||||
} catch (error) {
|
||||
// Revert UI state on error
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, false);
|
||||
setAutoModeRunning(currentProject.id, branchName, false);
|
||||
logger.error('Error starting auto mode:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
isTransitioningRef.current = false;
|
||||
}
|
||||
}, [currentProject, branchName, setAutoModeRunning]);
|
||||
}, [currentProject, branchName, setAutoModeRunning, getMaxConcurrencyForWorktree]);
|
||||
|
||||
// Stop auto mode - calls backend to stop the auto loop for this worktree
|
||||
const stop = useCallback(async () => {
|
||||
@@ -604,6 +632,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
isTransitioningRef.current = true;
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.stop) {
|
||||
@@ -631,12 +660,16 @@ 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());
|
||||
} catch (error) {
|
||||
// Revert UI state on error
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, true);
|
||||
setAutoModeRunning(currentProject.id, branchName, true);
|
||||
logger.error('Error stopping auto mode:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
isTransitioningRef.current = false;
|
||||
}
|
||||
}, [currentProject, branchName, setAutoModeRunning]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user