Skip to content

Commit b049fae

Browse files
authored
🤖 fix: auto-refresh ReviewPanel when workspace opened after tool completion (#1242)
When file-modifying tools (`file_edit_*`, `bash`) complete while a workspace's ReviewPanel is not mounted, the panel now correctly fetches fresh data on mount instead of using stale cached data. ## Changes - Track file-modifying tool completion timestamps in WorkspaceStore - ReviewPanel checks timestamp on mount to skip cache if tools ran while closed - Simplified from dual mechanism (callbacks + timestamps) to single MapStore-based approach ## How it works 1. Tool completes → store timestamp + bump MapStore subscription 2. If panel open: subscription fires → debounced refresh 3. If panel was closed: on mount, detects timestamp → skips cache → fresh fetch --- _Generated with `mux` • Model: `anthropic:claude-opus-4-5` • Thinking: `high`_
1 parent 4602709 commit b049fae

File tree

2 files changed

+54
-37
lines changed

2 files changed

+54
-37
lines changed

‎src/browser/components/RightSidebar/CodeReview/ReviewPanel.tsx‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,6 @@ function makeReviewPanelCacheKey(params: {
127127

128128
type ExecuteBashResult = Awaited<ReturnType<APIClient["workspace"]["executeBash"]>>;
129129

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-
135130
/** Debounce delay for auto-refresh after tool completion */
136131
const TOOL_REFRESH_DEBOUNCE_MS = 3000;
137132

@@ -204,6 +199,12 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
204199
const lastDiffRefreshTriggerRef = useRef<number | null>(null);
205200
const lastFileTreeRefreshTriggerRef = useRef<number | null>(null);
206201

202+
// Check if tools completed while we were unmounted - skip cache on initial mount if so.
203+
// Computed once on mount, consumed after first load to avoid re-fetching on every mount.
204+
const skipCacheOnMountRef = useRef(
205+
workspaceStore.getFileModifyingToolMs(workspaceId) !== undefined
206+
);
207+
207208
// Unified search state (per-workspace persistence)
208209
const [searchState, setSearchState] = usePersistedState<ReviewSearchState>(
209210
getReviewSearchStateKey(workspaceId),
@@ -309,11 +310,8 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
309310
debounceTimer = setTimeout(performRefresh, TOOL_REFRESH_DEBOUNCE_MS);
310311
};
311312

312-
const unsubscribe = workspaceStore.onToolCallEnd((wsId, toolName) => {
313-
if (wsId !== workspaceId) return;
314-
if (!isFileModifyingTool(toolName)) return;
315-
scheduleRefresh();
316-
});
313+
// Subscribe to file-modifying tool completions for this workspace
314+
const unsubscribe = workspaceStore.subscribeFileModifyingTool(workspaceId, scheduleRefresh);
317315

318316
return () => {
319317
unsubscribe();
@@ -422,8 +420,9 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
422420
gitCommand: numstatCommand,
423421
});
424422

425-
// Fast path: use cached tree when switching workspaces (unless user explicitly refreshed).
426-
if (!isManualRefresh) {
423+
// Fast path: use cached tree when switching workspaces (unless user explicitly refreshed
424+
// or tools completed while we were unmounted).
425+
if (!isManualRefresh && !skipCacheOnMountRef.current) {
427426
const cachedTree = reviewPanelCache.get(cacheKey) as FileTreeNode | undefined;
428427
if (cachedTree) {
429428
setFileTree(cachedTree);
@@ -503,8 +502,9 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
503502
gitCommand: diffCommand,
504503
});
505504

506-
// Fast path: use cached diff when switching workspaces (unless user explicitly refreshed).
507-
if (!isManualRefresh) {
505+
// Fast path: use cached diff when switching workspaces (unless user explicitly refreshed
506+
// or tools completed while we were unmounted).
507+
if (!isManualRefresh && !skipCacheOnMountRef.current) {
508508
const cached = reviewPanelCache.get(cacheKey) as ReviewPanelDiffCacheValue | undefined;
509509
if (cached) {
510510
setDiagnosticInfo(cached.diagnosticInfo);
@@ -524,6 +524,13 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
524524
}
525525
}
526526

527+
// Clear the skip-cache flag and store timestamp after first load.
528+
// This prevents re-fetching on every filter change.
529+
if (skipCacheOnMountRef.current) {
530+
skipCacheOnMountRef.current = false;
531+
workspaceStore.clearFileModifyingToolMs(workspaceId);
532+
}
533+
527534
// Transition to appropriate loading state:
528535
// - "refreshing" if we have data (keeps UI stable during refresh)
529536
// - "loading" if no data yet

‎src/browser/stores/WorkspaceStore.ts‎

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,12 @@ export class WorkspaceStore {
268268
// Idle compaction notification callbacks (called when backend signals idle compaction needed)
269269
private idleCompactionCallbacks = new Set<(workspaceId: string) => void>();
270270

271-
// Tool-call-end callbacks (for file-modifying tool completions that trigger diff refresh)
272-
private toolCallEndCallbacks = new Set<
273-
(workspaceId: string, toolName: string, toolCallId: string) => void
274-
>();
271+
// Tracks when a file-modifying tool (file_edit_*, bash) last completed per workspace.
272+
// ReviewPanel subscribes to trigger diff refresh. Two structures:
273+
// - timestamps: actual Date.now() values for cache invalidation checks
274+
// - subscriptions: MapStore for per-workspace subscription support
275+
private fileModifyingToolMs = new Map<string, number>();
276+
private fileModifyingToolSubs = new MapStore<string, void>();
275277

276278
// Idle callback handles for high-frequency delta events to reduce re-renders during streaming.
277279
// Data is always updated immediately in the aggregator; only UI notification is scheduled.
@@ -399,7 +401,12 @@ export class WorkspaceStore {
399401
aggregator.handleToolCallEnd(data as never);
400402
this.states.bump(workspaceId);
401403
this.consumerManager.scheduleCalculation(workspaceId, aggregator);
402-
this.notifyToolCallEnd(workspaceId, toolCallEnd.toolName, toolCallEnd.toolCallId);
404+
405+
// Track file-modifying tools for ReviewPanel diff refresh
406+
if (toolCallEnd.toolName.startsWith("file_edit_") || toolCallEnd.toolName === "bash") {
407+
this.fileModifyingToolMs.set(workspaceId, Date.now());
408+
this.fileModifyingToolSubs.bump(workspaceId);
409+
}
403410
},
404411
"reasoning-delta": (workspaceId, aggregator, data) => {
405412
aggregator.handleReasoningDelta(data as never);
@@ -1321,27 +1328,26 @@ export class WorkspaceStore {
13211328
}
13221329

13231330
/**
1324-
* Subscribe to tool-call-end events (for diff refresh on file modifications).
1325-
* Returns unsubscribe function.
1331+
* Subscribe to file-modifying tool completions for a workspace.
1332+
* Used by ReviewPanel to trigger diff refresh.
13261333
*/
1327-
onToolCallEnd(
1328-
callback: (workspaceId: string, toolName: string, toolCallId: string) => void
1329-
): () => void {
1330-
this.toolCallEndCallbacks.add(callback);
1331-
return () => this.toolCallEndCallbacks.delete(callback);
1334+
subscribeFileModifyingTool(workspaceId: string, listener: () => void): () => void {
1335+
return this.fileModifyingToolSubs.subscribeKey(workspaceId, listener);
13321336
}
13331337

13341338
/**
1335-
* Notify all listeners that a tool call completed.
1339+
* Get when a file-modifying tool last completed for this workspace.
1340+
* Returns undefined if no tools have completed since last clear.
13361341
*/
1337-
private notifyToolCallEnd(workspaceId: string, toolName: string, toolCallId: string): void {
1338-
for (const callback of this.toolCallEndCallbacks) {
1339-
try {
1340-
callback(workspaceId, toolName, toolCallId);
1341-
} catch (error) {
1342-
console.error("Error in tool-call-end callback:", error);
1343-
}
1344-
}
1342+
getFileModifyingToolMs(workspaceId: string): number | undefined {
1343+
return this.fileModifyingToolMs.get(workspaceId);
1344+
}
1345+
1346+
/**
1347+
* Clear the file-modifying tool timestamp after ReviewPanel has consumed it.
1348+
*/
1349+
clearFileModifyingToolMs(workspaceId: string): void {
1350+
this.fileModifyingToolMs.delete(workspaceId);
13451351
}
13461352

13471353
// Private methods
@@ -1578,8 +1584,12 @@ function getStoreInstance(): WorkspaceStore {
15781584
export const workspaceStore = {
15791585
onIdleCompactionNeeded: (callback: (workspaceId: string) => void) =>
15801586
getStoreInstance().onIdleCompactionNeeded(callback),
1581-
onToolCallEnd: (callback: (workspaceId: string, toolName: string, toolCallId: string) => void) =>
1582-
getStoreInstance().onToolCallEnd(callback),
1587+
subscribeFileModifyingTool: (workspaceId: string, listener: () => void) =>
1588+
getStoreInstance().subscribeFileModifyingTool(workspaceId, listener),
1589+
getFileModifyingToolMs: (workspaceId: string) =>
1590+
getStoreInstance().getFileModifyingToolMs(workspaceId),
1591+
clearFileModifyingToolMs: (workspaceId: string) =>
1592+
getStoreInstance().clearFileModifyingToolMs(workspaceId),
15831593
};
15841594

15851595
/**

0 commit comments

Comments
 (0)