Diff hunks + thread context on review comments; preview env links; hide matrix placeholder

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgBEW6qAwgn2fcQbA2f4ZQ
This commit is contained in:
Joshua Coles
2026-07-28 14:51:01 +00:00
co-authored by Claude Fable 5
parent df0e82e372
commit d14b6ebed3
11 changed files with 170 additions and 15 deletions
+1
View File
@@ -68,6 +68,7 @@ export function categorize(rules: RepoRules | undefined, contexts: CheckContext[
const buckets = new Map<CheckCategory, CheckInfo[]>(CATEGORIES.map((c) => [c, []]));
for (const ctx of dedupeByName(contexts)) {
const name = ctx.__typename === 'StatusContext' ? ctx.context : ctx.name;
if (rules?.ignore.has(name)) continue;
buckets.get(categoryFor(rules, name))!.push({
name,
state: checkState(ctx),
+16 -1
View File
@@ -15,6 +15,14 @@ const repoSchema = z.object({
e2e: categorySchema.optional(),
})
.default({}),
ignoreChecks: z.array(z.string()).default([]),
preview: z
.object({
label: z.string(),
/** `{number}` is replaced with the PR number. */
urlTemplate: z.string(),
})
.optional(),
});
const muteRuleSchema = z.object({
@@ -47,6 +55,8 @@ interface GlobMatcher {
export interface RepoRules {
exact: Map<string, CheckCategory>;
globs: GlobMatcher[];
ignore: Set<string>;
preview?: { label: string; urlTemplate: string };
}
export interface AppConfig extends RawConfig {
@@ -98,7 +108,12 @@ export function loadConfig(path = join(rootDir, 'config.json')): AppConfig {
}
}
}
rulesByRepo.set(repo.name, { exact, globs });
rulesByRepo.set(repo.name, {
exact,
globs,
ignore: new Set(repo.ignoreChecks),
preview: repo.preview,
});
}
const parts = ['is:pr', 'is:open', ...raw.repos.map((r) => `repo:${r.name}`)];
+12 -3
View File
@@ -55,6 +55,7 @@ export interface EventInsert {
reviewState: string | null;
threadId: string | null;
path: string | null;
diffHunk: string | null;
read: boolean;
}
@@ -76,6 +77,11 @@ export class Db {
this.db.exec('PRAGMA foreign_keys = ON');
this.db.exec(SCHEMA);
const eventCols = this.db.prepare('PRAGMA table_info(events)').all() as { name: string }[];
if (!eventCols.some((c) => c.name === 'diff_hunk')) {
this.db.exec('ALTER TABLE events ADD COLUMN diff_hunk TEXT');
}
const clauses: string[] = [];
for (const rule of opts.muteRules ?? []) {
let clause = 'e.actor = ?';
@@ -155,10 +161,11 @@ export class Db {
this.db
.prepare(
`INSERT INTO events (id, pr_id, kind, actor, created_at, updated_at, body_excerpt, url,
review_state, thread_id, path, read, read_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
review_state, thread_id, path, diff_hunk, read, read_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
ON CONFLICT(id) DO UPDATE SET
body_excerpt = excluded.body_excerpt, updated_at = excluded.updated_at`,
body_excerpt = excluded.body_excerpt, updated_at = excluded.updated_at,
diff_hunk = excluded.diff_hunk`,
)
.run(
e.id,
@@ -172,6 +179,7 @@ export class Db {
e.reviewState,
e.threadId,
e.path,
e.diffHunk,
e.read ? 1 : 0,
);
}
@@ -229,6 +237,7 @@ export class Db {
reviewState: r.review_state as string | null,
path: r.path as string | null,
read: (r.read as number) === 1,
diffHunk: (r.diff_hunk as string | null) ?? null,
inReplyTo:
r.root_id && r.root_id !== r.id
? { actor: r.root_actor as string, bodyExcerpt: (r.root_body as string) ?? '' }
+5 -1
View File
@@ -16,6 +16,7 @@ export const LIST_QUERY = /* GraphQL */ `
headRefName
headRefOid
repository { nameWithOwner }
labels(first: 20) { nodes { name } }
commits(last: 1) { nodes { commit { statusCheckRollup { state } } } }
reviewThreads(first: 50) { totalCount nodes { isResolved } }
comments(first: 1) { totalCount }
@@ -38,6 +39,7 @@ export interface ListPr {
headRefName: string;
headRefOid: string;
repository: { nameWithOwner: string };
labels: { nodes: { name: string }[] };
commits: { nodes: { commit: { statusCheckRollup: { state: string } | null } }[] };
reviewThreads: { totalCount: number; nodes: { isResolved: boolean }[] };
comments: { totalCount: number };
@@ -90,7 +92,7 @@ export const DETAIL_QUERY = /* GraphQL */ `
isResolved
path
comments(first: 30) {
nodes { id author { login } bodyText createdAt updatedAt url }
nodes { id author { login } bodyText createdAt updatedAt url diffHunk }
}
}
}
@@ -110,6 +112,8 @@ export interface DetailComment {
createdAt: string;
updatedAt: string;
url: string;
/** Present only on review-thread comments. */
diffHunk?: string;
}
export interface DetailPr {
+11 -2
View File
@@ -36,6 +36,7 @@ function signature(pr: ListPr): string {
pr.comments.totalCount,
pr.reviews.totalCount,
pr.latestReviews.nodes.map((r) => `${r.author?.login}:${r.state}`),
pr.labels.nodes.map((l) => l.name).sort(),
]),
)
.digest('hex');
@@ -59,14 +60,20 @@ async function fetchList(config: AppConfig): Promise<ListPr[]> {
function buildSnapshot(config: AppConfig, pr: ListPr, detail: DetailPr | undefined, db: Db): PrSnapshot {
const repo = pr.repository.nameWithOwner;
const rules = config.rulesByRepo.get(repo);
let categories;
if (detail) {
const contexts = detail.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? [];
categories = categorize(config.rulesByRepo.get(repo), contexts);
categories = categorize(rules, contexts);
} else {
// Detail node inaccessible this round — keep the previous check breakdown.
categories = db.getSnapshot(pr.id)?.categories ?? categorize(config.rulesByRepo.get(repo), []);
categories = db.getSnapshot(pr.id)?.categories ?? categorize(rules, []);
}
const labels = pr.labels.nodes.map((l) => l.name);
const previewUrl =
rules?.preview && labels.includes(rules.preview.label)
? rules.preview.urlTemplate.replaceAll('{number}', String(pr.number))
: null;
return {
id: pr.id,
repo,
@@ -77,6 +84,8 @@ function buildSnapshot(config: AppConfig, pr: ListPr, detail: DetailPr | undefin
isDraft: pr.isDraft,
branch: pr.headRefName,
updatedAt: pr.updatedAt,
labels,
previewUrl,
...deriveReviewState(pr),
verdicts: deriveVerdicts(pr),
categories,
+14
View File
@@ -8,6 +8,17 @@ function excerpt(text: string): string {
return clean.length > EXCERPT_LEN ? `${clean.slice(0, EXCERPT_LEN - 1)}…` : clean;
}
const HUNK_LINES = 8;
const HUNK_CHARS = 1200;
/** GitHub's diffHunk ends at the commented line — keep the trailing context. */
function trimHunk(hunk: string | undefined): string | null {
if (!hunk) return null;
const lines = hunk.split('\n');
const out = lines.slice(-HUNK_LINES).join('\n');
return out.length > HUNK_CHARS ? out.slice(-HUNK_CHARS) : out;
}
export interface TimelineOpts {
user: string;
/** First-ever sync: mark the historical backfill as already read. */
@@ -32,6 +43,7 @@ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number
reviewState: null,
threadId: null,
path: null,
diffHunk: null,
read: false,
});
}
@@ -50,6 +62,7 @@ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number
reviewState: null,
threadId: thread.id,
path: thread.path,
diffHunk: trimHunk(c.diffHunk),
read: false,
});
}
@@ -72,6 +85,7 @@ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number
reviewState: r.state,
threadId: null,
path: null,
diffHunk: null,
read: false,
});
}