authorgravatar for 75276669+Gigioxx@users.noreply.github.comGuillermo Casanova <75276669+Gigioxx@users.noreply.github.com> 2026-10-05 18:20:38-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 14:20:38-07:00
log21eb9a421aff7a8fbe044b1a34bb3e364fb94a68
tree61de166464a3f95c63ea4785b264cb51cc021ef5
parentf973b1d7d5cd7ffb4dd3c1c61cc0caece09fd1a6
signature Signed by PGP key B5690EEEBB952194

fix(server): PR watch wakes the agent when a bot edits its review comment (#15415)


8 files changed, 89 insertions(+), 10 deletions(-)

apps/server/src/orchestration-v2/pullRequestWatch.test.ts+21-1
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import type {1import type {
2 PullRequestCheck,2 PullRequestCheck,
3 PullRequestComment,3 PullRequestComment,
4 PullRequestDetail,
5 ThreadPullRequestWatch,4 ThreadPullRequestWatch,
6} from "@t3tools/contracts";5} from "@t3tools/contracts";
7import { assert, describe, it } from "@effect/vitest";6import { assert, describe, it } from "@effect/vitest";
...@@ -119,6 +118,27 @@ describe("evaluatePullRequestWatch", () => {...@@ -119,6 +118,27 @@ describe("evaluatePullRequestWatch", () => {
119 assert.deepEqual(again.next.remarkIds, [first.id, "late"]);118 assert.deepEqual(again.next.remarkIds, [first.id, "late"]);
120 });119 });
121120
121 it("reports edits after the watermark once and counts them toward the wake limit", () => {
122 const old = remark("greptile[bot]", "2026-10-02T11:00:00Z");
123 const watching = watch({
124 headSha: "aaaaaaaaaa",
125 remarkIds: [old.id],
126 wakes: PULL_REQUEST_WATCH_WAKE_LIMIT - 1,
127 });
128 assert.deepEqual(evaluatePullRequestWatch(watching, detail(), [old]).changes, []);
129 const edited = { ...old, editedAt: "2026-10-02T12:06:00Z" };
130 const report = evaluatePullRequestWatch(watching, detail(), [edited]);
131 assert.deepEqual(report.changes, [{ kind: "remarks", remarks: [edited] }]);
132 assert.equal(report.next.remarksThrough, edited.editedAt);
133 assert.deepEqual(report.next.remarkIds, [old.id]);
134 assert.isTrue(report.exhausted);
135 assert.deepEqual(evaluatePullRequestWatch(report.next, detail(), [edited]).changes, []);
136 const late = { ...edited, id: "late" };
137 assert.deepEqual(evaluatePullRequestWatch(report.next, detail(), [edited, late]).changes, [
138 { kind: "remarks", remarks: [late] },
139 ]);
140 });
141
122 it("does not treat a failed check read as a rerun", () => {142 it("does not treat a failed check read as a rerun", () => {
123 const failed = detail({ checks: [check("lint", "failure")] });143 const failed = detail({ checks: [check("lint", "failure")] });
124 const reported = evaluatePullRequestWatch(watch(), failed, noRemarks);144 const reported = evaluatePullRequestWatch(watch(), failed, noRemarks);
apps/server/src/orchestration-v2/pullRequestWatch.ts+6-4
...@@ -71,18 +71,20 @@ export function evaluatePullRequestWatch(...@@ -71,18 +71,20 @@ export function evaluatePullRequestWatch(
7171
72 const own = (detail.viewer ?? detail.author?.login)?.toLowerCase();72 const own = (detail.viewer ?? detail.author?.login)?.toLowerCase();
73 const through = Date.parse(watch.remarksThrough);73 const through = Date.parse(watch.remarksThrough);
74 // An edit counts as new activity, so bots that rewrite one summary comment still wake the agent.
75 const activeAt = (remark: PullRequestComment) => remark.editedAt ?? remark.createdAt;
74 // GitHub times are per second, so remarks at the boundary time are told apart by ID.76 // GitHub times are per second, so remarks at the boundary time are told apart by ID.
75 const fresh = (remarks ?? []).filter((remark) => {77 const fresh = (remarks ?? []).filter((remark) => {
76 const at = Date.parse(remark.createdAt);78 const at = Date.parse(activeAt(remark));
77 return (79 return (
78 (at > through || (at === through && !watch.remarkIds.includes(remark.id))) &&80 (at > through || (at === through && !watch.remarkIds.includes(remark.id))) &&
79 remark.author?.login.toLowerCase() !== own81 remark.author?.login.toLowerCase() !== own
80 );82 );
81 });83 });
82 if (fresh.length > 0) changes.push({ kind: "remarks", remarks: fresh });84 if (fresh.length > 0) changes.push({ kind: "remarks", remarks: fresh });
83 const latest = Math.max(through, ...fresh.map((remark) => Date.parse(remark.createdAt)));85 const latest = Math.max(through, ...fresh.map((remark) => Date.parse(activeAt(remark))));
84 const atLatest = fresh.filter((remark) => Date.parse(remark.createdAt) === latest);86 const atLatest = fresh.filter((remark) => Date.parse(activeAt(remark)) === latest);
85 const remarksThrough = latest === through ? watch.remarksThrough : atLatest[0]!.createdAt;87 const remarksThrough = latest === through ? watch.remarksThrough : activeAt(atLatest[0]!);
86 const remarkIds = [88 const remarkIds = [
87 ...(latest === through ? watch.remarkIds : []),89 ...(latest === through ? watch.remarkIds : []),
88 ...atLatest.map((remark) => remark.id),90 ...atLatest.map((remark) => remark.id),
apps/server/src/pullRequest/GitHubPullRequestCli.ts+3
...@@ -2338,6 +2338,7 @@ export const make = Effect.gen(function* () {...@@ -2338,6 +2338,7 @@ export const make = Effect.gen(function* () {
2338 let reviewers: ReadonlyArray<PullRequestActor> = [];2338 let reviewers: ReadonlyArray<PullRequestActor> = [];
2339 let reactions: GitHubReviewThreadPage["reactions"] = [];2339 let reactions: GitHubReviewThreadPage["reactions"] = [];
2340 const reactionsById = new Map<string, ReadonlyArray<PullRequestReaction>>();2340 const reactionsById = new Map<string, ReadonlyArray<PullRequestReaction>>();
2341 const editedAtById = new Map<string, string>();
2341 let commits: GitHubReviewThreadPage["commits"] = [];2342 let commits: GitHubReviewThreadPage["commits"] = [];
2342 let viewer: GitHubReviewThreadPage["viewer"] = { canUpdate: true, didAuthor: false };2343 let viewer: GitHubReviewThreadPage["viewer"] = { canUpdate: true, didAuthor: false };
2343 const dismissalsByReviewId = new Map<string, string>();2344 const dismissalsByReviewId = new Map<string, string>();
...@@ -2356,6 +2357,7 @@ export const make = Effect.gen(function* () {...@@ -2356,6 +2357,7 @@ export const make = Effect.gen(function* () {
2356 reviewers = read.reviewers;2357 reviewers = read.reviewers;
2357 reactions = read.reactions;2358 reactions = read.reactions;
2358 for (const [id, entry] of read.reactionsById) reactionsById.set(id, entry);2359 for (const [id, entry] of read.reactionsById) reactionsById.set(id, entry);
2360 for (const [id, editedAt] of read.editedAtById) editedAtById.set(id, editedAt);
2359 commits = read.commits;2361 commits = read.commits;
2360 viewer = read.viewer;2362 viewer = read.viewer;
2361 for (const [id, message] of read.dismissalsByReviewId)2363 for (const [id, message] of read.dismissalsByReviewId)
...@@ -2412,6 +2414,7 @@ export const make = Effect.gen(function* () {...@@ -2412,6 +2414,7 @@ export const make = Effect.gen(function* () {
2412 reviewThreadsTruncated: cursor !== null,2414 reviewThreadsTruncated: cursor !== null,
2413 reactions,2415 reactions,
2414 reactionsById,2416 reactionsById,
2417 editedAtById,
2415 reviewers,2418 reviewers,
2416 avatarsByLogin,2419 avatarsByLogin,
2417 botLogins,2420 botLogins,
apps/server/src/pullRequest/GitHubPullRequestProvider.test.ts+3
...@@ -885,6 +885,7 @@ describe("getChangeRequest commits", () => {...@@ -885,6 +885,7 @@ describe("getChangeRequest commits", () => {
885 reviewThreadsTruncated: false,885 reviewThreadsTruncated: false,
886 reactions: [],886 reactions: [],
887 reactionsById: new Map<string, ReadonlyArray<PullRequestReaction>>(),887 reactionsById: new Map<string, ReadonlyArray<PullRequestReaction>>(),
888 editedAtById: new Map<string, string>(),
888 reviewers: [],889 reviewers: [],
889 avatarsByLogin: new Map<string, string>(),890 avatarsByLogin: new Map<string, string>(),
890 botLogins: new Set<string>(),891 botLogins: new Set<string>(),
...@@ -971,6 +972,7 @@ describe("getChangeRequestActivity dismissed reviews", () => {...@@ -971,6 +972,7 @@ describe("getChangeRequestActivity dismissed reviews", () => {
971 reviewThreadsTruncated: false,972 reviewThreadsTruncated: false,
972 reactions: [],973 reactions: [],
973 reactionsById: new Map(),974 reactionsById: new Map(),
975 editedAtById: new Map([["PRR_1", "2026-07-04T00:00:00Z"]]),
974 reviewers: [],976 reviewers: [],
975 avatarsByLogin: new Map(),977 avatarsByLogin: new Map(),
976 botLogins: new Set(["macroscopeapp"]),978 botLogins: new Set(["macroscopeapp"]),
...@@ -1002,6 +1004,7 @@ describe("getChangeRequestActivity dismissed reviews", () => {...@@ -1002,6 +1004,7 @@ describe("getChangeRequestActivity dismissed reviews", () => {
1002 Effect.map((activity) => {1004 Effect.map((activity) => {
1003 expect(activity.comments[0]?.body).toBe("Dismissing prior approval to re-evaluate 9b66581");1005 expect(activity.comments[0]?.body).toBe("Dismissing prior approval to re-evaluate 9b66581");
1004 expect(activity.comments[0]?.author?.isBot).toBe(true);1006 expect(activity.comments[0]?.author?.isBot).toBe(true);
1007 expect(activity.comments[0]?.editedAt).toBe("2026-07-04T00:00:00Z");
1005 }),1008 }),
1006 Effect.provide(layerFor("<!-- Macroscope (Approvability) review body marker -->")),1009 Effect.provide(layerFor("<!-- Macroscope (Approvability) review body marker -->")),
1007 ),1010 ),
apps/server/src/pullRequest/GitHubPullRequestProvider.ts+2
...@@ -409,6 +409,7 @@ export const make = Effect.gen(function* () {...@@ -409,6 +409,7 @@ export const make = Effect.gen(function* () {
409 dismissalsByReviewId: new Map<string, string>(),409 dismissalsByReviewId: new Map<string, string>(),
410 reactions: [],410 reactions: [],
411 reactionsById: new Map<string, ReadonlyArray<PullRequestReaction>>(),411 reactionsById: new Map<string, ReadonlyArray<PullRequestReaction>>(),
412 editedAtById: new Map<string, string>(),
412 reviewThreads: [],413 reviewThreads: [],
413 commentCount: 0,414 commentCount: 0,
414 truncated: true,415 truncated: true,
...@@ -475,6 +476,7 @@ export const make = Effect.gen(function* () {...@@ -475,6 +476,7 @@ export const make = Effect.gen(function* () {
475 // A comment out of `gh pr view --json` carries none of its own: that read476 // A comment out of `gh pr view --json` carries none of its own: that read
476 // reports no reaction at all, so they arrive from the GraphQL page by node id.477 // reports no reaction at all, so they arrive from the GraphQL page by node id.
477 reactions: comment.reactions ?? reviewThreads.reactionsById.get(comment.id) ?? [],478 reactions: comment.reactions ?? reviewThreads.reactionsById.get(comment.id) ?? [],
479 editedAt: reviewThreads.editedAtById.get(comment.id) ?? comment.editedAt ?? null,
478 }))480 }))
479 .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)),481 .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)),
480 // `gh pr view --json comments,reviews` follows GitHub's cursors itself, so those two482 // `gh pr view --json comments,reviews` follows GitHub's cursors itself, so those two
apps/server/src/pullRequest/gitHubPullRequestJson.test.ts+33-1
...@@ -1105,7 +1105,13 @@ describe("review thread decoding", () => {...@@ -1105,7 +1105,13 @@ describe("review thread decoding", () => {
1105 path: "src/a.ts",1105 path: "src/a.ts",
1106 line: 42,1106 line: 42,
1107 diffSide: "LEFT",1107 diffSide: "LEFT",
1108 comments: { totalCount: 2, nodes: [comment("c1", "first"), comment("c2", "second")] },1108 comments: {
1109 totalCount: 2,
1110 nodes: [
1111 { ...comment("c1", "first"), lastEditedAt: "2026-07-02T00:00:00Z" },
1112 comment("c2", "second"),
1113 ],
1114 },
1109 },1115 },
1110 ]),1116 ]),
1111 ),1117 ),
...@@ -1124,6 +1130,7 @@ describe("review thread decoding", () => {...@@ -1124,6 +1130,7 @@ describe("review thread decoding", () => {
1124 author: { login: "bilal", name: null, avatarUrl: "https://avatars/b.png" },1130 author: { login: "bilal", name: null, avatarUrl: "https://avatars/b.png" },
1125 body: "first",1131 body: "first",
1126 createdAt: "2026-07-01T00:00:00Z",1132 createdAt: "2026-07-01T00:00:00Z",
1133 editedAt: "2026-07-02T00:00:00Z",
1127 url: "https://github.com/acme/web/pull/1#discussion_rc1",1134 url: "https://github.com/acme/web/pull/1#discussion_rc1",
1128 reactions: [],1135 reactions: [],
1129 },1136 },
...@@ -1132,12 +1139,16 @@ describe("review thread decoding", () => {...@@ -1132,12 +1139,16 @@ describe("review thread decoding", () => {
1132 author: { login: "bilal", name: null, avatarUrl: "https://avatars/b.png" },1139 author: { login: "bilal", name: null, avatarUrl: "https://avatars/b.png" },
1133 body: "second",1140 body: "second",
1134 createdAt: "2026-07-01T00:00:00Z",1141 createdAt: "2026-07-01T00:00:00Z",
1142 editedAt: null,
1135 url: "https://github.com/acme/web/pull/1#discussion_rc2",1143 url: "https://github.com/acme/web/pull/1#discussion_rc2",
1136 reactions: [],1144 reactions: [],
1137 },1145 },
1138 ],1146 ],
1139 },1147 },
1140 ]);1148 ]);
1149 expect(
1150 reviewThreadConversation(reviewThreads.threads.map((entry) => entry.thread))[0]?.editedAt,
1151 ).toBe("2026-07-02T00:00:00Z");
1141 });1152 });
11421153
1143 it("leaves an outdated thread without a line rather than pinning it to a stale one", () => {1154 it("leaves an outdated thread without a line rather than pinning it to a stale one", () => {
...@@ -1186,6 +1197,27 @@ describe("review thread decoding", () => {...@@ -1186,6 +1197,27 @@ describe("review thread decoding", () => {
1186 expect(threads).toHaveLength(1);1197 expect(threads).toHaveLength(1);
1187 });1198 });
11881199
1200 it("reads edit times for issue comments and reviews without using reaction updates", () => {
1201 const editedAt = "2026-10-02T12:06:00Z";
1202 const result = expectSuccess(
1203 decodeReviewThreadsJson(
1204 threadsJson([], {
1205 comments: {
1206 nodes: [
1207 { id: "edited", lastEditedAt: editedAt },
1208 { id: "reaction-only", lastEditedAt: null, updatedAt: editedAt },
1209 ],
1210 },
1211 reviews: { nodes: [{ id: "review", lastEditedAt: editedAt }] },
1212 }),
1213 ),
1214 );
1215 expect([...result.editedAtById]).toEqual([
1216 ["edited", editedAt],
1217 ["review", editedAt],
1218 ]);
1219 });
1220
1189 it("puts an issue comment's and a review's reactions in reactionsById, and the pull request's own in reactions", () => {1221 it("puts an issue comment's and a review's reactions in reactionsById, and the pull request's own in reactions", () => {
1190 const result = expectSuccess(1222 const result = expectSuccess(
1191 decodeReviewThreadsJson(1223 decodeReviewThreadsJson(
apps/server/src/pullRequest/gitHubPullRequestJson.ts+19-4
...@@ -362,6 +362,7 @@ function toReactions(...@@ -362,6 +362,7 @@ function toReactions(
362362
363const RawCommentSchema = Schema.Struct({363const RawCommentSchema = Schema.Struct({
364 id: Schema.String,364 id: Schema.String,
365 lastEditedAt: Schema.optional(Schema.NullOr(Schema.String)),
365 author: Schema.optional(Schema.NullOr(RawActorSchema)),366 author: Schema.optional(Schema.NullOr(RawActorSchema)),
366 body: Schema.optional(Schema.String),367 body: Schema.optional(Schema.String),
367 createdAt: Schema.String,368 createdAt: Schema.String,
...@@ -372,6 +373,7 @@ const RawCommentSchema = Schema.Struct({...@@ -372,6 +373,7 @@ const RawCommentSchema = Schema.Struct({
372373
373const RawReviewSchema = Schema.Struct({374const RawReviewSchema = Schema.Struct({
374 id: Schema.String,375 id: Schema.String,
376 lastEditedAt: Schema.optional(Schema.NullOr(Schema.String)),
375 author: Schema.optional(Schema.NullOr(RawActorSchema)),377 author: Schema.optional(Schema.NullOr(RawActorSchema)),
376 body: Schema.optional(Schema.String),378 body: Schema.optional(Schema.String),
377 state: Schema.optional(Schema.NullOr(Schema.String)),379 state: Schema.optional(Schema.NullOr(Schema.String)),
...@@ -490,6 +492,7 @@ const RawReviewThreadsSchema = Schema.Struct({...@@ -490,6 +492,7 @@ const RawReviewThreadsSchema = Schema.Struct({
490 Schema.Struct({492 Schema.Struct({
491 id: Schema.optional(Schema.NullOr(Schema.String)),493 id: Schema.optional(Schema.NullOr(Schema.String)),
492 author: Schema.optional(Schema.NullOr(RawActorSchema)),494 author: Schema.optional(Schema.NullOr(RawActorSchema)),
495 lastEditedAt: Schema.optional(Schema.NullOr(Schema.String)),
493 reactionGroups: RawReactionGroupsSchema,496 reactionGroups: RawReactionGroupsSchema,
494 }),497 }),
495 ),498 ),
...@@ -507,6 +510,7 @@ const RawReviewThreadsSchema = Schema.Struct({...@@ -507,6 +510,7 @@ const RawReviewThreadsSchema = Schema.Struct({
507 Schema.Struct({510 Schema.Struct({
508 id: Schema.optional(Schema.NullOr(Schema.String)),511 id: Schema.optional(Schema.NullOr(Schema.String)),
509 author: Schema.optional(Schema.NullOr(RawActorSchema)),512 author: Schema.optional(Schema.NullOr(RawActorSchema)),
513 lastEditedAt: Schema.optional(Schema.NullOr(Schema.String)),
510 reactionGroups: RawReactionGroupsSchema,514 reactionGroups: RawReactionGroupsSchema,
511 }),515 }),
512 ),516 ),
...@@ -885,7 +889,7 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin...@@ -885,7 +889,7 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin
885 comments(first: 10) {889 comments(first: 10) {
886 totalCount890 totalCount
887 pageInfo { hasNextPage endCursor }891 pageInfo { hasNextPage endCursor }
888 nodes { id author { __typename login avatarUrl } body createdAt url ${REACTION_GROUPS_FIELDS} }892 nodes { id author { __typename login avatarUrl } body createdAt lastEditedAt url ${REACTION_GROUPS_FIELDS} }
889 }893 }
890 }894 }
891 }895 }
...@@ -894,9 +898,9 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin...@@ -894,9 +898,9 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin
894 author { __typename login avatarUrl }898 author { __typename login avatarUrl }
895 ${REACTION_GROUPS_FIELDS}899 ${REACTION_GROUPS_FIELDS}
896 comments(first: ${GRAPHQL_PAGE_SIZE}) {900 comments(first: ${GRAPHQL_PAGE_SIZE}) {
897 nodes { id author { __typename login avatarUrl } ${REACTION_GROUPS_FIELDS} }901 nodes { id lastEditedAt author { __typename login avatarUrl } ${REACTION_GROUPS_FIELDS} }
898 }902 }
899 reviews(first: ${GRAPHQL_PAGE_SIZE}) { nodes { id author { __typename login avatarUrl } ${REACTION_GROUPS_FIELDS} } }903 reviews(first: ${GRAPHQL_PAGE_SIZE}) { nodes { id lastEditedAt author { __typename login avatarUrl } ${REACTION_GROUPS_FIELDS} } }
900 reviewRequests(first: 50) {904 reviewRequests(first: 50) {
901 nodes {905 nodes {
902 requestedReviewer {906 requestedReviewer {
...@@ -942,7 +946,7 @@ export const REVIEW_THREAD_COMMENTS_GRAPHQL_QUERY = `query($owner: String!, $nam...@@ -942,7 +946,7 @@ export const REVIEW_THREAD_COMMENTS_GRAPHQL_QUERY = `query($owner: String!, $nam
942 pullRequest { id }946 pullRequest { id }
943 comments(first: ${GRAPHQL_PAGE_SIZE}, after: $cursor) {947 comments(first: ${GRAPHQL_PAGE_SIZE}, after: $cursor) {
944 pageInfo { hasNextPage endCursor }948 pageInfo { hasNextPage endCursor }
945 nodes { id author { __typename login avatarUrl } body createdAt url ${REACTION_GROUPS_FIELDS} }949 nodes { id author { __typename login avatarUrl } body createdAt lastEditedAt url ${REACTION_GROUPS_FIELDS} }
946 }950 }
947 }951 }
948 }952 }
...@@ -1542,6 +1546,7 @@ function toComments(raw: {...@@ -1542,6 +1546,7 @@ function toComments(raw: {
1542 author: toActor(comment.author),1546 author: toActor(comment.author),
1543 body: comment.body ?? "",1547 body: comment.body ?? "",
1544 createdAt: comment.createdAt,1548 createdAt: comment.createdAt,
1549 editedAt: comment.lastEditedAt ?? null,
1545 url: trimmed(comment.url),1550 url: trimmed(comment.url),
1546 path: null,1551 path: null,
1547 reviewState: null,1552 reviewState: null,
...@@ -1566,6 +1571,7 @@ function toComments(raw: {...@@ -1566,6 +1571,7 @@ function toComments(raw: {
1566 author: toActor(review.author),1571 author: toActor(review.author),
1567 body: review.body ?? "",1572 body: review.body ?? "",
1568 createdAt: submittedAt,1573 createdAt: submittedAt,
1574 editedAt: review.lastEditedAt ?? null,
1569 url: trimmed(review.url),1575 url: trimmed(review.url),
1570 path: null,1576 path: null,
1571 reviewState,1577 reviewState,
...@@ -2098,6 +2104,7 @@ export interface GitHubReviewThreadComments {...@@ -2098,6 +2104,7 @@ export interface GitHubReviewThreadComments {
2098 readonly reactions: ReadonlyArray<PullRequestReaction>;2104 readonly reactions: ReadonlyArray<PullRequestReaction>;
2099 /** Reactions by node id, for the comments and reviews the `gh` JSON read carries no reaction on. */2105 /** Reactions by node id, for the comments and reviews the `gh` JSON read carries no reaction on. */
2100 readonly reactionsById: ReadonlyMap<string, ReadonlyArray<PullRequestReaction>>;2106 readonly reactionsById: ReadonlyMap<string, ReadonlyArray<PullRequestReaction>>;
2107 readonly editedAtById: ReadonlyMap<string, string>;
2101 /**2108 /**
2102 * Everyone on the review: those still asked and those who have already answered. Whoever has2109 * Everyone on the review: those still asked and those who have already answered. Whoever has
2103 * reviewed is no longer an outstanding request, so asking only for requests reports nobody on2110 * reviewed is no longer an outstanding request, so asking only for requests reports nobody on
...@@ -2147,6 +2154,7 @@ export interface GitHubReviewThreadPage {...@@ -2147,6 +2154,7 @@ export interface GitHubReviewThreadPage {
2147 * for without any. Only ids with a reaction are here; the rest carry none.2154 * for without any. Only ids with a reaction are here; the rest carry none.
2148 */2155 */
2149 readonly reactionsById: ReadonlyMap<string, ReadonlyArray<PullRequestReaction>>;2156 readonly reactionsById: ReadonlyMap<string, ReadonlyArray<PullRequestReaction>>;
2157 readonly editedAtById: ReadonlyMap<string, string>;
2150 readonly reviewers: ReadonlyArray<PullRequestActor>;2158 readonly reviewers: ReadonlyArray<PullRequestActor>;
2151 readonly avatarsByLogin: ReadonlyMap<string, string>;2159 readonly avatarsByLogin: ReadonlyMap<string, string>;
2152 readonly botLogins: ReadonlySet<string>;2160 readonly botLogins: ReadonlySet<string>;
...@@ -2177,6 +2185,7 @@ export function reviewThreadConversation(...@@ -2177,6 +2185,7 @@ export function reviewThreadConversation(
2177 author: comment.author,2185 author: comment.author,
2178 body: comment.body,2186 body: comment.body,
2179 createdAt: comment.createdAt,2187 createdAt: comment.createdAt,
2188 editedAt: comment.editedAt ?? null,
2180 url: comment.url,2189 url: comment.url,
2181 path: thread.path,2190 path: thread.path,
2182 reviewState: null,2191 reviewState: null,
...@@ -2276,6 +2285,7 @@ export function decodeReviewThreadsJson(...@@ -2276,6 +2285,7 @@ export function decodeReviewThreadsJson(
2276 author: toActor(comment.author),2285 author: toActor(comment.author),
2277 body: comment.body ?? "",2286 body: comment.body ?? "",
2278 createdAt: comment.createdAt,2287 createdAt: comment.createdAt,
2288 editedAt: comment.lastEditedAt ?? null,
2279 url: trimmed(comment.url),2289 url: trimmed(comment.url),
2280 reactions: toReactions(comment.reactionGroups, viewer),2290 reactions: toReactions(comment.reactionGroups, viewer),
2281 })),2291 })),
...@@ -2342,12 +2352,15 @@ export function decodeReviewThreadsJson(...@@ -2342,12 +2352,15 @@ export function decodeReviewThreadsJson(
2342 });2352 });
2343 }2353 }
2344 const reactionsById = new Map<string, ReadonlyArray<PullRequestReaction>>();2354 const reactionsById = new Map<string, ReadonlyArray<PullRequestReaction>>();
2355 const editedAtById = new Map<string, string>();
2345 for (const node of [2356 for (const node of [
2346 ...(pullRequest.comments?.nodes ?? []),2357 ...(pullRequest.comments?.nodes ?? []),
2347 ...(pullRequest.reviews?.nodes ?? []),2358 ...(pullRequest.reviews?.nodes ?? []),
2348 ]) {2359 ]) {
2349 const id = trimmed(node.id);2360 const id = trimmed(node.id);
2350 if (id === null) continue;2361 if (id === null) continue;
2362 const editedAt = trimmed(node.lastEditedAt);
2363 if (editedAt !== null) editedAtById.set(id, editedAt);
2351 const reactions = toReactions(node.reactionGroups, viewer);2364 const reactions = toReactions(node.reactionGroups, viewer);
2352 if (reactions.length > 0) reactionsById.set(id, reactions);2365 if (reactions.length > 0) reactionsById.set(id, reactions);
2353 }2366 }
...@@ -2356,6 +2369,7 @@ export function decodeReviewThreadsJson(...@@ -2356,6 +2369,7 @@ export function decodeReviewThreadsJson(
2356 nextCursor: nextCursorOf(threads.pageInfo),2369 nextCursor: nextCursorOf(threads.pageInfo),
2357 reactions: toReactions(pullRequest.reactionGroups, viewer),2370 reactions: toReactions(pullRequest.reactionGroups, viewer),
2358 reactionsById,2371 reactionsById,
2372 editedAtById,
2359 reviewers: [...reviewers.values()],2373 reviewers: [...reviewers.values()],
2360 avatarsByLogin,2374 avatarsByLogin,
2361 botLogins,2375 botLogins,
...@@ -2392,6 +2406,7 @@ export function decodeReviewThreadCommentsJson(raw: string): Result.Result<...@@ -2392,6 +2406,7 @@ export function decodeReviewThreadCommentsJson(raw: string): Result.Result<
2392 author: toActor(comment.author),2406 author: toActor(comment.author),
2393 body: comment.body ?? "",2407 body: comment.body ?? "",
2394 createdAt: comment.createdAt,2408 createdAt: comment.createdAt,
2409 editedAt: comment.lastEditedAt ?? null,
2395 url: trimmed(comment.url),2410 url: trimmed(comment.url),
2396 reactions: toReactions(comment.reactionGroups, viewer),2411 reactions: toReactions(comment.reactionGroups, viewer),
2397 })),2412 })),
packages/contracts/src/pullRequest.ts+2
...@@ -203,6 +203,7 @@ export const PullRequestComment = Schema.Struct({...@@ -203,6 +203,7 @@ export const PullRequestComment = Schema.Struct({
203 author: Schema.NullOr(PullRequestActor),203 author: Schema.NullOr(PullRequestActor),
204 body: Schema.String,204 body: Schema.String,
205 createdAt: IsoDateTime,205 createdAt: IsoDateTime,
206 editedAt: Schema.optional(Schema.NullOr(IsoDateTime)),
206 url: Schema.NullOr(Schema.String),207 url: Schema.NullOr(Schema.String),
207 path: Schema.NullOr(Schema.String),208 path: Schema.NullOr(Schema.String),
208 reviewState: Schema.NullOr(Schema.String),209 reviewState: Schema.NullOr(Schema.String),
...@@ -228,6 +229,7 @@ export const PullRequestThreadComment = Schema.Struct({...@@ -228,6 +229,7 @@ export const PullRequestThreadComment = Schema.Struct({
228 author: Schema.NullOr(PullRequestActor),229 author: Schema.NullOr(PullRequestActor),
229 body: Schema.String,230 body: Schema.String,
230 createdAt: IsoDateTime,231 createdAt: IsoDateTime,
232 editedAt: Schema.optional(Schema.NullOr(IsoDateTime)),
231 url: Schema.NullOr(Schema.String),233 url: Schema.NullOr(Schema.String),
232 reactions: Schema.optional(Schema.Array(PullRequestReaction)),234 reactions: Schema.optional(Schema.Array(PullRequestReaction)),
233});235});