Skip to content

Commit f7b8860

Browse files
committed
feat(git): add uncommitted review action to diff panel
1 parent 12fbef8 commit f7b8860

10 files changed

Lines changed: 92 additions & 13 deletions

File tree

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ function MainApp() {
588588
sendUserMessageToThread,
589589
startFork,
590590
startReview,
591+
startUncommittedReview,
591592
startResume,
592593
startCompact,
593594
startApps,
@@ -2263,6 +2264,7 @@ function MainApp() {
22632264
onUnstageGitFile: handleUnstageGitFile,
22642265
onRevertGitFile: handleRevertGitFile,
22652266
onRevertAllGitChanges: handleRevertAllGitChanges,
2267+
onReviewUncommittedChanges: startUncommittedReview,
22662268
gitDiffs: activeDiffs,
22672269
gitDiffLoading: activeDiffLoading,
22682270
gitDiffError: activeDiffError,

src/features/git/components/GitDiffPanel.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ describe("GitDiffPanel", () => {
115115
expect(onCommit).toHaveBeenCalledTimes(1);
116116
});
117117

118+
it("runs uncommitted review from unstaged section actions", () => {
119+
const onReviewUncommittedChanges = vi.fn();
120+
render(
121+
<GitDiffPanel
122+
{...baseProps}
123+
onReviewUncommittedChanges={onReviewUncommittedChanges}
124+
unstagedFiles={[
125+
{ path: "src/file.ts", status: "M", additions: 4, deletions: 1 },
126+
]}
127+
/>,
128+
);
129+
130+
const reviewButton = screen.getByRole("button", {
131+
name: "Review uncommitted changes",
132+
});
133+
fireEvent.click(reviewButton);
134+
expect(onReviewUncommittedChanges).toHaveBeenCalledTimes(1);
135+
});
136+
118137
it("adds a show in file manager option for file context menus", async () => {
119138
clipboardWriteText.mockClear();
120139
const { container } = render(

src/features/git/components/GitDiffPanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ type GitDiffPanelProps = {
116116
onStageFile?: (path: string) => Promise<void> | void;
117117
onUnstageFile?: (path: string) => Promise<void> | void;
118118
onRevertFile?: (path: string) => Promise<void> | void;
119+
onReviewUncommittedChanges?: () => void | Promise<void>;
119120
logEntries: GitLogEntry[];
120121
selectedCommitSha?: string | null;
121122
onSelectCommit?: (entry: GitLogEntry) => void;
@@ -201,6 +202,7 @@ export function GitDiffPanel({
201202
onStageFile,
202203
onUnstageFile,
203204
onRevertFile,
205+
onReviewUncommittedChanges,
204206
onGitRootScanDepthChange,
205207
onScanGitRoots,
206208
onSelectGitRoot,
@@ -747,6 +749,7 @@ export function GitDiffPanel({
747749
onUnstageFile={onUnstageFile}
748750
onDiscardFile={onRevertFile ? discardFile : undefined}
749751
onDiscardFiles={onRevertFile ? discardFiles : undefined}
752+
onReviewUncommittedChanges={onReviewUncommittedChanges}
750753
selectedFiles={selectedFiles}
751754
selectedPath={selectedPath}
752755
onSelectFile={onSelectFile}

src/features/git/components/GitDiffPanelModeContent.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ type GitDiffModeContentProps = {
341341
onUnstageFile?: (path: string) => Promise<void> | void;
342342
onDiscardFile?: (path: string) => Promise<void> | void;
343343
onDiscardFiles?: (paths: string[]) => Promise<void> | void;
344+
onReviewUncommittedChanges?: () => void | Promise<void>;
344345
selectedFiles: Set<string>;
345346
selectedPath: string | null;
346347
onSelectFile?: (path: string) => void;
@@ -397,6 +398,7 @@ export function GitDiffModeContent({
397398
onUnstageFile,
398399
onDiscardFile,
399400
onDiscardFiles,
401+
onReviewUncommittedChanges,
400402
selectedFiles,
401403
selectedPath,
402404
onSelectFile,
@@ -413,6 +415,7 @@ export function GitDiffModeContent({
413415
: missingRepo
414416
? "This workspace isn't a Git repository yet."
415417
: "Choose a repo for this workspace.";
418+
const generateCommitMessageTooltip = "Generate commit message";
416419

417420
return (
418421
<div className="diff-list" onClick={onDiffListClick}>
@@ -524,19 +527,17 @@ export function GitDiffModeContent({
524527
/>
525528
<button
526529
type="button"
527-
className="commit-message-generate-button"
530+
className="commit-message-generate-button diff-row-action"
528531
onClick={() => {
529532
if (!canGenerateCommitMessage) {
530533
return;
531534
}
532535
void onGenerateCommitMessage?.();
533536
}}
534537
disabled={commitMessageLoading || !canGenerateCommitMessage}
535-
title={
536-
stagedFiles.length > 0
537-
? "Generate commit message from staged changes"
538-
: "Generate commit message from unstaged changes"
539-
}
538+
title={generateCommitMessageTooltip}
539+
data-tooltip={generateCommitMessageTooltip}
540+
data-tooltip-placement="bottom"
540541
aria-label="Generate commit message"
541542
>
542543
{commitMessageLoading ? (
@@ -649,6 +650,7 @@ export function GitDiffModeContent({
649650
onStageFile={onStageFile}
650651
onDiscardFile={onDiscardFile}
651652
onDiscardFiles={onDiscardFiles}
653+
onReviewUncommittedChanges={onReviewUncommittedChanges}
652654
onFileClick={onFileClick}
653655
onShowFileMenu={onShowFileMenu}
654656
/>

src/features/git/components/GitDiffPanelShared.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Plus from "lucide-react/dist/esm/icons/plus";
66
import RotateCcw from "lucide-react/dist/esm/icons/rotate-ccw";
77
import Upload from "lucide-react/dist/esm/icons/upload";
88
import X from "lucide-react/dist/esm/icons/x";
9+
import { MagicSparkleIcon } from "../../shared/components/MagicSparkleIcon";
910
import { formatRelativeTime } from "../../../utils/time";
1011
import {
1112
getStatusClass,
@@ -263,6 +264,7 @@ type DiffSectionProps = {
263264
onUnstageFile?: (path: string) => Promise<void> | void;
264265
onDiscardFile?: (path: string) => Promise<void> | void;
265266
onDiscardFiles?: (paths: string[]) => Promise<void> | void;
267+
onReviewUncommittedChanges?: () => Promise<void> | void;
266268
onFileClick: (
267269
event: ReactMouseEvent<HTMLDivElement>,
268270
path: string,
@@ -287,6 +289,7 @@ export function DiffSection({
287289
onUnstageFile,
288290
onDiscardFile,
289291
onDiscardFiles,
292+
onReviewUncommittedChanges,
290293
onFileClick,
291294
onShowFileMenu,
292295
}: DiffSectionProps) {
@@ -297,7 +300,12 @@ export function DiffSection({
297300
filePaths.length > 0;
298301
const canUnstageAll = section === "staged" && Boolean(onUnstageFile) && filePaths.length > 0;
299302
const canDiscardAll = section === "unstaged" && Boolean(onDiscardFiles) && filePaths.length > 0;
300-
const showSectionActions = canStageAll || canUnstageAll || canDiscardAll;
303+
const canReviewUncommitted =
304+
section === "unstaged" &&
305+
Boolean(onReviewUncommittedChanges) &&
306+
filePaths.length > 0;
307+
const showSectionActions =
308+
canStageAll || canUnstageAll || canDiscardAll || canReviewUncommitted;
301309

302310
return (
303311
<div className="diff-section">
@@ -307,6 +315,19 @@ export function DiffSection({
307315
</span>
308316
{showSectionActions && (
309317
<div className="diff-section-actions" role="group" aria-label={`${title} actions`}>
318+
{canReviewUncommitted && (
319+
<button
320+
type="button"
321+
className="diff-row-action diff-row-action--review"
322+
onClick={() => {
323+
void onReviewUncommittedChanges?.();
324+
}}
325+
data-tooltip="Review Uncommitted Changes"
326+
aria-label="Review uncommitted changes"
327+
>
328+
<MagicSparkleIcon size={12} />
329+
</button>
330+
)}
310331
{canStageAll && (
311332
<button
312333
type="button"

src/features/layout/hooks/layoutNodes/buildGitNodes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export function buildGitNodes(options: LayoutNodesOptions): GitLayoutNodes {
137137
onUnstageFile={options.onUnstageGitFile}
138138
onRevertFile={options.onRevertGitFile}
139139
onRevertAllChanges={options.onRevertAllGitChanges}
140+
onReviewUncommittedChanges={options.onReviewUncommittedChanges}
140141
commitMessage={options.commitMessage}
141142
commitMessageLoading={options.commitMessageLoading}
142143
commitMessageError={options.commitMessageError}

src/features/layout/hooks/layoutNodes/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ export type LayoutNodesOptions = {
330330
onUnstageGitFile: (path: string) => Promise<void>;
331331
onRevertGitFile: (path: string) => Promise<void>;
332332
onRevertAllGitChanges: () => Promise<void>;
333+
onReviewUncommittedChanges: () => Promise<void>;
333334
diffSource: GitDiffSource;
334335
gitDiffs: GitDiffViewerItem[];
335336
gitDiffLoading: boolean;

src/features/threads/hooks/useThreadMessaging.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,10 @@ export function useThreadMessaging({
657657
],
658658
);
659659

660+
const startUncommittedReview = useCallback(async () => {
661+
await startReviewTarget({ type: "uncommittedChanges" });
662+
}, [startReviewTarget]);
663+
660664
const startStatus = useCallback(
661665
async (_text: string) => {
662666
if (!activeWorkspace) {
@@ -1027,6 +1031,7 @@ export function useThreadMessaging({
10271031
sendUserMessageToThread,
10281032
startFork,
10291033
startReview,
1034+
startUncommittedReview,
10301035
startResume,
10311036
startCompact,
10321037
startApps,

src/features/threads/hooks/useThreads.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,7 @@ export function useThreads({
688688
sendUserMessageToThread,
689689
startFork,
690690
startReview,
691+
startUncommittedReview,
691692
startResume,
692693
startCompact,
693694
startApps,
@@ -854,6 +855,7 @@ export function useThreads({
854855
sendUserMessageToThread,
855856
startFork,
856857
startReview,
858+
startUncommittedReview,
857859
startResume,
858860
startCompact,
859861
startApps,

src/styles/diff.css

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,12 @@
622622
color: #ff6b6b;
623623
}
624624

625+
.diff-row-action--review:hover {
626+
background: rgba(90, 169, 255, 0.14);
627+
border-color: rgba(90, 169, 255, 0.35);
628+
color: #5aa9ff;
629+
}
630+
625631
.diff-row-action--apply:hover {
626632
background: rgba(90, 169, 255, 0.14);
627633
border-color: rgba(90, 169, 255, 0.35);
@@ -667,18 +673,30 @@
667673
border-top: 1px solid var(--border-subtle);
668674
}
669675

670-
.diff-row-action:last-child[data-tooltip]::after {
676+
.diff-row-action:last-child[data-tooltip]:not([data-tooltip-placement="bottom"])::after {
671677
left: auto;
672678
right: 0;
673679
transform: translateX(0) translateY(4px);
674680
}
675681

676-
.diff-row-action:last-child[data-tooltip]::before {
682+
.diff-row-action:last-child[data-tooltip]:not([data-tooltip-placement="bottom"])::before {
677683
left: auto;
678684
right: 8px;
679685
transform: translateX(0) translateY(4px) rotate(45deg);
680686
}
681687

688+
.diff-row-action[data-tooltip][data-tooltip-placement="bottom"]::after {
689+
top: calc(100% + 8px);
690+
bottom: auto;
691+
left: auto;
692+
right: 0;
693+
transform: translateY(-4px);
694+
}
695+
696+
.diff-row-action[data-tooltip][data-tooltip-placement="bottom"]::before {
697+
display: none;
698+
}
699+
682700
.diff-row-action:hover::before,
683701
.diff-row-action:hover::after,
684702
.diff-row-action:focus-visible::before,
@@ -692,13 +710,18 @@
692710
transform: translateX(-50%) translateY(0) rotate(45deg);
693711
}
694712

695-
.diff-row-action:last-child:hover::after,
696-
.diff-row-action:last-child:focus-visible::after {
713+
.diff-row-action[data-tooltip][data-tooltip-placement="bottom"]:hover::after,
714+
.diff-row-action[data-tooltip][data-tooltip-placement="bottom"]:focus-visible::after {
715+
transform: translateY(0);
716+
}
717+
718+
.diff-row-action:last-child:hover:not([data-tooltip-placement="bottom"])::after,
719+
.diff-row-action:last-child:focus-visible:not([data-tooltip-placement="bottom"])::after {
697720
transform: translateX(0) translateY(0);
698721
}
699722

700-
.diff-row-action:last-child:hover::before,
701-
.diff-row-action:last-child:focus-visible::before {
723+
.diff-row-action:last-child:hover:not([data-tooltip-placement="bottom"])::before,
724+
.diff-row-action:last-child:focus-visible:not([data-tooltip-placement="bottom"])::before {
702725
transform: translateX(0) translateY(0) rotate(45deg);
703726
}
704727

0 commit comments

Comments
 (0)