Skip to content

Commit 86aaf8e

Browse files
authored
🤖 Add Ctrl+R keyboard shortcut for Code Review refresh (#316)
Add keyboard shortcut for refreshing the diff in Code Review panel. ## Changes - **Added REFRESH_REVIEW keybind** to centralized `KEYBINDS` registry (`Ctrl+R` / `Cmd+R`) - **Implemented global keyboard handler** in `ReviewPanel` to trigger diff refresh - **Updated RefreshButton tooltip** to display the keybind hint: `Refresh diff (⌘R)` - **Removed Ctrl+R from Electron reload menu** - Replaced `role: 'reload'` with custom click handler that preserves reload functionality without keyboard shortcut ## Pattern Follows existing keybind patterns used throughout the app: - Uses `matchesKeybind` for event detection - Uses `formatKeybind` for display in tooltips - Global keyboard listener (not panel-focused like j/k navigation) ## Why Remove Reload Shortcut? Ctrl+R is traditionally a browser/Electron reload shortcut, but in a development tool like cmux: - Users rarely need to manually reload the app - Code Review refresh is a much more frequent operation - Reload is still accessible via View menu when needed ## Testing - Verify `Ctrl+R` / `Cmd+R` refreshes the diff when Code Review tab is open - Verify tooltip shows the correct keybind on hover over refresh button - Verify keybind works regardless of panel focus state - Verify View > Reload still works (just no keyboard shortcut) _Generated with `cmux`_
1 parent 1ad2f4f commit 86aaf8e

File tree

6 files changed

+144
-36
lines changed

6 files changed

+144
-36
lines changed

src/components/RightSidebar/CodeReview/FileTree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ export const FileTree: React.FC<FileTreeExternalProps> = ({
251251
</TreeHeader>
252252
{commonPrefix && <CommonPrefix>{commonPrefix}/</CommonPrefix>}
253253
<TreeContainer>
254-
{isLoading ? (
254+
{isLoading && !startNode ? (
255255
<EmptyState>Loading file tree...</EmptyState>
256256
) : startNode ? (
257257
startNode.children.map((child) => (
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* RefreshButton - Animated refresh button with graceful spin-down
3+
*/
4+
5+
import React, { useState, useRef, useEffect } from "react";
6+
import styled from "@emotion/styled";
7+
import { TooltipWrapper, Tooltip } from "@/components/Tooltip";
8+
import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
9+
10+
interface RefreshButtonProps {
11+
onClick: () => void;
12+
isLoading?: boolean;
13+
}
14+
15+
const Button = styled.button<{ $animationState: "idle" | "spinning" | "stopping" }>`
16+
background: transparent;
17+
border: none;
18+
padding: 2px;
19+
cursor: ${(props) => (props.$animationState !== "idle" ? "default" : "pointer")};
20+
display: flex;
21+
align-items: center;
22+
justify-content: center;
23+
color: ${(props) => (props.$animationState === "spinning" ? "#007acc" : "#888")};
24+
transition: color 1.5s ease-out;
25+
26+
&:hover {
27+
color: ${(props) => (props.$animationState === "spinning" ? "#007acc" : "#ccc")};
28+
}
29+
30+
svg {
31+
width: 12px;
32+
height: 12px;
33+
}
34+
35+
&.spinning svg {
36+
animation: spin 0.8s linear infinite;
37+
}
38+
39+
&.spin-once svg {
40+
animation: spin-once 0.8s ease-out forwards;
41+
}
42+
43+
@keyframes spin {
44+
from {
45+
transform: rotate(0deg);
46+
}
47+
to {
48+
transform: rotate(360deg);
49+
}
50+
}
51+
52+
@keyframes spin-once {
53+
from {
54+
transform: rotate(0deg);
55+
}
56+
to {
57+
transform: rotate(360deg);
58+
}
59+
}
60+
`;
61+
62+
export const RefreshButton: React.FC<RefreshButtonProps> = ({ onClick, isLoading = false }) => {
63+
// Track animation state for graceful stopping
64+
const [animationState, setAnimationState] = useState<"idle" | "spinning" | "stopping">("idle");
65+
const spinOnceTimeoutRef = useRef<number | null>(null);
66+
67+
// Manage animation state based on loading prop
68+
useEffect(() => {
69+
if (isLoading) {
70+
// Start spinning
71+
setAnimationState("spinning");
72+
// Clear any pending stop timeout
73+
if (spinOnceTimeoutRef.current) {
74+
clearTimeout(spinOnceTimeoutRef.current);
75+
spinOnceTimeoutRef.current = null;
76+
}
77+
} else if (animationState === "spinning") {
78+
// Gracefully stop: do one final rotation
79+
setAnimationState("stopping");
80+
// After animation completes, return to idle
81+
spinOnceTimeoutRef.current = window.setTimeout(() => {
82+
setAnimationState("idle");
83+
spinOnceTimeoutRef.current = null;
84+
}, 800); // Match animation duration
85+
}
86+
87+
return () => {
88+
if (spinOnceTimeoutRef.current) {
89+
clearTimeout(spinOnceTimeoutRef.current);
90+
}
91+
};
92+
}, [isLoading, animationState]);
93+
94+
const className =
95+
animationState === "spinning" ? "spinning" : animationState === "stopping" ? "spin-once" : "";
96+
97+
return (
98+
<TooltipWrapper inline>
99+
<Button onClick={onClick} $animationState={animationState} className={className}>
100+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
101+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2" />
102+
</svg>
103+
</Button>
104+
<Tooltip position="bottom" align="left">
105+
{animationState !== "idle"
106+
? "Refreshing..."
107+
: `Refresh diff (${formatKeybind(KEYBINDS.REFRESH_REVIEW)})`}
108+
</Tooltip>
109+
</TooltipWrapper>
110+
);
111+
};

src/components/RightSidebar/CodeReview/ReviewControls.tsx

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44

55
import React, { useState } from "react";
66
import styled from "@emotion/styled";
7-
import { TooltipWrapper, Tooltip } from "@/components/Tooltip";
87
import { usePersistedState } from "@/hooks/usePersistedState";
98
import type { ReviewFilters, ReviewStats } from "@/types/review";
9+
import { RefreshButton } from "./RefreshButton";
1010

1111
interface ReviewControlsProps {
1212
filters: ReviewFilters;
1313
stats: ReviewStats;
1414
onFiltersChange: (filters: ReviewFilters) => void;
1515
onRefresh?: () => void;
16+
isLoading?: boolean;
1617
}
1718

1819
const ControlsContainer = styled.div`
@@ -74,27 +75,6 @@ const Separator = styled.div`
7475
background: #3e3e42;
7576
`;
7677

77-
const RefreshButton = styled.button`
78-
background: transparent;
79-
border: none;
80-
padding: 2px;
81-
cursor: pointer;
82-
display: flex;
83-
align-items: center;
84-
justify-content: center;
85-
color: #888;
86-
transition: color 0.2s ease;
87-
88-
&:hover {
89-
color: #ccc;
90-
}
91-
92-
svg {
93-
width: 12px;
94-
height: 12px;
95-
}
96-
`;
97-
9878
const CheckboxLabel = styled.label`
9979
display: flex;
10080
align-items: center;
@@ -136,6 +116,7 @@ export const ReviewControls: React.FC<ReviewControlsProps> = ({
136116
stats,
137117
onFiltersChange,
138118
onRefresh,
119+
isLoading = false,
139120
}) => {
140121
// Local state for input value - only commit on blur/Enter
141122
const [inputValue, setInputValue] = useState(filters.diffBase);
@@ -187,18 +168,7 @@ export const ReviewControls: React.FC<ReviewControlsProps> = ({
187168

188169
return (
189170
<ControlsContainer>
190-
{onRefresh && (
191-
<TooltipWrapper inline>
192-
<RefreshButton onClick={onRefresh}>
193-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
194-
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2" />
195-
</svg>
196-
</RefreshButton>
197-
<Tooltip position="bottom" align="left">
198-
Refresh diff
199-
</Tooltip>
200-
</TooltipWrapper>
201-
)}
171+
{onRefresh && <RefreshButton onClick={onRefresh} isLoading={isLoading} />}
202172
<Label>Base:</Label>
203173
<BaseInput
204174
type="text"

src/components/RightSidebar/CodeReview/ReviewPanel.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from "@/utils/git/numstatParser";
1919
import type { DiffHunk, ReviewFilters as ReviewFiltersType } from "@/types/review";
2020
import type { FileTreeNode } from "@/utils/git/numstatParser";
21+
import { matchesKeybind, KEYBINDS } from "@/utils/ui/keybinds";
2122

2223
interface ReviewPanelProps {
2324
workspaceId: string;
@@ -499,6 +500,19 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
499500
return () => window.removeEventListener("keydown", handleKeyDown);
500501
}, [isPanelFocused, selectedHunkId, filteredHunks]);
501502

503+
// Global keyboard shortcut for refresh (Ctrl+R / Cmd+R)
504+
useEffect(() => {
505+
const handleKeyDown = (e: KeyboardEvent) => {
506+
if (matchesKeybind(e, KEYBINDS.REFRESH_REVIEW)) {
507+
e.preventDefault();
508+
setRefreshTrigger((prev) => prev + 1);
509+
}
510+
};
511+
512+
window.addEventListener("keydown", handleKeyDown);
513+
return () => window.removeEventListener("keydown", handleKeyDown);
514+
}, []);
515+
502516
return (
503517
<PanelContainer
504518
tabIndex={0}
@@ -511,6 +525,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
511525
stats={stats}
512526
onFiltersChange={setFilters}
513527
onRefresh={() => setRefreshTrigger((prev) => prev + 1)}
528+
isLoading={isLoadingHunks || isLoadingTree}
514529
/>
515530

516531
{error ? (

src/main.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,15 @@ function createMenu() {
169169
{
170170
label: "View",
171171
submenu: [
172-
{ role: "reload" },
172+
// Reload without Ctrl+R shortcut (reserved for Code Review refresh)
173+
{
174+
label: "Reload",
175+
click: (_item, focusedWindow) => {
176+
if (focusedWindow && "reload" in focusedWindow) {
177+
(focusedWindow as BrowserWindow).reload();
178+
}
179+
},
180+
},
173181
{ role: "forceReload" },
174182
{ role: "toggleDevTools" },
175183
{ type: "separator" },

src/utils/ui/keybinds.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,8 @@ export const KEYBINDS = {
250250
/** Switch to Review tab in right sidebar */
251251
// macOS: Cmd+2, Win/Linux: Ctrl+2
252252
REVIEW_TAB: { key: "2", ctrl: true, description: "Review tab" },
253+
254+
/** Refresh diff in Code Review panel */
255+
// macOS: Cmd+R, Win/Linux: Ctrl+R
256+
REFRESH_REVIEW: { key: "r", ctrl: true },
253257
} as const;

0 commit comments

Comments
 (0)