Merge remote-tracking branch 'upstream/v0.15.0rc' into feat/add-zai-usage-tracking

# Conflicts:
#	apps/ui/src/components/usage-popover.tsx
#	apps/ui/src/components/views/board-view/mobile-usage-bar.tsx
This commit is contained in:
gsxdsm
2026-02-17 11:19:06 -08:00
118 changed files with 17736 additions and 8749 deletions

View File

@@ -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';
logger.info(
`[AutoMode] Syncing UI state with backend for ${worktreeDesc} in ${currentProject.path}: ${backendIsRunning ? 'ON' : 'OFF'}`
);
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]);

View File

@@ -28,22 +28,27 @@ const PROGRESS_DEBOUNCE_MAX_WAIT = 2000;
* feature moving to custom pipeline columns (fixes GitHub issue #668)
*/
const FEATURE_LIST_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [
'auto_mode_feature_start',
'auto_mode_feature_complete',
'auto_mode_error',
'auto_mode_started',
'auto_mode_stopped',
'plan_approval_required',
'plan_approved',
'plan_rejected',
'pipeline_step_started',
'pipeline_step_complete',
'feature_status_changed',
'features_reconciled',
];
/**
* Events that should invalidate a specific feature (features.single query)
* Note: pipeline_step_started is NOT included here because it already invalidates
* features.all() above, which also invalidates child queries (features.single)
* Note: auto_mode_feature_start and pipeline_step_started are NOT included here
* because they already invalidate features.all() above, which also invalidates
* child queries (features.single)
*/
const SINGLE_FEATURE_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [
'auto_mode_feature_start',
'auto_mode_phase',
'auto_mode_phase_complete',
'auto_mode_task_status',