auto-claude: 204-fix-pr-review-ui-not-updating-without-manual-navig (#1734)

* auto-claude: subtask-2-1 - Add refresh callback system to PR review store

* auto-claude: subtask-2-2 - Register refresh callback in useGitHubPRs hook

* auto-claude: subtask-2-3 - Ensure GitHubPRs component re-renders on PR list u

Add useEffect to sync UI state when PR list updates via auto-refresh.
Following the pattern from PRDetail.tsx, ensure selected PR is validated
after list updates to prevent stale state when PRs are closed/merged.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: Use returned cleanup functions from IPC listeners and clear refreshCallbacks

The on* IPC methods return cleanup functions but they were being ignored.
Instead, the code tried to call non-existent remove* methods. Now captures
the returned cleanup functions directly. Also clears refreshCallbacks in
cleanupPRReviewListeners to prevent stale callbacks during HMR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Add projectId dependency, handle async callbacks, wire up listener cleanup

- Add [projectId] to useEffect dependency array so state resets on project switch
- Use Promise.resolve().catch() for refresh callbacks since fetchPRs is async
- Export cleanupPRReviewListeners from barrel and call it in App.tsx cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Test User <test@example.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-02-09 10:25:09 +01:00
committed by GitHub
parent f085c08bd0
commit 087091cef8
5 changed files with 98 additions and 7 deletions
+2 -1
View File
@@ -60,7 +60,7 @@ import { useTaskStore, loadTasks } from './stores/task-store';
import { useSettingsStore, loadSettings, loadProfiles, saveSettings } from './stores/settings-store'; import { useSettingsStore, loadSettings, loadProfiles, saveSettings } from './stores/settings-store';
import { useClaudeProfileStore, loadClaudeProfiles } from './stores/claude-profile-store'; import { useClaudeProfileStore, loadClaudeProfiles } from './stores/claude-profile-store';
import { useTerminalStore, restoreTerminalSessions } from './stores/terminal-store'; import { useTerminalStore, restoreTerminalSessions } from './stores/terminal-store';
import { initializeGitHubListeners } from './stores/github'; import { initializeGitHubListeners, cleanupGitHubListeners } from './stores/github';
import { initDownloadProgressListener } from './stores/download-store'; import { initDownloadProgressListener } from './stores/download-store';
import { GlobalDownloadIndicator } from './components/GlobalDownloadIndicator'; import { GlobalDownloadIndicator } from './components/GlobalDownloadIndicator';
import { useIpcListeners } from './hooks/useIpc'; import { useIpcListeners } from './hooks/useIpc';
@@ -193,6 +193,7 @@ export function App() {
return () => { return () => {
cleanupDownloadListener(); cleanupDownloadListener();
cleanupGitHubListeners();
}; };
}, []); }, []);
@@ -1,4 +1,4 @@
import { useCallback } from "react"; import { useCallback, useEffect } from "react";
import { GitPullRequest, RefreshCw, ExternalLink, Settings } from "lucide-react"; import { GitPullRequest, RefreshCw, ExternalLink, Settings } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useProjectStore } from "../../stores/project-store"; import { useProjectStore } from "../../stores/project-store";
@@ -103,6 +103,20 @@ export function GitHubPRs({ onOpenSettings, isActive = false }: GitHubPRsProps)
hasActiveFilters, hasActiveFilters,
} = usePRFiltering(prs, getReviewStateForPR); } = usePRFiltering(prs, getReviewStateForPR);
// Sync UI state when PR list updates (e.g., after auto-refresh from review completion)
// Following pattern from PRDetail.tsx for state syncing
useEffect(() => {
// Ensure selected PR is still valid after list updates
// This prevents stale state if a PR was closed/merged while selected
if (selectedPRNumber && prs.length > 0) {
const selectedStillExists = prs.some(pr => pr.number === selectedPRNumber);
if (!selectedStillExists) {
// Selected PR was removed/closed, clear selection to prevent stale state
selectPR(null);
}
}
}, [prs, selectedPRNumber, selectPR]);
const handleRunReview = useCallback(() => { const handleRunReview = useCallback(() => {
if (selectedPRNumber) { if (selectedPRNumber) {
runReview(selectedPRNumber); runReview(selectedPRNumber);
@@ -98,6 +98,8 @@ export function useGitHubPRs(
const prReviews = usePRReviewStore((state) => state.prReviews); const prReviews = usePRReviewStore((state) => state.prReviews);
const getPRReviewState = usePRReviewStore((state) => state.getPRReviewState); const getPRReviewState = usePRReviewStore((state) => state.getPRReviewState);
const setNewCommitsCheckAction = usePRReviewStore((state) => state.setNewCommitsCheck); const setNewCommitsCheckAction = usePRReviewStore((state) => state.setNewCommitsCheck);
const registerRefreshCallback = usePRReviewStore((state) => state.registerRefreshCallback);
const unregisterRefreshCallback = usePRReviewStore((state) => state.unregisterRefreshCallback);
// Get review state for the selected PR from the store - optimized with targeted selector // Get review state for the selected PR from the store - optimized with targeted selector
// Only subscribes to changes for this specific PR, not all PRs // Only subscribes to changes for this specific PR, not all PRs
@@ -257,7 +259,7 @@ export function useGitHubPRs(
checkNewCommitsAbortRef.current.abort(); checkNewCommitsAbortRef.current.abort();
checkNewCommitsAbortRef.current = null; checkNewCommitsAbortRef.current = null;
} }
}, []); }, [projectId]);
// Cleanup abort controller on unmount to prevent memory leaks // Cleanup abort controller on unmount to prevent memory leaks
// and avoid state updates on unmounted components // and avoid state updates on unmounted components
@@ -269,6 +271,19 @@ export function useGitHubPRs(
}; };
}, []); }, []);
// Register refresh callback to auto-refresh PR list when reviews complete
useEffect(() => {
if (!projectId) return;
// Register fetchPRs to be called when any PR review completes
registerRefreshCallback(fetchPRs);
// Unregister on unmount or when dependencies change
return () => {
unregisterRefreshCallback(fetchPRs);
};
}, [projectId, fetchPRs, registerRefreshCallback, unregisterRefreshCallback]);
// No need for local IPC listeners - they're handled globally in github-store // No need for local IPC listeners - they're handled globally in github-store
const selectPR = useCallback( const selectPR = useCallback(
@@ -23,10 +23,12 @@ export {
export { export {
usePRReviewStore, usePRReviewStore,
initializePRReviewListeners, initializePRReviewListeners,
cleanupPRReviewListeners,
startPRReview, startPRReview,
startFollowupReview startFollowupReview
} from './pr-review-store'; } from './pr-review-store';
import { initializePRReviewListeners as _initPRReviewListeners } from './pr-review-store'; import { initializePRReviewListeners as _initPRReviewListeners } from './pr-review-store';
import { cleanupPRReviewListeners as _cleanupPRReviewListeners } from './pr-review-store';
// Investigation Store // Investigation Store
export { export {
@@ -49,6 +51,14 @@ export function initializeGitHubListeners(): void {
// Add other global listeners here as needed // Add other global listeners here as needed
} }
/**
* Cleanup all global GitHub listeners.
* Call this during app unmount or hot-reload.
*/
export function cleanupGitHubListeners(): void {
_cleanupPRReviewListeners();
}
// Re-export types for convenience // Re-export types for convenience
export type { export type {
PRReviewProgress, PRReviewProgress,
@@ -40,8 +40,15 @@ interface PRReviewStoreState {
// Selectors // Selectors
getPRReviewState: (projectId: string, prNumber: number) => PRReviewState | null; getPRReviewState: (projectId: string, prNumber: number) => PRReviewState | null;
getActivePRReviews: (projectId: string) => PRReviewState[]; getActivePRReviews: (projectId: string) => PRReviewState[];
// Refresh callbacks - called when reviews complete
registerRefreshCallback: (callback: () => void) => void;
unregisterRefreshCallback: (callback: () => void) => void;
} }
// Store for refresh callbacks outside of Zustand state (to avoid re-renders on registration)
const refreshCallbacks = new Set<() => void>();
export const usePRReviewStore = create<PRReviewStoreState>((set, get) => ({ export const usePRReviewStore = create<PRReviewStoreState>((set, get) => ({
// Initial state // Initial state
prReviews: {}, prReviews: {},
@@ -214,6 +221,15 @@ export const usePRReviewStore = create<PRReviewStoreState>((set, get) => ({
return Object.values(prReviews).filter( return Object.values(prReviews).filter(
review => review.projectId === projectId && review.isReviewing review => review.projectId === projectId && review.isReviewing
); );
},
// Refresh callbacks - called when reviews complete
registerRefreshCallback: (callback: () => void) => {
refreshCallbacks.add(callback);
},
unregisterRefreshCallback: (callback: () => void) => {
refreshCallbacks.delete(callback);
} }
})); }));
@@ -223,6 +239,7 @@ export const usePRReviewStore = create<PRReviewStoreState>((set, get) => ({
* regardless of which component is mounted. * regardless of which component is mounted.
*/ */
let prReviewListenersInitialized = false; let prReviewListenersInitialized = false;
let cleanupFunctions: (() => void)[] = [];
export function initializePRReviewListeners(): void { export function initializePRReviewListeners(): void {
if (prReviewListenersInitialized) { if (prReviewListenersInitialized) {
@@ -231,29 +248,45 @@ export function initializePRReviewListeners(): void {
const store = usePRReviewStore.getState(); const store = usePRReviewStore.getState();
// Check if GitHub PR Review API is available
if (!window.electronAPI?.github?.onPRReviewProgress) {
console.warn('[GitHub PR Store] GitHub PR Review API not available, skipping listener setup');
return;
}
// Listen for PR review progress events // Listen for PR review progress events
window.electronAPI.github.onPRReviewProgress( // Each on* method returns a cleanup function — capture them for proper teardown
const cleanupProgress = window.electronAPI.github.onPRReviewProgress(
(projectId: string, progress: PRReviewProgress) => { (projectId: string, progress: PRReviewProgress) => {
store.setPRReviewProgress(projectId, progress); store.setPRReviewProgress(projectId, progress);
} }
); );
cleanupFunctions.push(cleanupProgress);
// Listen for PR review completion events // Listen for PR review completion events
window.electronAPI.github.onPRReviewComplete( const cleanupComplete = window.electronAPI.github.onPRReviewComplete(
(projectId: string, result: PRReviewResult) => { (projectId: string, result: PRReviewResult) => {
store.setPRReviewResult(projectId, result); store.setPRReviewResult(projectId, result);
// Trigger all registered refresh callbacks when review completes
refreshCallbacks.forEach(callback => {
Promise.resolve(callback()).catch(error => {
console.error('[PRReviewStore] Error in refresh callback:', error);
});
});
} }
); );
cleanupFunctions.push(cleanupComplete);
// Listen for PR review error events // Listen for PR review error events
window.electronAPI.github.onPRReviewError( const cleanupError = window.electronAPI.github.onPRReviewError(
(projectId: string, data: { prNumber: number; error: string }) => { (projectId: string, data: { prNumber: number; error: string }) => {
store.setPRReviewError(projectId, data.prNumber, data.error); store.setPRReviewError(projectId, data.prNumber, data.error);
} }
); );
cleanupFunctions.push(cleanupError);
// Listen for GitHub auth changes - clear all PR review state when account changes // Listen for GitHub auth changes - clear all PR review state when account changes
window.electronAPI.github.onGitHubAuthChanged( const cleanupAuthChanged = window.electronAPI.github.onGitHubAuthChanged(
(data: { oldUsername: string | null; newUsername: string }) => { (data: { oldUsername: string | null; newUsername: string }) => {
console.warn( console.warn(
`[PRReviewStore] GitHub auth changed from "${data.oldUsername ?? 'none'}" to "${data.newUsername}". ` + `[PRReviewStore] GitHub auth changed from "${data.oldUsername ?? 'none'}" to "${data.newUsername}". ` +
@@ -263,10 +296,28 @@ export function initializePRReviewListeners(): void {
usePRReviewStore.setState({ prReviews: {} }); usePRReviewStore.setState({ prReviews: {} });
} }
); );
cleanupFunctions.push(cleanupAuthChanged);
prReviewListenersInitialized = true; prReviewListenersInitialized = true;
} }
/**
* Cleanup PR review listeners.
* Call this when the app is being unmounted or during hot-reload.
*/
export function cleanupPRReviewListeners(): void {
for (const cleanup of cleanupFunctions) {
try {
cleanup();
} catch {
// Ignore cleanup errors
}
}
cleanupFunctions = [];
refreshCallbacks.clear();
prReviewListenersInitialized = false;
}
/** /**
* Start a PR review and track it in the store * Start a PR review and track it in the store
*/ */