@@ -43,6 +43,7 @@ import { matchesKeybind, KEYBINDS, formatKeybind } from "@/browser/utils/ui/keyb
4343import { applyFrontendFilters } from "@/browser/utils/review/filterHunks" ;
4444import { cn } from "@/common/lib/utils" ;
4545import { useAPI , type APIClient } from "@/browser/contexts/API" ;
46+ import { workspaceStore } from "@/browser/stores/WorkspaceStore" ;
4647
4748/** Stats reported to parent for tab display */
4849interface ReviewPanelStats {
@@ -126,7 +127,13 @@ function makeReviewPanelCacheKey(params: {
126127
127128type ExecuteBashResult = Awaited < ReturnType < APIClient [ "workspace" ] [ "executeBash" ] > > ;
128129
129- const REVIEW_AUTO_REFRESH_INTERVAL_MS = 30_000 ;
130+ /** Check if a tool may modify files and should trigger diff refresh */
131+ function isFileModifyingTool ( toolName : string ) : boolean {
132+ return toolName . startsWith ( "file_edit_" ) || toolName === "bash" ;
133+ }
134+
135+ /** Debounce delay for auto-refresh after tool completion */
136+ const TOOL_REFRESH_DEBOUNCE_MS = 3000 ;
130137
131138function getOriginBranchForFetch ( diffBase : string ) : string | null {
132139 const trimmed = diffBase . trim ( ) ;
@@ -234,60 +241,40 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
234241 [ diffState ]
235242 ) ;
236243
237- const [ autoRefreshSecondsRemaining , setAutoRefreshSecondsRemaining ] = useState < number | null > (
238- null
239- ) ;
240- const [ isAutoRefreshing , setIsAutoRefreshing ] = useState ( false ) ;
241- const autoRefreshDeadlineRef = useRef < number | null > ( null ) ;
244+ // Track whether refresh is in-flight (for origin/* fetch)
245+ const isRefreshingRef = useRef ( false ) ;
246+
247+ // Track user interaction with review notes (pause auto-refresh while focused)
248+ const isUserInteractingRef = useRef ( false ) ;
249+ const pendingRefreshRef = useRef ( false ) ;
250+
242251 const [ filters , setFilters ] = useState < ReviewFiltersType > ( {
243252 showReadHunks : showReadHunks ,
244253 diffBase : diffBase ,
245254 includeUncommitted : includeUncommitted ,
246255 } ) ;
247256
248- // Auto-refresh diffs every 30s (with a user-visible countdown ).
249- // For origin/* bases, fetches from remote first to pick up upstream changes .
257+ // Auto-refresh on file-modifying tool completions (debounced 3s ).
258+ // Respects user interaction - if user is focused on review input, queues refresh for after blur .
250259 useEffect ( ( ) => {
251- if ( ! api || isCreating ) {
252- autoRefreshDeadlineRef . current = null ;
253- setAutoRefreshSecondsRemaining ( null ) ;
254- return ;
255- }
256-
257- autoRefreshDeadlineRef . current = Date . now ( ) + REVIEW_AUTO_REFRESH_INTERVAL_MS ;
258-
259- const resetCountdown = ( ) => {
260- autoRefreshDeadlineRef . current = Date . now ( ) + REVIEW_AUTO_REFRESH_INTERVAL_MS ;
261- setAutoRefreshSecondsRemaining ( Math . ceil ( REVIEW_AUTO_REFRESH_INTERVAL_MS / 1000 ) ) ;
262- } ;
263-
264- resetCountdown ( ) ;
265-
266- let lastRenderedSeconds : number | null = null ;
260+ if ( ! api || isCreating ) return ;
267261
268- const interval = setInterval ( ( ) => {
269- const deadline = autoRefreshDeadlineRef . current ;
270- if ( ! deadline ) return ;
262+ let debounceTimer : ReturnType < typeof setTimeout > | null = null ;
271263
272- const msRemaining = deadline - Date . now ( ) ;
273- const secondsRemaining = Math . max ( 0 , Math . ceil ( msRemaining / 1000 ) ) ;
274- if ( secondsRemaining !== lastRenderedSeconds ) {
275- lastRenderedSeconds = secondsRemaining ;
276- setAutoRefreshSecondsRemaining ( secondsRemaining ) ;
264+ const performRefresh = ( ) => {
265+ // Skip if user is actively entering a review note
266+ if ( isUserInteractingRef . current ) {
267+ pendingRefreshRef . current = true ;
268+ return ;
277269 }
278270
279- // Fire when deadline passed (not when display shows 0)
280- if ( msRemaining > 0 ) return ;
281- if ( isAutoRefreshing ) return ;
282-
283- setIsAutoRefreshing ( true ) ;
284-
285- // Reset early so we don't immediately re-fire if fetch takes time.
286- resetCountdown ( ) ;
271+ // Skip if already refreshing (for origin/* bases with fetch)
272+ if ( isRefreshingRef . current ) return ;
287273
288274 const originBranch = getOriginBranchForFetch ( filters . diffBase ) ;
289275 if ( originBranch ) {
290276 // Remote base: fetch before refreshing diff
277+ isRefreshingRef . current = true ;
291278 api . workspace
292279 . executeBash ( {
293280 workspaceId,
@@ -298,22 +285,42 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
298285 console . debug ( "ReviewPanel origin fetch failed" , err ) ;
299286 } )
300287 . finally ( ( ) => {
301- setIsAutoRefreshing ( false ) ;
288+ isRefreshingRef . current = false ;
302289 setRefreshTrigger ( ( prev ) => prev + 1 ) ;
303290 } ) ;
304291 } else {
305292 // Local base: just refresh diff
306- setIsAutoRefreshing ( false ) ;
307293 setRefreshTrigger ( ( prev ) => prev + 1 ) ;
308294 }
309- } , 250 ) ;
295+ } ;
296+
297+ const scheduleRefresh = ( ) => {
298+ if ( debounceTimer ) clearTimeout ( debounceTimer ) ;
299+ debounceTimer = setTimeout ( performRefresh , TOOL_REFRESH_DEBOUNCE_MS ) ;
300+ } ;
301+
302+ const unsubscribe = workspaceStore . onToolCallEnd ( ( wsId , toolName ) => {
303+ if ( wsId !== workspaceId ) return ;
304+ if ( ! isFileModifyingTool ( toolName ) ) return ;
305+ scheduleRefresh ( ) ;
306+ } ) ;
310307
311308 return ( ) => {
312- clearInterval ( interval ) ;
313- autoRefreshDeadlineRef . current = null ;
314- setAutoRefreshSecondsRemaining ( null ) ;
309+ unsubscribe ( ) ;
310+ if ( debounceTimer ) clearTimeout ( debounceTimer ) ;
315311 } ;
316- } , [ api , workspaceId , filters . diffBase , isCreating , isAutoRefreshing ] ) ;
312+ } , [ api , workspaceId , filters . diffBase , isCreating ] ) ;
313+
314+ // Sync panel focus with interaction tracking; fire pending refresh on blur
315+ useEffect ( ( ) => {
316+ isUserInteractingRef . current = isPanelFocused ;
317+
318+ // When user stops interacting, fire any pending refresh
319+ if ( ! isPanelFocused && pendingRefreshRef . current ) {
320+ pendingRefreshRef . current = false ;
321+ handleRefreshRef . current ( ) ;
322+ }
323+ } , [ isPanelFocused ] ) ;
317324
318325 // Focus panel when focusTrigger changes (preserves current hunk selection)
319326
@@ -323,18 +330,15 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
323330 handleRefreshRef . current = ( ) => {
324331 if ( ! api || isCreating ) return ;
325332
326- // Reset countdown on manual refresh so the user doesn't see an immediate auto-refresh.
327- autoRefreshDeadlineRef . current = Date . now ( ) + REVIEW_AUTO_REFRESH_INTERVAL_MS ;
328- setAutoRefreshSecondsRemaining ( Math . ceil ( REVIEW_AUTO_REFRESH_INTERVAL_MS / 1000 ) ) ;
333+ // Skip if already refreshing (for origin/* bases with fetch)
334+ if ( isRefreshingRef . current ) {
335+ setRefreshTrigger ( ( prev ) => prev + 1 ) ;
336+ return ;
337+ }
329338
330339 const originBranch = getOriginBranchForFetch ( filters . diffBase ) ;
331340 if ( originBranch ) {
332- if ( isAutoRefreshing ) {
333- setRefreshTrigger ( ( prev ) => prev + 1 ) ;
334- return ;
335- }
336-
337- setIsAutoRefreshing ( true ) ;
341+ isRefreshingRef . current = true ;
338342
339343 api . workspace
340344 . executeBash ( {
@@ -346,7 +350,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
346350 console . debug ( "ReviewPanel origin fetch failed" , err ) ;
347351 } )
348352 . finally ( ( ) => {
349- setIsAutoRefreshing ( false ) ;
353+ isRefreshingRef . current = false ;
350354 setRefreshTrigger ( ( prev ) => prev + 1 ) ;
351355 } ) ;
352356
@@ -899,12 +903,8 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
899903 stats = { stats }
900904 onFiltersChange = { setFilters }
901905 onRefresh = { handleRefresh }
902- autoRefreshSecondsRemaining = { autoRefreshSecondsRemaining }
903906 isLoading = {
904- diffState . status === "loading" ||
905- diffState . status === "refreshing" ||
906- isLoadingTree ||
907- isAutoRefreshing
907+ diffState . status === "loading" || diffState . status === "refreshing" || isLoadingTree
908908 }
909909 workspaceId = { workspaceId }
910910 workspacePath = { workspacePath }
0 commit comments