mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
feat: add discard changes functionality for worktrees
- Introduced a new POST /discard-changes endpoint to discard all uncommitted changes in a worktree, including resetting staged changes, discarding modifications to tracked files, and removing untracked files. - Implemented a corresponding handler in the UI to confirm and execute the discard operation, enhancing user control over worktree changes. - Added a ViewWorktreeChangesDialog component to display changes in the worktree, improving the user experience for managing worktree states. - Updated the WorktreePanel and WorktreeActionsDropdown components to integrate the new functionality, allowing users to view and discard changes directly from the UI. This update streamlines the management of worktree changes, providing users with essential tools for version control.
This commit is contained in:
@@ -8,3 +8,4 @@ export { EditFeatureDialog } from './edit-feature-dialog';
|
||||
export { FollowUpDialog, type FollowUpHistoryEntry } from './follow-up-dialog';
|
||||
export { PlanApprovalDialog } from './plan-approval-dialog';
|
||||
export { MassEditDialog } from './mass-edit-dialog';
|
||||
export { ViewWorktreeChangesDialog } from './view-worktree-changes-dialog';
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { FileText } from 'lucide-react';
|
||||
import { GitDiffPanel } from '@/components/ui/git-diff-panel';
|
||||
|
||||
interface WorktreeInfo {
|
||||
path: string;
|
||||
branch: string;
|
||||
isMain: boolean;
|
||||
hasChanges?: boolean;
|
||||
changedFilesCount?: number;
|
||||
}
|
||||
|
||||
interface ViewWorktreeChangesDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
worktree: WorktreeInfo | null;
|
||||
projectPath: string;
|
||||
}
|
||||
|
||||
export function ViewWorktreeChangesDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
worktree,
|
||||
projectPath,
|
||||
}: ViewWorktreeChangesDialogProps) {
|
||||
if (!worktree) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-full h-full max-w-full max-h-full sm:w-[90vw] sm:max-w-[900px] sm:max-h-[100vh] sm:h-auto sm:rounded-xl rounded-none flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
View Changes
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Changes in the{' '}
|
||||
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code> worktree.
|
||||
{worktree.changedFilesCount !== undefined && worktree.changedFilesCount > 0 && (
|
||||
<span className="ml-1">
|
||||
({worktree.changedFilesCount} file
|
||||
{worktree.changedFilesCount > 1 ? 's' : ''} changed)
|
||||
</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 sm:min-h-[600px] sm:max-h-[60vh] overflow-y-auto scrollbar-visible -mx-6 -mb-6">
|
||||
<div className="h-full px-6 pb-6">
|
||||
<GitDiffPanel
|
||||
projectPath={projectPath}
|
||||
featureId={worktree.branch}
|
||||
useWorktrees={true}
|
||||
compact={false}
|
||||
className="mt-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -25,11 +25,13 @@ import {
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
Copy,
|
||||
Eye,
|
||||
ScrollText,
|
||||
Terminal,
|
||||
SquarePlus,
|
||||
SplitSquareHorizontal,
|
||||
Zap,
|
||||
Undo2,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -65,6 +67,8 @@ interface WorktreeActionsDropdownProps {
|
||||
onOpenInEditor: (worktree: WorktreeInfo, editorCommand?: string) => void;
|
||||
onOpenInIntegratedTerminal: (worktree: WorktreeInfo, mode?: 'tab' | 'split') => void;
|
||||
onOpenInExternalTerminal: (worktree: WorktreeInfo, terminalId?: string) => void;
|
||||
onViewChanges: (worktree: WorktreeInfo) => void;
|
||||
onDiscardChanges: (worktree: WorktreeInfo) => void;
|
||||
onCommit: (worktree: WorktreeInfo) => void;
|
||||
onCreatePR: (worktree: WorktreeInfo) => void;
|
||||
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
@@ -99,6 +103,8 @@ export function WorktreeActionsDropdown({
|
||||
onOpenInEditor,
|
||||
onOpenInIntegratedTerminal,
|
||||
onOpenInExternalTerminal,
|
||||
onViewChanges,
|
||||
onDiscardChanges,
|
||||
onCommit,
|
||||
onCreatePR,
|
||||
onAddressPRComments,
|
||||
@@ -434,6 +440,13 @@ export function WorktreeActionsDropdown({
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{worktree.hasChanges && (
|
||||
<DropdownMenuItem onClick={() => onViewChanges(worktree)} className="text-xs">
|
||||
<Eye className="w-3.5 h-3.5 mr-2" />
|
||||
View Changes
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{worktree.hasChanges && (
|
||||
<TooltipWrapper
|
||||
showTooltip={!gitRepoStatus.isGitRepo}
|
||||
@@ -509,9 +522,30 @@ export function WorktreeActionsDropdown({
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{worktree.hasChanges && (
|
||||
<TooltipWrapper
|
||||
showTooltip={!gitRepoStatus.isGitRepo}
|
||||
tooltipContent="Not a git repository"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => gitRepoStatus.isGitRepo && onDiscardChanges(worktree)}
|
||||
disabled={!gitRepoStatus.isGitRepo}
|
||||
className={cn(
|
||||
'text-xs text-destructive focus:text-destructive',
|
||||
!gitRepoStatus.isGitRepo && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Undo2 className="w-3.5 h-3.5 mr-2" />
|
||||
Discard Changes
|
||||
{!gitRepoStatus.isGitRepo && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
{!worktree.isMain && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDeleteWorktree(worktree)}
|
||||
className="text-xs text-destructive focus:text-destructive"
|
||||
|
||||
@@ -42,6 +42,8 @@ interface WorktreeTabProps {
|
||||
onOpenInEditor: (worktree: WorktreeInfo, editorCommand?: string) => void;
|
||||
onOpenInIntegratedTerminal: (worktree: WorktreeInfo, mode?: 'tab' | 'split') => void;
|
||||
onOpenInExternalTerminal: (worktree: WorktreeInfo, terminalId?: string) => void;
|
||||
onViewChanges: (worktree: WorktreeInfo) => void;
|
||||
onDiscardChanges: (worktree: WorktreeInfo) => void;
|
||||
onCommit: (worktree: WorktreeInfo) => void;
|
||||
onCreatePR: (worktree: WorktreeInfo) => void;
|
||||
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
@@ -90,6 +92,8 @@ export function WorktreeTab({
|
||||
onOpenInEditor,
|
||||
onOpenInIntegratedTerminal,
|
||||
onOpenInExternalTerminal,
|
||||
onViewChanges,
|
||||
onDiscardChanges,
|
||||
onCommit,
|
||||
onCreatePR,
|
||||
onAddressPRComments,
|
||||
@@ -375,6 +379,8 @@ export function WorktreeTab({
|
||||
onOpenInEditor={onOpenInEditor}
|
||||
onOpenInIntegratedTerminal={onOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={onOpenInExternalTerminal}
|
||||
onViewChanges={onViewChanges}
|
||||
onDiscardChanges={onDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
|
||||
@@ -22,6 +22,9 @@ import {
|
||||
BranchSwitchDropdown,
|
||||
} from './components';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { ViewWorktreeChangesDialog } from '../dialogs';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { Undo2 } from 'lucide-react';
|
||||
|
||||
export function WorktreePanel({
|
||||
projectPath,
|
||||
@@ -156,6 +159,14 @@ export function WorktreePanel({
|
||||
// Track whether init script exists for the project
|
||||
const [hasInitScript, setHasInitScript] = useState(false);
|
||||
|
||||
// View changes dialog state
|
||||
const [viewChangesDialogOpen, setViewChangesDialogOpen] = useState(false);
|
||||
const [viewChangesWorktree, setViewChangesWorktree] = useState<WorktreeInfo | null>(null);
|
||||
|
||||
// Discard changes confirmation dialog state
|
||||
const [discardChangesDialogOpen, setDiscardChangesDialogOpen] = useState(false);
|
||||
const [discardChangesWorktree, setDiscardChangesWorktree] = useState<WorktreeInfo | null>(null);
|
||||
|
||||
// Log panel state management
|
||||
const [logPanelOpen, setLogPanelOpen] = useState(false);
|
||||
const [logPanelWorktree, setLogPanelWorktree] = useState<WorktreeInfo | null>(null);
|
||||
@@ -242,6 +253,41 @@ export function WorktreePanel({
|
||||
[projectPath]
|
||||
);
|
||||
|
||||
const handleViewChanges = useCallback((worktree: WorktreeInfo) => {
|
||||
setViewChangesWorktree(worktree);
|
||||
setViewChangesDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDiscardChanges = useCallback((worktree: WorktreeInfo) => {
|
||||
setDiscardChangesWorktree(worktree);
|
||||
setDiscardChangesDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDiscardChanges = useCallback(async () => {
|
||||
if (!discardChangesWorktree) return;
|
||||
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.worktree.discardChanges(discardChangesWorktree.path);
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Changes discarded', {
|
||||
description: `Discarded changes in ${discardChangesWorktree.branch}`,
|
||||
});
|
||||
// Refresh worktrees to update the changes status
|
||||
fetchWorktrees({ silent: true });
|
||||
} else {
|
||||
toast.error('Failed to discard changes', {
|
||||
description: result.error || 'Unknown error',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to discard changes', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}, [discardChangesWorktree, fetchWorktrees]);
|
||||
|
||||
// Handle opening the log panel for a specific worktree
|
||||
const handleViewDevServerLogs = useCallback((worktree: WorktreeInfo) => {
|
||||
setLogPanelWorktree(worktree);
|
||||
@@ -312,6 +358,8 @@ export function WorktreePanel({
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||
onViewChanges={handleViewChanges}
|
||||
onDiscardChanges={handleDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
@@ -357,6 +405,36 @@ export function WorktreePanel({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* View Changes Dialog */}
|
||||
<ViewWorktreeChangesDialog
|
||||
open={viewChangesDialogOpen}
|
||||
onOpenChange={setViewChangesDialogOpen}
|
||||
worktree={viewChangesWorktree}
|
||||
projectPath={projectPath}
|
||||
/>
|
||||
|
||||
{/* Discard Changes Confirmation Dialog */}
|
||||
<ConfirmDialog
|
||||
open={discardChangesDialogOpen}
|
||||
onOpenChange={setDiscardChangesDialogOpen}
|
||||
onConfirm={handleConfirmDiscardChanges}
|
||||
title="Discard Changes"
|
||||
description={`Are you sure you want to discard all changes in "${discardChangesWorktree?.branch}"? This will reset staged changes, discard modifications to tracked files, and remove untracked files. This action cannot be undone.`}
|
||||
icon={Undo2}
|
||||
iconClassName="text-destructive"
|
||||
confirmText="Discard Changes"
|
||||
confirmVariant="destructive"
|
||||
/>
|
||||
|
||||
{/* Dev Server Logs Panel */}
|
||||
<DevServerLogsPanel
|
||||
open={logPanelOpen}
|
||||
onClose={handleCloseLogPanel}
|
||||
worktree={logPanelWorktree}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -403,6 +481,8 @@ export function WorktreePanel({
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||
onViewChanges={handleViewChanges}
|
||||
onDiscardChanges={handleDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
@@ -465,6 +545,8 @@ export function WorktreePanel({
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||
onViewChanges={handleViewChanges}
|
||||
onDiscardChanges={handleDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
@@ -511,6 +593,27 @@ export function WorktreePanel({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* View Changes Dialog */}
|
||||
<ViewWorktreeChangesDialog
|
||||
open={viewChangesDialogOpen}
|
||||
onOpenChange={setViewChangesDialogOpen}
|
||||
worktree={viewChangesWorktree}
|
||||
projectPath={projectPath}
|
||||
/>
|
||||
|
||||
{/* Discard Changes Confirmation Dialog */}
|
||||
<ConfirmDialog
|
||||
open={discardChangesDialogOpen}
|
||||
onOpenChange={setDiscardChangesDialogOpen}
|
||||
onConfirm={handleConfirmDiscardChanges}
|
||||
title="Discard Changes"
|
||||
description={`Are you sure you want to discard all changes in "${discardChangesWorktree?.branch}"? This will reset staged changes, discard modifications to tracked files, and remove untracked files. This action cannot be undone.`}
|
||||
icon={Undo2}
|
||||
iconClassName="text-destructive"
|
||||
confirmText="Discard Changes"
|
||||
confirmVariant="destructive"
|
||||
/>
|
||||
|
||||
{/* Dev Server Logs Panel */}
|
||||
<DevServerLogsPanel
|
||||
open={logPanelOpen}
|
||||
|
||||
@@ -2024,6 +2024,20 @@ function createMockWorktreeAPI(): WorktreeAPI {
|
||||
console.log('[Mock] Unsubscribing from init script events');
|
||||
};
|
||||
},
|
||||
|
||||
discardChanges: async (worktreePath: string) => {
|
||||
console.log('[Mock] Discarding changes:', { worktreePath });
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
discarded: true,
|
||||
filesDiscarded: 0,
|
||||
filesRemaining: 0,
|
||||
branch: 'main',
|
||||
message: 'Mock: Changes discarded successfully',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1851,6 +1851,8 @@ export class HttpApiClient implements ElectronAPI {
|
||||
this.httpDelete('/api/worktree/init-script', { projectPath }),
|
||||
runInitScript: (projectPath: string, worktreePath: string, branch: string) =>
|
||||
this.post('/api/worktree/run-init-script', { projectPath, worktreePath, branch }),
|
||||
discardChanges: (worktreePath: string) =>
|
||||
this.post('/api/worktree/discard-changes', { worktreePath }),
|
||||
onInitScriptEvent: (
|
||||
callback: (event: {
|
||||
type: 'worktree:init-started' | 'worktree:init-output' | 'worktree:init-completed';
|
||||
|
||||
13
apps/ui/src/types/electron.d.ts
vendored
13
apps/ui/src/types/electron.d.ts
vendored
@@ -1218,6 +1218,19 @@ export interface WorktreeAPI {
|
||||
payload: unknown;
|
||||
}) => void
|
||||
) => () => void;
|
||||
|
||||
// Discard changes for a worktree
|
||||
discardChanges: (worktreePath: string) => Promise<{
|
||||
success: boolean;
|
||||
result?: {
|
||||
discarded: boolean;
|
||||
filesDiscarded: number;
|
||||
filesRemaining: number;
|
||||
branch: string;
|
||||
message: string;
|
||||
};
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface GitAPI {
|
||||
|
||||
Reference in New Issue
Block a user