Skip to content

Commit 73c20b2

Browse files
committed
fix: make review diff refresh immediate and non-interactive
- Manual refresh now bumps refreshTrigger immediately (no blocking fetch) - Move origin/* git fetch into diff/tree load with GIT_TERMINAL_PROMPT=0 - Share origin fetch across file tree + diff via a keyed promise ref
1 parent 531d7d2 commit 73c20b2

File tree

2 files changed

+71
-38
lines changed

2 files changed

+71
-38
lines changed

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,53 @@ const reviewPanelCache = new LRUCache<string, ReviewPanelCacheValue>({
143143
sizeCalculation: (value) => estimateJsonSizeBytes(value),
144144
});
145145

146+
function getOriginBranchForFetch(diffBase: string): string | null {
147+
const trimmed = diffBase.trim();
148+
if (!trimmed.startsWith("origin/")) return null;
149+
150+
const branch = trimmed.slice("origin/".length);
151+
152+
// Avoid shell injection; diffBase is user-controlled.
153+
if (!/^[0-9A-Za-z._/-]+$/.test(branch)) return null;
154+
155+
return branch;
156+
}
157+
158+
interface OriginFetchState {
159+
key: string;
160+
promise: Promise<void>;
161+
}
162+
163+
async function ensureOriginFetched(params: {
164+
api: APIClient;
165+
workspaceId: string;
166+
diffBase: string;
167+
refreshToken: number;
168+
originFetchRef: React.MutableRefObject<OriginFetchState | null>;
169+
}): Promise<void> {
170+
const originBranch = getOriginBranchForFetch(params.diffBase);
171+
if (!originBranch) return;
172+
173+
const key = [params.workspaceId, params.diffBase, String(params.refreshToken)].join("\u0000");
174+
const existing = params.originFetchRef.current;
175+
if (existing?.key === key) {
176+
await existing.promise;
177+
return;
178+
}
179+
180+
// Ensure manual refresh doesn't hang on credential prompts.
181+
const promise = params.api.workspace
182+
.executeBash({
183+
workspaceId: params.workspaceId,
184+
script: `GIT_TERMINAL_PROMPT=0 git fetch origin ${originBranch} --quiet || true`,
185+
options: { timeout_secs: 30 },
186+
})
187+
.then(() => undefined)
188+
.catch(() => undefined);
189+
190+
params.originFetchRef.current = { key, promise };
191+
await promise;
192+
}
146193
function makeReviewPanelCacheKey(params: {
147194
workspaceId: string;
148195
workspacePath: string;
@@ -186,6 +233,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
186233
isCreating = false,
187234
onStatsChange,
188235
}) => {
236+
const originFetchRef = useRef<OriginFetchState | null>(null);
189237
const { api } = useAPI();
190238
const panelRef = useRef<HTMLDivElement>(null);
191239
const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -334,6 +382,15 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
334382
const loadFileTree = async () => {
335383
setIsLoadingTree(true);
336384
try {
385+
await ensureOriginFetched({
386+
api,
387+
workspaceId,
388+
diffBase: filters.diffBase,
389+
refreshToken: refreshTrigger,
390+
originFetchRef,
391+
});
392+
if (cancelled) return;
393+
337394
const tree = await executeWorkspaceBashAndCache({
338395
api,
339396
workspaceId,
@@ -445,6 +502,15 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
445502

446503
const loadDiff = async () => {
447504
try {
505+
await ensureOriginFetched({
506+
api,
507+
workspaceId,
508+
diffBase: filters.diffBase,
509+
refreshToken: refreshTrigger,
510+
originFetchRef,
511+
});
512+
if (cancelled) return;
513+
448514
// Git-level filters (affect what data is fetched):
449515
// - diffBase: what to diff against
450516
// - includeUncommitted: include working directory changes

src/browser/hooks/useReviewRefreshController.ts

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,6 @@ import { usePersistedState } from "@/browser/hooks/usePersistedState";
77
/** Debounce delay for auto-refresh after tool completion */
88
const TOOL_REFRESH_DEBOUNCE_MS = 3000;
99

10-
/**
11-
* Extract branch name from an "origin/..." diff base for git fetch.
12-
* Returns null if not an origin ref or if branch name is unsafe for shell.
13-
*/
14-
function getOriginBranchForFetch(diffBase: string): string | null {
15-
const trimmed = diffBase.trim();
16-
if (!trimmed.startsWith("origin/")) return null;
17-
18-
const branch = trimmed.slice("origin/".length);
19-
20-
// Avoid shell injection; diffBase is user-controlled.
21-
if (!/^[0-9A-Za-z._/-]+$/.test(branch)) return null;
22-
23-
return branch;
24-
}
25-
2610
export interface UseReviewRefreshControllerOptions {
2711
workspaceId: string;
2812
api: APIClient | null;
@@ -53,7 +37,6 @@ export interface ReviewRefreshController {
5337
*
5438
* Delegates debouncing, visibility/focus handling, and in-flight guards to RefreshController.
5539
* Keeps ReviewPanel-specific logic:
56-
* - Origin branch fetch before refresh
5740
* - Scroll position preservation
5841
* - User interaction pause state
5942
*/
@@ -63,9 +46,6 @@ export function useReviewRefreshController(
6346
const { workspaceId, api, isCreating, scrollContainerRef } = options;
6447

6548
// Refs for values that executeRefresh needs at call time (avoid stale closures)
66-
const diffBaseRef = useRef(options.diffBase);
67-
diffBaseRef.current = options.diffBase;
68-
6949
const onRefreshRef = useRef(options.onRefresh);
7050
onRefreshRef.current = options.onRefresh;
7151

@@ -89,35 +69,22 @@ export function useReviewRefreshController(
8969
const ctrl = new RefreshController({
9070
debounceMs: TOOL_REFRESH_DEBOUNCE_MS,
9171
isPaused: () => isInteractingRef.current,
92-
onRefresh: async () => {
93-
if (!api || isCreating) return;
72+
onRefresh: () => {
73+
if (isCreating) return;
9474

9575
// Save scroll position before refresh
9676
savedScrollTopRef.current = scrollContainerRef.current?.scrollTop ?? null;
9777

98-
const originBranch = getOriginBranchForFetch(diffBaseRef.current);
99-
if (originBranch) {
100-
try {
101-
await api.workspace.executeBash({
102-
workspaceId,
103-
script: `git fetch origin ${originBranch} --quiet || true`,
104-
options: { timeout_secs: 30 },
105-
});
106-
} catch (err) {
107-
console.debug("ReviewPanel origin fetch failed", err);
108-
}
109-
}
110-
11178
onRefreshRef.current();
11279
onGitStatusRefreshRef.current?.();
11380
},
11481
onRefreshComplete: setLastRefreshInfo,
11582
});
11683
ctrl.bindListeners();
11784
return ctrl;
118-
// workspaceId/api/isCreating changes require new controller with updated closure
119-
// Note: options.onRefresh is accessed via ref to avoid recreating controller on every render
120-
}, [workspaceId, api, isCreating, scrollContainerRef, setLastRefreshInfo]);
85+
// isCreating/scrollContainerRef/setLastRefreshInfo changes require a new controller.
86+
// Note: options.onRefresh is accessed via ref to avoid recreating controller on every render.
87+
}, [isCreating, scrollContainerRef, setLastRefreshInfo]);
12188

12289
// Cleanup on unmount or when controller changes
12390
useEffect(() => {

0 commit comments

Comments
 (0)