From 9bd663be7fc67ebde62ea0268b53b2e9e48ef3a1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Feb 2026 23:20:45 -0800 Subject: [PATCH] Add inline branch creation to branch picker search - let users create a new local branch directly from the branch combobox search - remove separate "New branch" form/button flow in `BranchToolbar` - add `inputClassName` support to `ComboboxInput` for native input styling control --- apps/web/src/components/BranchToolbar.tsx | 114 ++++++++-------------- apps/web/src/components/ui/combobox.tsx | 10 +- 2 files changed, 51 insertions(+), 73 deletions(-) diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index b60d38126e4e964f9907aaa03e62fffd2e846788..f5b670cecf0aa3a02543c0ce800b7e5535eb14b6 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -19,8 +19,7 @@ import { ComboboxPopup, ComboboxTrigger, } from "./ui/combobox"; -import { ChevronDownIcon, PlusIcon } from "lucide-react"; -import { InputGroup, InputGroupAddon, InputGroupInput } from "./ui/input-group"; +import { ChevronDownIcon } from "lucide-react"; interface BranchToolbarProps { envMode: "local" | "worktree"; @@ -34,8 +33,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B const queryClient = useQueryClient(); const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); - const [isCreatingBranch, setIsCreatingBranch] = useState(false); - const [newBranchName, setNewBranchName] = useState(""); + const [branchQuery, setBranchQuery] = useState(""); const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId); const activeProject = state.projects.find((project) => project.id === activeThread?.projectId); @@ -51,6 +49,16 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B const branches = branchesQuery.data?.branches ?? []; const branchNames = branches.map((branch) => branch.name); const branchByName = new Map(branches.map((branch) => [branch.name, branch])); + const trimmedBranchQuery = branchQuery.trim(); + const canCreateBranch = envMode === "local" && trimmedBranchQuery.length > 0; + const hasExactBranchMatch = branchByName.has(trimmedBranchQuery); + const createBranchItemValue = canCreateBranch + ? `__create_new_branch__:${trimmedBranchQuery}` + : null; + const branchPickerItems = + createBranchItemValue && !hasExactBranchMatch + ? [...branchNames, createBranchItemValue] + : branchNames; // ── Mutations ───────────────────────────────────────────────────────── const checkoutMutation = useMutation({ @@ -86,8 +94,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B useEffect(() => { if (isBranchMenuOpen) return; - setIsCreatingBranch(false); - setNewBranchName(""); + setBranchQuery(""); }, [isBranchMenuOpen]); // ── Helpers ─────────────────────────────────────────────────────────── @@ -140,15 +147,14 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B }); }; - const createBranch = () => { - const name = newBranchName.trim(); - if (!api || !activeThreadId || !branchCwd || !name) return; + const createBranch = (rawName: string) => { + const name = rawName.trim(); + if (!api || !activeThreadId || !branchCwd || !name || createBranchMutation.isPending) return; createBranchMutation.mutate(name, { onSuccess: () => { setThreadError(null); setThreadBranch(name, activeWorktreePath); - setNewBranchName(""); - setIsCreatingBranch(false); + setBranchQuery(""); setIsBranchMenuOpen(false); }, }); @@ -177,7 +183,7 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B setIsBranchMenuOpen(open)} open={isBranchMenuOpen} @@ -198,20 +204,35 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B -
+
setBranchQuery(event.target.value)} />
No branches found. - {(branchName) => { - const branch = branchByName.get(branchName); + {(itemValue) => { + if (createBranchItemValue && itemValue === createBranchItemValue) { + return ( + createBranch(trimmedBranchQuery)} + > + Create new branch "{trimmedBranchQuery}" + + ); + } + + const branch = branchByName.get(itemValue); if (!branch) return null; const hasSecondaryWorktree = @@ -219,15 +240,15 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B return ( selectBranch(branch)} >
- {branchName} + {itemValue} {(branch.current || branch.isDefault || hasSecondaryWorktree) && ( {branch.current ? "current" : hasSecondaryWorktree ? "worktree" : "default"} @@ -238,57 +259,6 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B ); }} - {envMode === "local" && ( - <> - {isCreatingBranch ? ( -
{ - event.preventDefault(); - createBranch(); - }} - > - - setNewBranchName(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Escape") { - event.stopPropagation(); - setIsCreatingBranch(false); - setNewBranchName(""); - } - }} - autoFocus - /> - - - - -
- ) : ( - - )} - - )}
diff --git a/apps/web/src/components/ui/combobox.tsx b/apps/web/src/components/ui/combobox.tsx index a797de643a4c91c7960463d1f7ed2d0cb82f7a33..ee49975290723499ddc5a457a8d4379d0ef40c49 100644 --- a/apps/web/src/components/ui/combobox.tsx +++ b/apps/web/src/components/ui/combobox.tsx @@ -54,12 +54,14 @@ function ComboboxChipsInput({ function ComboboxInput({ className, + inputClassName, showTrigger = true, showClear = false, startAddon, size, ...props }: Omit & { + inputClassName?: string; showTrigger?: boolean; showClear?: boolean; startAddon?: React.ReactNode; @@ -89,7 +91,13 @@ function ComboboxInput({ className, )} data-slot="combobox-input" - render={} + render={ + + } {...props} /> {showTrigger && ( -- 2.54.0