diff --git a/client/src/components/lanes/LaneGitBar.tsx b/client/src/components/lanes/LaneGitBar.tsx index b19cd47a..b1df11d5 100644 --- a/client/src/components/lanes/LaneGitBar.tsx +++ b/client/src/components/lanes/LaneGitBar.tsx @@ -132,6 +132,8 @@ export function LaneGitBar({ const [remoteBranches, setRemoteBranches] = useState([]); const [worktrees, setWorktrees] = useState([]); const [picked, setPicked] = useState(""); + // Issue #88: typing narrows the switcher to the branches whose name holds it. + const [branchQuery, setBranchQuery] = useState(""); const [newBranch, setNewBranch] = useState(""); // Set when git needed a login for `action`; the form resends that action. const [login, setLogin] = useState<{ action: Action; auth: GitAuthNeeded } | null>(null); @@ -160,6 +162,7 @@ export function LaneGitBar({ setRemoteBranches([]); setWorktrees([]); setPicked(""); + setBranchQuery(""); setNewBranch(""); setLogin(null); setPassword(""); @@ -176,6 +179,26 @@ export function LaneGitBar({ // A remote branch: switching creates its local tracking branch; it cannot // be deleted here or given a sibling worktree before that. const pickedRemote = remoteBranches.includes(picked); + // The switcher's lists, narrowed by the search box (case-insensitive, any + // part of the name). The picked branch always stays in its list, so the + // select never shows a value it has no option for. + const query = branchQuery.trim().toLowerCase(); + const matches = (b: string) => !query || b.toLowerCase().includes(query) || b === picked; + const shownBranches = (branches || []).filter(matches); + const shownRemote = remoteBranches.filter(matches); + const matchCount = query + ? [...(branches || []), ...remoteBranches].filter((b) => b.toLowerCase().includes(query)) + .length + : null; + const searchBranches = (text: string) => { + setBranchQuery(text); + const q = text.trim().toLowerCase(); + if (!q) return; + const hits = [...(branches || []), ...remoteBranches].filter((b) => + b.toLowerCase().includes(q) + ); + if (hits.length === 1) setPicked(hits[0]!); + }; const merging = !!git.merging; const run = async (action: Action, body?: RunBody) => { @@ -526,6 +549,27 @@ export function LaneGitBar({ {switching && (
+ searchBranches(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Escape" && branchQuery) { + e.stopPropagation(); + setBranchQuery(""); + } else if (isSubmitEnter(e)) { + e.preventDefault(); + const first = [...shownBranches, ...shownRemote].find( + (b) => b.toLowerCase().includes(query) && b !== git.branch + ); + if (first) setPicked(first); + } + }} + className="w-36 rounded border border-border-light bg-surface-0 px-2 py-0.5 font-mono text-[11px] text-fg-primary placeholder:text-fg-muted focus:border-accent focus:outline-none" + />