fix(agent-manager): use Popover portal for base branch dropdown in advanced options (#6388)

The branch selector dropdown in the New Worktree dialog's Advanced
section was clipped by the dialog's overflow. Replace the hand-rolled
absolute-positioned dropdown with the Popover component (portal-based),
matching the pattern used by the Import tab's branch selector.
This commit is contained in:
Marius
2026-02-26 14:10:37 +01:00
committed by GitHub
parent 70b5a0743b
commit 8ccbcb70b7
2 changed files with 108 additions and 167 deletions
@@ -2135,94 +2135,110 @@ const NewWorktreeDialog: Component<{ onClose: () => void }> = (props) => {
</div>
<div class="am-advanced-field">
<span class="am-nv-config-label">{t("agentManager.dialog.baseBranch")}</span>
<div class="am-branch-selector-wrapper">
<button
class="am-branch-selector-trigger"
onClick={() => setBaseBranchOpen(!baseBranchOpen())}
type="button"
<div class="am-selector-wrapper">
<Popover
open={baseBranchOpen()}
onOpenChange={(open) => {
setBaseBranchOpen(open)
if (!open) {
setBranchSearch("")
setHighlightedIndex(0)
}
}}
placement="bottom-start"
sameWidth
class="am-dropdown"
trigger={
<button class="am-selector-trigger" type="button">
<span class="am-selector-left">
<Icon name="branch" size="small" />
<span class="am-selector-value" style={{ color: "var(--text-base)" }}>
{effectiveBaseBranch()}
</span>
<Show when={!baseBranch()}>
<span class="am-branch-badge">{t("agentManager.dialog.branchBadge.default")}</span>
</Show>
</span>
<span class="am-selector-right">
<Icon name="selector" size="small" />
</span>
</button>
}
>
<Icon name="branch" size="small" />
<span class="am-branch-selector-value">{effectiveBaseBranch()}</span>
<Show when={!baseBranch()}>
<span class="am-branch-badge">{t("agentManager.dialog.branchBadge.default")}</span>
</Show>
<Icon name="selector" size="small" />
</button>
<Show when={baseBranchOpen()}>
<div class="am-branch-dropdown" onWheel={(e) => e.stopPropagation()}>
<div class="am-branch-search">
<Icon name="magnifying-glass" size="small" />
<input
class="am-branch-search-input"
type="text"
placeholder={t("agentManager.dialog.searchBranches")}
value={branchSearch()}
ref={(el) => requestAnimationFrame(() => el.focus())}
onInput={(e) => {
setBranchSearch(e.currentTarget.value)
setHighlightedIndex(0)
}}
onKeyDown={(e) => {
const items = filteredBranches()
if (e.key === "ArrowDown") {
e.preventDefault()
e.stopPropagation()
const next = Math.min(highlightedIndex() + 1, items.length - 1)
setHighlightedIndex(next)
requestAnimationFrame(() => {
document
.querySelector(`.am-branch-item[data-index="${next}"]`)
?.scrollIntoView({ block: "nearest" })
})
} else if (e.key === "ArrowUp") {
e.preventDefault()
e.stopPropagation()
const prev = Math.max(highlightedIndex() - 1, 0)
setHighlightedIndex(prev)
requestAnimationFrame(() => {
document
.querySelector(`.am-branch-item[data-index="${prev}"]`)
?.scrollIntoView({ block: "nearest" })
})
} else if (e.key === "Enter") {
e.preventDefault()
e.stopPropagation()
const selected = items[highlightedIndex()]
if (selected) {
setBaseBranch(selected.name)
setBaseBranchOpen(false)
setBranchSearch("")
setHighlightedIndex(0)
}
} else if (e.key === "Escape") {
e.preventDefault()
e.stopPropagation()
<div class="am-dropdown-search">
<Icon name="magnifying-glass" size="small" />
<input
class="am-dropdown-search-input"
type="text"
placeholder={t("agentManager.dialog.searchBranches")}
value={branchSearch()}
autofocus
onInput={(e) => {
setBranchSearch(e.currentTarget.value)
setHighlightedIndex(0)
}}
onKeyDown={(e) => {
const items = filteredBranches()
if (e.key === "ArrowDown") {
e.preventDefault()
e.stopPropagation()
const next = Math.min(highlightedIndex() + 1, items.length - 1)
setHighlightedIndex(next)
requestAnimationFrame(() => {
document
.querySelector(`.am-branch-item[data-index="${next}"]`)
?.scrollIntoView({ block: "nearest" })
})
} else if (e.key === "ArrowUp") {
e.preventDefault()
e.stopPropagation()
const prev = Math.max(highlightedIndex() - 1, 0)
setHighlightedIndex(prev)
requestAnimationFrame(() => {
document
.querySelector(`.am-branch-item[data-index="${prev}"]`)
?.scrollIntoView({ block: "nearest" })
})
} else if (e.key === "Enter") {
e.preventDefault()
e.stopPropagation()
const selected = items[highlightedIndex()]
if (selected) {
setBaseBranch(selected.name)
setBaseBranchOpen(false)
setBranchSearch("")
setHighlightedIndex(0)
}
}}
/>
</div>
<div class="am-branch-list">
<For each={filteredBranches()}>
{(branch, index) => (
<button
class="am-branch-item"
classList={{
"am-branch-item-active": effectiveBaseBranch() === branch.name,
"am-branch-item-highlighted": highlightedIndex() === index(),
}}
data-index={index()}
onClick={() => {
setBaseBranch(branch.name)
setBaseBranchOpen(false)
setBranchSearch("")
setHighlightedIndex(0)
}}
onMouseEnter={() => setHighlightedIndex(index())}
type="button"
>
} else if (e.key === "Escape") {
e.preventDefault()
e.stopPropagation()
setBaseBranchOpen(false)
setBranchSearch("")
setHighlightedIndex(0)
}
}}
/>
</div>
<div class="am-dropdown-list">
<For each={filteredBranches()}>
{(branch, index) => (
<button
class="am-branch-item"
classList={{
"am-branch-item-active": effectiveBaseBranch() === branch.name,
"am-branch-item-highlighted": highlightedIndex() === index(),
}}
data-index={index()}
onClick={() => {
setBaseBranch(branch.name)
setBaseBranchOpen(false)
setBranchSearch("")
setHighlightedIndex(0)
}}
onMouseEnter={() => setHighlightedIndex(index())}
type="button"
>
<span class="am-branch-item-left">
<Icon name="branch" size="small" />
<span class="am-branch-item-name">{branch.name}</span>
<Show when={branch.isDefault}>
@@ -2233,15 +2249,15 @@ const NewWorktreeDialog: Component<{ onClose: () => void }> = (props) => {
{t("agentManager.dialog.branchBadge.remote")}
</span>
</Show>
<Show when={branch.lastCommitDate}>
<span class="am-branch-item-time">{formatRelativeDate(branch.lastCommitDate!)}</span>
</Show>
</button>
)}
</For>
</div>
</span>
<Show when={branch.lastCommitDate}>
<span class="am-branch-item-time">{formatRelativeDate(branch.lastCommitDate!)}</span>
</Show>
</button>
)}
</For>
</div>
</Show>
</Popover>
</div>
</div>
</div>
@@ -1578,38 +1578,7 @@ button.am-section-toggle:hover .am-section-label {
color: var(--text-weaker);
}
/* Branch selector trigger + dropdown */
.am-branch-selector-wrapper {
position: relative;
}
.am-branch-selector-trigger {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 6px 10px;
border: 1px solid var(--border-base);
border-radius: var(--radius-sm);
background: var(--surface-base);
color: var(--text-base);
font-size: 12px;
cursor: pointer;
transition: border-color 120ms;
}
.am-branch-selector-trigger:hover {
border-color: var(--border-hover);
}
.am-branch-selector-value {
flex: 1;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Branch selector */
.am-branch-badge {
font-size: 10px;
@@ -1626,50 +1595,6 @@ button.am-section-toggle:hover .am-section-label {
opacity: 0.7;
}
.am-branch-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 100;
border: 1px solid var(--border-base);
border-radius: var(--radius-sm);
background: var(--surface-float-base, var(--surface-raised-base));
box-shadow: var(--shadow-lg-border-base, 0 8px 24px rgba(0, 0, 0, 0.3));
max-height: 280px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.am-branch-search {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-bottom: 1px solid var(--border-base);
}
.am-branch-search-input {
flex: 1;
border: none;
background: none;
color: var(--text-base);
font-size: 12px;
font-family: inherit;
outline: none;
}
.am-branch-search-input::placeholder {
color: var(--text-weaker);
}
.am-branch-list {
overflow-y: auto;
max-height: 220px;
padding: 4px 0;
}
.am-branch-item {
display: flex;
align-items: center;