pierre-review 0.1.79 → 0.1.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/routes/activity.js +1 -0
- package/dist/db/queries.js +140 -37
- package/dist/pro/contract.js +1 -0
- package/package.json +1 -1
- package/public/assets/{AiFixTab-BxRIMl9R.js → AiFixTab-BPDIpOW9.js} +1 -1
- package/public/assets/{ClaudeReviewTab-BTBkfp6H.js → ClaudeReviewTab-pDDtTXRI.js} +1 -1
- package/public/assets/{index-Bwrc3KJr.css → index-B4Hy-uho.css} +1 -1
- package/public/assets/index-BzIHcEkg.js +53 -0
- package/public/index.html +2 -2
- package/public/assets/index-Cv9-yWxl.js +0 -53
|
@@ -35,6 +35,7 @@ export async function activityRoutes(app) {
|
|
|
35
35
|
offset: Number.isFinite(offset) && offset > 0 ? offset : 0,
|
|
36
36
|
excludeBots: q.excludeBots === 'true',
|
|
37
37
|
allowBotIds: parseIntList(q.allowBotIds),
|
|
38
|
+
botsOnly: q.botsOnly === 'true',
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
// Mark the Activity Feed as seen (bumps the account's server-side "seen" marker to
|
package/dist/db/queries.js
CHANGED
|
@@ -636,19 +636,26 @@ export async function getTimeline(filters) {
|
|
|
636
636
|
return { prs, events: timelineEvents };
|
|
637
637
|
}
|
|
638
638
|
export async function getFeed(accountId, opts = {}) {
|
|
639
|
-
const { daysBefore = 14, repoIds = null, userIds = null, watchedOnly = false } = opts;
|
|
640
|
-
|
|
639
|
+
const { daysBefore = 14, repoIds = null, userIds = null, watchedOnly = false, prId = null, botActorIds = null } = opts;
|
|
640
|
+
// Isolated to one PR → no time window (epoch 0); otherwise the rolling `daysBefore` window.
|
|
641
|
+
const since = prId != null ? new Date(0) : new Date(Date.now() - daysBefore * 24 * 60 * 60 * 1000);
|
|
641
642
|
const conds = [
|
|
642
643
|
eq(events.accountId, accountId),
|
|
643
644
|
gte(events.occurredAt, since),
|
|
644
645
|
ne(events.type, 'commit_pushed'),
|
|
645
646
|
];
|
|
647
|
+
if (prId != null)
|
|
648
|
+
conds.push(eq(events.prId, prId));
|
|
646
649
|
if (watchedOnly)
|
|
647
650
|
conds.push(eq(repos.inboxWatch, true));
|
|
648
651
|
if (repoIds)
|
|
649
652
|
conds.push(inArray(events.repoId, repoIds));
|
|
650
653
|
if (userIds && userIds.length > 0)
|
|
651
654
|
conds.push(inArray(events.actorId, userIds));
|
|
655
|
+
// Bot feed: restrict to the automated-reviewer actor set (before the cap). An empty set
|
|
656
|
+
// means "no bots" → force an unsatisfiable predicate so the feed is empty (not unfiltered).
|
|
657
|
+
if (botActorIds != null)
|
|
658
|
+
conds.push(botActorIds.length > 0 ? inArray(events.actorId, botActorIds) : sql `1 = 0`);
|
|
652
659
|
const rows = await db
|
|
653
660
|
.select({
|
|
654
661
|
id: events.id,
|
|
@@ -1476,6 +1483,10 @@ export async function getActivity(accountId, repoIds, userIds = null) {
|
|
|
1476
1483
|
// Every My Turn (participated) event is always kept; the plain activity rows are bounded to
|
|
1477
1484
|
// the most recent, so a busy multi-repo account doesn't render thousands of them.
|
|
1478
1485
|
const FEED_EVENT_CAP = 250;
|
|
1486
|
+
// The Bots pane's bot-only feed filters to automated reviewers IN SQL, so its cap governs bot
|
|
1487
|
+
// activity alone (not a slice of all activity). Set generously so it spans the full window and
|
|
1488
|
+
// tracks the ROI thread counts; the feed is paginated + DOM-windowed, so a high total is cheap.
|
|
1489
|
+
const BOT_FEED_EVENT_CAP = 1000;
|
|
1479
1490
|
// A top-level PR comment and a coinciding "host" event by the SAME actor on the SAME PR are
|
|
1480
1491
|
// folded into ONE card when they land within this window of each other (issue comments carry
|
|
1481
1492
|
// no head SHA, so time is the only proxy for "posted together"). Symmetric around the host.
|
|
@@ -1573,7 +1584,7 @@ export function coalesceEventComments(items, byId) {
|
|
|
1573
1584
|
const COMMIT_ITEM_SCAN_CAP = 600; // most-recent commit_pushed events scanned
|
|
1574
1585
|
const COMMIT_RUN_GAP_MS = 6 * 60 * 60 * 1000; // >6h between an author's commits splits runs
|
|
1575
1586
|
async function getCommitThreadItems(accountId, opts) {
|
|
1576
|
-
const { repoIds, userIds, botIds, since } = opts;
|
|
1587
|
+
const { repoIds, userIds, botIds, since, prId = null } = opts;
|
|
1577
1588
|
const conds = [
|
|
1578
1589
|
eq(events.accountId, accountId),
|
|
1579
1590
|
eq(events.type, 'commit_pushed'),
|
|
@@ -1582,6 +1593,8 @@ async function getCommitThreadItems(accountId, opts) {
|
|
|
1582
1593
|
isNotNull(events.prId),
|
|
1583
1594
|
gte(events.occurredAt, since),
|
|
1584
1595
|
];
|
|
1596
|
+
if (prId != null)
|
|
1597
|
+
conds.push(eq(events.prId, prId));
|
|
1585
1598
|
if (repoIds)
|
|
1586
1599
|
conds.push(inArray(events.repoId, repoIds));
|
|
1587
1600
|
if (userIds && userIds.length > 0)
|
|
@@ -1797,7 +1810,9 @@ async function getCommitThreadItems(accountId, opts) {
|
|
|
1797
1810
|
// per PR = that PR's most-recent SUCCEEDED run finished within the feed window, repo-scoped.
|
|
1798
1811
|
// Gated on the feature flag (force-off in cloud) so no items appear where Claude Review
|
|
1799
1812
|
// doesn't exist. Never member-/bot-filtered (a run has no member author).
|
|
1800
|
-
async function getClaudeReviewFeedItems(accountId, repoIds, since
|
|
1813
|
+
async function getClaudeReviewFeedItems(accountId, repoIds, since,
|
|
1814
|
+
// Single-PR isolation (see getFeed): scope to this PR's runs (with a widened `since`).
|
|
1815
|
+
prId = null) {
|
|
1801
1816
|
if (!getProCapabilities().claudeReview)
|
|
1802
1817
|
return [];
|
|
1803
1818
|
const conds = [
|
|
@@ -1805,6 +1820,8 @@ async function getClaudeReviewFeedItems(accountId, repoIds, since) {
|
|
|
1805
1820
|
eq(claudeReviews.status, 'succeeded'),
|
|
1806
1821
|
gte(sql `coalesce(${claudeReviews.finishedAt}, ${claudeReviews.createdAt})`, tsBound(since)),
|
|
1807
1822
|
];
|
|
1823
|
+
if (prId != null)
|
|
1824
|
+
conds.push(eq(claudeReviews.prId, prId));
|
|
1808
1825
|
if (repoIds)
|
|
1809
1826
|
conds.push(inArray(pullRequests.repoId, repoIds));
|
|
1810
1827
|
const rows = await db
|
|
@@ -2859,7 +2876,7 @@ scopeRepoIds) {
|
|
|
2859
2876
|
// `repoIds` further narrows WITHIN watched (null → all watched). One flat, newest-first
|
|
2860
2877
|
// stream of real activity, each row flagged isMyTurn by participation (CORE; feed/my-turn.ts).
|
|
2861
2878
|
export async function getConsolidatedFeed(accountId, opts = {}) {
|
|
2862
|
-
const { repoIds = null, userIds = null, prId = null, limit = null, offset = 0, excludeBots = false, allowBotIds = null, } = opts;
|
|
2879
|
+
const { repoIds = null, userIds = null, prId = null, limit = null, offset = 0, excludeBots = false, allowBotIds = null, botsOnly = false, } = opts;
|
|
2863
2880
|
// Restrict to the account's WATCHED repos; a passed repoIds narrows within them. An
|
|
2864
2881
|
// out-of-scope / empty selection → a valid empty page (also avoids an inArray([]) below).
|
|
2865
2882
|
const watchedIds = (await listRepos(accountId)).filter((r) => r.inboxWatch).map((r) => r.id);
|
|
@@ -2873,18 +2890,42 @@ export async function getConsolidatedFeed(accountId, opts = {}) {
|
|
|
2873
2890
|
// their activity stays visible. getFeed doesn't filter bots, so we do it here; the commit
|
|
2874
2891
|
// helper filters in its own SQL.
|
|
2875
2892
|
const allowBots = new Set(allowBotIds ?? []);
|
|
2876
|
-
|
|
2893
|
+
// excludeBots is meaningless in the bot-only feed (it would drop everything) — force it off.
|
|
2894
|
+
const botIds = !botsOnly && excludeBots
|
|
2877
2895
|
? new Set((await botUserIds()).filter((id) => !allowBots.has(id)))
|
|
2878
2896
|
: new Set();
|
|
2879
2897
|
const notBot = (id) => !excludeBots || id == null || !botIds.has(id);
|
|
2880
|
-
|
|
2898
|
+
// Isolated to a single PR (the Open-PRs filter) → show that PR's FULL history: scope every
|
|
2899
|
+
// source to the PR and drop the 14-day window (epoch since). The scan is one PR, so it's
|
|
2900
|
+
// cheap, and the reader sees the opened event + all activity even on a long-idle PR — not an
|
|
2901
|
+
// empty pane. The un-isolated feed keeps the rolling 14-day window (a live activity stream).
|
|
2902
|
+
const feedSince = prId != null ? new Date(0) : new Date(Date.now() - 14 * 24 * 60 * 60 * 1000);
|
|
2903
|
+
// Bot-only feed: resolve the automated-reviewer actor set (vendors + classified in-house /
|
|
2904
|
+
// Pierre — the SAME set the ROI panel counts, so it catches deepsource-io etc. that aren't
|
|
2905
|
+
// users.isBot). Empty → nobody's classified → an empty feed. Filtered IN SQL by getFeed.
|
|
2906
|
+
const botActorIds = botsOnly ? await automatedReviewerUserIds(accountId) : null;
|
|
2907
|
+
if (botsOnly && (botActorIds == null || botActorIds.length === 0)) {
|
|
2908
|
+
return { items: [], users: [], total: 0, generatedAt: new Date().toISOString() };
|
|
2909
|
+
}
|
|
2881
2910
|
const [feed, commitItems, claudeItems] = await Promise.all([
|
|
2882
|
-
getFeed(accountId, { daysBefore: 14, repoIds: effectiveRepoIds, userIds }),
|
|
2911
|
+
getFeed(accountId, { daysBefore: 14, prId, repoIds: effectiveRepoIds, userIds, botActorIds }),
|
|
2883
2912
|
// Commit-push activity that ADDRESSED a review thread (the only commit rows we surface
|
|
2884
|
-
// — plain pushes are noise). Each carries the affected threads inline.
|
|
2885
|
-
|
|
2913
|
+
// — plain pushes are noise). Each carries the affected threads inline. Skipped in the
|
|
2914
|
+
// bot-only feed (a commit push is the AUTHOR responding, not review-bot activity).
|
|
2915
|
+
botsOnly
|
|
2916
|
+
? Promise.resolve([])
|
|
2917
|
+
: getCommitThreadItems(accountId, {
|
|
2918
|
+
repoIds: effectiveRepoIds,
|
|
2919
|
+
userIds,
|
|
2920
|
+
botIds,
|
|
2921
|
+
since: feedSince,
|
|
2922
|
+
prId,
|
|
2923
|
+
}),
|
|
2886
2924
|
// Claude Review runs surfaced as their own feed item kind (local-only; empty in cloud).
|
|
2887
|
-
|
|
2925
|
+
// Skipped in the bot-only feed (they're the user's own runs, not review-bot activity).
|
|
2926
|
+
botsOnly
|
|
2927
|
+
? Promise.resolve([])
|
|
2928
|
+
: getClaudeReviewFeedItems(accountId, effectiveRepoIds, feedSince, prId),
|
|
2888
2929
|
]);
|
|
2889
2930
|
// The "My Turn" participation flag (isMyTurn / myTurnReasons / reasonTag) is CORE / free
|
|
2890
2931
|
// (see feed/my-turn.ts). Core builds every item as a PLAIN row (isMyTurn:false), then
|
|
@@ -2957,6 +2998,24 @@ export async function getConsolidatedFeed(accountId, opts = {}) {
|
|
|
2957
2998
|
// two. Runs BEFORE the my-turn enrich/cap/paginate so `total`, participation and page bounds
|
|
2958
2999
|
// reflect the collapsed set (a comment folded here is never separately flagged/capped).
|
|
2959
3000
|
coalesceEventComments(items, byId);
|
|
3001
|
+
// Attach each thread-bearing item's review-thread derived state (untouched / replied /
|
|
3002
|
+
// likely_addressed / resolved) — powers the Bots pane's state-filter pills. One query over
|
|
3003
|
+
// the distinct thread ids referenced by this page's items; non-thread items stay null.
|
|
3004
|
+
const feedThreadIds = [
|
|
3005
|
+
...new Set(items.map((i) => i.threadId).filter((t) => t != null)),
|
|
3006
|
+
];
|
|
3007
|
+
if (feedThreadIds.length > 0) {
|
|
3008
|
+
const stateRows = await db
|
|
3009
|
+
.select({ id: reviewThreads.id, derivedState: reviewThreads.derivedState })
|
|
3010
|
+
.from(reviewThreads)
|
|
3011
|
+
.where(inArray(reviewThreads.id, feedThreadIds))
|
|
3012
|
+
.execute();
|
|
3013
|
+
const stateById = new Map(stateRows.map((r) => [r.id, r.derivedState]));
|
|
3014
|
+
for (const it of items) {
|
|
3015
|
+
if (it.threadId != null)
|
|
3016
|
+
it.derivedState = stateById.get(it.threadId) ?? null;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
2960
3019
|
// "My Turn" enrichment (CORE / free): flag each item `isMyTurn` by the viewer's participation
|
|
2961
3020
|
// in its PR. Runs BEFORE the cap so uncapped My-Turn rows survive.
|
|
2962
3021
|
await enrichMyTurn(accountId, items);
|
|
@@ -2970,7 +3029,7 @@ export async function getConsolidatedFeed(accountId, opts = {}) {
|
|
|
2970
3029
|
const feedRows = scoped
|
|
2971
3030
|
.filter((i) => !i.isMyTurn && i.kind !== 'claude_review')
|
|
2972
3031
|
.sort((a, b) => b.occurredAt.localeCompare(a.occurredAt))
|
|
2973
|
-
.slice(0, FEED_EVENT_CAP);
|
|
3032
|
+
.slice(0, botsOnly ? BOT_FEED_EVENT_CAP : FEED_EVENT_CAP);
|
|
2974
3033
|
const ordered = [...alwaysRows, ...feedRows].sort((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
|
2975
3034
|
// Paginate: `total` is the full stream length; `page` is the requested window. Only
|
|
2976
3035
|
// the page is enriched + has its users backfilled, so hidden items cost nothing.
|
|
@@ -5483,6 +5542,39 @@ scopeRepoIds) {
|
|
|
5483
5542
|
}
|
|
5484
5543
|
const kindMap = await classificationKindForUser(accountId);
|
|
5485
5544
|
const hideRules = (await listBotMuteRules(accountId)).filter((r) => r.action === 'hide');
|
|
5545
|
+
// Per-REVIEWER identity (so in-house bots — all kind 'in_house' — separate into their own rows
|
|
5546
|
+
// instead of collapsing). Label preference: the account's custom classification label →
|
|
5547
|
+
// the vendor's pretty name (for known vendors) → the reviewer's login/display name.
|
|
5548
|
+
const classLabel = new Map();
|
|
5549
|
+
for (const r of await db
|
|
5550
|
+
.select({ id: botReviewClassification.authorUserId, label: botReviewClassification.label })
|
|
5551
|
+
.from(botReviewClassification)
|
|
5552
|
+
.where(eq(botReviewClassification.accountId, accountId))
|
|
5553
|
+
.execute()) {
|
|
5554
|
+
if (r.label != null && r.label.trim() !== '')
|
|
5555
|
+
classLabel.set(r.id, r.label.trim());
|
|
5556
|
+
}
|
|
5557
|
+
const loginById = new Map(); // display fallback (name || login)
|
|
5558
|
+
const rawLoginById = new Map(); // the github login — the per-bot cost key
|
|
5559
|
+
if (automatedIds.length > 0) {
|
|
5560
|
+
for (const r of await db
|
|
5561
|
+
.select({ id: users.id, login: users.githubLogin, name: users.displayName })
|
|
5562
|
+
.from(users)
|
|
5563
|
+
.where(inArray(users.id, automatedIds))
|
|
5564
|
+
.execute()) {
|
|
5565
|
+
loginById.set(r.id, r.name?.trim() || r.login || `#${r.id}`);
|
|
5566
|
+
if (r.login)
|
|
5567
|
+
rawLoginById.set(r.id, r.login);
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
const reviewerLabel = (userId, kind) => {
|
|
5571
|
+
const custom = classLabel.get(userId);
|
|
5572
|
+
if (custom)
|
|
5573
|
+
return custom;
|
|
5574
|
+
if (kind !== 'in_house' && kind !== 'pierre')
|
|
5575
|
+
return labelForKind(kind);
|
|
5576
|
+
return loginById.get(userId) ?? labelForKind(kind);
|
|
5577
|
+
};
|
|
5486
5578
|
// Automated-reviewer threads over the 12-week trend span (⊇ the selected window).
|
|
5487
5579
|
const threadRows = await db
|
|
5488
5580
|
.select({
|
|
@@ -5530,21 +5622,24 @@ scopeRepoIds) {
|
|
|
5530
5622
|
}
|
|
5531
5623
|
return false;
|
|
5532
5624
|
};
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5625
|
+
// Keyed by REVIEWER user id (not kind) so each bot — including every in-house bot that shares
|
|
5626
|
+
// kind 'in_house' — gets its own row. `kind` rides along for colour / cost / verdict semantics.
|
|
5627
|
+
const byUser = new Map();
|
|
5628
|
+
const accFor = (userId, kind) => {
|
|
5629
|
+
let a = byUser.get(userId);
|
|
5536
5630
|
if (!a) {
|
|
5537
5631
|
a = {
|
|
5632
|
+
kind,
|
|
5538
5633
|
reviewers: new Set(),
|
|
5539
5634
|
threads: 0,
|
|
5540
5635
|
actedOn: 0,
|
|
5541
5636
|
untouched: 0,
|
|
5542
5637
|
oldestUntouchedMs: null,
|
|
5543
5638
|
humanFollow: 0,
|
|
5544
|
-
weekly: Array.from({ length: 12 }, () => ({ threads: 0, actedOn: 0 })),
|
|
5639
|
+
weekly: Array.from({ length: 12 }, () => ({ threads: 0, actedOn: 0, untouched: 0 })),
|
|
5545
5640
|
buckets: new Map(),
|
|
5546
5641
|
};
|
|
5547
|
-
|
|
5642
|
+
byUser.set(userId, a);
|
|
5548
5643
|
}
|
|
5549
5644
|
return a;
|
|
5550
5645
|
};
|
|
@@ -5558,7 +5653,7 @@ scopeRepoIds) {
|
|
|
5558
5653
|
continue;
|
|
5559
5654
|
if (isHidden(t, kind))
|
|
5560
5655
|
continue;
|
|
5561
|
-
const acc = accFor(kind);
|
|
5656
|
+
const acc = accFor(t.userId, kind);
|
|
5562
5657
|
const acted = t.state === 'resolved' || t.state === 'likely_addressed';
|
|
5563
5658
|
// Trend bucket by created week.
|
|
5564
5659
|
const wk = Math.min(11, Math.max(0, Math.floor((t.createdAt.getTime() - trendFrom.getTime()) / (7 * 86_400_000))));
|
|
@@ -5566,6 +5661,8 @@ scopeRepoIds) {
|
|
|
5566
5661
|
bucket.threads += 1;
|
|
5567
5662
|
if (acted)
|
|
5568
5663
|
bucket.actedOn += 1;
|
|
5664
|
+
if (t.state === 'untouched')
|
|
5665
|
+
bucket.untouched += 1;
|
|
5569
5666
|
// Headline metrics use only the selected window. NOTE: acc.actedOn is accumulated LATER
|
|
5570
5667
|
// (after the human-follow-up pass) under the merged "acted-on" definition (item 6) —
|
|
5571
5668
|
// resolved | likely_addressed | a human replied after the bot's last comment.
|
|
@@ -5584,7 +5681,7 @@ scopeRepoIds) {
|
|
|
5584
5681
|
if (t.state === 'untouched')
|
|
5585
5682
|
b.untouched += 1;
|
|
5586
5683
|
acc.buckets.set(pb, b);
|
|
5587
|
-
windowThreads.push({ id: t.id, kind, path: t.path, state: t.state, createdAt: t.createdAt });
|
|
5684
|
+
windowThreads.push({ id: t.id, userId: t.userId, kind, path: t.path, state: t.state, createdAt: t.createdAt });
|
|
5588
5685
|
}
|
|
5589
5686
|
}
|
|
5590
5687
|
// Human follow-through: of the bot's window threads, the ones where a human commented after
|
|
@@ -5608,10 +5705,10 @@ scopeRepoIds) {
|
|
|
5608
5705
|
arr.push({ authorId: r.authorId, at: r.createdAt.getTime() });
|
|
5609
5706
|
byThread.set(r.threadId, arr);
|
|
5610
5707
|
}
|
|
5611
|
-
const
|
|
5708
|
+
const reviewerByThread = new Map(windowThreads.map((t) => [t.id, { userId: t.userId, kind: t.kind }]));
|
|
5612
5709
|
for (const [threadId, comments] of byThread) {
|
|
5613
|
-
const
|
|
5614
|
-
if (!
|
|
5710
|
+
const rv = reviewerByThread.get(threadId);
|
|
5711
|
+
if (!rv)
|
|
5615
5712
|
continue;
|
|
5616
5713
|
let botLastAt = -Infinity;
|
|
5617
5714
|
for (const c of comments) {
|
|
@@ -5621,7 +5718,7 @@ scopeRepoIds) {
|
|
|
5621
5718
|
const humanAfter = comments.some((c) => c.authorId != null && !automatedIds.includes(c.authorId) && c.at > botLastAt);
|
|
5622
5719
|
if (humanAfter) {
|
|
5623
5720
|
humanFollowSet.add(threadId);
|
|
5624
|
-
accFor(kind).humanFollow += 1;
|
|
5721
|
+
accFor(rv.userId, rv.kind).humanFollow += 1;
|
|
5625
5722
|
}
|
|
5626
5723
|
}
|
|
5627
5724
|
}
|
|
@@ -5630,31 +5727,32 @@ scopeRepoIds) {
|
|
|
5630
5727
|
for (const t of windowThreads) {
|
|
5631
5728
|
const baseActed = t.state === 'resolved' || t.state === 'likely_addressed';
|
|
5632
5729
|
if (baseActed || humanFollowSet.has(t.id))
|
|
5633
|
-
accFor(t.kind).actedOn += 1;
|
|
5730
|
+
accFor(t.userId, t.kind).actedOn += 1;
|
|
5634
5731
|
}
|
|
5635
|
-
// Comments volume per
|
|
5732
|
+
// Comments volume per REVIEWER (bot-authored review comments in the window).
|
|
5636
5733
|
const commentRows = await db
|
|
5637
5734
|
.select({ authorId: reviewComments.authorId })
|
|
5638
5735
|
.from(reviewComments)
|
|
5639
5736
|
.innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
|
|
5640
5737
|
.where(and(eq(pullRequests.accountId, accountId), inArray(reviewComments.authorId, automatedIds), gte(reviewComments.createdAt, from), lte(reviewComments.createdAt, to), ...repoScopeFilter))
|
|
5641
5738
|
.execute();
|
|
5642
|
-
const
|
|
5739
|
+
const commentsByUser = new Map();
|
|
5643
5740
|
for (const r of commentRows) {
|
|
5644
5741
|
if (r.authorId == null)
|
|
5645
5742
|
continue;
|
|
5646
|
-
|
|
5647
|
-
if (!kind)
|
|
5743
|
+
if (!kindMap.get(r.authorId))
|
|
5648
5744
|
continue;
|
|
5649
|
-
|
|
5745
|
+
commentsByUser.set(r.authorId, (commentsByUser.get(r.authorId) ?? 0) + 1);
|
|
5650
5746
|
}
|
|
5651
5747
|
const suggestions = [];
|
|
5652
5748
|
const vendors = [];
|
|
5653
|
-
for (const [
|
|
5654
|
-
|
|
5749
|
+
for (const [userId, acc] of byUser) {
|
|
5750
|
+
const comments = commentsByUser.get(userId) ?? 0;
|
|
5751
|
+
if (acc.threads === 0 && comments === 0)
|
|
5655
5752
|
continue;
|
|
5753
|
+
const kind = acc.kind;
|
|
5754
|
+
const label = reviewerLabel(userId, kind);
|
|
5656
5755
|
const actedOnPct = acc.threads > 0 ? Math.round((acc.actedOn / acc.threads) * 100) : null;
|
|
5657
|
-
const comments = commentsByKind.get(kind) ?? 0;
|
|
5658
5756
|
const oldestUntouchedDays = acc.oldestUntouchedMs == null ? null : Math.floor((nowMs - acc.oldestUntouchedMs) / 86_400_000);
|
|
5659
5757
|
const humanFollowThroughPct = acc.threads > 0 ? Math.round((acc.humanFollow / acc.threads) * 100) : null;
|
|
5660
5758
|
// Noise ratio: untouched-share proxy (see the header — true severity is often unknowable).
|
|
@@ -5663,10 +5761,15 @@ scopeRepoIds) {
|
|
|
5663
5761
|
weekStart: new Date(trendFrom.getTime() + i * 7 * 86_400_000).toISOString(),
|
|
5664
5762
|
threads: w.threads,
|
|
5665
5763
|
actedOnPct: w.threads > 0 ? Math.round((w.actedOn / w.threads) * 100) : null,
|
|
5764
|
+
untouched: w.untouched,
|
|
5666
5765
|
}));
|
|
5667
5766
|
vendors.push({
|
|
5767
|
+
// A stable per-reviewer row key (kind repeats across in-house bots, so the table can't key
|
|
5768
|
+
// on kind). `kind` still drives colour / cost / verdict; `label` is the per-bot name.
|
|
5769
|
+
key: `u${userId}`,
|
|
5668
5770
|
kind,
|
|
5669
|
-
label
|
|
5771
|
+
label,
|
|
5772
|
+
login: rawLoginById.get(userId) ?? null,
|
|
5670
5773
|
reviewers: acc.reviewers.size,
|
|
5671
5774
|
threads: acc.threads,
|
|
5672
5775
|
comments,
|
|
@@ -5681,8 +5784,8 @@ scopeRepoIds) {
|
|
|
5681
5784
|
costPerActedOnUsd: null,
|
|
5682
5785
|
trend,
|
|
5683
5786
|
});
|
|
5684
|
-
// Deterministic tuning suggestions (§3h): a (
|
|
5685
|
-
// untouchedPct ≥ 70 → "mute this".
|
|
5787
|
+
// Deterministic tuning suggestions (§3h): a (reviewer, path-bucket) with volume ≥ 5 and
|
|
5788
|
+
// untouchedPct ≥ 70 → "mute this". vendorKind stays the kind (mute rules match by kind).
|
|
5686
5789
|
for (const [pb, b] of acc.buckets) {
|
|
5687
5790
|
if (b.volume < 5)
|
|
5688
5791
|
continue;
|
|
@@ -5691,12 +5794,12 @@ scopeRepoIds) {
|
|
|
5691
5794
|
continue;
|
|
5692
5795
|
suggestions.push({
|
|
5693
5796
|
vendorKind: kind,
|
|
5694
|
-
label
|
|
5797
|
+
label,
|
|
5695
5798
|
pathGlob: pb,
|
|
5696
5799
|
severity: null,
|
|
5697
5800
|
untouchedPct,
|
|
5698
5801
|
volume: b.volume,
|
|
5699
|
-
rationale: `${untouchedPct}% of ${
|
|
5802
|
+
rationale: `${untouchedPct}% of ${label}'s ${b.volume} threads in ${pb} went untouched — mute them?`,
|
|
5700
5803
|
});
|
|
5701
5804
|
}
|
|
5702
5805
|
}
|
package/dist/pro/contract.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pierre-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.81",
|
|
4
4
|
"description": "Dashboard for tracking your team's GitHub PR activity across repos — local (SQLite + gh) or self-hosted multi-tenant cloud (Postgres + GitHub App).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Alex Wakeman",
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{r as o,j as e}from"./tanstack-R-s9abdD.js";import{D as te,w as W,A as re,c as ne,d as ae,q as ie,G as ce,I as J,M as K,i as le,l as oe,k as de,C as ue,b as xe,R as Q,a as he,p as X,F as Z,r as I,E as ge,J as me,n as be,m as pe,y as fe,j as Y}from"./index-
|
|
1
|
+
import{r as o,j as e}from"./tanstack-R-s9abdD.js";import{D as te,w as W,A as re,c as ne,d as ae,q as ie,G as ce,I as J,M as K,i as le,l as oe,k as de,C as ue,b as xe,R as Q,a as he,p as X,F as Z,r as I,E as ge,J as me,n as be,m as pe,y as fe,j as Y}from"./index-BzIHcEkg.js";import"./markdown-BUNN1sk6.js";import"./highlight-C8vPehS1.js";import"./vis-C3BCkP5U.js";const S="whitespace-nowrap rounded border border-blue-400 px-2.5 py-1 text-xs text-blue-600 hover:bg-blue-50 disabled:opacity-50 dark:border-blue-600 dark:text-blue-400 dark:hover:bg-blue-900/30",j="whitespace-nowrap rounded border border-gray-300 px-2.5 py-1 text-xs hover:border-gray-400 disabled:opacity-50 dark:border-gray-700 dark:hover:border-gray-500",ye={fetching_diff:"Reading the PR",cloning:"Checking out the code",fixing:"Applying the fix",capturing:"Capturing changes",persisting:"Saving"};function je(s){var t;if(!s||s.status==="idle")return null;if(s.status==="queued")return 6;const r=s.progress;if(!r)return 10;switch(r.phase){case"fetching_diff":return 12;case"cloning":return 28;case"fixing":return Math.min(90,45+(((t=r.recentActivity)==null?void 0:t.length)??0)*3);case"capturing":return 92;case"persisting":return 96;default:return 20}}const ke={cloning:"Checking out the code",applying_fix:"Applying the fix",fetching_trunk:"Fetching the trunk",rebasing:"Rebasing onto the trunk",merging:"Merging the trunk in",resolving_conflicts:"Resolving conflicts with Claude",verifying:"Verifying the result",pushing:"Pushing"};function Ne(s){var t;if(!s||s.status==="idle")return null;if(s.status==="queued")return 6;const r=s.progress;if(!r)return 10;switch(r.phase){case"cloning":return 15;case"applying_fix":return 25;case"fetching_trunk":return 35;case"rebasing":case"merging":return 45;case"resolving_conflicts":return Math.min(90,55+(((t=r.recentActivity)==null?void 0:t.length)??0)*3);case"verifying":return 92;case"pushing":return 96;default:return 20}}function L({children:s}){return e.jsx("div",{className:"px-4 pb-1 pt-3 text-xs font-semibold uppercase tracking-wide text-gray-400",children:s})}const ve={high:"bg-green-100 text-green-700 dark:bg-green-950/40 dark:text-green-300",medium:"bg-amber-100 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300",low:"bg-red-100 text-red-700 dark:bg-red-950/40 dark:text-red-300"};function z({label:s,value:r}){return r?e.jsxs("span",{className:`rounded px-1.5 py-0.5 text-[10px] font-medium uppercase ${ve[r]}`,title:s==="Fixability"?"How confident the analysis is that Pierre's agent could fix this":"How confident the analysis is about the root cause",children:[s,": ",r]}):null}function De({pr:s}){const{aiAnalysis:r,aiFix:t}=te(),n=W(x=>x.aiFixTabFocus),i=W(x=>x.consumeAiFixTabFocus),[c,m]=o.useState(null);return o.useEffect(()=>{n&&n.prId===s.id&&(n.reviewText&&m(n.reviewText),i())},[n,s.id,i]),!r&&!t?e.jsx("div",{className:"p-4 text-sm text-gray-500",children:"AI Analysis and Fix is not enabled."}):e.jsxs("div",{className:"pb-6",children:[r&&e.jsx("div",{className:"border-b border-gray-200 dark:border-gray-800",children:e.jsx(re,{pr:s})}),e.jsx(Ce,{pr:s}),r&&e.jsx(Pe,{pr:s,canFix:t}),t&&e.jsx(Re,{pr:s,seedReviewText:c,onSeedConsumed:()=>m(null)})]})}function Ce({pr:s}){const r=s.checkRuns;return r.length===0?null:e.jsxs("div",{className:"border-b border-gray-200 dark:border-gray-800",children:[e.jsx(L,{children:"CI status"}),e.jsxs("div",{className:"px-4 pb-3",children:[e.jsx(ne,{prId:s.id,checks:r}),e.jsx(ae,{prId:s.id,checks:r,viewerCanPush:s.viewerCanPush})]})]})}function Pe({pr:s,canFix:r}){const{data:t}=ie(s.id,!0),n=ce(s.id),i=J(s.id),c=o.useMemo(()=>s.checkRuns.filter(l=>l.state==="failure"||l.state==="error").map(l=>({name:l.name,jobId:l.jobId,state:l.state})),[s.checkRuns]);if(!(c.length>0||((t==null?void 0:t.hasFailures)??!1))&&!(t!=null&&t.analysis))return null;const x=(t==null?void 0:t.fixability)==="low";return e.jsxs("div",{className:"border-b border-gray-200 dark:border-gray-800",children:[e.jsx(L,{children:"CI failure analysis"}),e.jsxs("div",{className:"px-4 pb-3",children:[(t==null?void 0:t.analysis)&&(t.rootCauseConfidence||t.fixability)&&e.jsxs("div",{className:"mb-1.5 flex flex-wrap items-center gap-1.5",children:[e.jsx("span",{className:"text-[10px] font-semibold uppercase tracking-wide text-gray-400",title:"How confident the analysis is — not a severity rating",children:"Confidence"}),e.jsx(z,{label:"Root cause",value:t.rootCauseConfidence}),e.jsx(z,{label:"Fixability",value:t.fixability})]}),t!=null&&t.analysis?e.jsx("div",{className:"prose prose-sm max-w-none dark:prose-invert",children:e.jsx(K,{children:t.analysis})}):e.jsx("p",{className:"text-sm text-gray-500",children:"Diagnose the failing CI: likely root cause + a suggested fix, from the full logs of every failing check."}),e.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-2",children:[e.jsx("button",{type:"button",className:j,disabled:n.isPending||c.length===0,onClick:()=>n.mutate(c),children:n.isPending?"Analyzing…":t!=null&&t.analysis?"Re-analyze":"Analyze CI failure"}),r&&(t==null?void 0:t.analysis)&&e.jsx("button",{type:"button",className:S,disabled:i.isPending,onClick:()=>i.mutate({model:"claude-sonnet-5",seed:"ci_analysis"}),title:"Launch an agent to fix the CI failure",children:"Fix it →"}),r&&x&&(t==null?void 0:t.analysis)&&e.jsx("span",{className:"text-[11px] text-gray-500",children:"low confidence this is auto-fixable"}),n.isError&&e.jsx("span",{className:"text-[11px] text-red-500",children:D(n.error)})]})]})]})}function Re({pr:s,seedReviewText:r,onSeedConsumed:t}){var k,F,N,A;const{data:n,isLoading:i}=le(s.id,!0),[c,m]=o.useState("claude-sonnet-5"),x=J(s.id),l=oe(s.id),p=((k=n==null?void 0:n.fix)==null?void 0:k.status)??null,h=p==="running"||p==="queued"||x.isPending,{status:d}=de(s.id,h),R=(d==null?void 0:d.status)??p??"idle",f=R==="running"||R==="queued",w=r?"review":"plain",M=()=>{x.mutate({model:c,seed:w,reviewText:r??void 0},{onSuccess:()=>t()})},u=(n==null?void 0:n.fix)??null;return e.jsxs("div",{children:[e.jsx(L,{children:"AI Fix"}),e.jsxs("div",{className:"px-4",children:[(n==null?void 0:n.enabled)===!1?e.jsx("p",{className:"text-sm text-gray-500",children:"The agentic fixer is disabled."}):(n==null?void 0:n.auth)==="none"?e.jsx("p",{className:"rounded border border-amber-300 bg-amber-50 p-2 text-xs text-amber-700 dark:border-amber-700 dark:bg-amber-950/30 dark:text-amber-300",children:n.authMessage??"No Claude authentication found. Sign in to Claude or set an API key, then restart."}):e.jsxs(e.Fragment,{children:[r&&!f&&e.jsx("div",{className:"mb-2 rounded border border-blue-200 bg-blue-50 p-2 text-xs text-blue-700 dark:border-blue-800 dark:bg-blue-950/30 dark:text-blue-300",children:"Ready to generate a fix from the selected review."}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("select",{className:"rounded border border-gray-300 bg-transparent px-2 py-1 text-xs dark:border-gray-700",value:c,onChange:y=>m(y.target.value),disabled:f,children:ue.map(y=>e.jsx("option",{value:y,children:xe[y]},y))}),f?e.jsx("button",{type:"button",className:j,onClick:()=>l.mutate(),children:"Cancel"}):e.jsx("button",{type:"button",className:S,disabled:x.isPending,onClick:M,children:u?"Generate new fix":"Generate fix"}),x.isError&&e.jsx("span",{className:"text-[11px] text-red-500",children:D(x.error)})]}),f&&e.jsxs("div",{className:"mt-3",children:[e.jsx(Q,{active:!0,label:"Running AI fix",value:je(d),timeConstantSec:40}),e.jsx("div",{className:"mt-1 text-[11px] text-gray-500",children:ye[((F=d==null?void 0:d.progress)==null?void 0:F.phase)??""]??"Working…"}),((N=d==null?void 0:d.progress)==null?void 0:N.recentActivity)&&d.progress.recentActivity.length>0&&e.jsx("pre",{className:"mt-1 max-h-32 overflow-auto rounded bg-gray-50 p-2 text-[11px] leading-relaxed text-gray-500 dark:bg-gray-900",children:d.progress.recentActivity.slice(-8).join(`
|
|
2
2
|
`)})]}),!f&&u&&e.jsx(we,{pr:s,fix:u,headInfo:(n==null?void 0:n.headInfo)??null,viewerCanPush:(n==null?void 0:n.viewerCanPush)??!1}),i&&!u&&e.jsx("p",{className:"mt-3 text-xs text-gray-400",children:"Loading…"})]}),e.jsx(Fe,{history:(n==null?void 0:n.history)??[],currentFixId:((A=n==null?void 0:n.fix)==null?void 0:A.id)??null})]})]})}function we({pr:s,fix:r,headInfo:t,viewerCanPush:n}){const i=o.useMemo(()=>X(r.patch),[r.patch]);if(r.status==="failed")return e.jsxs("div",{className:"mt-3 rounded border border-red-200 bg-red-50 p-2 text-xs text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-300",children:["The fix run failed: ",r.error??"unknown error"]});if(r.status==="cancelled")return e.jsx("p",{className:"mt-3 text-xs text-gray-500",children:"The fix run was cancelled."});if(r.status!=="succeeded")return e.jsx(e.Fragment,{});const c=!r.patch||r.filesChanged.length===0;return e.jsxs("div",{className:"mt-3",children:[r.summary&&e.jsx("div",{className:"prose prose-sm max-w-none dark:prose-invert",children:e.jsx(K,{children:r.summary})}),c?e.jsx("p",{className:"mt-2 text-xs text-gray-500",children:"The agent made no changes."}):e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-1 mt-2 text-[11px] text-gray-500",children:[r.filesChanged.length," file",r.filesChanged.length===1?"":"s"," changed"]}),e.jsx("div",{className:"overflow-hidden rounded border border-gray-200 text-gray-800 dark:border-gray-800 dark:text-gray-200",children:e.jsx(Z,{files:i})}),e.jsx(Se,{pr:s,fix:r,headInfo:t,viewerCanPush:n})]})]})}function _({fix:s}){return e.jsxs("div",{className:"mb-2 rounded border border-green-200 bg-green-50 p-2 text-xs text-green-700 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300",children:[e.jsxs("div",{children:["Pushed to ",e.jsx("span",{className:"font-mono",children:s.pushedBranch}),s.pushedPrUrl&&e.jsxs(e.Fragment,{children:[" · ",e.jsxs("a",{href:s.pushedPrUrl,target:"_blank",rel:"noreferrer",className:"underline",children:["PR #",s.pushedPrNumber]})]}),s.pushedAt&&e.jsxs(e.Fragment,{children:[" · ",I(s.pushedAt)]})]}),s.commitMessage&&e.jsx("div",{className:"mt-1 font-mono text-[11px] text-green-800 dark:text-green-300",children:s.commitMessage})]})}function Fe({history:s,currentFixId:r}){const t=s.filter(n=>n.pushedAt!=null&&n.id!==r);return t.length===0?null:e.jsxs("div",{className:"mt-4 border-t border-gray-200 pt-3 dark:border-gray-800",children:[e.jsx("div",{className:"mb-1.5 text-xs font-semibold uppercase tracking-wide text-gray-400",children:"Fixes pushed via Pierre"}),e.jsx("ul",{className:"space-y-2",children:t.map(n=>e.jsxs("li",{className:"text-xs",children:[e.jsx("div",{className:"font-mono text-gray-700 dark:text-gray-200",children:n.commitMessage??"(no commit message)"}),e.jsxs("div",{className:"mt-0.5 text-[11px] text-gray-400",children:[n.pushedBranch&&e.jsx("span",{className:"font-mono",children:n.pushedBranch}),n.pushedPrUrl&&e.jsxs(e.Fragment,{children:[" · ",e.jsxs("a",{href:n.pushedPrUrl,target:"_blank",rel:"noreferrer",className:"underline",children:["PR #",n.pushedPrNumber]})]}),n.pushedAt&&e.jsxs(e.Fragment,{children:[" · ",I(n.pushedAt)]})]})]},n.id))})]})}function Ae({preview:s,loading:r}){if(r)return e.jsx("p",{className:"text-[11px] text-gray-400",children:"Checking against the trunk…"});if(!s||!s.available||!s.trunkSha)return null;if(!s.clean){const t=s.conflictFiles;return e.jsxs("div",{className:"rounded border border-amber-300 bg-amber-50 p-2 text-[11px] text-amber-700 dark:border-amber-700 dark:bg-amber-950/30 dark:text-amber-300",children:["⚠ Conflicts with ",e.jsx("span",{className:"font-mono",children:s.trunk}),t.length>0?e.jsxs(e.Fragment,{children:[" in ",t.length," file",t.length===1?"":"s",e.jsxs("span",{className:"text-amber-600/90 dark:text-amber-400/90",children:[": ",t.slice(0,6).join(", "),t.length>6?"…":""]})]}):null,". Resolving before you push avoids a conflicted PR."]})}return s.behindBy>0?e.jsxs("p",{className:"text-[11px] text-gray-500",children:[e.jsx("span",{className:"font-mono",children:s.trunk})," is ",s.behindBy," ","commit",s.behindBy===1?"":"s"," ahead — the fix merges cleanly."]}):e.jsxs("p",{className:"text-[11px] text-green-600 dark:text-green-400",children:["Up to date with ",e.jsx("span",{className:"font-mono",children:s.trunk})," — no conflicts."]})}function Ee({resolved:s,target:r,pushing:t,onPush:n,onRedo:i,disabled:c}){const m=o.useMemo(()=>X(s.diff),[s.diff]);return e.jsxs("div",{className:"mt-2 space-y-2",children:[e.jsxs("div",{className:"rounded border border-green-200 bg-green-50 p-2 text-[11px] text-green-700 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300",children:["Rebased onto ",e.jsx("span",{className:"font-mono",children:s.trunk}),".",s.resolvedConflicts?` Claude resolved conflicts in ${s.conflictFiles.length} file${s.conflictFiles.length===1?"":"s"}${s.conflictFiles.length?`: ${s.conflictFiles.join(", ")}`:""}.`:" No conflicts."," ","Review the result below, then push."]}),e.jsxs("div",{className:"text-[11px] text-gray-500",children:[s.filesChanged.length," file",s.filesChanged.length===1?"":"s"," in the rebased result"]}),e.jsx("div",{className:"overflow-hidden rounded border border-gray-200 text-gray-800 dark:border-gray-800 dark:text-gray-200",children:e.jsx(Z,{files:m})}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("button",{type:"button",className:S,disabled:t||c,onClick:n,children:t?"Pushing…":r==="new"?"Push rebased + open PR":"Push rebased (force-with-lease)"}),e.jsx("button",{type:"button",className:j,disabled:t,onClick:i,children:"Re-rebase"})]})]})}function Se({pr:s,fix:r,headInfo:t,viewerCanPush:n}){var V,G;const i=ge(s.id),c=me(),m=be(),x=pe(),l=fe(),p=(t==null?void 0:t.canPushSameBranch)??!1,[h,d]=o.useState(p?"existing":"new"),[R,f]=o.useState((t==null?void 0:t.suggestedBranch)??""),[w,M]=o.useState(!0),[u,k]=o.useState("idle"),[F,N]=o.useState(null),A=o.useRef(!1),y=o.useRef(!1);o.useEffect(()=>{!A.current&&(t!=null&&t.suggestedBranch)&&(f(t.suggestedBranch),A.current=!0,t.canPushSameBranch||d("new"))},[t]);const g=r.pushedAt!=null,q=g&&r.pushedPrNumber==null,U=!g||q;o.useEffect(()=>{!y.current&&n&&U&&(y.current=!0,l.mutate(r.id))},[n,U,r.id]);const a=l.data??null,{status:v}=Y(s.id,r.id,"rebase",u==="rebasing"),{status:C}=Y(s.id,r.id,"push",u==="pushing");if(o.useEffect(()=>{u==="rebasing"&&v&&v.status!=="running"&&v.status!=="queued"&&(k("idle"),v.error&&N(v.error))},[u,v]),o.useEffect(()=>{u==="pushing"&&C&&C.status!=="running"&&C.status!=="queued"&&(k("idle"),C.error&&N(C.error))},[u,C]),!n)return g?e.jsx(_,{fix:r}):e.jsx("p",{className:"mt-2 text-xs text-gray-500",children:"You need write access to this repository to push this fix."});if(g&&!q)return e.jsx(_,{fix:r});const B=h==="new"&&R.trim().length===0,P=u!=="idle"||i.isPending||c.isPending,H=r.model,ee=g&&!!a&&a.available&&a.clean&&a.behindBy===0,T=E=>{N(null),i.mutate({fixId:r.id,body:{target:h,branch:h==="new"?R.trim():void 0,strategy:E,autoResolve:w,model:H}},{onSuccess:se=>{"pushedBranch"in se||k("pushing")}})},O=()=>{N(null),c.mutate({fixId:r.id,body:{autoResolve:w,model:H}},{onSuccess:()=>k("rebasing")})},b=u==="rebasing"?v:C,$=r.resolved;return e.jsxs("div",{className:"mt-3 rounded border border-gray-200 p-2 dark:border-gray-800",children:[g&&e.jsx(_,{fix:r}),e.jsx("div",{className:"mb-2 text-xs font-semibold text-gray-600 dark:text-gray-300",children:g?"Reconcile with the trunk":"Push this fix"}),!g&&r.commitMessage&&e.jsxs("div",{className:"mb-2 rounded bg-gray-50 p-2 text-[11px] dark:bg-gray-900",children:[e.jsx("span",{className:"uppercase tracking-wide text-gray-400",children:"Commit message"}),e.jsx("div",{className:"mt-0.5 font-mono text-gray-700 dark:text-gray-200",children:r.commitMessage})]}),e.jsxs("label",{className:`flex items-center gap-2 text-xs ${p?"":"opacity-40"}`,title:p?void 0:"The PR head is a fork you cannot push to — use a new branch",children:[e.jsx("input",{type:"radio",checked:h==="existing",disabled:!p||P,onChange:()=>d("existing")}),"Push to the PR branch",(t==null?void 0:t.headRef)&&e.jsxs("span",{className:"font-mono text-gray-400",children:["(",t.headRef,")"]})]}),e.jsxs("label",{className:"mt-1 flex items-center gap-2 text-xs",children:[e.jsx("input",{type:"radio",checked:h==="new",disabled:P,onChange:()=>d("new")}),"New branch + open a PR"]}),h==="new"&&e.jsx("input",{type:"text",className:"mt-1 w-full rounded border border-gray-300 bg-transparent px-2 py-1 font-mono text-xs dark:border-gray-700",value:R,disabled:P,onChange:E=>f(E.target.value),placeholder:"branch-name"}),e.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-2",children:[e.jsx("div",{className:"min-w-0 flex-1",children:e.jsx(Ae,{preview:a,loading:l.isPending})}),e.jsx("button",{type:"button",className:j,disabled:l.isPending||P,onClick:()=>l.mutate(r.id),title:"Re-check this fix against the current trunk",children:l.isPending?"Checking…":"Re-check trunk"}),l.isError&&!l.isPending&&e.jsx("span",{className:"text-[11px] text-red-500",children:"Couldn't check the trunk — try again."})]}),u!=="idle"?e.jsxs("div",{className:"mt-2",children:[e.jsx(Q,{active:!0,label:u==="rebasing"?"Rebasing & resolving":"Pushing",value:Ne(b),timeConstantSec:40}),e.jsxs("div",{className:"mt-1 flex items-center gap-2 text-[11px] text-gray-500",children:[e.jsx("span",{children:ke[((V=b==null?void 0:b.progress)==null?void 0:V.phase)??""]??"Working…"}),e.jsx("button",{type:"button",className:j,onClick:()=>u==="rebasing"?m.mutate(r.id):x.mutate(r.id),children:"Cancel"})]}),((G=b==null?void 0:b.progress)==null?void 0:G.recentActivity)&&b.progress.recentActivity.length>0&&e.jsx("pre",{className:"mt-1 max-h-32 overflow-auto rounded bg-gray-50 p-2 text-[11px] leading-relaxed text-gray-500 dark:bg-gray-900",children:b.progress.recentActivity.slice(-8).join(`
|
|
3
3
|
`)})]}):$?e.jsx(Ee,{resolved:$,target:h,pushing:i.isPending,disabled:B,onPush:()=>T("rebase"),onRedo:O}):ee?e.jsxs("p",{className:"mt-2 text-[11px] text-gray-500",children:["No trunk changes to reconcile — the pushed fix is up to date with"," ",e.jsx("span",{className:"font-mono",children:a==null?void 0:a.trunk}),"."]}):e.jsxs("div",{className:"mt-2 space-y-2",children:[a&&a.available&&!a.clean&&e.jsxs("div",{className:"text-[11px] text-gray-500",children:["Recommended: rebase onto"," ",e.jsx("span",{className:"font-mono",children:a.trunk})," — Claude resolves the conflicts and you review the result before it pushes."]}),e.jsxs("label",{className:"flex items-center gap-2 text-[11px] text-gray-600 dark:text-gray-300",children:[e.jsx("input",{type:"checkbox",checked:w,onChange:E=>M(E.target.checked)}),"Let Claude resolve conflicts"]}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs("button",{type:"button",className:a&&!a.clean?S:j,disabled:P||B,onClick:O,title:"Rebase the fix onto the trunk; Claude resolves conflicts and you review before a force-with-lease push",children:["Rebase onto ",(a==null?void 0:a.trunk)??"trunk"]}),!g&&e.jsxs("button",{type:"button",className:j,disabled:P||B,onClick:()=>T("merge"),title:"Merge the trunk into the fix branch (a merge commit; never force-pushes)",children:["Merge ",(a==null?void 0:a.trunk)??"trunk"," in"]}),!g&&e.jsxs("button",{type:"button",className:!a||a.clean?S:j,disabled:P||B,onClick:()=>T("plain"),title:"Push the fix as-is (never force-pushes). If it conflicts with the trunk, the PR will show as conflicted.",children:[h==="new"?"Push + open PR":"Push",a&&!a.clean?" anyway":""]})]}),g&&e.jsxs("p",{className:"text-[11px] text-gray-400",children:["Rebasing replays the fix onto"," ",e.jsx("span",{className:"font-mono",children:(a==null?void 0:a.trunk)??"the trunk"})," ","and force-pushes (with lease) onto the PR branch — the safe way to reconcile an already-pushed fix."]}),h==="existing"&&e.jsxs("p",{className:"text-[11px] text-gray-400",children:["Rebase force-pushes (with lease) onto"," ",e.jsx("span",{className:"font-mono",children:t==null?void 0:t.headRef}),"; merge and push do not."]})]}),(F||i.isError||c.isError)&&e.jsx("div",{className:"mt-2 text-[11px] text-red-500",children:F??D(i.error??c.error)})]})}function D(s){return s instanceof he||s instanceof Error?s.message:"Something went wrong"}export{De as AiFixTab};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{e as be,r as c,j as e}from"./tanstack-R-s9abdD.js";import{f as ye,s as we,x as Ce,o as Re,L as Pe,K as Se,B as Ee,z as Te,v as Ae,t as $e,C as Fe,b as _e,R as Ie,h as fe,g as se,e as ve,D as re,w as Be,u as Me,H as Le,M as ee}from"./index-
|
|
1
|
+
import{e as be,r as c,j as e}from"./tanstack-R-s9abdD.js";import{f as ye,s as we,x as Ce,o as Re,L as Pe,K as Se,B as Ee,z as Te,v as Ae,t as $e,C as Fe,b as _e,R as Ie,h as fe,g as se,e as ve,D as re,w as Be,u as Me,H as Le,M as ee}from"./index-BzIHcEkg.js";import"./markdown-BUNN1sk6.js";import"./highlight-C8vPehS1.js";import"./vis-C3BCkP5U.js";function He(t,s){return be({queryKey:["review-learnings",t],queryFn:()=>ye.reviewLearnings(t),enabled:s&&t!=null})}function Oe(t,s){return be({queryKey:["review-actions",t],queryFn:()=>ye.reviewActions(t),enabled:s&&t!=null})}function De({label:t,children:s}){return e.jsxs("div",{className:"flex gap-3 px-4 py-1.5 text-sm",children:[e.jsx("span",{className:"w-28 shrink-0 text-xs uppercase tracking-wide text-gray-400",children:t}),e.jsx("div",{className:"min-w-0 flex-1",children:s})]})}const Z=t=>t?t.slice(0,7):"—",z={skip:"Skipped",diff_only:"Quick",worktree:"Deep"},Ve=[{value:"auto",label:"Auto (router decides)"},{value:"diff_only",label:"Quick — diff only"},{value:"worktree",label:"Deep — full worktree"}],V=(t,s)=>`${t} ${s}${t===1?"":"s"}`;function Ge(t){const s=t.routeReason;if(!s)return;const n=`${V(s.changedFiles,"file")} · ${V(s.linesChanged,"line")} · ${V(s.dirsTouched,"dir")}`;return s.decidedBy==="user"?`You chose ${z[t.reviewMode??"worktree"]} for this run (${n}).`:s.trippedBy?`Auto chose Deep — ${n}; over the ${s.trippedBy} limit.`:`Auto chose ${z[t.reviewMode??"diff_only"]} — ${n}.`}const te={COMMENT:"Comment",REQUEST_CHANGES:"Request changes",APPROVE:"Approve"},Ue={APPROVE:"bg-green-500/10 text-green-700 dark:text-green-400",REQUEST_CHANGES:"bg-red-500/10 text-red-700 dark:text-red-400",COMMENT:"bg-gray-500/10 text-gray-600 dark:text-gray-300"};function qe({verdict:t}){return e.jsx("span",{className:`inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium ${Ue[t]}`,children:te[t]})}const me={blocker:0,warning:1,nit:2,question:3,praise:4},We={blocker:"bg-red-500/10 text-red-700 dark:text-red-400",warning:"bg-orange-500/10 text-orange-700 dark:text-orange-400",nit:"bg-yellow-500/10 text-yellow-700 dark:text-yellow-500",question:"bg-blue-500/10 text-blue-700 dark:text-blue-400",praise:"bg-green-500/10 text-green-700 dark:text-green-400"};function Qe(t){const s=[t.model];return t.costUsd!=null&&s.push(`${fe(t.costUsd)} cr`),t.numTurns!=null&&s.push(`${t.numTurns} turns`),t.excludedFiles.length>0&&s.push(`${t.excludedFiles.length} noise files excluded`),s.join(" · ")}const Ye={cloning:"Cloning the worktree",fetching_diff:"Fetching the diff",deciding:"Deciding scope",reviewing:"Reviewing",persisting:"Saving findings"};function Ke(t){var n,a,r;if(t==null)return null;if(t.status==="queued")return 5;const s=(n=t.progress)==null?void 0:n.phase;if(s==null)return 8;switch(s){case"fetching_diff":return 15;case"deciding":return 28;case"cloning":return 42;case"reviewing":{const o=((r=(a=t.progress)==null?void 0:a.recentActivity)==null?void 0:r.length)??0;return Math.min(90,55+o*3)}case"persisting":return 95;default:return null}}function K(t){return t>=1e6?`${(t/1e6).toFixed(1)}M`:t>=1e3?`${(t/1e3).toFixed(1)}k`:String(t)}function ze({review:t}){const{inputTokens:s,outputTokens:n,cacheReadTokens:a,cacheCreationTokens:r}=t;if((s??0)+(n??0)+(a??0)+(r??0)<=0)return null;const d=[];return n!=null&&d.push({key:"out",label:"↓ out",value:n,title:"Output tokens generated (billed at the output rate — the priciest per token)"}),s!=null&&d.push({key:"in",label:"↑ in",value:s,title:"New (uncached) input tokens"}),a!=null&&d.push({key:"cr",label:"⟳ cache read",value:a,title:"Cached input tokens re-read each turn — billed at ~10% of the input rate, but the volume driver of a multi-turn run"}),r!=null&&d.push({key:"cw",label:"✎ cache write",value:r,title:"Tokens written to the prompt cache (billed at ~1.25× the input rate)"}),e.jsx("div",{className:"flex flex-wrap items-center gap-x-3 gap-y-0.5 font-mono text-xs text-gray-500 dark:text-gray-400",children:d.map(i=>e.jsxs("span",{title:i.title,children:[i.label," ",K(i.value)]},i.key))})}function Je({lines:t}){const s=c.useRef(null);return c.useEffect(()=>{const n=s.current;n!=null&&(n.scrollTop=n.scrollHeight)},[t]),t.length===0?null:e.jsx("div",{ref:s,className:"mt-2 max-h-32 overflow-y-auto rounded border border-gray-100 bg-gray-50 px-2 py-1.5 font-mono text-[11px] leading-snug text-gray-500 dark:border-gray-800 dark:bg-gray-900/60 dark:text-gray-400",children:t.map((n,a)=>e.jsx("div",{className:"whitespace-pre-wrap break-words",children:n===""?" ":n},a))})}function ge(t){return t.startsWith("@@")?"text-violet-500":t.startsWith("+")?"text-green-700 dark:text-green-400":t.startsWith("-")?"text-red-700 dark:text-red-400":"text-gray-500 dark:text-gray-400"}const Y="whitespace-nowrap rounded border border-blue-400 px-2 py-0.5 text-xs text-blue-600 hover:bg-blue-50 disabled:opacity-50 dark:border-blue-600 dark:text-blue-400 dark:hover:bg-blue-900/30",B="whitespace-nowrap rounded border border-gray-300 px-2 py-0.5 text-xs hover:border-gray-400 disabled:opacity-50 dark:border-gray-700 dark:hover:border-gray-500";function Xe({hunk:t}){const[s,n]=c.useState(!1),a=t.replace(/\n$/,"").split(`
|
|
2
2
|
`),r=a.find(o=>o.startsWith("@@"))??a.at(-1)??"";return s?e.jsxs("div",{className:"mt-1 rounded bg-gray-50 dark:bg-gray-900/60",children:[e.jsx("pre",{className:"overflow-x-auto px-2 py-1.5 font-mono text-xs leading-snug",children:a.map((o,d)=>e.jsx("div",{className:ge(o),children:o===""?" ":o},d))}),e.jsx("button",{type:"button",onClick:()=>n(!1),className:"px-2 pb-1.5 text-[11px] text-gray-400 hover:text-gray-600 dark:hover:text-gray-300",children:"⌃ Hide code"})]}):e.jsxs("button",{type:"button",onClick:()=>n(!0),title:"Show the code hunk",className:"mt-1 flex w-full items-center gap-2 overflow-hidden rounded bg-gray-50 px-2 py-1.5 text-left font-mono text-xs dark:bg-gray-900/60",children:[e.jsx("span",{className:"shrink-0 text-gray-400",children:"▸"}),e.jsx("span",{className:`min-w-0 flex-1 truncate ${ge(r)}`,children:r===""?" ":r}),a.length>1&&e.jsxs("span",{className:"shrink-0 font-sans text-[10px] text-gray-400",children:[a.length," lines"]})]})}function Ze({prId:t,finding:s,editable:n,prUrl:a,repoFullName:r,headSha:o,posting:d,postError:i,onToggle:b,onReword:k,onPostComment:g}){const[P,E]=c.useState(!1),x=c.useRef(null);c.useEffect(()=>()=>{x.current!=null&&clearTimeout(x.current)},[]);const[p,j]=c.useState(!1),[S,m]=c.useState(s.editedBody??"");c.useEffect(()=>{p||m(s.editedBody??"")},[s.editedBody,p]);const y=s.editedBody!=null&&s.editedBody.trim()!=="",h=p&&S.trim()!==""&&S!==(s.editedBody??"")?S:null,T=h!=null||y,J=h??(y?s.editedBody:s.body),A=s.line!=null?`${s.path}:${s.line}`:s.path,f=s.postedAt!=null,v=s.githubCommentId!=null?s.postedCommentKind==="pr_comment"?`${a}#issuecomment-${s.githubCommentId}`:`${a}#discussion_r${s.githubCommentId}`:null,$=s.line!=null?`${a}/files#diff-${s.diffAnchorId}${s.side==="LEFT"?"L":"R"}${s.line}`:`${a}/files#diff-${s.diffAnchorId}`,G=o!=null&&s.line!=null&&s.side==="RIGHT"?`https://github.com/${r}/blob/${o}/${s.path.split("/").map(encodeURIComponent).join("/")}#L${s.line}`:null,N=v??$,w=v==null?G:null,U=n,q=()=>{let _=`${s.title}
|
|
3
3
|
|
|
4
4
|
${J}`;s.suggestion!=null&&s.suggestion!==""&&(_+=`
|