Commitb558f23unknown_key
feat(pr): commits tab, inline title edit, update-branch refinements
feat(pr): commits tab, inline title edit, update-branch refinements - Commits tab: new tab on PR detail showing git log between base and head (base..head). Each commit row shows the short message, author, relative timestamp, and 7-char SHA link to the commit view. Tab badge shows ahead count when branchAhead > 0. - Inline title edit: owners/authors can click "Edit" in the PR title row to rename the PR in-place. Pure JS show/hide with no page reload on cancel; POST /pulls/:n/edit saves to DB and redirects back. - Removed redundant brace in ternary chain (commits/files/conversation tabs now correctly structured as a triple ternary). https://claude.ai/code/session_01ACsT2Pc8GRoZwZRF8SK68Y
1 file changed+167−6b558f2306ab3436ac0c1136d9fb5ed92818450ae
1 changed file+167−6
Modifiedsrc/routes/pulls.tsx+167−6View fileUnifiedSplit
@@ -82,8 +82,9 @@ import {
8282 resolveRef,
8383 getBlob,
8484 createOrUpdateFileOnBranch,
85 commitsBetween,
8586} from "../git/repository";
86import type { GitDiffFile } from "../git/repository";
87import type { GitDiffFile, GitCommit } from "../git/repository";
8788import { html } from "hono/html";
8889import {
8990 getPreviewForBranch,
@@ -1318,6 +1319,29 @@ const PRS_DETAIL_STYLES = `
13181319 .prs-linked-issue-state { font-size: 11px; font-weight: 600; padding: 1px 7px; border-radius: 9999px; }
13191320 .prs-linked-issue-state.is-open { color: #34d399; background: rgba(52,211,153,0.10); }
13201321 .prs-linked-issue-state.is-closed { color: #8b949e; background: var(--bg-tertiary); }
1322
1323 /* ─── Commits tab ─── */
1324 .prs-commits-list { display: flex; flex-direction: column; gap: 0; margin-top: 14px; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
1325 .prs-commit-row { display: flex; align-items: flex-start; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); text-decoration: none; color: inherit; }
1326 .prs-commit-row:last-child { border-bottom: none; }
1327 .prs-commit-row:hover { background: var(--bg-hover); }
1328 .prs-commit-dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); margin-top: 6px; }
1329 .prs-commit-body { flex: 1 1 auto; min-width: 0; }
1330 .prs-commit-msg { font-size: 13.5px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); }
1331 .prs-commit-meta { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
1332 .prs-commit-sha { flex: 0 0 auto; font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); background: var(--bg-elevated); padding: 2px 7px; border-radius: 6px; border: 1px solid var(--border); text-decoration: none; white-space: nowrap; }
1333 .prs-commit-sha:hover { color: var(--accent); }
1334 .prs-commits-empty { padding: 32px; text-align: center; color: var(--text-muted); font-size: 13.5px; }
1335
1336 /* ─── Edit PR title/body ─── */
1337 .prs-edit-title-wrap { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
1338 .prs-edit-btn { background: none; border: 1px solid var(--border); color: var(--text-muted); font-size: 12px; padding: 3px 10px; border-radius: 6px; cursor: pointer; transition: color 120ms, border-color 120ms; }
1339 .prs-edit-btn:hover { color: var(--text); border-color: var(--text-muted); }
1340 .prs-edit-form { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; }
1341 .prs-edit-form input[type=text] { font-size: 15px; padding: 9px 12px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg-elevated); color: var(--text); width: 100%; box-sizing: border-box; }
1342 .prs-edit-actions { display: flex; gap: 8px; }
1343 .prs-edit-save-btn { padding: 7px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; background: var(--accent); color: #fff; border: none; cursor: pointer; }
1344 .prs-edit-cancel-btn { padding: 7px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; background: var(--bg-elevated); color: var(--text); border: 1px solid var(--border); cursor: pointer; }
13211345`;
13221346
13231347/**
@@ -3274,6 +3298,12 @@ pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), as
32743298 (c) => !c.passed && c.name !== "Merge check"
32753299 );
32763300
3301 // Commits tab — list commits included in this PR (base..head range)
3302 let prCommits: GitCommit[] = [];
3303 if (tab === "commits") {
3304 prCommits = await commitsBetween(ownerName, repoName, pr.baseBranch, pr.headBranch).catch(() => []);
3305 }
3306
32773307 return c.html(
32783308 <Layout
32793309 title={`${pr.title} #${pr.number} — ${ownerName}/${repoName}`}
@@ -3307,10 +3337,60 @@ pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), as
33073337 />
33083338
33093339 <div class="prs-detail-hero">
3310 <h1 class="prs-detail-title">
3311 {pr.title}{" "}
3312 <span class="prs-detail-num">#{pr.number}</span>
3313 </h1>
3340 <div class="prs-edit-title-wrap">
3341 <h1 class="prs-detail-title" id="pr-title-display">
3342 {pr.title}{" "}
3343 <span class="prs-detail-num">#{pr.number}</span>
3344 </h1>
3345 {canManage && pr.state === "open" && (
3346 <button
3347 type="button"
3348 class="prs-edit-btn"
3349 id="pr-edit-toggle"
3350 onclick={`
3351 document.getElementById('pr-title-display').style.display='none';
3352 document.getElementById('pr-edit-toggle').style.display='none';
3353 document.getElementById('pr-edit-form').style.display='flex';
3354 document.getElementById('pr-title-input').focus();
3355 `}
3356 >
3357 Edit
3358 </button>
3359 )}
3360 </div>
3361 {canManage && pr.state === "open" && (
3362 <form
3363 id="pr-edit-form"
3364 method="post"
3365 action={`/${ownerName}/${repoName}/pulls/${pr.number}/edit`}
3366 class="prs-edit-form"
3367 style="display:none"
3368 >
3369 <input
3370 id="pr-title-input"
3371 type="text"
3372 name="title"
3373 value={pr.title}
3374 required
3375 maxlength={256}
3376 placeholder="Pull request title"
3377 />
3378 <div class="prs-edit-actions">
3379 <button type="submit" class="prs-edit-save-btn">Save</button>
3380 <button
3381 type="button"
3382 class="prs-edit-cancel-btn"
3383 onclick={`
3384 document.getElementById('pr-edit-form').style.display='none';
3385 document.getElementById('pr-title-display').style.display='';
3386 document.getElementById('pr-edit-toggle').style.display='';
3387 `}
3388 >
3389 Cancel
3390 </button>
3391 </div>
3392 </form>
3393 )}
33143394 <div class="prs-detail-meta">
33153395 <span class={`prs-state-pill state-${stateKey}`}>
33163396 <span aria-hidden="true">{stateIcon}</span>
@@ -3421,6 +3501,15 @@ pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), as
34213501 Conversation
34223502 <span class="prs-detail-tab-count">{commentCount}</span>
34233503 </a>
3504 <a
3505 class={`prs-detail-tab${tab === "commits" ? " is-active" : ""}`}
3506 href={`/${ownerName}/${repoName}/pulls/${pr.number}?tab=commits`}
3507 >
3508 Commits
3509 {branchAhead > 0 && (
3510 <span class="prs-detail-tab-count">{branchAhead}</span>
3511 )}
3512 </a>
34243513 <a
34253514 class={`prs-detail-tab${tab === "files" ? " is-active" : ""}`}
34263515 href={`/${ownerName}/${repoName}/pulls/${pr.number}?tab=files`}
@@ -3432,7 +3521,33 @@ pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), as
34323521 </a>
34333522 </nav>
34343523
3435 {tab === "files" ? (
3524 {tab === "commits" ? (
3525 <div class="prs-commits-list">
3526 {prCommits.length === 0 ? (
3527 <div class="prs-commits-empty">No commits between {pr.baseBranch} and {pr.headBranch}.</div>
3528 ) : (
3529 prCommits.map((commit) => (
3530 <div class="prs-commit-row">
3531 <span class="prs-commit-dot" aria-hidden="true"></span>
3532 <div class="prs-commit-body">
3533 <div class="prs-commit-msg" title={commit.message}>{commit.message}</div>
3534 <div class="prs-commit-meta">
3535 <strong>{commit.author}</strong> committed{" "}
3536 {formatRelative(new Date(commit.date))}
3537 </div>
3538 </div>
3539 <a
3540 href={`/${ownerName}/${repoName}/commit/${commit.sha}`}
3541 class="prs-commit-sha"
3542 title="View commit"
3543 >
3544 {commit.sha.slice(0, 7)}
3545 </a>
3546 </div>
3547 ))
3548 )}
3549 </div>
3550 ) : tab === "files" ? (
34363551 <DiffView
34373552 raw={diffRaw}
34383553 files={diffFiles}
@@ -3941,6 +4056,52 @@ pulls.post(
39414056 }
39424057);
39434058
4059// Edit PR title (and optionally body). Owner or author only.
4060pulls.post(
4061 "/:owner/:repo/pulls/:number/edit",
4062 softAuth,
4063 requireAuth,
4064 requireRepoAccess("write"),
4065 async (c) => {
4066 const { owner: ownerName, repo: repoName } = c.req.param();
4067 const prNum = parseInt(c.req.param("number"), 10);
4068 const user = c.get("user")!;
4069 const resolved = await resolveRepo(ownerName, repoName);
4070 if (!resolved) return c.redirect(`/${ownerName}/${repoName}`);
4071
4072 const [pr] = await db
4073 .select()
4074 .from(pullRequests)
4075 .where(and(
4076 eq(pullRequests.repositoryId, resolved.repo.id),
4077 eq(pullRequests.number, prNum),
4078 ))
4079 .limit(1);
4080 if (!pr || pr.state !== "open") {
4081 return c.redirect(`/${ownerName}/${repoName}/pulls/${prNum}`);
4082 }
4083 const canEdit = user.id === resolved.owner.id || user.id === pr.authorId;
4084 if (!canEdit) {
4085 return c.redirect(`/${ownerName}/${repoName}/pulls/${prNum}`);
4086 }
4087
4088 const body = await c.req.parseBody();
4089 const newTitle = String(body.title || "").trim().slice(0, 256);
4090 if (!newTitle) {
4091 return c.redirect(
4092 `/${ownerName}/${repoName}/pulls/${prNum}?error=${encodeURIComponent("Title cannot be empty")}`
4093 );
4094 }
4095
4096 await db
4097 .update(pullRequests)
4098 .set({ title: newTitle, updatedAt: new Date() })
4099 .where(eq(pullRequests.id, pr.id));
4100
4101 return c.redirect(`/${ownerName}/${repoName}/pulls/${prNum}?info=${encodeURIComponent("Title updated")}`);
4102 }
4103);
4104
39444105// Add comment to PR.
39454106//
39464107// Permission model mirrors `issues.tsx`: any logged-in user with read
39474108