diff --git a/src/features/app/components/MainHeader.tsx b/src/features/app/components/MainHeader.tsx index 2dfa972cf..ac16245b6 100644 --- a/src/features/app/components/MainHeader.tsx +++ b/src/features/app/components/MainHeader.tsx @@ -65,8 +65,7 @@ export function MainHeader({ }: MainHeaderProps) { const [menuOpen, setMenuOpen] = useState(false); const [infoOpen, setInfoOpen] = useState(false); - const [isCreating, setIsCreating] = useState(false); - const [newBranch, setNewBranch] = useState(""); + const [branchQuery, setBranchQuery] = useState(""); const [error, setError] = useState(null); const [copyFeedback, setCopyFeedback] = useState(false); const copyTimeoutRef = useRef(null); @@ -76,7 +75,50 @@ export function MainHeader({ const renameConfirmRef = useRef(null); const renameOnCancel = worktreeRename?.onCancel; - const recentBranches = branches.slice(0, 12); + const recentBranches = branches; + const trimmedQuery = branchQuery.trim(); + const lowercaseQuery = trimmedQuery.toLowerCase(); + const filteredBranches = + trimmedQuery.length > 0 + ? recentBranches.filter((branch) => + branch.name.toLowerCase().includes(lowercaseQuery), + ) + : recentBranches.slice(0, 12); + const exactMatch = trimmedQuery + ? recentBranches.find((branch) => branch.name === trimmedQuery) ?? null + : null; + const canCreate = trimmedQuery.length > 0 && !exactMatch; + const branchValidationMessage = (() => { + if (trimmedQuery.length === 0) { + return null; + } + if (trimmedQuery === "." || trimmedQuery === "..") { + return "Branch name cannot be '.' or '..'."; + } + if (/\s/.test(trimmedQuery)) { + return "Branch name cannot contain spaces."; + } + if (trimmedQuery.startsWith("/") || trimmedQuery.endsWith("/")) { + return "Branch name cannot start or end with '/'."; + } + if (trimmedQuery.endsWith(".lock")) { + return "Branch name cannot end with '.lock'."; + } + if (trimmedQuery.includes("..")) { + return "Branch name cannot contain '..'."; + } + if (trimmedQuery.includes("@{")) { + return "Branch name cannot contain '@{'."; + } + const invalidChars = ["~", "^", ":", "?", "*", "[", "\\"]; + if (invalidChars.some((char) => trimmedQuery.includes(char))) { + return "Branch name contains invalid characters."; + } + if (trimmedQuery.endsWith(".")) { + return "Branch name cannot end with '.'."; + } + return null; + })(); const resolvedWorktreePath = worktreePath ?? workspace.path; const relativeWorktreePath = parentPath && resolvedWorktreePath.startsWith(`${parentPath}/`) @@ -95,8 +137,7 @@ export function MainHeader({ if (!menuContains && !infoContains) { setMenuOpen(false); setInfoOpen(false); - setIsCreating(false); - setNewBranch(""); + setBranchQuery(""); setError(null); } }; @@ -313,55 +354,93 @@ export function MainHeader({ data-tauri-drag-region="false" >
- {!isCreating ? ( - - ) : ( -
- setNewBranch(event.target.value)} - placeholder="new-branch-name" - className="branch-input" - autoFocus - data-tauri-drag-region="false" - /> - + } + }} + placeholder="Search or create branch" + className="branch-input" + autoFocus + data-tauri-drag-region="false" + aria-label="Search branches" + /> + +
+ {branchValidationMessage && ( +
{branchValidationMessage}
+ )} + {canCreate && !branchValidationMessage && ( +
+ Create branch “{trimmedQuery}”
)}
- {recentBranches.map((branch) => ( + {filteredBranches.map((branch) => (
diff --git a/src/styles/main.css b/src/styles/main.css index 13f179fc9..198f3aa01 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -609,6 +609,16 @@ gap: 6px; } +.branch-search { + display: flex; + gap: 6px; + align-items: center; +} + +.branch-search .branch-input { + flex: 1; +} + .branch-action { display: inline-flex; @@ -673,6 +683,18 @@ background: var(--surface-control-hover); } +.branch-create-button:disabled { + cursor: not-allowed; + opacity: 0.6; + background: var(--surface-card); +} + +.branch-create-hint { + font-size: 11px; + color: var(--text-faint); + padding: 2px 8px 0; +} + .branch-list { display: flex; flex-direction: column;