Skip to content

feat(gtd-sidebar): GitHub PR status on thread cards - #89

Draft
trieloff wants to merge 4 commits into
smsunarto:mainfrom
trieloff:gtd-sidebar-pr-status
Draft

feat(gtd-sidebar): GitHub PR status on thread cards#89
trieloff wants to merge 4 commits into
smsunarto:mainfrom
trieloff:gtd-sidebar-pr-status

Conversation

@trieloff

Copy link
Copy Markdown

Draft — opening for direction on scope before polishing.

Thread cards in the GTD sidebar currently say nothing about the pull request a
branch belongs to. This adds that: the PR number on the card, coloured by what
the PR actually needs from you.

What's here

PR index (REST, not GraphQL). bb's own hook spends GraphQL points per
environment on mount, and reloading the plugin stampedes it until every badge
goes blank. This takes the separate REST budget instead: one open-PR list per
repository per tick, a numbered GET when the list misses (merged PRs drop off
it), then title-text and a local cache when REST is too tight to spend.

Live colour via webhooks. A Cloudflare trycloudflare tunnel receives
pull_request, check_run, check_suite, status and review events and
repaints the affected card without waiting for the ten-minute reconcile. The
tunnel is opt-in, its status is surfaced in settings, and hooks are registered
per repository on demand. It needs cloudflared on PATH and falls back to the
polling path when it is absent.

Snooze that wakes on the PR. Snoozing a thread against its PR polls hourly
and wakes the thread when the PR moves, so "come back when CI is green" becomes
expressible.

In-app browser. PR links open in a bb tab rather than bouncing out to the
system browser.

The bug behind the last commit. /repos/:owner/:repo/pulls does not return
mergeable_state — GitHub computes it lazily and serves it only from the
numbered GET. Every PR matched off the open list therefore parsed as
"unknown", collapsed to attention "none", and fell through to the open-PR
green, so conflicts, failing checks and branch protection were all invisible.
Listed open PRs whose state the list cannot decide now buy it with a numbered
GET, deduped per PR and bounded by the existing per-tick cap; draft,
auto-merge, merged and closed stay decidable from the list and spend nothing.
The webhook path had the same hole — pull_request_review and issue_comment
embed a shortened pull with no mergeable_state — so it refetches too.
Conflicts additionally gain a strikethrough, separating the one red that needs
a local rebase from the reds that need a re-run or a review.

Commits

9ad0f10  feat: GitHub PR badges, snooze watch, in-app browser
9e6fa6a  feat: GitHub webhooks via Cloudflare trycloudflare
7498bcd  fix: address webhook PR review comments
34733b0  fix: read merge state the PR list omits

Eight changesets, all @smsunarto/bb-plugin-gtd-sidebar: six patch, two minor.

Verification

Typecheck and the plugin's 217 tests pass. I also rebased this onto current
main (b5fe05b) in a scratch worktree: all four commits cherry-pick with no
conflicts, bun install --frozen-lockfile is clean, and typecheck passes there
too — including against the SDK 0.4.21 pin, which is newer than the 0.4.8 this
was written against.

Two things not run locally:

  • bb plugin build. config.bbVersion on main is 0.40.0 and I have 0.39.0
    installed; the build scripts hard-fail on a pin mismatch. CI installs the
    pinned bb-app and will cover it.
  • This branch is based on d86a264, the merge-base, rather than current
    main — deliberately, so the diff is these four commits and nothing else. My
    token has no workflow scope, so I cannot push a branch carrying main's newer
    .github/workflows changes to my fork. Happy to rebase onto current main if
    you would prefer; it applies cleanly, as above.

Open question

Is this the right scope for one PR? It is ~5,400 lines across four commits. The
PR index and the webhook transport could reasonably be separate reviews, though
server.ts interleaves them and splitting after the fact is the expensive
direction. Glad to carve it up if you would rather review it in pieces.

Grok Build and others added 4 commits August 27, 2026 10:52
Colour draft and merge-queue PRs, and look up merged PRs by number so they
stay purple after they leave GitHub's open list. Resolve sidebar badges
with one REST list per repository plus cache and title fallbacks instead of
per-card GraphQL. Watch snoozed PRs hourly and unsnooze plus ping the agent
when comments, reviews, checks, or deployments change. Open a PR number in
bb's in-app browser; any modifier key uses the system browser.

Signed-off-by: Lars Trieloff <lars@trieloff.net>
Colour open PRs with bb's success green, and match merged/closed PRs from
the recent closed list instead of a stale open cache. Receive signed GitHub
webhooks in realtime: a setting checks for cloudflared and opens a
trycloudflare tunnel to a webhook-only local port, not the bb API and not
a session-gated getbb.app URL. Hydrate on mount and reconcile every 10
minutes; the hourly snooze watch stays as backup.

Signed-off-by: Lars Trieloff <lars@trieloff.net>
Back off when a live cloudflared process exits so URL rotation cannot tight-loop.
Prefer an explicit owner/repo#N in the thread title over the checkout repository.
Always PATCH the GitHub hook secret, and only adopt a hook we previously persisted.
Ignore superseded PR-index RPC responses.

Signed-off-by: Lars Trieloff <lars@trieloff.net>
`/repos/:owner/:repo/pulls` does not return `mergeable_state`; GitHub
computes it lazily and serves it only from the numbered GET. Every PR
matched off the open list therefore parsed as "unknown", collapsed to
attention "none" in `restPullAttention`, and fell through to the
open-PR green. Conflicts, failing checks and branch protection were all
invisible — vercel-labs/just-bash#377 read as a healthy open PR while it
was blocked with a failing check rollup. Only the PRs that missed the
list and took the numbered fallback ever turned red.

`needsMergeStateLookup` marks the pulls the list cannot decide, and
`resolveThreadPullRequests` buys their real state through the existing
`lookupNumbered` — already deduped per PR and capped at
MAX_PR_LOOKUPS_PER_TICK, so the ten-minute reconcile costs at most 240
REST calls an hour against a 5,000 budget. Draft, auto-merge, merged and
closed stay decidable from the list and spend nothing; a failed or
budget-denied lookup falls back to the listed pull.

The webhook path had the same hole. `pull_request_review` and
`issue_comment` embed a shortened pull with no `mergeable_state`, and a
fresh `synchronize` has not recomputed it yet, so that value went
straight to the sidebar over realtime and repainted a blocked PR green
until the next reconcile. It now refetches through `fetchPullDetail`,
shared with the check-run branch that already did this.

Conflicts also gain a strikethrough, separating the one red that needs a
local rebase from the reds that need a re-run or a review. The card's
`hover:underline` sets the same property and Tailwind emits it later,
and tailwind-merge files the two in different groups so it keeps both —
hence the `!` on the hover utility, without which mousing over the
number traded the conflict signal for an underline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Lars Trieloff <lars@trieloff.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants