feat: Replace Select with Popover+Command for branch selection UI

This commit is contained in:
gsxdsm
2026-02-17 22:08:22 -08:00
parent 9af63bc1ef
commit cb99c4b4e8

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { import {
Dialog, Dialog,
@@ -11,21 +11,22 @@ import {
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { import {
Select, Command,
SelectContent, CommandEmpty,
SelectGroup, CommandGroup,
SelectItem, CommandInput,
SelectLabel, CommandItem,
SelectSeparator, CommandList,
SelectTrigger, CommandSeparator,
SelectValue, } from '@/components/ui/command';
} from '@/components/ui/select';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { getHttpApiClient } from '@/lib/http-api-client'; import { getHttpApiClient } from '@/lib/http-api-client';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { GitBranchPlus, RefreshCw } from 'lucide-react'; import { Check, ChevronsUpDown, GitBranchPlus, Globe, RefreshCw } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
interface WorktreeInfo { interface WorktreeInfo {
path: string; path: string;
@@ -62,6 +63,9 @@ export function CreateBranchDialog({
const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [isLoadingBranches, setIsLoadingBranches] = useState(false);
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [baseBranchPopoverOpen, setBaseBranchPopoverOpen] = useState(false);
const baseBranchTriggerRef = useRef<HTMLButtonElement>(null);
const [baseBranchTriggerWidth, setBaseBranchTriggerWidth] = useState<number>(0);
const fetchBranches = useCallback(async () => { const fetchBranches = useCallback(async () => {
if (!worktree) return; if (!worktree) return;
@@ -93,10 +97,23 @@ export function CreateBranchDialog({
setBaseBranch(''); setBaseBranch('');
setError(null); setError(null);
setBranches([]); setBranches([]);
setBaseBranchPopoverOpen(false);
fetchBranches(); fetchBranches();
} }
}, [open, fetchBranches]); }, [open, fetchBranches]);
// Track trigger width for popover sizing
useEffect(() => {
const el = baseBranchTriggerRef.current;
if (!el) return;
const observer = new ResizeObserver(() => {
setBaseBranchTriggerWidth(el.offsetWidth);
});
observer.observe(el);
setBaseBranchTriggerWidth(el.offsetWidth);
return () => observer.disconnect();
}, [baseBranchPopoverOpen]);
const handleCreate = async () => { const handleCreate = async () => {
if (!worktree || !branchName.trim()) return; if (!worktree || !branchName.trim()) return;
@@ -141,8 +158,16 @@ export function CreateBranchDialog({
}; };
// Separate local and remote branches // Separate local and remote branches
const localBranches = branches.filter((b) => !b.isRemote); const localBranches = useMemo(() => branches.filter((b) => !b.isRemote), [branches]);
const remoteBranches = branches.filter((b) => b.isRemote); const remoteBranches = useMemo(() => branches.filter((b) => b.isRemote), [branches]);
// Display label for the selected base branch
const baseBranchDisplayLabel = useMemo(() => {
if (!baseBranch) return null;
const found = branches.find((b) => b.name === baseBranch);
if (!found) return baseBranch;
return found.isCurrent ? `${found.name} (current)` : found.name;
}, [baseBranch, branches]);
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@@ -200,44 +225,94 @@ export function CreateBranchDialog({
<span className="text-sm text-muted-foreground">Loading branches...</span> <span className="text-sm text-muted-foreground">Loading branches...</span>
</div> </div>
) : ( ) : (
<Select value={baseBranch} onValueChange={setBaseBranch} disabled={isCreating}> <Popover open={baseBranchPopoverOpen} onOpenChange={setBaseBranchPopoverOpen}>
<SelectTrigger id="base-branch"> <PopoverTrigger asChild>
<SelectValue placeholder="Select base branch" /> <Button
</SelectTrigger> id="base-branch"
<SelectContent> ref={baseBranchTriggerRef}
{localBranches.length > 0 && ( variant="outline"
<SelectGroup> role="combobox"
<SelectLabel>Local Branches</SelectLabel> aria-expanded={baseBranchPopoverOpen}
{localBranches.map((branch) => ( disabled={isCreating}
<SelectItem key={branch.name} value={branch.name}> className="w-full justify-between font-normal"
<span className={branch.isCurrent ? 'font-medium' : ''}> >
{branch.name} <span className="truncate text-sm">
{branch.isCurrent ? ' (current)' : ''} {baseBranchDisplayLabel ?? (
</span> <span className="text-muted-foreground">Select base branch</span>
</SelectItem> )}
))} </span>
</SelectGroup> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
)} </Button>
{remoteBranches.length > 0 && ( </PopoverTrigger>
<> <PopoverContent
{localBranches.length > 0 && <SelectSeparator />} className="p-0"
<SelectGroup> style={{ width: Math.max(baseBranchTriggerWidth, 200) }}
<SelectLabel>Remote Branches</SelectLabel> onWheel={(e) => e.stopPropagation()}
{remoteBranches.map((branch) => ( onTouchMove={(e) => e.stopPropagation()}
<SelectItem key={branch.name} value={branch.name}> >
{branch.name} <Command shouldFilter={true}>
</SelectItem> <CommandInput placeholder="Filter branches..." className="h-9" />
))} <CommandList>
</SelectGroup> <CommandEmpty>No matching branches</CommandEmpty>
</> {localBranches.length > 0 && (
)} <CommandGroup heading="Local Branches">
{localBranches.length === 0 && remoteBranches.length === 0 && ( {localBranches.map((branch) => (
<SelectItem value="HEAD" disabled> <CommandItem
No branches found key={branch.name}
</SelectItem> value={branch.name}
)} onSelect={(value) => {
</SelectContent> setBaseBranch(value);
</Select> setBaseBranchPopoverOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4 shrink-0',
baseBranch === branch.name ? 'opacity-100' : 'opacity-0'
)}
/>
<span className={cn('truncate', branch.isCurrent && 'font-medium')}>
{branch.name}
</span>
{branch.isCurrent && (
<span className="ml-1.5 text-xs text-muted-foreground shrink-0">
(current)
</span>
)}
</CommandItem>
))}
</CommandGroup>
)}
{remoteBranches.length > 0 && (
<>
{localBranches.length > 0 && <CommandSeparator />}
<CommandGroup heading="Remote Branches">
{remoteBranches.map((branch) => (
<CommandItem
key={branch.name}
value={branch.name}
onSelect={(value) => {
setBaseBranch(value);
setBaseBranchPopoverOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4 shrink-0',
baseBranch === branch.name ? 'opacity-100' : 'opacity-0'
)}
/>
<Globe className="mr-1.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">{branch.name}</span>
</CommandItem>
))}
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)} )}
</div> </div>