From 3170e22383006967ec034a176da245c055599211 Mon Sep 17 00:00:00 2001 From: Shirone Date: Thu, 15 Jan 2026 19:10:35 +0100 Subject: [PATCH] fix(ui): add missing cache invalidation for React Query - Add cache invalidation to useBoardPersistence after create/update/delete - Add useAutoModeQueryInvalidation to board-view for WebSocket events - Add cache invalidation to github-issues-view after converting issue to task - Add cache invalidation to analysis-view after generating features - Fix UI not updating when features are added, updated, or completed Co-Authored-By: Claude Opus 4.5 --- .../ui/src/components/views/analysis-view.tsx | 10 ++++++++- apps/ui/src/components/views/board-view.tsx | 4 ++++ .../board-view/hooks/use-board-persistence.ts | 21 ++++++++++++++++--- .../components/views/github-issues-view.tsx | 9 +++++++- apps/ui/src/hooks/use-query-invalidation.ts | 8 ++++++- 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/apps/ui/src/components/views/analysis-view.tsx b/apps/ui/src/components/views/analysis-view.tsx index f551e0a8..7737eda1 100644 --- a/apps/ui/src/components/views/analysis-view.tsx +++ b/apps/ui/src/components/views/analysis-view.tsx @@ -1,7 +1,9 @@ import { useCallback, useState } from 'react'; import { createLogger } from '@automaker/utils/logger'; +import { useQueryClient } from '@tanstack/react-query'; import { useAppStore, FileTreeNode, ProjectAnalysis } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; +import { queryKeys } from '@/lib/query-keys'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { @@ -72,6 +74,7 @@ export function AnalysisView() { const [isGeneratingFeatureList, setIsGeneratingFeatureList] = useState(false); const [featureListGenerated, setFeatureListGenerated] = useState(false); const [featureListError, setFeatureListError] = useState(null); + const queryClient = useQueryClient(); // Recursively scan directory const scanDirectory = useCallback( @@ -647,6 +650,11 @@ ${Object.entries(projectAnalysis.filesByExtension) } as any); } + // Invalidate React Query cache to sync UI + queryClient.invalidateQueries({ + queryKey: queryKeys.features.all(currentProject.path), + }); + setFeatureListGenerated(true); } catch (error) { logger.error('Failed to generate feature list:', error); @@ -656,7 +664,7 @@ ${Object.entries(projectAnalysis.filesByExtension) } finally { setIsGeneratingFeatureList(false); } - }, [currentProject, projectAnalysis]); + }, [currentProject, projectAnalysis, queryClient]); // Toggle folder expansion const toggleFolder = (path: string) => { diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 6462b092..c50d8dae 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -82,6 +82,7 @@ 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'; +import { useAutoModeQueryInvalidation } from '@/hooks/use-query-invalidation'; // Stable empty array to avoid infinite loop in selector const EMPTY_WORKTREES: ReturnType['getWorktrees']> = []; @@ -115,6 +116,9 @@ export function BoardView() { // Fetch pipeline config via React Query const { data: pipelineConfig } = usePipelineConfig(currentProject?.path); const queryClient = useQueryClient(); + + // Subscribe to auto mode events for React Query cache invalidation + useAutoModeQueryInvalidation(currentProject?.path); // 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 diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts b/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts index 3c860251..5390edef 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts @@ -1,8 +1,10 @@ import { useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; import { Feature } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; import { useAppStore } from '@/store/app-store'; import { createLogger } from '@automaker/utils/logger'; +import { queryKeys } from '@/lib/query-keys'; const logger = createLogger('BoardPersistence'); @@ -12,6 +14,7 @@ interface UseBoardPersistenceProps { export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps) { const { updateFeature } = useAppStore(); + const queryClient = useQueryClient(); // Persist feature update to API (replaces saveFeatures) const persistFeatureUpdate = useCallback( @@ -41,12 +44,16 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps ); if (result.success && result.feature) { updateFeature(result.feature.id, result.feature); + // Invalidate React Query cache to sync UI + queryClient.invalidateQueries({ + queryKey: queryKeys.features.all(currentProject.path), + }); } } catch (error) { logger.error('Failed to persist feature update:', error); } }, - [currentProject, updateFeature] + [currentProject, updateFeature, queryClient] ); // Persist feature creation to API @@ -64,12 +71,16 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps const result = await api.features.create(currentProject.path, feature); if (result.success && result.feature) { updateFeature(result.feature.id, result.feature); + // Invalidate React Query cache to sync UI + queryClient.invalidateQueries({ + queryKey: queryKeys.features.all(currentProject.path), + }); } } catch (error) { logger.error('Failed to persist feature creation:', error); } }, - [currentProject, updateFeature] + [currentProject, updateFeature, queryClient] ); // Persist feature deletion to API @@ -85,11 +96,15 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps } await api.features.delete(currentProject.path, featureId); + // Invalidate React Query cache to sync UI + queryClient.invalidateQueries({ + queryKey: queryKeys.features.all(currentProject.path), + }); } catch (error) { logger.error('Failed to persist feature deletion:', error); } }, - [currentProject] + [currentProject, queryClient] ); return { diff --git a/apps/ui/src/components/views/github-issues-view.tsx b/apps/ui/src/components/views/github-issues-view.tsx index e1e09cad..22f374c8 100644 --- a/apps/ui/src/components/views/github-issues-view.tsx +++ b/apps/ui/src/components/views/github-issues-view.tsx @@ -2,6 +2,7 @@ import { useState, useCallback, useMemo } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { CircleDot, RefreshCw } from 'lucide-react'; +import { useQueryClient } from '@tanstack/react-query'; import { getElectronAPI, GitHubIssue, IssueValidationResult } from '@/lib/electron'; import { useAppStore } from '@/store/app-store'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; @@ -9,6 +10,7 @@ import { LoadingState } from '@/components/ui/loading-state'; import { ErrorState } from '@/components/ui/error-state'; import { cn, pathsEqual } from '@/lib/utils'; import { toast } from 'sonner'; +import { queryKeys } from '@/lib/query-keys'; import { useGithubIssues, useIssueValidation } from './github-issues-view/hooks'; import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components'; import { ValidationDialog } from './github-issues-view/dialogs'; @@ -27,6 +29,7 @@ export function GitHubIssuesView() { useState(null); const { currentProject, getCurrentWorktree, worktreesByProject } = useAppStore(); + const queryClient = useQueryClient(); // Model override for validation const validationModelOverride = useModelOverride({ phase: 'validationModel' }); @@ -109,6 +112,10 @@ export function GitHubIssuesView() { const result = await api.features.create(currentProject.path, feature); if (result.success) { + // Invalidate React Query cache to sync UI + queryClient.invalidateQueries({ + queryKey: queryKeys.features.all(currentProject.path), + }); toast.success(`Created task: ${issue.title}`); } else { toast.error(result.error || 'Failed to create task'); @@ -119,7 +126,7 @@ export function GitHubIssuesView() { toast.error(err instanceof Error ? err.message : 'Failed to create task'); } }, - [currentProject?.path, currentBranch] + [currentProject?.path, currentBranch, queryClient] ); if (loading) { diff --git a/apps/ui/src/hooks/use-query-invalidation.ts b/apps/ui/src/hooks/use-query-invalidation.ts index 4d8878da..eb0bfb4d 100644 --- a/apps/ui/src/hooks/use-query-invalidation.ts +++ b/apps/ui/src/hooks/use-query-invalidation.ts @@ -143,7 +143,13 @@ export function useGitHubValidationQueryInvalidation(projectPath: string | undef if (!projectPath) return; const api = getElectronAPI(); - const unsubscribe = api.github?.onValidationEvent((event: IssueValidationEvent) => { + + // Check if GitHub API is available before subscribing + if (!api.github?.onValidationEvent) { + return; + } + + const unsubscribe = api.github.onValidationEvent((event: IssueValidationEvent) => { if (event.type === 'validation_complete' || event.type === 'validation_error') { // Invalidate all validations for this project queryClient.invalidateQueries({