better-review 0.4.0 → 0.5.0

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/cli.js CHANGED
@@ -25647,6 +25647,16 @@ var trackedEntrySchema = external_exports.union([
25647
25647
  ]);
25648
25648
  var diffSideSchema = external_exports.enum(["LEFT", "RIGHT"]);
25649
25649
  var reviewEventSchema = external_exports.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES"]);
25650
+ var reactionContentSchema = external_exports.enum([
25651
+ "THUMBS_UP",
25652
+ "THUMBS_DOWN",
25653
+ "LAUGH",
25654
+ "HOORAY",
25655
+ "CONFUSED",
25656
+ "HEART",
25657
+ "ROCKET",
25658
+ "EYES"
25659
+ ]);
25650
25660
  var newThreadSchema = external_exports.discriminatedUnion("subjectType", [
25651
25661
  external_exports.object({
25652
25662
  subjectType: external_exports.literal("LINE"),
@@ -25782,148 +25792,15 @@ var GitHub = class {
25782
25792
  };
25783
25793
 
25784
25794
  // ../server/src/github/queries.ts
25785
- var PR_SUMMARY = (
25795
+ var REACTION_FIELDS = (
25786
25796
  /* GraphQL */
25787
25797
  `
25788
- fragment PrSummary on PullRequest {
25789
- id
25790
- number
25791
- title
25792
- url
25793
- isDraft
25794
- createdAt
25795
- updatedAt
25796
- additions
25797
- deletions
25798
- changedFiles
25799
- headRefOid
25800
- reviewDecision
25801
- repository {
25802
- nameWithOwner
25803
- }
25804
- author {
25805
- login
25806
- avatarUrl
25807
- }
25808
- labels(first: 10) {
25809
- nodes {
25810
- name
25811
- color
25812
- }
25813
- }
25814
- commits(last: 1) {
25815
- nodes {
25816
- commit {
25817
- statusCheckRollup {
25818
- state
25819
- }
25820
- }
25821
- }
25822
- }
25823
- reviewRequests(first: 20) {
25824
- totalCount
25825
- nodes {
25826
- requestedReviewer {
25827
- __typename
25828
- ... on User {
25829
- login
25830
- }
25831
- ... on Team {
25832
- slug
25833
- }
25834
- }
25835
- }
25836
- }
25837
- latestOpinionatedReviews(first: 20) {
25838
- nodes {
25839
- state
25840
- author {
25841
- login
25842
- avatarUrl
25843
- }
25844
- }
25845
- }
25846
- viewerLatestReview {
25847
- state
25848
- submittedAt
25849
- commit {
25850
- oid
25851
- }
25852
- }
25853
- reviewThreads(first: 100) {
25854
- nodes {
25855
- isResolved
25856
- comments(last: 1) {
25857
- nodes {
25858
- author {
25859
- login
25860
- }
25861
- createdAt
25862
- }
25863
- }
25864
- }
25865
- }
25866
- comments(last: 1) {
25867
- nodes {
25868
- author {
25869
- login
25870
- }
25871
- createdAt
25872
- }
25873
- }
25874
- files(first: 100) {
25875
- totalCount
25876
- nodes {
25877
- viewerViewedState
25878
- }
25879
- }
25880
- }
25881
- `
25882
- );
25883
- var INBOX_SECTION_IDS = [
25884
- "requested",
25885
- "reviewed",
25886
- "mine",
25887
- "involved",
25888
- "stale",
25889
- "unreviewed"
25890
- ];
25891
- var INBOX_IDS_QUERY = (
25892
- /* GraphQL */
25893
- `
25894
- query InboxIds(${INBOX_SECTION_IDS.map((id) => `$${id}: String!`).join(", ")}) {
25895
- viewer { login avatarUrl }
25896
- ${INBOX_SECTION_IDS.map(
25897
- (id) => `${id}: search(query: $${id}, type: ISSUE, first: 50) {
25898
- nodes { ... on PullRequest { id } }
25899
- }`
25900
- ).join("\n")}
25901
- }
25902
- `
25903
- );
25904
- var PR_SUMMARIES_QUERY = (
25905
- /* GraphQL */
25906
- `
25907
- query PrSummaries($ids: [ID!]!) {
25908
- nodes(ids: $ids) {
25909
- ...PrSummary
25910
- }
25911
- }
25912
- ${PR_SUMMARY}
25913
- `
25914
- );
25915
- var INBOX_FINGERPRINT_QUERY = (
25916
- /* GraphQL */
25917
- `
25918
- query InboxFingerprint($q: String!) {
25919
- search(query: $q, type: ISSUE, first: 100) {
25920
- issueCount
25921
- nodes {
25922
- ... on PullRequest {
25923
- id
25924
- updatedAt
25925
- headRefOid
25926
- }
25798
+ fragment ReactionFields on Reactable {
25799
+ reactionGroups {
25800
+ content
25801
+ viewerHasReacted
25802
+ reactors {
25803
+ totalCount
25927
25804
  }
25928
25805
  }
25929
25806
  }
@@ -25954,7 +25831,9 @@ var REVIEW_COMMENT_FIELDS = (
25954
25831
  }
25955
25832
  viewerCanUpdate
25956
25833
  viewerCanDelete
25834
+ ...ReactionFields
25957
25835
  }
25836
+ ${REACTION_FIELDS}
25958
25837
  `
25959
25838
  );
25960
25839
  var THREAD_FIELDS = (
@@ -26009,6 +25888,7 @@ var PR_QUERY = (
26009
25888
  updatedAt
26010
25889
  body
26011
25890
  bodyHTML
25891
+ ...ReactionFields
26012
25892
  baseRefName
26013
25893
  headRefName
26014
25894
  baseRefOid
@@ -26025,31 +25905,6 @@ var PR_QUERY = (
26025
25905
  headRepository {
26026
25906
  nameWithOwner
26027
25907
  }
26028
- lastCommit: commits(last: 1) {
26029
- nodes {
26030
- commit {
26031
- statusCheckRollup {
26032
- state
26033
- contexts(first: 100) {
26034
- nodes {
26035
- __typename
26036
- ... on CheckRun {
26037
- name
26038
- conclusion
26039
- status
26040
- detailsUrl
26041
- }
26042
- ... on StatusContext {
26043
- context
26044
- state
26045
- targetUrl
26046
- }
26047
- }
26048
- }
26049
- }
26050
- }
26051
- }
26052
- }
26053
25908
  reviewRequests(first: 20) {
26054
25909
  nodes {
26055
25910
  requestedReviewer {
@@ -26111,6 +25966,7 @@ var PR_QUERY = (
26111
25966
  url
26112
25967
  body
26113
25968
  bodyHTML
25969
+ ...ReactionFields
26114
25970
  createdAt
26115
25971
  author {
26116
25972
  login
@@ -26125,6 +25981,7 @@ var PR_QUERY = (
26125
25981
  state
26126
25982
  body
26127
25983
  bodyHTML
25984
+ ...ReactionFields
26128
25985
  submittedAt
26129
25986
  createdAt
26130
25987
  url
@@ -26271,48 +26128,242 @@ async function fetchTexts(gh, ref, expressions) {
26271
26128
  return texts;
26272
26129
  }
26273
26130
 
26131
+ // ../server/src/github/inboxQueries.ts
26132
+ var BOARD_FIELDS = (
26133
+ /* GraphQL */
26134
+ `
26135
+ fragment BoardFields on PullRequest {
26136
+ id
26137
+ number
26138
+ title
26139
+ url
26140
+ isDraft
26141
+ createdAt
26142
+ updatedAt
26143
+ headRefOid
26144
+ reviewDecision
26145
+ isInMergeQueue
26146
+ mergedAt
26147
+ repository {
26148
+ nameWithOwner
26149
+ }
26150
+ author {
26151
+ login
26152
+ avatarUrl
26153
+ }
26154
+ reviews {
26155
+ totalCount
26156
+ }
26157
+ latestOpinionatedReviews(first: 10) {
26158
+ nodes {
26159
+ state
26160
+ author {
26161
+ login
26162
+ avatarUrl
26163
+ }
26164
+ }
26165
+ }
26166
+ reviewRequests(first: 10) {
26167
+ nodes {
26168
+ requestedReviewer {
26169
+ __typename
26170
+ ... on User {
26171
+ login
26172
+ }
26173
+ ... on Team {
26174
+ slug
26175
+ }
26176
+ }
26177
+ }
26178
+ }
26179
+ }
26180
+ `
26181
+ );
26182
+ var BOARD_QUERY = (
26183
+ /* GraphQL */
26184
+ `
26185
+ query Board($ids: [ID!]!) {
26186
+ nodes(ids: $ids) {
26187
+ ...BoardFields
26188
+ }
26189
+ }
26190
+ ${BOARD_FIELDS}
26191
+ `
26192
+ );
26193
+ var CHECKS_FIELDS = (
26194
+ /* GraphQL */
26195
+ `
26196
+ commits(last: 1) {
26197
+ nodes {
26198
+ commit {
26199
+ statusCheckRollup {
26200
+ state
26201
+ }
26202
+ }
26203
+ }
26204
+ }
26205
+ `
26206
+ );
26207
+ var CHECKS_QUERY = (
26208
+ /* GraphQL */
26209
+ `
26210
+ query BoardChecks($ids: [ID!]!) {
26211
+ nodes(ids: $ids) {
26212
+ ... on PullRequest {
26213
+ id
26214
+ ${CHECKS_FIELDS}
26215
+ reviewThreads(first: 100) {
26216
+ nodes {
26217
+ isResolved
26218
+ }
26219
+ }
26220
+ }
26221
+ }
26222
+ }
26223
+ `
26224
+ );
26225
+ var DETAILS_QUERY = (
26226
+ /* GraphQL */
26227
+ `
26228
+ query BoardDetails($ids: [ID!]!) {
26229
+ nodes(ids: $ids) {
26230
+ ... on PullRequest {
26231
+ id
26232
+ additions
26233
+ deletions
26234
+ ${CHECKS_FIELDS}
26235
+ viewerLatestReview {
26236
+ state
26237
+ commit {
26238
+ oid
26239
+ }
26240
+ }
26241
+ comments(last: 1) {
26242
+ nodes {
26243
+ author {
26244
+ login
26245
+ }
26246
+ createdAt
26247
+ }
26248
+ }
26249
+ reviewThreads(first: 100) {
26250
+ nodes {
26251
+ isResolved
26252
+ comments(last: 1) {
26253
+ nodes {
26254
+ author {
26255
+ login
26256
+ }
26257
+ createdAt
26258
+ }
26259
+ }
26260
+ }
26261
+ }
26262
+ files(first: 100) {
26263
+ totalCount
26264
+ nodes {
26265
+ viewerViewedState
26266
+ }
26267
+ }
26268
+ }
26269
+ }
26270
+ }
26271
+ `
26272
+ );
26273
+ var SEARCH_IDS_QUERY = (
26274
+ /* GraphQL */
26275
+ `
26276
+ query SearchIds($q: String!, $after: String) {
26277
+ viewer {
26278
+ login
26279
+ avatarUrl
26280
+ }
26281
+ search(query: $q, type: ISSUE, first: 100, after: $after) {
26282
+ issueCount
26283
+ pageInfo {
26284
+ hasNextPage
26285
+ endCursor
26286
+ }
26287
+ nodes {
26288
+ ... on PullRequest {
26289
+ id
26290
+ }
26291
+ }
26292
+ }
26293
+ }
26294
+ `
26295
+ );
26296
+ var INBOX_FINGERPRINT_QUERY = (
26297
+ /* GraphQL */
26298
+ `
26299
+ query InboxFingerprint($q: String!) {
26300
+ search(query: $q, type: ISSUE, first: 100) {
26301
+ issueCount
26302
+ nodes {
26303
+ ... on PullRequest {
26304
+ id
26305
+ updatedAt
26306
+ headRefOid
26307
+ reviewDecision
26308
+ isInMergeQueue
26309
+ ${CHECKS_FIELDS}
26310
+ }
26311
+ }
26312
+ }
26313
+ }
26314
+ `
26315
+ );
26316
+
26274
26317
  // ../server/src/inbox.ts
26318
+ var MERGED_WINDOW_MS = 24 * 36e5;
26319
+ function scopeTerms(config2) {
26320
+ return [...config2.repos.map((r) => `repo:${r}`), ...config2.orgs.map((o) => `user:${o}`)];
26321
+ }
26275
26322
  var SCOPE_CHUNK = 20;
26276
26323
  function scopeChunks(config2) {
26277
- const terms = [...config2.repos.map((r) => `repo:${r}`), ...config2.orgs.map((o) => `user:${o}`)];
26324
+ const terms = scopeTerms(config2);
26278
26325
  const chunks = [];
26279
26326
  for (let i = 0; i < terms.length; i += SCOPE_CHUNK) chunks.push(terms.slice(i, i + SCOPE_CHUNK).join(" "));
26280
26327
  return chunks;
26281
26328
  }
26282
- function searchQueries(scope, staleDays, now) {
26283
- const base = `is:pr is:open archived:false ${scope}`;
26284
- const staleBefore = new Date(now - staleDays * 864e5).toISOString().slice(0, 10);
26329
+ function searchQueries(scope, mergedSince) {
26330
+ const open2 = `is:pr is:open archived:false ${scope}`;
26331
+ const since = new Date(mergedSince).toISOString().replace(/\.\d+Z$/, "Z");
26285
26332
  return {
26286
- requested: `${base} review-requested:@me`,
26287
- reviewed: `${base} reviewed-by:@me -author:@me`,
26288
- mine: `${base} author:@me`,
26289
- involved: `${base} involves:@me -author:@me`,
26290
- stale: `${base} updated:<${staleBefore}`,
26291
- unreviewed: `${base} draft:false review:none`
26333
+ open: `${open2} sort:updated-desc`,
26334
+ requested: `${open2} review-requested:@me`,
26335
+ involved: `${open2} involves:@me -author:@me`,
26336
+ reviewed: `${open2} reviewed-by:@me -author:@me`,
26337
+ merged: `is:pr is:merged archived:false ${scope} merged:>=${since} sort:updated-desc`
26292
26338
  };
26293
26339
  }
26294
26340
  function prKey(owner, repo, number4) {
26295
26341
  return `${owner}/${repo}#${number4}`.toLowerCase();
26296
26342
  }
26343
+ function isApproved(decision, states) {
26344
+ if (decision !== null) return decision === "APPROVED";
26345
+ return states.includes("APPROVED") && !states.includes("CHANGES_REQUESTED");
26346
+ }
26297
26347
  function toInboxPr(node2, me, visit2) {
26298
26348
  const [owner = "", repo = ""] = node2.repository.nameWithOwner.split("/");
26299
- let open2 = 0;
26300
- let awaitingMyReply = 0;
26349
+ let threads;
26301
26350
  let lastOtherActivity = 0;
26302
- for (const thread of node2.reviewThreads.nodes) {
26303
- const last = thread.comments.nodes[0];
26304
- const lastIsOther = last?.author?.login !== me;
26305
- if (!thread.isResolved) {
26306
- open2++;
26307
- if (last && lastIsOther) awaitingMyReply++;
26351
+ if (node2.reviewThreads) {
26352
+ threads = { total: node2.reviewThreads.nodes.length, open: 0, awaitingMyReply: 0 };
26353
+ for (const thread of node2.reviewThreads.nodes) {
26354
+ const last = thread.comments?.nodes[0];
26355
+ const lastIsOther = last?.author?.login !== me;
26356
+ if (!thread.isResolved) {
26357
+ threads.open++;
26358
+ if (last && lastIsOther) threads.awaitingMyReply++;
26359
+ }
26360
+ if (last && lastIsOther) lastOtherActivity = Math.max(lastOtherActivity, Date.parse(last.createdAt));
26308
26361
  }
26309
- if (last && lastIsOther) lastOtherActivity = Math.max(lastOtherActivity, Date.parse(last.createdAt));
26310
26362
  }
26311
- const lastComment = node2.comments.nodes[0];
26363
+ const lastComment = node2.comments?.nodes[0];
26312
26364
  if (lastComment && lastComment.author?.login !== me) {
26313
26365
  lastOtherActivity = Math.max(lastOtherActivity, Date.parse(lastComment.createdAt));
26314
26366
  }
26315
- const files = node2.files?.nodes ?? [];
26316
26367
  const myReview = node2.viewerLatestReview;
26317
26368
  return {
26318
26369
  id: node2.id,
@@ -26322,15 +26373,13 @@ function toInboxPr(node2, me, visit2) {
26322
26373
  title: node2.title,
26323
26374
  url: node2.url,
26324
26375
  isDraft: node2.isDraft,
26376
+ inMergeQueue: node2.isInMergeQueue,
26377
+ mergedAt: node2.mergedAt,
26325
26378
  author: node2.author,
26326
26379
  createdAt: node2.createdAt,
26327
26380
  updatedAt: node2.updatedAt,
26328
- additions: node2.additions,
26329
- deletions: node2.deletions,
26330
- changedFiles: node2.changedFiles,
26331
- labels: node2.labels.nodes,
26332
- ci: node2.commits.nodes[0]?.commit.statusCheckRollup?.state ?? null,
26333
26381
  reviewDecision: node2.reviewDecision,
26382
+ reviewCount: node2.reviews.totalCount,
26334
26383
  reviews: node2.latestOpinionatedReviews.nodes.flatMap(
26335
26384
  (r) => r.author ? [{ login: r.author.login, avatarUrl: r.author.avatarUrl, state: r.state }] : []
26336
26385
  ),
@@ -26338,115 +26387,183 @@ function toInboxPr(node2, me, visit2) {
26338
26387
  const name = r.requestedReviewer?.login ?? r.requestedReviewer?.slug;
26339
26388
  return name ? [name] : [];
26340
26389
  }),
26341
- myReview: myReview ? {
26342
- state: myReview.state,
26343
- submittedAt: myReview.submittedAt,
26344
- upToDate: myReview.commit?.oid === node2.headRefOid
26345
- } : null,
26346
- threads: { total: node2.reviewThreads.nodes.length, open: open2, awaitingMyReply },
26347
- files: {
26348
- viewed: files.filter((f) => f.viewerViewedState === "VIEWED").length,
26349
- total: node2.files?.totalCount ?? node2.changedFiles
26350
- },
26390
+ ...node2.commits ? { ci: node2.commits.nodes[0]?.commit.statusCheckRollup?.state ?? null } : {},
26391
+ ...threads ? { threads } : {},
26392
+ ...node2.additions !== void 0 ? { additions: node2.additions, deletions: node2.deletions ?? 0 } : {},
26393
+ ...myReview !== void 0 ? {
26394
+ myReview: myReview && { state: myReview.state, upToDate: myReview.commit?.oid === node2.headRefOid }
26395
+ } : {},
26396
+ ...node2.files ? {
26397
+ files: {
26398
+ viewed: node2.files.nodes.filter((f) => f.viewerViewedState === "VIEWED").length,
26399
+ total: node2.files.totalCount
26400
+ }
26401
+ } : {},
26351
26402
  lastVisitAt: visit2?.visitedAt ?? null,
26352
26403
  unread: !visit2 || visit2.headOid !== node2.headRefOid || lastOtherActivity > visit2.visitedAt
26353
26404
  };
26354
26405
  }
26355
- function classifyInbox(results, me, visits) {
26356
- const make = (node2) => {
26406
+ function stageOf(pr) {
26407
+ if (pr.mergedAt !== null) return "merged";
26408
+ if (pr.inMergeQueue) return "queued";
26409
+ if (pr.isDraft) return "draft";
26410
+ if (isApproved(
26411
+ pr.reviewDecision,
26412
+ pr.reviews.map((r) => r.state)
26413
+ )) {
26414
+ const green = pr.ci === null || pr.ci === "SUCCESS";
26415
+ return green && (pr.threads?.open ?? 0) === 0 ? "ready" : "approved";
26416
+ }
26417
+ const reviewed = pr.reviewDecision === "CHANGES_REQUESTED" || pr.reviewCount > 0 || pr.reviews.length > 0 || (pr.threads?.total ?? 0) > 0;
26418
+ return reviewed ? "review" : "waiting";
26419
+ }
26420
+ function buildBoard(sources, me, visits, staleBefore) {
26421
+ const nodes = /* @__PURE__ */ new Map();
26422
+ for (const node2 of [...sources.open, ...sources.merged]) nodes.set(node2.id, node2);
26423
+ const prs = [...nodes.values()].map((node2) => {
26357
26424
  const [owner = "", repo = ""] = node2.repository.nameWithOwner.split("/");
26358
- return toInboxPr(node2, me, visits.get(prKey(owner, repo, node2.number)));
26359
- };
26360
- const placed = /* @__PURE__ */ new Set();
26361
- const take = (nodes, keep = () => true) => {
26362
- const out = [];
26363
- for (const node2 of nodes) {
26364
- if (placed.has(node2.id)) continue;
26365
- const pr = make(node2);
26366
- if (!keep(pr)) continue;
26367
- placed.add(node2.id);
26368
- out.push(pr);
26369
- }
26370
- return out;
26371
- };
26372
- const requested = take(results.requested);
26373
- const rereview = take(results.reviewed, (pr) => pr.myReview !== null && !pr.myReview.upToDate);
26374
- const mine = take(results.mine);
26375
- const mineDraft = mine.filter((pr) => pr.isDraft);
26376
- const mineTodo = mine.filter(
26377
- (pr) => !pr.isDraft && (pr.threads.awaitingMyReply > 0 || pr.reviewDecision === "CHANGES_REQUESTED")
26378
- );
26379
- const mineReady = mine.filter(
26380
- (pr) => !pr.isDraft && !mineTodo.includes(pr) && pr.reviewDecision === "APPROVED" && (pr.ci === null || pr.ci === "SUCCESS") && pr.threads.open === 0
26425
+ const pr = toInboxPr(node2, me, visits.get(prKey(owner, repo, node2.number)));
26426
+ const stage = stageOf(pr);
26427
+ const mine = pr.author?.login === me;
26428
+ const tags = mine ? ["mine"] : [];
26429
+ if (stage === "merged") return { ...pr, stage, tags };
26430
+ if (mine) {
26431
+ const feedback = (pr.threads?.awaitingMyReply ?? 0) > 0 || pr.reviewDecision === "CHANGES_REQUESTED";
26432
+ if (!pr.isDraft && feedback) tags.push("todo");
26433
+ } else {
26434
+ if (sources.requested.has(node2.id)) tags.push("requested");
26435
+ if (pr.myReview && !pr.myReview.upToDate) tags.push("rereview");
26436
+ if (tags.length === 0 && (sources.involved.has(node2.id) || pr.myReview)) tags.push("involved");
26437
+ }
26438
+ if (Date.parse(pr.updatedAt) < staleBefore) tags.push("stale");
26439
+ if (stage === "waiting" && pr.requested.length === 0) tags.push("unreviewed");
26440
+ return { ...pr, stage, tags };
26441
+ });
26442
+ return prs.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
26443
+ }
26444
+ var MAX_SEARCH_PAGES = 10;
26445
+ var BATCH2 = 25;
26446
+ var PARALLEL = 12;
26447
+ async function retryOnTimeout(run2) {
26448
+ try {
26449
+ return await run2();
26450
+ } catch (error62) {
26451
+ if (error62 instanceof GitHubError && (error62.status === 502 || error62.status === 504)) return run2();
26452
+ throw error62;
26453
+ }
26454
+ }
26455
+ async function pool(tasks) {
26456
+ const results = new Array(tasks.length);
26457
+ let next = 0;
26458
+ const worker = async () => {
26459
+ while (next < tasks.length) {
26460
+ const i = next++;
26461
+ results[i] = await tasks[i]();
26462
+ }
26463
+ };
26464
+ await Promise.all(Array.from({ length: Math.min(PARALLEL, tasks.length) }, worker));
26465
+ return results;
26466
+ }
26467
+ async function fetchNodes(gh, query, ids) {
26468
+ const batches = [];
26469
+ for (let i = 0; i < ids.length; i += BATCH2) batches.push(ids.slice(i, i + BATCH2));
26470
+ const results = await pool(
26471
+ batches.map((batch) => () => retryOnTimeout(() => gh.graphql(query, { ids: batch })))
26381
26472
  );
26382
- const mineWaiting = mine.filter((pr) => !pr.isDraft && !mineTodo.includes(pr) && !mineReady.includes(pr));
26383
- const involved = take(results.involved);
26384
- const stale = results.stale.map(make);
26385
- const unreviewed = results.unreviewed.map(make).filter((pr) => pr.requested.length === 0);
26386
- const byUpdated = (a, b) => b.updatedAt.localeCompare(a.updatedAt);
26387
- const sections = [
26388
- { id: "requested", prs: requested },
26389
- { id: "rereview", prs: rereview },
26390
- { id: "mine-todo", prs: mineTodo },
26391
- { id: "mine-waiting", prs: mineWaiting },
26392
- { id: "mine-ready", prs: mineReady },
26393
- { id: "mine-draft", prs: mineDraft },
26394
- { id: "involved", prs: involved },
26395
- { id: "stale", prs: stale.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt)) },
26396
- { id: "unreviewed", prs: unreviewed }
26397
- ];
26398
- for (const s of sections) if (s.id !== "stale") s.prs.sort(byUpdated);
26399
- return sections;
26473
+ return results.flatMap((r) => r.nodes.filter((n) => n !== null));
26400
26474
  }
26401
- async function runSearches(gh, config2, now) {
26402
- const idsBySection = Object.fromEntries(INBOX_SECTION_IDS.map((id) => [id, []]));
26475
+ async function searchIds(gh, q, pages) {
26476
+ const ids = [];
26477
+ let after = null;
26403
26478
  let viewer2 = null;
26404
- for (const scope of scopeChunks(config2)) {
26405
- const data = await gh.graphql(
26406
- INBOX_IDS_QUERY,
26407
- searchQueries(scope, config2.staleDays, now)
26408
- );
26479
+ let truncated = false;
26480
+ for (let page = 0; page < pages; page++) {
26481
+ const data = await gh.graphql(SEARCH_IDS_QUERY, { q, after });
26409
26482
  viewer2 = data.viewer;
26410
- for (const section of INBOX_SECTION_IDS) {
26411
- for (const node2 of data[section].nodes) {
26412
- if (node2.id && !idsBySection[section].includes(node2.id)) idsBySection[section].push(node2.id);
26413
- }
26414
- }
26415
- }
26416
- const unique = [...new Set(INBOX_SECTION_IDS.flatMap((s) => idsBySection[s]))];
26417
- const byId = /* @__PURE__ */ new Map();
26418
- for (let i = 0; i < unique.length; i += 100) {
26419
- const data = await gh.graphql(PR_SUMMARIES_QUERY, { ids: unique.slice(i, i + 100) });
26420
- for (const node2 of data.nodes) if (node2) byId.set(node2.id, node2);
26483
+ for (const node2 of data.search.nodes) if (node2.id) ids.push(node2.id);
26484
+ if (!data.search.pageInfo.hasNextPage) break;
26485
+ after = data.search.pageInfo.endCursor;
26486
+ truncated = page === pages - 1;
26421
26487
  }
26422
- const merged = Object.fromEntries(
26423
- INBOX_SECTION_IDS.map((s) => [s, idsBySection[s].flatMap((id) => byId.get(id) ?? [])])
26488
+ return { viewer: viewer2, ids, truncated };
26489
+ }
26490
+ var SIDE_SEARCHES = ["requested", "involved", "reviewed", "merged"];
26491
+ async function loadSources(gh, config2, now) {
26492
+ const open2 = scopeTerms(config2).map(
26493
+ (term) => () => searchIds(gh, searchQueries(term, 0).open, MAX_SEARCH_PAGES)
26424
26494
  );
26425
- return { viewer: viewer2, merged };
26495
+ const side = scopeChunks(config2).flatMap((chunk) => {
26496
+ const queries = searchQueries(chunk, now - MERGED_WINDOW_MS);
26497
+ return SIDE_SEARCHES.map((name) => async () => ({ name, ...await searchIds(gh, queries[name], 1) }));
26498
+ });
26499
+ const [openResults, sideResults] = await Promise.all([pool(open2), pool(side)]);
26500
+ const viewer2 = openResults[0]?.viewer ?? sideResults[0]?.viewer ?? null;
26501
+ const sideIds = (name) => new Set(sideResults.filter((r) => r.name === name).flatMap((r) => r.ids));
26502
+ const requested = sideIds("requested");
26503
+ const involved = sideIds("involved");
26504
+ const reviewed = sideIds("reviewed");
26505
+ const mergedIds = [...sideIds("merged")];
26506
+ const openIds = [...new Set(openResults.flatMap((r) => r.ids))];
26507
+ const board = await fetchNodes(gh, BOARD_QUERY, [.../* @__PURE__ */ new Set([...openIds, ...mergedIds])]);
26508
+ const byId = new Map(board.map((node2) => [node2.id, node2]));
26509
+ const me = viewer2?.login;
26510
+ const personal = [];
26511
+ const approved = [];
26512
+ for (const node2 of board) {
26513
+ if (node2.mergedAt !== null) continue;
26514
+ const concernsMe = requested.has(node2.id) || involved.has(node2.id) || reviewed.has(node2.id) || node2.author?.login === me;
26515
+ if (concernsMe) personal.push(node2.id);
26516
+ else if (isApproved(
26517
+ node2.reviewDecision,
26518
+ node2.latestOpinionatedReviews.nodes.map((r) => r.state)
26519
+ )) {
26520
+ approved.push(node2.id);
26521
+ }
26522
+ }
26523
+ const [details, checks] = await Promise.all([
26524
+ fetchNodes(gh, DETAILS_QUERY, personal),
26525
+ fetchNodes(gh, CHECKS_QUERY, approved)
26526
+ ]);
26527
+ for (const extra of [...details, ...checks]) {
26528
+ const node2 = byId.get(extra.id);
26529
+ if (node2) byId.set(extra.id, { ...node2, ...extra });
26530
+ }
26531
+ const nodesOf = (list) => list.flatMap((id) => byId.get(id) ?? []);
26532
+ return {
26533
+ viewer: viewer2,
26534
+ truncated: openResults.some((r) => r.truncated),
26535
+ sources: { open: nodesOf(openIds), merged: nodesOf(mergedIds), requested, involved }
26536
+ };
26426
26537
  }
26427
26538
  async function loadInbox(gh, config2, viewerFallback, lastVisits) {
26428
26539
  const now = Date.now();
26429
- const { viewer: viewer2, merged } = await runSearches(gh, config2, now);
26540
+ const { viewer: viewer2, truncated, sources } = await loadSources(gh, config2, now);
26430
26541
  const me = viewer2 ?? await viewerFallback();
26431
- const keys = INBOX_SECTION_IDS.flatMap(
26432
- (id) => merged[id].map((n) => {
26433
- const [owner = "", repo = ""] = n.repository.nameWithOwner.split("/");
26434
- return prKey(owner, repo, n.number);
26435
- })
26436
- );
26542
+ const keys = [...sources.open, ...sources.merged].map((n) => {
26543
+ const [owner = "", repo = ""] = n.repository.nameWithOwner.split("/");
26544
+ return prKey(owner, repo, n.number);
26545
+ });
26437
26546
  return {
26438
26547
  viewer: me,
26439
26548
  configured: config2.repos.length + config2.orgs.length > 0,
26440
- sections: classifyInbox(merged, me.login, lastVisits([...new Set(keys)])),
26549
+ prs: buildBoard(sources, me.login, lastVisits([...new Set(keys)]), now - config2.staleDays * 864e5),
26550
+ truncated,
26441
26551
  fetchedAt: now
26442
26552
  };
26443
26553
  }
26444
26554
  async function inboxFingerprint(gh, config2, signal) {
26445
26555
  const parts = [];
26446
26556
  for (const scope of scopeChunks(config2)) {
26447
- const data = await gh.graphql(INBOX_FINGERPRINT_QUERY, { q: `is:pr is:open archived:false ${scope}` }, signal);
26557
+ const data = await gh.graphql(
26558
+ INBOX_FINGERPRINT_QUERY,
26559
+ { q: `is:pr is:open archived:false ${scope} sort:updated-desc` },
26560
+ signal
26561
+ );
26448
26562
  parts.push(String(data.search.issueCount));
26449
- for (const n of data.search.nodes) parts.push(`${n.id}:${n.updatedAt}:${n.headRefOid}`);
26563
+ for (const n of data.search.nodes) {
26564
+ const ci = n.commits?.nodes[0]?.commit.statusCheckRollup?.state ?? "";
26565
+ parts.push(`${n.id}:${n.updatedAt}:${n.headRefOid}:${n.reviewDecision}:${n.isInMergeQueue}:${ci}`);
26566
+ }
26450
26567
  }
26451
26568
  return parts.join("|");
26452
26569
  }
@@ -26497,7 +26614,6 @@ async function loadPr(gh, store, ref) {
26497
26614
  reviewers.set(rr.login, { login: rr.login, avatarUrl: rr.avatarUrl, state: "REQUESTED" });
26498
26615
  else if (rr?.__typename === "Team") requestedTeams.push(rr.slug);
26499
26616
  }
26500
- const rollup = pr.lastCommit.nodes[0]?.commit.statusCheckRollup ?? null;
26501
26617
  const timeline = [
26502
26618
  ...pr.comments.nodes.map((c) => ({
26503
26619
  kind: "comment",
@@ -26505,6 +26621,7 @@ async function loadPr(gh, store, ref) {
26505
26621
  url: c.url,
26506
26622
  body: c.body,
26507
26623
  bodyHTML: c.bodyHTML,
26624
+ reactions: toReactions(c.reactionGroups),
26508
26625
  createdAt: c.createdAt,
26509
26626
  author: c.author,
26510
26627
  unread: c.author?.login !== me && Date.parse(c.createdAt) > lastVisitAt
@@ -26516,6 +26633,7 @@ async function loadPr(gh, store, ref) {
26516
26633
  state: r.state,
26517
26634
  body: r.body,
26518
26635
  bodyHTML: r.bodyHTML,
26636
+ reactions: toReactions(r.reactionGroups),
26519
26637
  createdAt: r.submittedAt ?? r.createdAt,
26520
26638
  author: r.author,
26521
26639
  unread: r.author?.login !== me && Date.parse(r.submittedAt ?? r.createdAt) > lastVisitAt
@@ -26537,6 +26655,7 @@ async function loadPr(gh, store, ref) {
26537
26655
  updatedAt: pr.updatedAt,
26538
26656
  body: pr.body,
26539
26657
  bodyHTML: pr.bodyHTML,
26658
+ reactions: toReactions(pr.reactionGroups),
26540
26659
  baseRefName: pr.baseRefName,
26541
26660
  headRefName: pr.headRefName,
26542
26661
  baseRefOid: pr.baseRefOid,
@@ -26549,12 +26668,6 @@ async function loadPr(gh, store, ref) {
26549
26668
  author: pr.author,
26550
26669
  sameRepo: pr.headRepository?.nameWithOwner.toLowerCase() === `${ref.owner}/${ref.repo}`.toLowerCase()
26551
26670
  },
26552
- checks: rollup ? {
26553
- state: rollup.state,
26554
- contexts: rollup.contexts.nodes.map(
26555
- (c) => c.__typename === "CheckRun" ? { name: c.name, state: c.conclusion ?? c.status, url: c.detailsUrl } : { name: c.context, state: c.state, url: c.targetUrl }
26556
- )
26557
- } : null,
26558
26671
  reviewers: [...reviewers.values()],
26559
26672
  requestedTeams,
26560
26673
  myReview: pr.viewerLatestReview ? {
@@ -26586,6 +26699,9 @@ async function loadPr(gh, store, ref) {
26586
26699
  seenHunks: store.seenHunks(key)
26587
26700
  };
26588
26701
  }
26702
+ function toReactions(groups) {
26703
+ return (groups ?? []).filter((g) => g.reactors.totalCount > 0).map((g) => ({ content: g.content, count: g.reactors.totalCount, mine: g.viewerHasReacted }));
26704
+ }
26589
26705
  function toComment(c) {
26590
26706
  return {
26591
26707
  id: c.id,
@@ -26593,6 +26709,7 @@ function toComment(c) {
26593
26709
  url: c.url,
26594
26710
  body: c.body,
26595
26711
  bodyHTML: c.bodyHTML,
26712
+ reactions: toReactions(c.reactionGroups),
26596
26713
  createdAt: c.createdAt,
26597
26714
  author: c.author,
26598
26715
  pending: c.state === "PENDING",
@@ -26720,6 +26837,17 @@ async function setFileViewed(gh, prId, path, viewed) {
26720
26837
  { id: prId, path }
26721
26838
  );
26722
26839
  }
26840
+ async function setReaction(gh, subjectId, content, on2) {
26841
+ const mutation = on2 ? "addReaction" : "removeReaction";
26842
+ const data = await gh.graphql(
26843
+ `mutation($id: ID!, $content: ReactionContent!) {
26844
+ ${mutation}(input: { subjectId: $id, content: $content }) { subject { ...ReactionFields } }
26845
+ }
26846
+ ${REACTION_FIELDS}`,
26847
+ { id: subjectId, content }
26848
+ );
26849
+ return toReactions(data[mutation].subject.reactionGroups);
26850
+ }
26723
26851
  async function replyToThread(gh, threadId, body, pendingReviewId) {
26724
26852
  const data = await gh.graphql(
26725
26853
  `mutation($thread: ID!, $body: String!, $review: ID) {
@@ -26816,7 +26944,7 @@ async function addIssueComment(gh, subjectId, body) {
26816
26944
  }`,
26817
26945
  { id: subjectId, body }
26818
26946
  );
26819
- return { kind: "comment", ...data.addComment.commentEdge.node, unread: false };
26947
+ return { kind: "comment", ...data.addComment.commentEdge.node, reactions: [], unread: false };
26820
26948
  }
26821
26949
  async function deleteReviewComment(gh, commentId) {
26822
26950
  await gh.graphql(
@@ -26873,6 +27001,102 @@ async function applySuggestion(gh, ref, input2) {
26873
27001
  return res.commit.sha;
26874
27002
  }
26875
27003
 
27004
+ // ../server/src/checks.ts
27005
+ var PR_CHECKS_QUERY = (
27006
+ /* GraphQL */
27007
+ `
27008
+ query PrChecks($owner: String!, $repo: String!, $number: Int!) {
27009
+ repository(owner: $owner, name: $repo) {
27010
+ pullRequest(number: $number) {
27011
+ commits(last: 1) {
27012
+ nodes {
27013
+ commit {
27014
+ statusCheckRollup {
27015
+ state
27016
+ contexts(first: 100) {
27017
+ nodes {
27018
+ __typename
27019
+ ... on CheckRun {
27020
+ name
27021
+ conclusion
27022
+ status
27023
+ detailsUrl
27024
+ title
27025
+ checkSuite {
27026
+ workflowRun {
27027
+ workflow {
27028
+ name
27029
+ }
27030
+ }
27031
+ }
27032
+ }
27033
+ ... on StatusContext {
27034
+ context
27035
+ state
27036
+ targetUrl
27037
+ description
27038
+ }
27039
+ }
27040
+ }
27041
+ }
27042
+ }
27043
+ }
27044
+ }
27045
+ }
27046
+ }
27047
+ }
27048
+ `
27049
+ );
27050
+ var FAILED = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ACTION_REQUIRED", "CANCELLED", "ERROR"]);
27051
+ var SKIPPED = /* @__PURE__ */ new Set(["NEUTRAL", "SKIPPED", "STALE"]);
27052
+ var RANK = { failure: 0, pending: 1, success: 2, skipped: 3 };
27053
+ function outcomeOf(state) {
27054
+ if (FAILED.has(state)) return "failure";
27055
+ if (state === "SUCCESS") return "success";
27056
+ if (SKIPPED.has(state)) return "skipped";
27057
+ return "pending";
27058
+ }
27059
+ function toChecks(rollup) {
27060
+ if (!rollup) return null;
27061
+ const all = rollup.contexts.nodes.map((c) => {
27062
+ if (c.__typename === "StatusContext") {
27063
+ return {
27064
+ name: c.context,
27065
+ outcome: outcomeOf(c.state),
27066
+ state: c.state.toLowerCase(),
27067
+ url: c.targetUrl,
27068
+ summary: c.description || null
27069
+ };
27070
+ }
27071
+ const state = c.conclusion ?? c.status;
27072
+ const workflow = c.checkSuite?.workflowRun?.workflow.name;
27073
+ return {
27074
+ name: workflow ? `${workflow} / ${c.name}` : c.name,
27075
+ outcome: outcomeOf(state),
27076
+ state: state.toLowerCase(),
27077
+ url: c.detailsUrl,
27078
+ summary: c.title || null
27079
+ };
27080
+ });
27081
+ const seen = /* @__PURE__ */ new Set();
27082
+ const checks = all.filter((c) => {
27083
+ const key = `${c.name}\0${c.outcome}`;
27084
+ if (seen.has(key)) return false;
27085
+ seen.add(key);
27086
+ return true;
27087
+ });
27088
+ checks.sort((a, b) => RANK[a.outcome] - RANK[b.outcome] || a.name.localeCompare(b.name));
27089
+ return { state: rollup.state, checks };
27090
+ }
27091
+ async function loadChecks(gh, ref) {
27092
+ const data = await gh.graphql(PR_CHECKS_QUERY, {
27093
+ owner: ref.owner,
27094
+ repo: ref.repo,
27095
+ number: ref.number
27096
+ });
27097
+ return toChecks(data.repository?.pullRequest?.commits.nodes[0]?.commit.statusCheckRollup ?? null);
27098
+ }
27099
+
26876
27100
  // ../../packages/language/src/paths.ts
26877
27101
  function normalizePath(path) {
26878
27102
  const segments = [];
@@ -27857,6 +28081,58 @@ async function loadLinks(gh, store, ref, base, head, fullPr) {
27857
28081
  return links;
27858
28082
  }
27859
28083
 
28084
+ // ../server/src/poll.ts
28085
+ function sleep(ms, signal) {
28086
+ const { promise: promise2, resolve } = Promise.withResolvers();
28087
+ const timer = setTimeout(resolve, ms);
28088
+ signal?.addEventListener(
28089
+ "abort",
28090
+ () => {
28091
+ clearTimeout(timer);
28092
+ resolve();
28093
+ },
28094
+ { once: true }
28095
+ );
28096
+ return promise2;
28097
+ }
28098
+ async function* pollChanges(intervalMs, signal, fingerprint, extraTrigger) {
28099
+ let last = null;
28100
+ try {
28101
+ last = await fingerprint();
28102
+ } catch (error62) {
28103
+ console.warn("[poll]", error62 instanceof Error ? error62.message : error62);
28104
+ }
28105
+ const triggerIt = extraTrigger?.[Symbol.asyncIterator]();
28106
+ const nextTrigger = () => {
28107
+ const promise2 = triggerIt?.next();
28108
+ promise2?.catch(() => void 0);
28109
+ return promise2;
28110
+ };
28111
+ let pendingTrigger = signal?.aborted ? void 0 : nextTrigger();
28112
+ try {
28113
+ while (!signal?.aborted) {
28114
+ const tick = sleep(intervalMs, signal).then(() => "tick");
28115
+ const winner = pendingTrigger ? await Promise.race([tick, pendingTrigger.then(() => "trigger")]) : await tick;
28116
+ if (signal?.aborted) return;
28117
+ if (winner === "trigger") {
28118
+ pendingTrigger = nextTrigger();
28119
+ last = null;
28120
+ yield { at: Date.now() };
28121
+ continue;
28122
+ }
28123
+ try {
28124
+ const next = await fingerprint();
28125
+ if (last !== null && next !== last) yield { at: Date.now() };
28126
+ last = next;
28127
+ } catch (error62) {
28128
+ console.warn("[poll]", error62 instanceof Error ? error62.message : error62);
28129
+ }
28130
+ }
28131
+ } finally {
28132
+ await triggerIt?.return?.().catch(() => void 0);
28133
+ }
28134
+ }
28135
+
27860
28136
  // ../../node_modules/.pnpm/@trpc+server@11.19.0_typescript@6.0.3/node_modules/@trpc/server/dist/initTRPC-B259os3T.mjs
27861
28137
  var middlewareMarker = "middlewareMarker";
27862
28138
  function createMiddlewareFactory() {
@@ -28202,51 +28478,10 @@ var procedure = t.procedure.use(githubErrors);
28202
28478
 
28203
28479
  // ../server/src/router.ts
28204
28480
  var PR_POLL_MS = 3e4;
28205
- var INBOX_POLL_MS = 12e4;
28481
+ var INBOX_POLL_MS = 6e4;
28206
28482
  async function viewer(ctx) {
28207
28483
  return (await ctx.gh.graphql("{ viewer { login avatarUrl } }")).viewer;
28208
28484
  }
28209
- function sleep(ms, signal) {
28210
- const { promise: promise2, resolve } = Promise.withResolvers();
28211
- const timer = setTimeout(resolve, ms);
28212
- signal?.addEventListener(
28213
- "abort",
28214
- () => {
28215
- clearTimeout(timer);
28216
- resolve();
28217
- },
28218
- { once: true }
28219
- );
28220
- return promise2;
28221
- }
28222
- async function* pollChanges(intervalMs, signal, fingerprint, extraTrigger) {
28223
- let last = null;
28224
- try {
28225
- last = await fingerprint();
28226
- } catch (error62) {
28227
- console.warn("[poll]", error62 instanceof Error ? error62.message : error62);
28228
- }
28229
- const triggerIt = extraTrigger?.[Symbol.asyncIterator]();
28230
- let pendingTrigger = triggerIt?.next();
28231
- while (!signal?.aborted) {
28232
- const tick = sleep(intervalMs, signal).then(() => "tick");
28233
- const winner = pendingTrigger ? await Promise.race([tick, pendingTrigger.then(() => "trigger")]) : await tick;
28234
- if (signal?.aborted) return;
28235
- if (winner === "trigger") {
28236
- pendingTrigger = triggerIt?.next();
28237
- last = null;
28238
- yield { at: Date.now() };
28239
- continue;
28240
- }
28241
- try {
28242
- const next = await fingerprint();
28243
- if (last !== null && next !== last) yield { at: Date.now() };
28244
- last = next;
28245
- } catch (error62) {
28246
- console.warn("[poll]", error62 instanceof Error ? error62.message : error62);
28247
- }
28248
- }
28249
- }
28250
28485
  var configRouter = router({
28251
28486
  get: procedure.query(({ ctx }) => ctx.config.get()),
28252
28487
  add: procedure.input(trackedEntrySchema).mutation(async ({ ctx, input: input2 }) => {
@@ -28276,6 +28511,8 @@ var inboxRouter = router({
28276
28511
  });
28277
28512
  var prRouter = router({
28278
28513
  get: procedure.input(prRefSchema).query(({ ctx, input: input2 }) => loadPr(ctx.gh, ctx.store, input2)),
28514
+ /** Polled by the PR page while checks run. */
28515
+ checks: procedure.input(prRefSchema).query(({ ctx, input: input2 }) => loadChecks(ctx.gh, input2)),
28279
28516
  diff: procedure.input(external_exports.object({ ref: prRefSchema, base: shaSchema, head: shaSchema, fullPr: external_exports.boolean() })).query(({ ctx, input: input2 }) => loadDiff(ctx.gh, ctx.store, input2.ref, input2.base, input2.head, input2.fullPr)),
28280
28517
  links: procedure.input(external_exports.object({ ref: prRefSchema, base: shaSchema, head: shaSchema, fullPr: external_exports.boolean() })).query(({ ctx, input: input2 }) => loadLinks(ctx.gh, ctx.store, input2.ref, input2.base, input2.head, input2.fullPr)),
28281
28518
  blobs: procedure.input(external_exports.object({ ref: prRefSchema, oids: external_exports.array(shaSchema).min(1).max(2) })).query(({ ctx, input: input2 }) => loadBlobs(ctx.gh, ctx.store, input2.ref, input2.oids)),
@@ -28316,6 +28553,7 @@ var prRouter = router({
28316
28553
  ({ ctx, input: input2 }) => replyToThread(ctx.gh, input2.threadId, input2.body, input2.pendingReviewId)
28317
28554
  ),
28318
28555
  setResolved: procedure.input(external_exports.object({ threadId: external_exports.string(), resolved: external_exports.boolean() })).mutation(({ ctx, input: input2 }) => setThreadResolved(ctx.gh, input2.threadId, input2.resolved)),
28556
+ react: procedure.input(external_exports.object({ subjectId: external_exports.string(), content: reactionContentSchema, on: external_exports.boolean() })).mutation(({ ctx, input: input2 }) => setReaction(ctx.gh, input2.subjectId, input2.content, input2.on)),
28319
28557
  addThread: procedure.input(
28320
28558
  external_exports.object({
28321
28559
  prId: external_exports.string(),