Skip to content

Commit 1ad2f4f

Browse files
authored
🤖 Add Code Review tab with interactive diff viewing and review notes (#315)
Adds an interactive Code Review tab to the right sidebar with diff viewing and inline review notes. ## Key Features **Review Panel** - Displays hunks from selected workspace with file tree navigation - Keyboard navigation (j/k when focused, respects editability) - Configurable diff base (HEAD, --staged, custom refs) - "Include dirty" option to show working tree changes - Refresh button with proper tooltip - Per-workspace persistence of settings _Generated with `cmux`_
1 parent 7d2ada3 commit 1ad2f4f

34 files changed

+3619
-453
lines changed

‎src/components/AIView.tsx‎

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { RetryBarrier } from "./Messages/ChatBarrier/RetryBarrier";
77
import { PinnedTodoList } from "./PinnedTodoList";
88
import { getAutoRetryKey } from "@/constants/storage";
99
import { ChatInput, type ChatInputAPI } from "./ChatInput";
10-
import { ChatMetaSidebar } from "./ChatMetaSidebar";
10+
import { RightSidebar, type TabType } from "./RightSidebar";
11+
import { useResizableSidebar } from "@/hooks/useResizableSidebar";
1112
import {
1213
shouldShowInterruptedBarrier,
1314
mergeConsecutiveStreamErrors,
@@ -44,7 +45,7 @@ const ViewContainer = styled.div`
4445

4546
const ChatArea = styled.div`
4647
flex: 1;
47-
min-width: 750px;
48+
min-width: 400px; /* Reduced from 750px to allow narrower layout when Review panel is wide */
4849
display: flex;
4950
flex-direction: column;
5051
`;
@@ -64,13 +65,26 @@ const WorkspaceTitle = styled.div`
6465
display: flex;
6566
align-items: center;
6667
gap: 8px;
68+
min-width: 0; /* Allow flex children to shrink */
69+
overflow: hidden;
6770
`;
6871

6972
const WorkspacePath = styled.span`
7073
font-family: var(--font-monospace);
7174
color: #888;
7275
font-weight: 400;
7376
font-size: 11px;
77+
white-space: nowrap;
78+
overflow: hidden;
79+
text-overflow: ellipsis;
80+
min-width: 0;
81+
`;
82+
83+
const WorkspaceName = styled.span`
84+
white-space: nowrap;
85+
overflow: hidden;
86+
text-overflow: ellipsis;
87+
min-width: 0;
7488
`;
7589

7690
const TerminalIconButton = styled.button`
@@ -197,12 +211,33 @@ const AIViewInner: React.FC<AIViewProps> = ({
197211
}) => {
198212
const chatAreaRef = useRef<HTMLDivElement>(null);
199213

200-
// NEW: Get workspace state from store (only re-renders when THIS workspace changes)
214+
// Track active tab to conditionally enable resize functionality
215+
// RightSidebar notifies us of tab changes via onTabChange callback
216+
const [activeTab, setActiveTab] = useState<TabType>("costs");
217+
const isReviewTabActive = activeTab === "review";
218+
219+
// Resizable sidebar for Review tab only
220+
// Hook encapsulates all drag logic, persistence, and constraints
221+
// Returns width to apply to RightSidebar and startResize for handle's onMouseDown
222+
const {
223+
width: sidebarWidth,
224+
isResizing,
225+
startResize,
226+
} = useResizableSidebar({
227+
enabled: isReviewTabActive, // Only active on Review tab
228+
defaultWidth: 600, // Initial width or fallback
229+
minWidth: 300, // Can't shrink smaller
230+
maxWidth: 1200, // Can't grow larger
231+
storageKey: "review-sidebar-width", // Persists across sessions
232+
});
233+
234+
// Get workspace state from store (only re-renders when THIS workspace changes)
201235
const workspaceState = useWorkspaceState(workspaceId);
202236
const aggregator = useWorkspaceAggregator(workspaceId);
203237

204238
// Get git status for this workspace
205239
const gitStatus = useGitStatus(workspaceId);
240+
206241
const [editingMessage, setEditingMessage] = useState<{ id: string; content: string } | undefined>(
207242
undefined
208243
);
@@ -239,6 +274,11 @@ const AIViewInner: React.FC<AIViewProps> = ({
239274
chatInputAPI.current = api;
240275
}, []);
241276

277+
// Handler for review notes from Code Review tab
278+
const handleReviewNote = useCallback((note: string) => {
279+
chatInputAPI.current?.appendText(note);
280+
}, []);
281+
242282
// Thinking level state from context
243283
const { thinkingLevel: currentWorkspaceThinking, setThinkingLevel } = useThinking();
244284

@@ -431,7 +471,9 @@ const AIViewInner: React.FC<AIViewProps> = ({
431471
workspaceId={workspaceId}
432472
tooltipPosition="bottom"
433473
/>
434-
{projectName} / {branch}
474+
<WorkspaceName>
475+
{projectName} / {branch}
476+
</WorkspaceName>
435477
<WorkspacePath>{namedWorkspacePath}</WorkspacePath>
436478
<TooltipWrapper inline>
437479
<TerminalIconButton onClick={handleOpenTerminal}>
@@ -555,7 +597,17 @@ const AIViewInner: React.FC<AIViewProps> = ({
555597
/>
556598
</ChatArea>
557599

558-
<ChatMetaSidebar key={workspaceId} workspaceId={workspaceId} chatAreaRef={chatAreaRef} />
600+
<RightSidebar
601+
key={workspaceId}
602+
workspaceId={workspaceId}
603+
workspacePath={namedWorkspacePath}
604+
chatAreaRef={chatAreaRef}
605+
onTabChange={setActiveTab} // Notifies us when tab changes
606+
width={isReviewTabActive ? sidebarWidth : undefined} // Custom width only on Review tab
607+
onStartResize={isReviewTabActive ? startResize : undefined} // Pass resize handler when Review active
608+
isResizing={isResizing} // Pass resizing state
609+
onReviewNote={handleReviewNote} // Pass review note handler to append to chat
610+
/>
559611
</ViewContainer>
560612
);
561613
};

‎src/components/ChatInput.tsx‎

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const InputSection = styled.div`
4646
display: flex;
4747
flex-direction: column;
4848
gap: 8px;
49+
container-type: inline-size; /* Enable container queries for responsive behavior */
4950
`;
5051

5152
const InputControls = styled.div`
@@ -72,6 +73,12 @@ const ModeToggleWrapper = styled.div`
7273
align-items: center;
7374
gap: 6px;
7475
margin-left: auto;
76+
77+
/* Hide mode toggle on narrow containers */
78+
/* Note: Text area border changes color with mode, so this omission is acceptable */
79+
@container (max-width: 700px) {
80+
display: none;
81+
}
7582
`;
7683

7784
const StyledToggleContainer = styled.div<{ mode: UIMode }>`
@@ -121,11 +128,19 @@ const ModelDisplayWrapper = styled.div`
121128
gap: 4px;
122129
margin-right: 12px;
123130
height: 11px;
131+
132+
/* Hide help indicators on narrow containers */
133+
@container (max-width: 700px) {
134+
.help-indicator-wrapper {
135+
display: none;
136+
}
137+
}
124138
`;
125139

126140
export interface ChatInputAPI {
127141
focus: () => void;
128142
restoreText: (text: string) => void;
143+
appendText: (text: string) => void;
129144
}
130145

131146
export interface ChatInputProps {
@@ -258,15 +273,26 @@ export const ChatInput: React.FC<ChatInputProps> = ({
258273
[focusMessageInput]
259274
);
260275

276+
// Method to append text to input (used by Code Review notes)
277+
const appendText = useCallback((text: string) => {
278+
setInput((prev) => {
279+
// Add blank line before if there's existing content
280+
const separator = prev.trim() ? "\n\n" : "";
281+
return prev + separator + text;
282+
});
283+
// Don't focus - user wants to keep reviewing
284+
}, []);
285+
261286
// Provide API to parent via callback
262287
useEffect(() => {
263288
if (onReady) {
264289
onReady({
265290
focus: focusMessageInput,
266291
restoreText,
292+
appendText,
267293
});
268294
}
269-
}, [onReady, focusMessageInput, restoreText]);
295+
}, [onReady, focusMessageInput, restoreText, appendText]);
270296

271297
useEffect(() => {
272298
const handleGlobalKeyDown = (event: KeyboardEvent) => {
@@ -871,25 +897,27 @@ export const ChatInput: React.FC<ChatInputProps> = ({
871897
recentModels={recentModels}
872898
onComplete={() => inputRef.current?.focus()}
873899
/>
874-
<TooltipWrapper inline>
875-
<HelpIndicator>?</HelpIndicator>
876-
<Tooltip className="tooltip" align="left" width="wide">
877-
<strong>Click to edit</strong> or use{" "}
878-
{formatKeybind(KEYBINDS.OPEN_MODEL_SELECTOR)}
879-
<br />
880-
<br />
881-
<strong>Abbreviations:</strong>
882-
<br />• <code>/model opus</code> - Claude Opus 4.1
883-
<br />• <code>/model sonnet</code> - Claude Sonnet 4.5
884-
<br />
885-
<br />
886-
<strong>Full format:</strong>
887-
<br />
888-
<code>/model provider:model-name</code>
889-
<br />
890-
(e.g., <code>/model anthropic:claude-sonnet-4-5</code>)
891-
</Tooltip>
892-
</TooltipWrapper>
900+
<span className="help-indicator-wrapper">
901+
<TooltipWrapper inline>
902+
<HelpIndicator>?</HelpIndicator>
903+
<Tooltip className="tooltip" align="left" width="wide">
904+
<strong>Click to edit</strong> or use{" "}
905+
{formatKeybind(KEYBINDS.OPEN_MODEL_SELECTOR)}
906+
<br />
907+
<br />
908+
<strong>Abbreviations:</strong>
909+
<br />• <code>/model opus</code> - Claude Opus 4.1
910+
<br />• <code>/model sonnet</code> - Claude Sonnet 4.5
911+
<br />
912+
<br />
913+
<strong>Full format:</strong>
914+
<br />
915+
<code>/model provider:model-name</code>
916+
<br />
917+
(e.g., <code>/model anthropic:claude-sonnet-4-5</code>)
918+
</Tooltip>
919+
</TooltipWrapper>
920+
</span>
893921
</ModelDisplayWrapper>
894922
</ChatToggles>
895923
<ModeToggleWrapper>
@@ -903,18 +931,20 @@ export const ChatInput: React.FC<ChatInputProps> = ({
903931
onChange={setMode}
904932
/>
905933
</StyledToggleContainer>
906-
<TooltipWrapper inline>
907-
<HelpIndicator>?</HelpIndicator>
908-
<Tooltip className="tooltip" align="center" width="wide">
909-
<strong>Exec Mode:</strong> AI edits files and execute commands
910-
<br />
911-
<br />
912-
<strong>Plan Mode:</strong> AI proposes plans but does not edit files
913-
<br />
914-
<br />
915-
Toggle with: {formatKeybind(KEYBINDS.TOGGLE_MODE)}
916-
</Tooltip>
917-
</TooltipWrapper>
934+
<span className="help-indicator-wrapper">
935+
<TooltipWrapper inline>
936+
<HelpIndicator>?</HelpIndicator>
937+
<Tooltip className="tooltip" align="center" width="wide">
938+
<strong>Exec Mode:</strong> AI edits files and execute commands
939+
<br />
940+
<br />
941+
<strong>Plan Mode:</strong> AI proposes plans but does not edit files
942+
<br />
943+
<br />
944+
Toggle with: {formatKeybind(KEYBINDS.TOGGLE_MODE)}
945+
</Tooltip>
946+
</TooltipWrapper>
947+
</span>
918948
</ModeToggleWrapper>
919949
</ModeTogglesRow>
920950
</ModeToggles>

0 commit comments

Comments
 (0)