fix: Address code review comments

This commit is contained in:
gsxdsm
2026-02-17 23:04:42 -08:00
parent 43c19c70ca
commit dd4c738e91
43 changed files with 1128 additions and 359 deletions

View File

@@ -174,6 +174,7 @@ export function CherryPickDialog({
setCommitsError(null);
setCommitLimit(30);
setHasMoreCommits(false);
setLoadingBranches(false);
}
}, [open]);
@@ -321,9 +322,7 @@ export function CherryPickDialog({
} else {
// Check for conflicts
const errorMessage = result.error || '';
const hasConflicts =
errorMessage.toLowerCase().includes('conflict') ||
(result as { hasConflicts?: boolean }).hasConflicts;
const hasConflicts = errorMessage.toLowerCase().includes('conflict') || result.hasConflicts;
if (hasConflicts && onCreateConflictResolutionFeature) {
setConflictInfo({
@@ -333,7 +332,7 @@ export function CherryPickDialog({
});
setStep('conflict');
toast.error('Cherry-pick conflicts detected', {
description: 'The cherry-pick has conflicts that need to be resolved.',
description: 'The cherry-pick was aborted due to conflicts. No changes were applied.',
});
} else {
toast.error('Cherry-pick failed', {
@@ -359,7 +358,7 @@ export function CherryPickDialog({
});
setStep('conflict');
toast.error('Cherry-pick conflicts detected', {
description: 'The cherry-pick has conflicts that need to be resolved.',
description: 'The cherry-pick was aborted due to conflicts. No changes were applied.',
});
} else {
toast.error('Cherry-pick failed', {
@@ -469,7 +468,7 @@ export function CherryPickDialog({
<DialogContent className="w-full h-full max-w-full max-h-full sm:w-[90vw] sm:max-w-[640px] sm:max-h-[100dvh] sm:h-auto sm:rounded-xl rounded-none flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Cherry className="w-5 h-5 text-black dark:text-black" />
<Cherry className="w-5 h-5 text-foreground" />
Cherry Pick Commits
</DialogTitle>
<DialogDescription>
@@ -673,7 +672,7 @@ export function CherryPickDialog({
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Cherry className="w-5 h-5 text-black dark:text-black" />
<Cherry className="w-5 h-5 text-foreground" />
Cherry Pick
</DialogTitle>
<DialogDescription asChild>

View File

@@ -407,7 +407,7 @@ export function CommitWorktreeDialog({
const handleKeyDown = (e: React.KeyboardEvent) => {
if (
e.key === 'Enter' &&
e.metaKey &&
(e.metaKey || e.ctrlKey) &&
!isLoading &&
!isGenerating &&
message.trim() &&
@@ -658,7 +658,8 @@ export function CommitWorktreeDialog({
</div>
<p className="text-xs text-muted-foreground">
Press <kbd className="px-1 py-0.5 bg-muted rounded text-xs">Cmd+Enter</kbd> to commit
Press <kbd className="px-1 py-0.5 bg-muted rounded text-xs">Cmd/Ctrl+Enter</kbd> to
commit
</p>
</div>

View File

@@ -78,9 +78,13 @@ export function CreateBranchDialog({
if (result.success && result.result) {
setBranches(result.result.branches);
// Default to current branch
if (result.result.currentBranch) {
setBaseBranch(result.result.currentBranch);
// Only set the default base branch if no branch is currently selected,
// or if the currently selected branch is no longer present in the fetched list
const branchNames = result.result.branches.map((b: BranchInfo) => b.name);
if (!baseBranch || !branchNames.includes(baseBranch)) {
if (result.result.currentBranch) {
setBaseBranch(result.result.currentBranch);
}
}
}
} catch (err) {
@@ -88,7 +92,7 @@ export function CreateBranchDialog({
} finally {
setIsLoadingBranches(false);
}
}, [worktree]);
}, [worktree, baseBranch]);
// Reset state and fetch branches when dialog opens
useEffect(() => {

View File

@@ -75,6 +75,12 @@ export function CreatePRDialog({
const [remotes, setRemotes] = useState<RemoteInfo[]>([]);
const [selectedRemote, setSelectedRemote] = useState<string>('');
const [isLoadingRemotes, setIsLoadingRemotes] = useState(false);
// Keep a ref in sync with selectedRemote so fetchRemotes can read the latest value
// without needing it in its dependency array (which would cause re-fetch loops)
const selectedRemoteRef = useRef<string>(selectedRemote);
useEffect(() => {
selectedRemoteRef.current = selectedRemote;
}, [selectedRemote]);
// Generate description state
const [isGeneratingDescription, setIsGeneratingDescription] = useState(false);
@@ -110,10 +116,16 @@ export function CreatePRDialog({
);
setRemotes(remoteInfos);
// Auto-select 'origin' if available, otherwise first remote
// Preserve existing selection if it's still valid; otherwise fall back to 'origin' or first remote
if (remoteInfos.length > 0) {
const defaultRemote = remoteInfos.find((r) => r.name === 'origin') || remoteInfos[0];
setSelectedRemote(defaultRemote.name);
const remoteNames = remoteInfos.map((r) => r.name);
const currentSelection = selectedRemoteRef.current;
const currentSelectionStillExists =
currentSelection !== '' && remoteNames.includes(currentSelection);
if (!currentSelectionStillExists) {
const defaultRemote = remoteInfos.find((r) => r.name === 'origin') || remoteInfos[0];
setSelectedRemote(defaultRemote.name);
}
}
}
} catch {

View File

@@ -319,6 +319,7 @@ export function DiscardWorktreeChangesDialog({
}
} catch (err) {
console.warn('Failed to load diffs for discard dialog:', err);
setError(err instanceof Error ? err.message : String(err));
} finally {
setIsLoadingDiffs(false);
}
@@ -370,7 +371,7 @@ export function DiscardWorktreeChangesDialog({
if (result.success && result.result) {
if (result.result.discarded) {
const fileCount = filesToDiscard ? filesToDiscard.length : result.result.filesDiscarded;
const fileCount = filesToDiscard ? filesToDiscard.length : selectedFiles.size;
toast.success('Changes discarded', {
description: `Discarded ${fileCount} ${fileCount === 1 ? 'file' : 'files'} in ${worktree.branch}`,
});

View File

@@ -167,10 +167,15 @@ export function MergeRebaseDialog({
}
};
const handleConfirm = () => {
const handleConfirm = async () => {
if (!worktree || !selectedBranch) return;
onConfirm(worktree, selectedBranch, selectedStrategy);
onOpenChange(false);
try {
await onConfirm(worktree, selectedBranch, selectedStrategy);
onOpenChange(false);
} catch (err) {
logger.error('Failed to confirm merge/rebase:', err);
throw err;
}
};
const selectedRemoteData = remotes.find((r) => r.name === selectedRemote);
@@ -347,7 +352,11 @@ export function MergeRebaseDialog({
className="bg-purple-600 hover:bg-purple-700 text-white"
>
<GitMerge className="w-4 h-4 mr-2" />
Merge & Rebase
{selectedStrategy === 'merge'
? 'Merge'
: selectedStrategy === 'rebase'
? 'Rebase'
: 'Merge & Rebase'}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -69,6 +69,12 @@ export function SelectRemoteDialog({
url: r.url,
}));
setRemotes(remoteInfos);
setSelectedRemote((prev) => {
if (prev && remoteInfos.some((r) => r.name === prev)) {
return prev;
}
return remoteInfos.find((r) => r.name === 'origin')?.name ?? remoteInfos[0]?.name ?? '';
});
} else {
setError(result.error || 'Failed to fetch remotes');
}
@@ -120,6 +126,12 @@ export function SelectRemoteDialog({
url: r.url,
}));
setRemotes(remoteInfos);
setSelectedRemote((prev) => {
if (prev && remoteInfos.some((r) => r.name === prev)) {
return prev;
}
return remoteInfos.find((r) => r.name === 'origin')?.name ?? remoteInfos[0]?.name ?? '';
});
} else {
setError(result.error || 'Failed to refresh remotes');
}

View File

@@ -132,6 +132,9 @@ function parseDiff(diffText: string): ParsedFileDiff[] {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Skip trailing empty string produced by a final newline in diffText
if (line === '' && i === lines.length - 1) continue;
if (line.startsWith('diff --git')) {
if (currentFile) {
if (currentHunk) currentFile.hunks.push(currentHunk);

View File

@@ -47,7 +47,9 @@ interface ViewCommitsDialogProps {
}
function formatRelativeDate(dateStr: string): string {
if (!dateStr) return 'unknown date';
const date = new Date(dateStr);
if (isNaN(date.getTime())) return 'unknown date';
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffSecs = Math.floor(diffMs / 1000);

View File

@@ -280,6 +280,8 @@ export function useBoardActions({
});
}
}
return createdFeature;
},
[
addFeature,
@@ -1221,13 +1223,12 @@ export function useBoardActions({
dependencies: [parentFeature.id],
};
await handleAddFeature(duplicatedFeatureData);
const newFeature = await handleAddFeature(duplicatedFeatureData);
// Get the newly created feature (last added feature) to use as parent for next iteration
const currentFeatures = useAppStore.getState().features;
const newestFeature = currentFeatures[currentFeatures.length - 1];
if (newestFeature) {
parentFeature = newestFeature;
// Use the returned feature directly as the parent for the next iteration,
// avoiding a fragile assumption that the newest feature is the last item in the store
if (newFeature) {
parentFeature = newFeature;
}
}

View File

@@ -451,31 +451,28 @@ export function WorktreeActionsDropdown({
)}
{/* Stash operations - combined submenu */}
{(onStashChanges || onViewStashes) && (
<TooltipWrapper
showTooltip={!gitRepoStatus.isGitRepo}
tooltipContent="Not a git repository"
>
<TooltipWrapper showTooltip={!canPerformGitOps} tooltipContent={gitOpsDisabledReason}>
<DropdownMenuSub>
<div className="flex items-center">
{/* Main clickable area - stash changes (primary action) */}
<DropdownMenuItem
onClick={() => {
if (!gitRepoStatus.isGitRepo) return;
if (!canPerformGitOps) return;
if (worktree.hasChanges && onStashChanges) {
onStashChanges(worktree);
} else if (onViewStashes) {
onViewStashes(worktree);
}
}}
disabled={!gitRepoStatus.isGitRepo}
disabled={!canPerformGitOps}
className={cn(
'text-xs flex-1 pr-0 rounded-r-none',
!gitRepoStatus.isGitRepo && 'opacity-50 cursor-not-allowed'
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
)}
>
<Archive className="w-3.5 h-3.5 mr-2" />
{worktree.hasChanges && onStashChanges ? 'Stash Changes' : 'Stashes'}
{!gitRepoStatus.isGitRepo && (
{!canPerformGitOps && (
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
)}
</DropdownMenuItem>
@@ -483,9 +480,9 @@ export function WorktreeActionsDropdown({
<DropdownMenuSubTrigger
className={cn(
'text-xs px-1 rounded-l-none border-l border-border/30 h-8',
!gitRepoStatus.isGitRepo && 'opacity-50 cursor-not-allowed'
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
)}
disabled={!gitRepoStatus.isGitRepo}
disabled={!canPerformGitOps}
/>
</div>
<DropdownMenuSubContent>

View File

@@ -20,6 +20,12 @@ interface UseWorktreeActionsOptions {
branchName: string;
previousBranch: string;
}) => void;
/** Callback when checkout fails AND the stash-pop restoration produces merge conflicts */
onStashPopConflict?: (info: {
worktreePath: string;
branchName: string;
stashPopConflictMessage: string;
}) => void;
}
export function useWorktreeActions(options?: UseWorktreeActionsOptions) {
@@ -29,6 +35,7 @@ export function useWorktreeActions(options?: UseWorktreeActionsOptions) {
// Use React Query mutations
const switchBranchMutation = useSwitchBranch({
onConflict: options?.onBranchSwitchConflict,
onStashPopConflict: options?.onStashPopConflict,
});
const pullMutation = usePullWorktree();
const pushMutation = usePushWorktree();

View File

@@ -86,6 +86,13 @@ export interface BranchSwitchConflictInfo {
previousBranch: string;
}
/** Info passed when a checkout failure triggers a stash-pop that itself produces conflicts */
export interface StashPopConflictInfo {
worktreePath: string;
branchName: string;
stashPopConflictMessage: string;
}
export interface WorktreePanelProps {
projectPath: string;
onCreateWorktree: () => void;
@@ -98,6 +105,8 @@ export interface WorktreePanelProps {
onCreateMergeConflictResolutionFeature?: (conflictInfo: MergeConflictInfo) => void;
/** Called when branch switch stash reapply results in merge conflicts */
onBranchSwitchConflict?: (conflictInfo: BranchSwitchConflictInfo) => void;
/** Called when checkout fails and the stash-pop restoration itself produces merge conflicts */
onStashPopConflict?: (conflictInfo: StashPopConflictInfo) => void;
/** Called when a branch is deleted during merge - features should be reassigned to main */
onBranchDeletedDuringMerge?: (branchName: string) => void;
onRemovedWorktrees?: (removedWorktrees: Array<{ path: string; branch: string }>) => void;

View File

@@ -60,6 +60,7 @@ export function WorktreePanel({
onResolveConflicts,
onCreateMergeConflictResolutionFeature,
onBranchSwitchConflict,
onStashPopConflict,
onBranchDeletedDuringMerge,
onRemovedWorktrees,
runningFeatureIds = [],
@@ -113,6 +114,7 @@ export function WorktreePanel({
handleOpenInExternalTerminal,
} = useWorktreeActions({
onBranchSwitchConflict: onBranchSwitchConflict,
onStashPopConflict: onStashPopConflict,
});
const { hasRunningFeatures } = useRunningFeatures({
@@ -563,8 +565,12 @@ export function WorktreePanel({
setSelectRemoteWorktree(worktree);
setSelectRemoteOperation('pull');
setSelectRemoteDialogOpen(true);
} else if (result.success && result.result && result.result.remotes.length === 1) {
// Exactly one remote - use it directly
const remoteName = result.result.remotes[0].name;
handlePull(worktree, remoteName);
} else {
// Single or no remote - proceed with default behavior
// No remotes - proceed with default behavior
handlePull(worktree);
}
} catch {
@@ -587,8 +593,12 @@ export function WorktreePanel({
setSelectRemoteWorktree(worktree);
setSelectRemoteOperation('push');
setSelectRemoteDialogOpen(true);
} else if (result.success && result.result && result.result.remotes.length === 1) {
// Exactly one remote - use it directly
const remoteName = result.result.remotes[0].name;
handlePush(worktree, remoteName);
} else {
// Single or no remote - proceed with default behavior
// No remotes - proceed with default behavior
handlePush(worktree);
}
} catch {