* feat(web): surface PR mergeability + conflict state in the CI popover
The CI hover popover showed checks/reviews but nothing about mergeability, so an
unmergeable PR silently dropped its merge button with no reason. Add a mergeability
row that reuses the detail panel's conflict banner + "Resolve conflicts" agent prompt
for dirty PRs, a "Behind base" chip, and a branch-protection note explaining why a
blocked PR is gated. Replace the compact merge split-button+dropdown with an inline
primary + "or <method>" buttons so the menu's detached portal no longer closes the
hover popover.
* feat(web): reflect PR mergeability in status colors and badges
The PR status pill, badge, and chat-input CI chip derived their colour from
checks/reviews only, so a conflicted or blocked PR still read as a green "all good"
check. Map dirty -> red, behind -> amber, and branch-protection blocked -> amber across
getPRStatusColor, the PRStatusIcon badge, and the chip glyph, so the indicators agree
with the popover. Blocked stays sky/in-progress while a requested review is still
outstanding (that's the awaiting-review gate, not a hard block).
* refactor(web): remove dead merge-menu keep-open plumbing
The compact merge control was redesigned to inline buttons (no dropdown portal),
which already fixes the hover popover closing. That leaves the keepOpen ref /
setKeepOpen / onMergeMenuOpenChange / onMenuOpenChange chain threaded through
usePopoverInteractions, PRCIPopover, and PRMergeButton unreachable: the popover
always renders PRMergeButton with compact, and the only DropdownMenu lives in the
non-compact detail-panel path that never passes the callback. Drop the unused
scaffolding (flagged by OpenCode + greptile).
* fix(web): keep CI popover quiet for blocked PRs only awaiting review
PRMergeabilityRow rendered "Blocked by branch protection" for any blocked PR,
including one blocked solely because a requested reviewer hasn't responded. That
contradicted the chip/pill/badge, which deliberately read that case as the calm
"awaiting review" state. Skip the note (render nothing) when isPRAwaitingReview is
true, and narrow blockedReason so an empty checks_state no longer falsely claims a
required status check failed. Flagged by Claude (blocker) and greptile.
* refactor(web): rename mergeability-notice compact prop to popover
The `compact` prop made the chip/text larger (to match the popover's text-xs rows)
while ConflictBanner's `compact` tightened padding — so "compact" rendered bigger,
which reads as inverted. Rename it to `popover` (the variant rendered inside the CI
hover popover) and document that the chip intentionally matches the popover rows.
No visual change. Flagged by cubic.
* fix(web): show default cursor on disabled compact merge alternates
The compact "or <method>" alternates kept cursor-pointer while disabled (during the
~1s merging state); add disabled:cursor-default to match the primary button. Flagged
by OpenCode.
* fix(github): pick allowed merge method so squash-only repos stop 405ing
The merge button sent an empty merge_method to GitHub, which defaults to
"merge" regardless of repo settings — squash-only / rebase-only repos
returned 405 "Merge commits are not allowed on this repository". The
backend now resolves an empty method to the first allowed one via a new
GetRepoMergeMethods endpoint (5-min cache), and the frontend renders a
GitHub-style split button so users can pick among the allowed methods.
* fix(github): address PR #999 review feedback
- pr-merge-button: extract repo-methods fetch to useRepoMergeMethods
hook (Claude bot); hide button while methods are loading so the
primary label doesn't flicker (Claude bot); drop e.preventDefault on
dropdown items so Radix closes the menu on selection (Greptile P1).
- gh_client / pat_client: treat missing allow_* fields as false instead
of true so a permission-gated response can't pick a disallowed merge
method (cubic P2).
- service.MergePR: only overwrite mergeMethod when pickDefaultMergeMethod
returns non-empty, so a degenerate all-false config falls through to
GitHub's default with the real error (Claude bot).
* fix(github): don't hide merge button on lookup failure
Round-2 PR feedback (Claude, Greptile, CodeRabbit, cubic): the
`if (!methods) return null` guard added in the previous round to
avoid label flicker turned a transient repo-methods lookup failure
into a permanent UI lockout — the button would stay hidden until
unmount/remount.
- pr-merge-button: revert the null-guard; treat null as "use backend
resolver", render the plain primary button without a dropdown.
- use-repo-merge-methods: add a 5-min frontend TTL to match the
backend cache so admin merge-strategy flips are visible without a
page reload. Catch block now triggers a rerender, and co-waiting
consumers attach both fulfilled/rejected handlers to avoid silent
rejection swallowing.
* test(github): cover useRepoMergeMethods + silence rejection warning
Round-3 PR feedback (Claude):
- Swallow rejections in the originating fetch chain instead of
rethrowing, so the first consumer's promise never escapes the hook
as an unhandled rejection. Coalesced consumers already re-render to
null, which is the same state a fresh "not loaded" call would show.
- Add use-repo-merge-methods.test.ts covering: null-on-missing-params,
loading then resolution, cache hit on remount, concurrent-fetch
coalescing, retry-on-failure (no cache poisoning, no unhandled
rejection), and TTL-expiry refetch via fake timers.
* test(github): cover httpGetRepoMergeMethods error branches
Round-4 PR feedback (Claude): the new endpoint had only happy-path
coverage. Add tests matching httpMergePR's existing shape:
- nil client → 503 (ErrNoClient)
- GitHubAPIError{404} → 404
- generic upstream error → 500
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat(github): add merge button when PR is ready to merge
Surfaces a one-click merge action in the PR detail panel header and as a
compact green pill in the PR topbar hover popover, gated by the existing
isPRReadyToMerge predicate (open, CI success, mergeable_state=clean, and
either approved or no review process).
Backend adds MergePR to the Client interface, implements it across
PAT/gh/Noop/Mock clients, exposes PUT /api/v1/github/prs/:owner/:repo/:number/merge,
and maps GitHub 405/409 to HTTP 409 so the UI can show a clear error.
* fix(github): address PR review feedback on merge endpoint
- Map gh CLI 404/403/405/409 stderr to *GitHubAPIError via a new
ghMergeStatusCode helper, so merge conflicts surface as HTTP 409 for
gh CLI users (previously only PAT users got the right status).
- Service.MergePR returns ErrNoClient when unconfigured; httpMergePR
translates that to HTTP 503 with github_not_configured, matching
httpListRepoBranches.
- httpMergePR distinguishes empty body (io.EOF, fall through to default
merge method) from malformed JSON (HTTP 400 invalid payload).
- PRMergeButton tracks a local merged flag so the button stays hidden
across the async refresh window after a successful merge.
* test(github): cover merge helpers and extract PRMergeButton
- TestGhMergeStatusCode locks in the gh CLI stderr → status-code parser
for nil, unrelated errors, the HTTP / status: / bare-phrase forms of
404/403/405/409, and a 500 non-match guard.
- Broaden ghMergeStatusCode to also match "405 Method Not Allowed" /
"409 Conflict" phrase forms so the merge endpoint's 409 mapping does
not depend on the gh CLI keeping its current "HTTP <code>" prefix.
- TestHttpMergePR_MalformedJSON covers the new non-EOF bind-error branch
(truncated body → 400). TestHttpMergePR_NoClient_Returns503 covers the
ErrNoClient → 503 path on Service.MergePR.
- Document the post/put error-type divergence on PATClient.post so a
future caller does not silently lose per-status mapping.
- Move PRMergeButton out of pr-shared.tsx into its own file, and reset
the local "merged" flag when taskPR.id changes so an instance that
re-renders for a different PR does not stay hidden.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>