Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit0369e77unknown_key

feat(pr): comment counts in list + branch ahead/behind indicator

feat(pr): comment counts in list + branch ahead/behind indicator

- PR list: show 💬 N badge with human comment count per row (excludes AI)
- PR detail: compute head↑N / base↓N ahead/behind counts via git rev-list,
  render as a color-coded sync pill (red = behind, green = synced)
- Merge strategy: persist mergeStrategy field already in PR schema

https://claude.ai/code/session_01ACsT2Pc8GRoZwZRF8SK68Y
Claude committed on May 28, 2026Parent: 1aef949
1 file changed+8350369e7714332f1c74aa8807cc5c815e5b3d72b1d
1 changed file+83−5
Modifiedsrc/routes/pulls.tsx+83−5View fileUnifiedSplit
467467 font-size: 14px;
468468 font-weight: 700;
469469 }
470 .prs-branch-sync {
471 display: inline-flex; align-items: center; gap: 4px;
472 font-size: 11.5px; font-weight: 600;
473 padding: 2px 8px;
474 border-radius: 9999px;
475 border: 1px solid var(--border);
476 background: var(--bg-secondary);
477 color: var(--text-muted);
478 cursor: default;
479 }
480 .prs-branch-sync.is-behind {
481 color: #f87171;
482 border-color: rgba(248,113,113,0.35);
483 background: rgba(248,113,113,0.07);
484 }
485 .prs-branch-sync.is-synced {
486 color: #34d399;
487 border-color: rgba(52,211,153,0.35);
488 background: rgba(52,211,153,0.07);
489 }
470490
471491 .prs-detail-actions {
472492 display: inline-flex; gap: 8px; margin-left: auto;
17841804 )
17851805 .orderBy(desc(pullRequests.createdAt));
17861806
1787 // Batch-load review states for all PRs in the list (approved / changes_requested badges)
1807 // Batch-load review states + comment counts for all PRs in the list
17881808 const reviewMap = new Map<string, { approved: boolean; changesRequested: boolean }>();
1809 const commentCountMap = new Map<string, number>();
17891810 if (prList.length > 0) {
17901811 const prIds = prList.map(({ pr }) => pr.id);
1791 const reviewRows = await db
1792 .select({ prId: prReviews.pullRequestId, state: prReviews.state })
1793 .from(prReviews)
1794 .where(inArray(prReviews.pullRequestId, prIds));
1812 const [reviewRows, commentRows] = await Promise.all([
1813 db
1814 .select({ prId: prReviews.pullRequestId, state: prReviews.state })
1815 .from(prReviews)
1816 .where(inArray(prReviews.pullRequestId, prIds)),
1817 db
1818 .select({
1819 prId: prComments.pullRequestId,
1820 cnt: sql<number>`count(*)::int`,
1821 })
1822 .from(prComments)
1823 .where(and(inArray(prComments.pullRequestId, prIds), eq(prComments.isAiReview, false)))
1824 .groupBy(prComments.pullRequestId),
1825 ]);
17951826 for (const r of reviewRows) {
17961827 const entry = reviewMap.get(r.prId) ?? { approved: false, changesRequested: false };
17971828 if (r.state === "approved") entry.approved = true;
17981829 if (r.state === "changes_requested") entry.changesRequested = true;
17991830 reviewMap.set(r.prId, entry);
18001831 }
1832 for (const r of commentRows) {
1833 commentCountMap.set(r.prId, Number(r.cnt));
1834 }
18011835 }
18021836
18031837 const [counts] = await db
19842018 {rv?.changesRequested && (
19852019 <span class="prs-tag is-changes" title="Changes requested">✗ Changes</span>
19862020 )}
2021 {(commentCountMap.get(pr.id) ?? 0) > 0 && (
2022 <span class="prs-tag" title={`${commentCountMap.get(pr.id)} comment${(commentCountMap.get(pr.id) ?? 0) === 1 ? "" : "s"}`}>
2023 💬 {commentCountMap.get(pr.id)}
2024 </span>
2025 )}
19872026 </span>
19882027 </div>
19892028 </div>
30003039 pr.headBranch
30013040 );
30023041
3042 // Branch ahead/behind counts — how many commits head is ahead of base and
3043 // how many commits base has advanced since head branched off.
3044 let branchAhead = 0;
3045 let branchBehind = 0;
3046 if (pr.state === "open") {
3047 try {
3048 const repoDir = getRepoPath(ownerName, repoName);
3049 const [aheadProc, behindProc] = [
3050 Bun.spawn(
3051 ["git", "rev-list", "--count", `${pr.baseBranch}..${pr.headBranch}`],
3052 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
3053 ),
3054 Bun.spawn(
3055 ["git", "rev-list", "--count", `${pr.headBranch}..${pr.baseBranch}`],
3056 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
3057 ),
3058 ];
3059 const [aheadTxt, behindTxt] = await Promise.all([
3060 new Response(aheadProc.stdout).text(),
3061 new Response(behindProc.stdout).text(),
3062 ]);
3063 await Promise.all([aheadProc.exited, behindProc.exited]);
3064 branchAhead = parseInt(aheadTxt.trim(), 10) || 0;
3065 branchBehind = parseInt(behindTxt.trim(), 10) || 0;
3066 } catch { /* non-blocking */ }
3067 }
3068
30033069 // Get diff for "Files changed" tab + load inline comments for that tab
30043070 let diffRaw = "";
30053071 let diffFiles: GitDiffFile[] = [];
31653231 <span class="prs-branch-arrow-lg">{"→"}</span>
31663232 <span class="prs-branch-pill">{pr.baseBranch}</span>
31673233 </span>
3234 {pr.state === "open" && (branchAhead > 0 || branchBehind > 0) && (
3235 <span
3236 class={`prs-branch-sync${branchBehind > 0 ? " is-behind" : " is-synced"}`}
3237 title={branchBehind > 0
3238 ? `This branch is ${branchBehind} commit${branchBehind === 1 ? "" : "s"} behind ${pr.baseBranch} — consider rebasing`
3239 : `This branch is ${branchAhead} commit${branchAhead === 1 ? "" : "s"} ahead of ${pr.baseBranch}`}
3240 >
3241 {branchAhead > 0 ? `↑${branchAhead}` : ""}
3242 {branchAhead > 0 && branchBehind > 0 ? " " : ""}
3243 {branchBehind > 0 ? `↓${branchBehind}` : ""}
3244 </span>
3245 )}
31683246 <span>opened {formatRelative(pr.createdAt)}</span>
31693247 <span
31703248 id="live-pill"
31713249