Skip to content

Commit 4c0b368

Browse files
authored
🤖 fix: default review base to origin/main and unify with git status (#1414)
## Summary Scope review-base settings to the project level and unify with git status tracking. ### Changes - **Per-project review-base storage**: Review diff base is now stored per-project (`review-default-base:{projectPath}`) instead of globally - **Unified git status**: GitStatusStore uses the same base ref as the review panel, so ahead/behind counts match the diff view - **Flexible base ref**: `generateGitStatusScript()` accepts a custom base ref; falls back to auto-detection for HEAD - **Prop threading**: Added `projectPath` prop through RightSidebar → ReviewPanel/ReviewControls - **Helpful error hint**: When a configured ref doesn't exist, shows a hint to use the dropdown ### Default Behavior The default remains `HEAD` for test compatibility. Users can change the base per-project via the dropdown (e.g., to `origin/main`, `origin/master`, etc.). Once changed, the git status indicator will use the same base. ### Testing - All integration tests pass - Typecheck passes - Static checks pass --- _Generated with `mux` • Model: `anthropic:claude-opus-4-5` • Thinking: `high`_
1 parent cd7246c commit 4c0b368

File tree

14 files changed

+294
-40
lines changed

14 files changed

+294
-40
lines changed

‎src/browser/components/AIView.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
800800
key={workspaceId}
801801
workspaceId={workspaceId}
802802
workspacePath={namedWorkspacePath}
803+
projectPath={projectPath}
803804
width={sidebarWidth}
804805
onStartResize={startResize}
805806
isResizing={isResizing}

‎src/browser/components/GitStatusIndicator.tsx‎

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import React, { useState, useCallback } from "react";
1+
import React, { useState, useCallback, useRef } from "react";
22
import type { GitStatus } from "@/common/types/workspace";
33
import { GIT_STATUS_INDICATOR_MODE_KEY } from "@/common/constants/storage";
4+
import { STORAGE_KEYS, WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults";
45
import { usePersistedState } from "@/browser/hooks/usePersistedState";
5-
import { useGitStatusRefreshing } from "@/browser/stores/GitStatusStore";
6+
import { invalidateGitStatus, useGitStatusRefreshing } from "@/browser/stores/GitStatusStore";
67
import { GitStatusIndicatorView, type GitStatusIndicatorMode } from "./GitStatusIndicatorView";
78
import { useGitBranchDetails } from "./hooks/useGitBranchDetails";
89

910
interface GitStatusIndicatorProps {
1011
gitStatus: GitStatus | null;
1112
workspaceId: string;
13+
projectPath: string;
1214
tooltipPosition?: "right" | "bottom";
1315
/** When true, shows blue pulsing styling to indicate agent is working */
1416
isWorking?: boolean;
@@ -22,10 +24,13 @@ interface GitStatusIndicatorProps {
2224
export const GitStatusIndicator: React.FC<GitStatusIndicatorProps> = ({
2325
gitStatus,
2426
workspaceId,
27+
projectPath,
2528
tooltipPosition = "right",
2629
isWorking = false,
2730
}) => {
2831
const [isOpen, setIsOpen] = useState(false);
32+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
33+
const pendingHoverCardCloseRef = useRef(false);
2934
const trimmedWorkspaceId = workspaceId.trim();
3035
const isRefreshing = useGitStatusRefreshing(trimmedWorkspaceId);
3136

@@ -35,6 +40,56 @@ export const GitStatusIndicator: React.FC<GitStatusIndicatorProps> = ({
3540
{ listener: true }
3641
);
3742

43+
// Per-project default base (fallback for new workspaces)
44+
const [projectDefaultBase] = usePersistedState<string>(
45+
STORAGE_KEYS.reviewDefaultBase(projectPath),
46+
WORKSPACE_DEFAULTS.reviewBase,
47+
{ listener: true }
48+
);
49+
50+
// Per-workspace base ref (shared with review panel, syncs via listener)
51+
const [baseRef, setBaseRef] = usePersistedState<string>(
52+
STORAGE_KEYS.reviewDiffBase(trimmedWorkspaceId),
53+
projectDefaultBase,
54+
{ listener: true }
55+
);
56+
57+
const handleBaseChange = useCallback(
58+
(value: string) => {
59+
setBaseRef(value);
60+
invalidateGitStatus(trimmedWorkspaceId);
61+
},
62+
[setBaseRef, trimmedWorkspaceId]
63+
);
64+
65+
// Prevent HoverCard from closing while the base selector popover is open.
66+
// If Radix requests a close while the popover is open, defer the close until
67+
// the popover closes (otherwise the hovercard can get "stuck" open).
68+
const handleHoverCardOpenChange = useCallback(
69+
(open: boolean) => {
70+
if (!open && isPopoverOpen) {
71+
pendingHoverCardCloseRef.current = true;
72+
return;
73+
}
74+
75+
pendingHoverCardCloseRef.current = false;
76+
setIsOpen(open);
77+
},
78+
[isPopoverOpen]
79+
);
80+
81+
const handlePopoverOpenChange = useCallback(
82+
(open: boolean) => {
83+
setIsPopoverOpen(open);
84+
85+
if (!open && pendingHoverCardCloseRef.current) {
86+
pendingHoverCardCloseRef.current = false;
87+
setIsOpen(false);
88+
}
89+
},
90+
[setIsPopoverOpen]
91+
);
92+
3893
const handleModeChange = useCallback(
3994
(nextMode: GitStatusIndicatorMode) => {
4095
setMode(nextMode);
@@ -65,8 +120,11 @@ export const GitStatusIndicator: React.FC<GitStatusIndicatorProps> = ({
65120
isLoading={isLoading}
66121
errorMessage={errorMessage}
67122
isOpen={isOpen}
68-
onOpenChange={setIsOpen}
123+
onOpenChange={handleHoverCardOpenChange}
69124
onModeChange={handleModeChange}
125+
baseRef={baseRef}
126+
onBaseChange={handleBaseChange}
127+
onPopoverOpenChange={handlePopoverOpenChange}
70128
isWorking={isWorking}
71129
isRefreshing={isRefreshing}
72130
/>

‎src/browser/components/GitStatusIndicatorView.tsx‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ import type { GitCommit, GitBranchHeader } from "@/common/utils/git/parseGitLog"
44
import { cn } from "@/common/lib/utils";
55
import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
66
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./ui/hover-card";
7+
import { BaseSelectorPopover } from "./RightSidebar/CodeReview/BaseSelectorPopover";
8+
9+
const RADIX_PORTAL_WRAPPER_SELECTOR = "[data-radix-popper-content-wrapper]" as const;
10+
11+
function preventHoverCardDismissForRadixPortals(e: {
12+
target: EventTarget | null;
13+
preventDefault: () => void;
14+
}) {
15+
const target = e.target;
16+
if (target instanceof HTMLElement && target.closest(RADIX_PORTAL_WRAPPER_SELECTOR)) {
17+
e.preventDefault();
18+
}
19+
}
720

821
// Helper for indicator colors
922
const getIndicatorColor = (branch: number): string => {
@@ -53,6 +66,11 @@ export interface GitStatusIndicatorViewProps {
5366
isOpen: boolean;
5467
onOpenChange: (open: boolean) => void;
5568
onModeChange: (nextMode: GitStatusIndicatorMode) => void;
69+
// Base ref for divergence (shared with review panel)
70+
baseRef: string;
71+
onBaseChange: (value: string) => void;
72+
/** Callback when the base selector popover open state changes */
73+
onPopoverOpenChange?: (open: boolean) => void;
5674
/** When true, shows blue pulsing styling to indicate agent is working */
5775
isWorking?: boolean;
5876
/** When true, shows shimmer effect to indicate git status is refreshing */
@@ -76,6 +94,9 @@ export const GitStatusIndicatorView: React.FC<GitStatusIndicatorViewProps> = ({
7694
isOpen,
7795
onOpenChange,
7896
onModeChange,
97+
baseRef,
98+
onBaseChange,
99+
onPopoverOpenChange,
79100
isWorking = false,
80101
isRefreshing = false,
81102
}) => {
@@ -222,8 +243,8 @@ export const GitStatusIndicatorView: React.FC<GitStatusIndicatorViewProps> = ({
222243
const additionsColor = isWorking ? "text-success-light" : "text-muted";
223244
const deletionsColor = isWorking ? "text-warning-light" : "text-muted";
224245

225-
// Popover content with git divergence details
226-
const popoverContent = (
246+
// HoverCard content with git divergence details
247+
const hoverCardContent = (
227248
<>
228249
<div className="border-separator-light mb-2 flex flex-col gap-1 border-b pb-2">
229250
<div className="flex items-center gap-2">
@@ -247,6 +268,15 @@ export const GitStatusIndicatorView: React.FC<GitStatusIndicatorViewProps> = ({
247268
</ToggleGroup>
248269
</div>
249270

271+
<div className="flex items-center gap-2">
272+
<span className="text-muted-light">Base:</span>
273+
<BaseSelectorPopover
274+
value={baseRef}
275+
onChange={onBaseChange}
276+
onOpenChange={onPopoverOpenChange}
277+
/>
278+
</div>
279+
250280
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px]">
251281
<span className="text-muted-light">Overview:</span>
252282
{outgoingHasDelta ? (
@@ -346,8 +376,10 @@ export const GitStatusIndicatorView: React.FC<GitStatusIndicatorViewProps> = ({
346376
sideOffset={8}
347377
collisionPadding={8}
348378
className="bg-modal-bg text-foreground border-separator-light z-[10000] max-h-[400px] w-auto max-w-96 min-w-0 overflow-auto px-3 py-2 font-mono text-[11px] whitespace-pre shadow-[0_4px_12px_rgba(0,0,0,0.5)]"
379+
onPointerDownOutside={preventHoverCardDismissForRadixPortals}
380+
onFocusOutside={preventHoverCardDismissForRadixPortals}
349381
>
350-
{popoverContent}
382+
{hoverCardContent}
351383
</HoverCardContent>
352384
</HoverCard>
353385
);

‎src/browser/components/RightSidebar.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export type { TabType };
9191
interface RightSidebarProps {
9292
workspaceId: string;
9393
workspacePath: string;
94+
projectPath: string;
9495
/** Custom width in pixels (persisted per-tab, provided by AIView) */
9596
width?: number;
9697
/** Drag start handler for resize */
@@ -106,6 +107,7 @@ interface RightSidebarProps {
106107
const RightSidebarComponent: React.FC<RightSidebarProps> = ({
107108
workspaceId,
108109
workspacePath,
110+
projectPath,
109111
width,
110112
onStartResize,
111113
isResizing = false,
@@ -336,6 +338,7 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
336338
key={workspaceId}
337339
workspaceId={workspaceId}
338340
workspacePath={workspacePath}
341+
projectPath={projectPath}
339342
onReviewNote={onReviewNote}
340343
focusTrigger={focusTrigger}
341344
isCreating={isCreating}

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,22 @@ const BASE_SUGGESTIONS = [
2121
interface BaseSelectorPopoverProps {
2222
value: string;
2323
onChange: (value: string) => void;
24+
onOpenChange?: (open: boolean) => void;
2425
className?: string;
2526
}
2627

27-
export function BaseSelectorPopover({ value, onChange, className }: BaseSelectorPopoverProps) {
28+
export function BaseSelectorPopover({
29+
value,
30+
onChange,
31+
onOpenChange,
32+
className,
33+
}: BaseSelectorPopoverProps) {
2834
const [isOpen, setIsOpen] = useState(false);
35+
36+
const handleOpenChange = (open: boolean) => {
37+
setIsOpen(open);
38+
onOpenChange?.(open);
39+
};
2940
const [inputValue, setInputValue] = useState(value);
3041
const inputRef = useRef<HTMLInputElement>(null);
3142

@@ -45,19 +56,19 @@ export function BaseSelectorPopover({ value, onChange, className }: BaseSelector
4556
const handleSelect = (selected: string) => {
4657
onChange(selected);
4758
setInputValue(selected);
48-
setIsOpen(false);
59+
handleOpenChange(false);
4960
};
5061

5162
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
5263
if (e.key === "Enter") {
5364
const trimmed = inputValue.trim();
5465
if (trimmed) {
5566
onChange(trimmed);
56-
setIsOpen(false);
67+
handleOpenChange(false);
5768
}
5869
} else if (e.key === "Escape") {
5970
setInputValue(value);
60-
setIsOpen(false);
71+
handleOpenChange(false);
6172
}
6273
};
6374

@@ -75,7 +86,7 @@ export function BaseSelectorPopover({ value, onChange, className }: BaseSelector
7586
const filteredSuggestions = BASE_SUGGESTIONS.filter((s) => s.toLowerCase().includes(searchLower));
7687

7788
return (
78-
<Popover open={isOpen} onOpenChange={setIsOpen}>
89+
<Popover open={isOpen} onOpenChange={handleOpenChange} modal>
7990
<PopoverTrigger asChild>
8091
<button
8192
className={cn(
@@ -86,7 +97,7 @@ export function BaseSelectorPopover({ value, onChange, className }: BaseSelector
8697
<span className="truncate">{value}</span>
8798
</button>
8899
</PopoverTrigger>
89-
<PopoverContent align="start" className="w-[160px] p-0">
100+
<PopoverContent align="start" className="z-[10001] w-[160px] p-0">
90101
{/* Search/edit input */}
91102
<div className="border-border border-b px-2 py-1.5">
92103
<input

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import React from "react";
66
import { usePersistedState } from "@/browser/hooks/usePersistedState";
7+
import { STORAGE_KEYS, WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults";
78
import type { ReviewFilters, ReviewStats, ReviewSortOrder } from "@/common/types/review";
89
import type { LastRefreshInfo } from "@/browser/utils/RefreshController";
910
import { RefreshButton } from "./RefreshButton";
@@ -25,6 +26,7 @@ interface ReviewControlsProps {
2526
isRefreshBlocked?: boolean;
2627
workspaceId: string;
2728
workspacePath: string;
29+
projectPath: string;
2830
refreshTrigger?: number;
2931
/** Debug info about last refresh */
3032
lastRefreshInfo?: LastRefreshInfo | null;
@@ -39,11 +41,16 @@ export const ReviewControls: React.FC<ReviewControlsProps> = ({
3941
isRefreshBlocked = false,
4042
workspaceId,
4143
workspacePath,
44+
projectPath,
4245
refreshTrigger,
4346
lastRefreshInfo,
4447
}) => {
45-
// Global default base (used for new workspaces)
46-
const [defaultBase, setDefaultBase] = usePersistedState<string>("review-default-base", "HEAD");
48+
// Per-project default base (used for new workspaces in this project)
49+
const [defaultBase, setDefaultBase] = usePersistedState<string>(
50+
STORAGE_KEYS.reviewDefaultBase(projectPath),
51+
WORKSPACE_DEFAULTS.reviewBase,
52+
{ listener: true }
53+
);
4754

4855
const handleBaseChange = (value: string) => {
4956
onFiltersChange({ ...filters, diffBase: value });

0 commit comments

Comments
 (0)