diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 2dd705b9..6462b092 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -79,6 +79,9 @@ import { SelectionActionBar, ListView } from './board-view/components'; import { MassEditDialog } from './board-view/dialogs'; import { InitScriptIndicator } from './board-view/init-script-indicator'; import { useInitScriptEvents } from '@/hooks/use-init-script-events'; +import { usePipelineConfig } from '@/hooks/queries'; +import { useQueryClient } from '@tanstack/react-query'; +import { queryKeys } from '@/lib/query-keys'; // Stable empty array to avoid infinite loop in selector const EMPTY_WORKTREES: ReturnType['getWorktrees']> = []; @@ -109,8 +112,9 @@ export function BoardView() { getPrimaryWorktreeBranch, setPipelineConfig, } = useAppStore(); - // Subscribe to pipelineConfigByProject to trigger re-renders when it changes - const pipelineConfigByProject = useAppStore((state) => state.pipelineConfigByProject); + // Fetch pipeline config via React Query + const { data: pipelineConfig } = usePipelineConfig(currentProject?.path); + const queryClient = useQueryClient(); // Subscribe to worktreePanelVisibleByProject to trigger re-renders when it changes const worktreePanelVisibleByProject = useAppStore((state) => state.worktreePanelVisibleByProject); // Subscribe to showInitScriptIndicatorByProject to trigger re-renders when it changes @@ -241,25 +245,6 @@ export function BoardView() { setFeaturesWithContext, }); - // Load pipeline config when project changes - useEffect(() => { - if (!currentProject?.path) return; - - const loadPipelineConfig = async () => { - try { - const api = getHttpApiClient(); - const result = await api.pipeline.getConfig(currentProject.path); - if (result.success && result.config) { - setPipelineConfig(currentProject.path, result.config); - } - } catch (error) { - logger.error('Failed to load pipeline config:', error); - } - }; - - loadPipelineConfig(); - }, [currentProject?.path, setPipelineConfig]); - // Auto mode hook const autoMode = useAutoMode(); // Get runningTasks from the hook (scoped to current project) @@ -1131,9 +1116,7 @@ export function BoardView() { }); // Build columnFeaturesMap for ListView - const pipelineConfig = currentProject?.path - ? pipelineConfigByProject[currentProject.path] || null - : null; + // pipelineConfig is now from usePipelineConfig React Query hook at the top const columnFeaturesMap = useMemo(() => { const columns = getColumnsWithPipeline(pipelineConfig); const map: Record = {}; @@ -1585,6 +1568,11 @@ export function BoardView() { if (!result.success) { throw new Error(result.error || 'Failed to save pipeline config'); } + // Invalidate React Query cache to refetch updated config + queryClient.invalidateQueries({ + queryKey: queryKeys.pipeline.config(currentProject.path), + }); + // Also update Zustand for backward compatibility setPipelineConfig(currentProject.path, config); }} /> diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx index 6916222e..2d3edd23 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck import { useEffect, useState, useMemo } from 'react'; import { Feature, ThinkingLevel, ParsedTask } from '@/store/app-store'; import type { ReasoningEffort } from '@automaker/types'; @@ -24,6 +23,7 @@ import { import { getElectronAPI } from '@/lib/electron'; import { SummaryDialog } from './summary-dialog'; import { getProviderIconForModel } from '@/components/ui/provider-icon'; +import { useFeature, useAgentOutput } from '@/hooks/queries'; /** * Formats thinking level for compact display @@ -58,6 +58,7 @@ function formatReasoningEffort(effort: ReasoningEffort | undefined): string { interface AgentInfoPanelProps { feature: Feature; + projectPath: string; contextContent?: string; summary?: string; isCurrentAutoTask?: boolean; @@ -65,23 +66,54 @@ interface AgentInfoPanelProps { export function AgentInfoPanel({ feature, + projectPath, contextContent, summary, isCurrentAutoTask, }: AgentInfoPanelProps) { - const [agentInfo, setAgentInfo] = useState(null); const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false); const [isTodosExpanded, setIsTodosExpanded] = useState(false); // Track real-time task status updates from WebSocket events const [taskStatusMap, setTaskStatusMap] = useState< Map >(new Map()); - // Fresh planSpec data fetched from API (store data is stale for task progress) - const [freshPlanSpec, setFreshPlanSpec] = useState<{ - tasks?: ParsedTask[]; - tasksCompleted?: number; - currentTaskId?: string; - } | null>(null); + + // Determine if we should poll for updates + const shouldPoll = isCurrentAutoTask || feature.status === 'in_progress'; + const shouldFetchData = feature.status !== 'backlog'; + + // Fetch fresh feature data for planSpec (store data can be stale for task progress) + const { data: freshFeature } = useFeature(projectPath, feature.id, { + enabled: shouldFetchData && !contextContent, + pollingInterval: shouldPoll ? 3000 : false, + }); + + // Fetch agent output for parsing + const { data: agentOutputContent } = useAgentOutput(projectPath, feature.id, { + enabled: shouldFetchData && !contextContent, + pollingInterval: shouldPoll ? 3000 : false, + }); + + // Parse agent output into agentInfo + const agentInfo = useMemo(() => { + if (contextContent) { + return parseAgentContext(contextContent); + } + if (agentOutputContent) { + return parseAgentContext(agentOutputContent); + } + return null; + }, [contextContent, agentOutputContent]); + + // Fresh planSpec data from API (more accurate than store data for task progress) + const freshPlanSpec = useMemo(() => { + if (!freshFeature?.planSpec) return null; + return { + tasks: freshFeature.planSpec.tasks, + tasksCompleted: freshFeature.planSpec.tasksCompleted || 0, + currentTaskId: freshFeature.planSpec.currentTaskId, + }; + }, [freshFeature?.planSpec]); // Derive effective todos from planSpec.tasks when available, fallback to agentInfo.todos // Uses freshPlanSpec (from API) for accurate progress, with taskStatusMap for real-time updates @@ -133,73 +165,6 @@ export function AgentInfoPanel({ taskStatusMap, ]); - useEffect(() => { - const loadContext = async () => { - if (contextContent) { - const info = parseAgentContext(contextContent); - setAgentInfo(info); - return; - } - - if (feature.status === 'backlog') { - setAgentInfo(null); - setFreshPlanSpec(null); - return; - } - - try { - const api = getElectronAPI(); - const currentProject = (window as any).__currentProject; - if (!currentProject?.path) return; - - if (api.features) { - // Fetch fresh feature data to get up-to-date planSpec (store data is stale) - try { - const featureResult = await api.features.get(currentProject.path, feature.id); - const freshFeature: any = (featureResult as any).feature; - if (featureResult.success && freshFeature?.planSpec) { - setFreshPlanSpec({ - tasks: freshFeature.planSpec.tasks, - tasksCompleted: freshFeature.planSpec.tasksCompleted || 0, - currentTaskId: freshFeature.planSpec.currentTaskId, - }); - } - } catch { - // Ignore errors fetching fresh planSpec - } - - const result = await api.features.getAgentOutput(currentProject.path, feature.id); - - if (result.success && result.content) { - const info = parseAgentContext(result.content); - setAgentInfo(info); - } - } else { - const contextPath = `${currentProject.path}/.automaker/features/${feature.id}/agent-output.md`; - const result = await api.readFile(contextPath); - - if (result.success && result.content) { - const info = parseAgentContext(result.content); - setAgentInfo(info); - } - } - } catch { - console.debug('[KanbanCard] No context file for feature:', feature.id); - } - }; - - loadContext(); - - // Poll for updates when feature is in_progress (not just isCurrentAutoTask) - // This ensures planSpec progress stays in sync - if (isCurrentAutoTask || feature.status === 'in_progress') { - const interval = setInterval(loadContext, 3000); - return () => { - clearInterval(interval); - }; - } - }, [feature.id, feature.status, contextContent, isCurrentAutoTask]); - // Listen to WebSocket events for real-time task status updates // This ensures the Kanban card shows the same progress as the Agent Output modal // Listen for ANY in-progress feature with planSpec tasks, not just isCurrentAutoTask diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx index 6f22e87e..a2845a3d 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx @@ -97,7 +97,7 @@ export const KanbanCard = memo(function KanbanCard({ isSelected = false, onToggleSelect, }: KanbanCardProps) { - const { useWorktrees } = useAppStore(); + const { useWorktrees, currentProject } = useAppStore(); const [isLifted, setIsLifted] = useState(false); useLayoutEffect(() => { @@ -213,6 +213,7 @@ export const KanbanCard = memo(function KanbanCard({ {/* Agent Info Panel */} (''); - const [isLoading, setIsLoading] = useState(true); + // Resolve project path - prefer prop, fallback to window.__currentProject + const resolvedProjectPath = projectPathProp || (window as any).__currentProject?.path || ''; + + // Track additional content from WebSocket events (appended to query data) + const [streamedContent, setStreamedContent] = useState(''); const [viewMode, setViewMode] = useState(null); - const [projectPath, setProjectPath] = useState(''); + + // Use React Query for initial output loading + const { data: initialOutput = '', isLoading } = useAgentOutput( + resolvedProjectPath, + featureId, + open && !!resolvedProjectPath + ); + + // Reset streamed content when modal opens or featureId changes + useEffect(() => { + if (open) { + setStreamedContent(''); + } + }, [open, featureId]); + + // Combine initial output from query with streamed content from WebSocket + const output = initialOutput + streamedContent; // Extract summary from output const summary = useMemo(() => extractSummary(output), [output]); @@ -52,7 +72,6 @@ export function AgentOutputModal({ const effectiveViewMode = viewMode ?? (summary ? 'summary' : 'parsed'); const scrollRef = useRef(null); const autoScrollRef = useRef(true); - const projectPathRef = useRef(''); const useWorktrees = useAppStore((state) => state.useWorktrees); // Auto-scroll to bottom when output changes @@ -62,50 +81,6 @@ export function AgentOutputModal({ } }, [output]); - // Load existing output from file - useEffect(() => { - if (!open) return; - - const loadOutput = async () => { - const api = getElectronAPI(); - if (!api) return; - - setIsLoading(true); - - try { - // Use projectPath prop if provided, otherwise fall back to window.__currentProject for backward compatibility - const resolvedProjectPath = projectPathProp || (window as any).__currentProject?.path; - if (!resolvedProjectPath) { - setIsLoading(false); - return; - } - - projectPathRef.current = resolvedProjectPath; - setProjectPath(resolvedProjectPath); - - // Use features API to get agent output - if (api.features) { - const result = await api.features.getAgentOutput(resolvedProjectPath, featureId); - - if (result.success) { - setOutput(result.content || ''); - } else { - setOutput(''); - } - } else { - setOutput(''); - } - } catch (error) { - console.error('Failed to load output:', error); - setOutput(''); - } finally { - setIsLoading(false); - } - }; - - loadOutput(); - }, [open, featureId, projectPathProp]); - // Listen to auto mode events and update output useEffect(() => { if (!open) return; @@ -264,8 +239,8 @@ export function AgentOutputModal({ } if (newContent) { - // Only update local state - server is the single source of truth for file writes - setOutput((prev) => prev + newContent); + // Append new content from WebSocket to streamed content + setStreamedContent((prev) => prev + newContent); } }); @@ -379,15 +354,15 @@ export function AgentOutputModal({ {/* Task Progress Panel - shows when tasks are being executed */} {effectiveViewMode === 'changes' ? (
- {projectPath ? ( + {resolvedProjectPath ? ( (null); const [browserUrl, setBrowserUrl] = useState(null); const [showBrowserFallback, setShowBrowserFallback] = useState(false); - // Branch fetching state - const [branches, setBranches] = useState([]); - const [isLoadingBranches, setIsLoadingBranches] = useState(false); // Track whether an operation completed that warrants a refresh const operationCompletedRef = useRef(false); + // Use React Query for branch fetching - only enabled when dialog is open + const { data: branchesData, isLoading: isLoadingBranches } = useWorktreeBranches( + open ? worktree?.path : undefined, + true // Include remote branches for PR base branch selection + ); + + // Filter out current worktree branch from the list + const branches = useMemo(() => { + if (!branchesData?.branches) return []; + return branchesData.branches.map((b) => b.name).filter((name) => name !== worktree?.branch); + }, [branchesData?.branches, worktree?.branch]); + // Common state reset function to avoid duplication const resetState = useCallback(() => { setTitle(''); @@ -71,44 +81,13 @@ export function CreatePRDialog({ setBrowserUrl(null); setShowBrowserFallback(false); operationCompletedRef.current = false; - setBranches([]); }, [defaultBaseBranch]); - // Fetch branches for autocomplete - const fetchBranches = useCallback(async () => { - if (!worktree?.path) return; - - setIsLoadingBranches(true); - try { - const api = getElectronAPI(); - if (!api?.worktree?.listBranches) { - return; - } - // Fetch both local and remote branches for PR base branch selection - const result = await api.worktree.listBranches(worktree.path, true); - if (result.success && result.result) { - // Extract branch names, filtering out the current worktree branch - const branchNames = result.result.branches - .map((b) => b.name) - .filter((name) => name !== worktree.branch); - setBranches(branchNames); - } - } catch { - // Silently fail - branches will default to main only - } finally { - setIsLoadingBranches(false); - } - }, [worktree?.path, worktree?.branch]); - // Reset state when dialog opens or worktree changes useEffect(() => { // Reset all state on both open and close resetState(); - if (open) { - // Fetch fresh branches when dialog opens - fetchBranches(); - } - }, [open, worktree?.path, resetState, fetchBranches]); + }, [open, worktree?.path, resetState]); const handleCreate = async () => { if (!worktree) return; diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts index 78c0526f..58877c92 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts @@ -14,6 +14,7 @@ import { getElectronAPI } from '@/lib/electron'; import { isConnectionError, handleServerOffline } from '@/lib/http-api-client'; import { toast } from 'sonner'; import { useAutoMode } from '@/hooks/use-auto-mode'; +import { useVerifyFeature, useResumeFeature } from '@/hooks/mutations'; import { truncateDescription } from '@/lib/utils'; import { getBlockingDependencies } from '@automaker/dependency-resolver'; import { createLogger } from '@automaker/utils/logger'; @@ -94,6 +95,10 @@ export function useBoardActions({ } = useAppStore(); const autoMode = useAutoMode(); + // React Query mutations for feature operations + const verifyFeatureMutation = useVerifyFeature(currentProject?.path ?? ''); + const resumeFeatureMutation = useResumeFeature(currentProject?.path ?? ''); + // Worktrees are created when adding/editing features with a branch name // This ensures the worktree exists before the feature starts execution @@ -480,28 +485,9 @@ export function useBoardActions({ const handleVerifyFeature = useCallback( async (feature: Feature) => { if (!currentProject) return; - - try { - const api = getElectronAPI(); - if (!api?.autoMode) { - logger.error('Auto mode API not available'); - return; - } - - const result = await api.autoMode.verifyFeature(currentProject.path, feature.id); - - if (result.success) { - logger.info('Feature verification started successfully'); - } else { - logger.error('Failed to verify feature:', result.error); - await loadFeatures(); - } - } catch (error) { - logger.error('Error verifying feature:', error); - await loadFeatures(); - } + verifyFeatureMutation.mutate(feature.id); }, - [currentProject, loadFeatures] + [currentProject, verifyFeatureMutation] ); const handleResumeFeature = useCallback( @@ -511,40 +497,9 @@ export function useBoardActions({ logger.error('No current project'); return; } - - try { - const api = getElectronAPI(); - if (!api?.autoMode) { - logger.error('Auto mode API not available'); - return; - } - - logger.info('Calling resumeFeature API...', { - projectPath: currentProject.path, - featureId: feature.id, - useWorktrees, - }); - - const result = await api.autoMode.resumeFeature( - currentProject.path, - feature.id, - useWorktrees - ); - - logger.info('resumeFeature result:', result); - - if (result.success) { - logger.info('Feature resume started successfully'); - } else { - logger.error('Failed to resume feature:', result.error); - await loadFeatures(); - } - } catch (error) { - logger.error('Error resuming feature:', error); - await loadFeatures(); - } + resumeFeatureMutation.mutate({ featureId: feature.id, useWorktrees }); }, - [currentProject, loadFeatures, useWorktrees] + [currentProject, resumeFeatureMutation, useWorktrees] ); const handleManualVerify = useCallback( diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-features.ts b/apps/ui/src/components/views/board-view/hooks/use-board-features.ts index e457e02e..1f60c458 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-features.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-features.ts @@ -1,8 +1,18 @@ -import { useState, useCallback, useEffect, useRef } from 'react'; -import { useAppStore, Feature } from '@/store/app-store'; +/** + * Board Features Hook + * + * React Query-based hook for managing features on the board view. + * Handles feature loading, categories, and auto-mode event notifications. + */ + +import { useState, useCallback, useEffect } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useAppStore } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; import { toast } from 'sonner'; import { createLogger } from '@automaker/utils/logger'; +import { useFeatures } from '@/hooks/queries'; +import { queryKeys } from '@/lib/query-keys'; const logger = createLogger('BoardFeatures'); @@ -11,105 +21,15 @@ interface UseBoardFeaturesProps { } export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) { - const { features, setFeatures } = useAppStore(); - const [isLoading, setIsLoading] = useState(true); + const queryClient = useQueryClient(); const [persistedCategories, setPersistedCategories] = useState([]); - // Track previous project path to detect project switches - const prevProjectPathRef = useRef(null); - const isInitialLoadRef = useRef(true); - const isSwitchingProjectRef = useRef(false); - - // Load features using features API - // IMPORTANT: Do NOT add 'features' to dependency array - it would cause infinite reload loop - const loadFeatures = useCallback(async () => { - if (!currentProject) return; - - const currentPath = currentProject.path; - const previousPath = prevProjectPathRef.current; - const isProjectSwitch = previousPath !== null && currentPath !== previousPath; - - // Get cached features from store (without adding to dependencies) - const cachedFeatures = useAppStore.getState().features; - - // If project switched, mark it but don't clear features yet - // We'll clear after successful API load to prevent data loss - if (isProjectSwitch) { - logger.info(`Project switch detected: ${previousPath} -> ${currentPath}`); - isSwitchingProjectRef.current = true; - isInitialLoadRef.current = true; - } - - // Update the ref to track current project - prevProjectPathRef.current = currentPath; - - // Only show loading spinner on initial load to prevent board flash during reloads - if (isInitialLoadRef.current) { - setIsLoading(true); - } - - try { - const api = getElectronAPI(); - if (!api.features) { - logger.error('Features API not available'); - // Keep cached features if API is unavailable - return; - } - - const result = await api.features.getAll(currentProject.path); - - if (result.success && result.features) { - const featuresWithIds = result.features.map((f: any, index: number) => ({ - ...f, - id: f.id || `feature-${index}-${Date.now()}`, - status: f.status || 'backlog', - startedAt: f.startedAt, // Preserve startedAt timestamp - // Ensure model and thinkingLevel are set for backward compatibility - model: f.model || 'opus', - thinkingLevel: f.thinkingLevel || 'none', - })); - // Successfully loaded features - now safe to set them - setFeatures(featuresWithIds); - - // Only clear categories on project switch AFTER successful load - if (isProjectSwitch) { - setPersistedCategories([]); - } - - // Check for interrupted features and resume them - // This handles server restarts where features were in pipeline steps - if (api.autoMode?.resumeInterrupted) { - try { - await api.autoMode.resumeInterrupted(currentProject.path); - logger.info('Checked for interrupted features'); - } catch (resumeError) { - logger.warn('Failed to check for interrupted features:', resumeError); - } - } - } else if (!result.success && result.error) { - logger.error('API returned error:', result.error); - // If it's a new project or the error indicates no features found, - // that's expected - start with empty array - if (isProjectSwitch) { - setFeatures([]); - setPersistedCategories([]); - } - // Otherwise keep cached features - } - } catch (error) { - logger.error('Failed to load features:', error); - // On error, keep existing cached features for the current project - // Only clear on project switch if we have no features from server - if (isProjectSwitch && cachedFeatures.length === 0) { - setFeatures([]); - setPersistedCategories([]); - } - } finally { - setIsLoading(false); - isInitialLoadRef.current = false; - isSwitchingProjectRef.current = false; - } - }, [currentProject, setFeatures]); + // Use React Query for features + const { + data: features = [], + isLoading, + refetch: loadFeatures, + } = useFeatures(currentProject?.path); // Load persisted categories from file const loadCategories = useCallback(async () => { @@ -125,12 +45,9 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) { setPersistedCategories(parsed); } } else { - // File doesn't exist, ensure categories are cleared setPersistedCategories([]); } - } catch (error) { - logger.error('Failed to load categories:', error); - // If file doesn't exist, ensure categories are cleared + } catch { setPersistedCategories([]); } }, [currentProject]); @@ -142,22 +59,17 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) { try { const api = getElectronAPI(); - - // Read existing categories let categories: string[] = [...persistedCategories]; - // Add new category if it doesn't exist if (!categories.includes(category)) { categories.push(category); - categories.sort(); // Keep sorted + categories.sort(); - // Write back to file await api.writeFile( `${currentProject.path}/.automaker/categories.json`, JSON.stringify(categories, null, 2) ); - // Update state setPersistedCategories(categories); } } catch (error) { @@ -167,29 +79,8 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) { [currentProject, persistedCategories] ); - // Subscribe to spec regeneration complete events to refresh kanban board - useEffect(() => { - const api = getElectronAPI(); - if (!api.specRegeneration) return; - - const unsubscribe = api.specRegeneration.onEvent((event) => { - // Refresh the kanban board when spec regeneration completes for the current project - if ( - event.type === 'spec_regeneration_complete' && - currentProject && - event.projectPath === currentProject.path - ) { - logger.info('Spec regeneration complete, refreshing features'); - loadFeatures(); - } - }); - - return () => { - unsubscribe(); - }; - }, [currentProject, loadFeatures]); - - // Listen for auto mode feature completion and errors to reload features + // Subscribe to auto mode events for notifications (ding sound, toasts) + // Note: Query invalidation is handled by useAutoModeQueryInvalidation in the root useEffect(() => { const api = getElectronAPI(); if (!api?.autoMode || !currentProject) return; @@ -198,42 +89,22 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) { const projectId = currentProject.id; const unsubscribe = api.autoMode.onEvent((event) => { - // Use event's projectPath or projectId if available, otherwise use current project - // Board view only reacts to events for the currently selected project const eventProjectId = ('projectId' in event && event.projectId) || projectId; if (event.type === 'auto_mode_feature_complete') { - // Reload features when a feature is completed - logger.info('Feature completed, reloading features...'); - loadFeatures(); // Play ding sound when feature is done (unless muted) const { muteDoneSound } = useAppStore.getState(); if (!muteDoneSound) { const audio = new Audio('/sounds/ding.mp3'); audio.play().catch((err) => logger.warn('Could not play ding sound:', err)); } - } else if (event.type === 'plan_approval_required') { - // Reload features when plan is generated and requires approval - // This ensures the feature card shows the "Approve Plan" button - logger.info('Plan approval required, reloading features...'); - loadFeatures(); - } else if (event.type === 'pipeline_step_started') { - // Pipeline steps update the feature status to `pipeline_*` before the step runs. - // Reload so the card moves into the correct pipeline column immediately. - logger.info('Pipeline step started, reloading features...'); - loadFeatures(); } else if (event.type === 'auto_mode_error') { - // Reload features when an error occurs (feature moved to waiting_approval) - logger.info('Feature error, reloading features...', event.error); - - // Remove from running tasks so it moves to the correct column + // Remove from running tasks if (event.featureId) { removeRunningTask(eventProjectId, event.featureId); } - loadFeatures(); - - // Check for authentication errors and show a more helpful message + // Show error toast const isAuthError = event.errorType === 'authentication' || (event.error && @@ -255,22 +126,46 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) { }); return unsubscribe; - }, [loadFeatures, currentProject]); + }, [currentProject]); + // Check for interrupted features on mount useEffect(() => { - loadFeatures(); - }, [loadFeatures]); + if (!currentProject) return; - // Load persisted categories on mount + const checkInterrupted = async () => { + const api = getElectronAPI(); + if (api.autoMode?.resumeInterrupted) { + try { + await api.autoMode.resumeInterrupted(currentProject.path); + logger.info('Checked for interrupted features'); + } catch (error) { + logger.warn('Failed to check for interrupted features:', error); + } + } + }; + + checkInterrupted(); + }, [currentProject]); + + // Load persisted categories on mount/project change useEffect(() => { loadCategories(); }, [loadCategories]); + // Clear categories when project changes + useEffect(() => { + setPersistedCategories([]); + }, [currentProject?.path]); + return { features, isLoading, persistedCategories, - loadFeatures, + loadFeatures: () => { + queryClient.invalidateQueries({ + queryKey: queryKeys.features.all(currentProject?.path ?? ''), + }); + }, loadCategories, saveCategory, };