| |
| |
| |
| |
| |
| |
|
|
| import { useEffect, useRef } from 'react'; |
| import { useQueryClient, QueryClient } from '@tanstack/react-query'; |
| import { getElectronAPI } from '@/lib/electron'; |
| import { queryKeys } from '@/lib/query-keys'; |
| import type { AutoModeEvent, SpecRegenerationEvent, StreamEvent } from '@/types/electron'; |
| import type { IssueValidationEvent } from '@automaker/types'; |
| import { debounce, type DebouncedFunction } from '@automaker/utils/debounce'; |
| import { useEventRecencyStore } from './use-event-recency'; |
| import { isAnyFeatureTransitioning } from '@/lib/feature-transition-state'; |
|
|
| |
| |
| |
| |
| |
| const PROGRESS_DEBOUNCE_WAIT = 150; |
| const PROGRESS_DEBOUNCE_MAX_WAIT = 2000; |
|
|
| |
| |
| |
| |
| |
| const FEATURE_LIST_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [ |
| 'auto_mode_feature_start', |
| 'auto_mode_feature_complete', |
| 'auto_mode_error', |
| |
| |
| |
| |
| 'plan_approval_required', |
| 'plan_approved', |
| 'plan_rejected', |
| 'pipeline_step_started', |
| 'pipeline_step_complete', |
| 'feature_status_changed', |
| 'features_reconciled', |
| ]; |
|
|
| |
| |
| |
| |
| |
| |
| const SINGLE_FEATURE_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [ |
| 'auto_mode_phase', |
| 'auto_mode_phase_complete', |
| 'auto_mode_task_status', |
| 'auto_mode_task_started', |
| 'auto_mode_task_complete', |
| 'auto_mode_summary', |
| ]; |
|
|
| |
| |
| |
| const RUNNING_AGENTS_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [ |
| 'auto_mode_feature_start', |
| 'auto_mode_feature_complete', |
| 'auto_mode_error', |
| 'auto_mode_resuming_features', |
| ]; |
|
|
| |
| |
| |
| const FEATURE_CLEANUP_EVENTS: AutoModeEvent['type'][] = [ |
| 'auto_mode_feature_complete', |
| 'auto_mode_error', |
| ]; |
|
|
| |
| |
| |
| function hasFeatureId(event: AutoModeEvent): event is AutoModeEvent & { featureId: string } { |
| return 'featureId' in event && typeof event.featureId === 'string'; |
| } |
|
|
| |
| |
| |
| function getFeatureKey(projectPath: string, featureId: string): string { |
| return `${projectPath}:${featureId}`; |
| } |
|
|
| |
| |
| |
| function createDebouncedInvalidation( |
| queryClient: QueryClient, |
| projectPath: string, |
| featureId: string |
| ): DebouncedFunction<() => void> { |
| return debounce( |
| () => { |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.features.agentOutput(projectPath, featureId), |
| }); |
| }, |
| PROGRESS_DEBOUNCE_WAIT, |
| { maxWait: PROGRESS_DEBOUNCE_MAX_WAIT } |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function useAutoModeQueryInvalidation(projectPath: string | undefined) { |
| const queryClient = useQueryClient(); |
| const recordGlobalEvent = useEventRecencyStore((state) => state.recordGlobalEvent); |
|
|
| |
| |
| const debouncedInvalidationsRef = useRef<Map<string, DebouncedFunction<() => void>>>(new Map()); |
|
|
| useEffect(() => { |
| if (!projectPath) return; |
|
|
| |
| const currentProjectPath = projectPath; |
| const debouncedInvalidations = debouncedInvalidationsRef.current; |
|
|
| |
| |
| |
| function getDebouncedInvalidation(featureId: string): DebouncedFunction<() => void> { |
| const key = getFeatureKey(currentProjectPath, featureId); |
| let debouncedFn = debouncedInvalidations.get(key); |
|
|
| if (!debouncedFn) { |
| debouncedFn = createDebouncedInvalidation(queryClient, currentProjectPath, featureId); |
| debouncedInvalidations.set(key, debouncedFn); |
| } |
|
|
| return debouncedFn; |
| } |
|
|
| |
| |
| |
| function cleanupFeatureDebounce(featureId: string): void { |
| const key = getFeatureKey(currentProjectPath, featureId); |
| const debouncedFn = debouncedInvalidations.get(key); |
|
|
| if (debouncedFn) { |
| |
| debouncedFn.flush(); |
| debouncedInvalidations.delete(key); |
| } |
| } |
|
|
| const api = getElectronAPI(); |
| if (!api.autoMode) return; |
| const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => { |
| |
| |
| recordGlobalEvent(); |
|
|
| |
| |
| |
| |
| |
| if (FEATURE_LIST_INVALIDATION_EVENTS.includes(event.type) && !isAnyFeatureTransitioning()) { |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.features.all(currentProjectPath), |
| }); |
| } |
|
|
| |
| if (RUNNING_AGENTS_INVALIDATION_EVENTS.includes(event.type)) { |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.runningAgents.all(), |
| }); |
| } |
|
|
| |
| if (SINGLE_FEATURE_INVALIDATION_EVENTS.includes(event.type) && hasFeatureId(event)) { |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.features.single(currentProjectPath, event.featureId), |
| }); |
| } |
|
|
| |
| |
| if (event.type === 'auto_mode_progress' && hasFeatureId(event)) { |
| const debouncedInvalidation = getDebouncedInvalidation(event.featureId); |
| debouncedInvalidation(); |
| } |
|
|
| |
| |
| if (FEATURE_CLEANUP_EVENTS.includes(event.type) && hasFeatureId(event)) { |
| cleanupFeatureDebounce(event.featureId); |
| } |
|
|
| |
| if (event.type === 'auto_mode_feature_complete' && hasFeatureId(event)) { |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.worktrees.all(currentProjectPath), |
| }); |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.worktrees.single(currentProjectPath, event.featureId), |
| }); |
| } |
| }); |
|
|
| |
| return () => { |
| unsubscribe(); |
|
|
| |
| for (const debouncedFn of debouncedInvalidations.values()) { |
| debouncedFn.flush(); |
| } |
| debouncedInvalidations.clear(); |
| }; |
| }, [projectPath, queryClient, recordGlobalEvent]); |
| } |
|
|
| |
| |
| |
| |
| |
| export function useSpecRegenerationQueryInvalidation(projectPath: string | undefined) { |
| const queryClient = useQueryClient(); |
| const recordGlobalEvent = useEventRecencyStore((state) => state.recordGlobalEvent); |
|
|
| useEffect(() => { |
| if (!projectPath) return; |
|
|
| const api = getElectronAPI(); |
| if (!api.specRegeneration) return; |
| const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => { |
| |
| if (event.projectPath !== projectPath) return; |
|
|
| |
| recordGlobalEvent(); |
|
|
| if (event.type === 'spec_regeneration_complete') { |
| |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.features.all(projectPath), |
| }); |
|
|
| |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.specRegeneration.status(projectPath), |
| }); |
| } |
| }); |
|
|
| return unsubscribe; |
| }, [projectPath, queryClient, recordGlobalEvent]); |
| } |
|
|
| |
| |
| |
| |
| |
| export function useGitHubValidationQueryInvalidation(projectPath: string | undefined) { |
| const queryClient = useQueryClient(); |
| const recordGlobalEvent = useEventRecencyStore((state) => state.recordGlobalEvent); |
|
|
| useEffect(() => { |
| if (!projectPath) return; |
|
|
| const api = getElectronAPI(); |
|
|
| |
| if (!api.github?.onValidationEvent) { |
| return; |
| } |
|
|
| const unsubscribe = api.github.onValidationEvent((event: IssueValidationEvent) => { |
| |
| recordGlobalEvent(); |
|
|
| if (event.type === 'issue_validation_complete' || event.type === 'issue_validation_error') { |
| |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.github.validations(projectPath), |
| }); |
|
|
| |
| if (event.issueNumber) { |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.github.validation(projectPath, event.issueNumber), |
| }); |
| } |
| } |
| }); |
|
|
| return unsubscribe; |
| }, [projectPath, queryClient, recordGlobalEvent]); |
| } |
|
|
| |
| |
| |
| |
| |
| export function useSessionQueryInvalidation(sessionId: string | undefined) { |
| const queryClient = useQueryClient(); |
| const recordGlobalEvent = useEventRecencyStore((state) => state.recordGlobalEvent); |
|
|
| useEffect(() => { |
| if (!sessionId) return; |
|
|
| const api = getElectronAPI(); |
| if (!api.agent) return; |
| const unsubscribe = api.agent.onStream((data: unknown) => { |
| const event = data as StreamEvent; |
| |
| if ('sessionId' in event && event.sessionId !== sessionId) return; |
|
|
| |
| recordGlobalEvent(); |
|
|
| |
| if (event.type === 'complete' || event.type === 'message') { |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.sessions.history(sessionId), |
| }); |
| } |
|
|
| |
| if (event.type === 'complete') { |
| queryClient.invalidateQueries({ |
| queryKey: queryKeys.sessions.all(), |
| }); |
| } |
| }); |
|
|
| return unsubscribe; |
| }, [sessionId, queryClient, recordGlobalEvent]); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function useQueryInvalidation( |
| projectPath: string | undefined, |
| sessionId?: string | undefined |
| ) { |
| useAutoModeQueryInvalidation(projectPath); |
| useSpecRegenerationQueryInvalidation(projectPath); |
| useGitHubValidationQueryInvalidation(projectPath); |
| useSessionQueryInvalidation(sessionId); |
| } |
|
|