@dev.fast/whiteboard 0.3.4 → 0.3.5-preview.20261021.131

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.
Files changed (120) hide show
  1. package/dist/{account-alias-CprZojXd.js → account-alias-CisiYUZT.js} +13 -8
  2. package/dist/agent-cli-BglNRDZN.js +2 -0
  3. package/dist/{agent-cli-CThN7Q59.js → agent-cli-TJ7_5_B3.js} +15 -10
  4. package/dist/{agent-client-BOOi7EUR.js → agent-client-H53ajvrm.js} +13 -3
  5. package/dist/{authoring-tools-BNwuMnz6.js → authoring-tools-BCquUsB3.js} +34 -27
  6. package/dist/{background-server-DLSDogkI.js → background-server-BSYXD9au.js} +1 -1
  7. package/dist/{background-server-CkvxA-6O.js → background-server-Xom8cA_u.js} +5 -5
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-R4yfa5DT.js → cli-BOnV4cCh.js} +1 -1
  10. package/dist/{cli-runner-Cuc9FSzu.js → cli-runner-cRY5NOno.js} +70 -34
  11. package/dist/cli.js +4 -3
  12. package/dist/{client-BN_FDxFr.js → client-UH1bEG7Z.js} +260 -49
  13. package/dist/{fuzzy-match-BO9bK37N.js → fuzzy-match-D_x3wumC.js} +6 -4
  14. package/dist/{headless-host-CVBOQyRS.js → headless-host-BEe-aFtB.js} +1 -1
  15. package/dist/{headless-host-Cog6SpS9.js → headless-host-CEb5d-A0.js} +563 -175
  16. package/dist/lens-selection-BxPk53c8.js +214 -0
  17. package/dist/{local-data-D_9tvdsp.js → local-data-mbCb1HDH.js} +2448 -347
  18. package/dist/{mcp-D1hziANa.js → mcp-cBtzXsmN.js} +9 -12
  19. package/dist/{package-paths-C--__jEv.js → package-paths-BkeQsdaw.js} +1 -1
  20. package/dist/profile-Dbyidrcr.js +2 -0
  21. package/dist/{profile-DokFw782.js → profile-duqp5uPa.js} +20 -8
  22. package/dist/{remote-attach-CEJ-kvDY.js → remote-attach-BeKuMm_x.js} +4 -4
  23. package/dist/{remote-extensions-GN8Gf_qG.js → remote-extensions-CX2fAec1.js} +1 -1
  24. package/dist/{remote-extensions-Blcjr9Ze.js → remote-extensions-CYWNTVSf.js} +3 -3
  25. package/dist/{remote-language-server-BTKBQ7x4.js → remote-language-server-DLWluAeR.js} +5 -5
  26. package/dist/{remote-uninstall-DW7Gdryh.js → remote-uninstall-Y3S0YmJs.js} +6 -6
  27. package/dist/{request-origin-Zcn_CjX4.js → request-origin-icXPv6jh.js} +55 -18
  28. package/dist/{review-app-launcher-CkwLclzR.js → review-app-launcher-DZvG5ZAh.js} +2 -2
  29. package/dist/{review-home-paths-j2BRAaNv.js → review-home-paths-BwH08ihj.js} +19 -17
  30. package/dist/{review-telemetry-BLNwYnLV.js → review-telemetry-AZ0imsmJ.js} +6 -6
  31. package/dist/runtime-CF4JXdb8.js +27 -0
  32. package/dist/runtime.d.ts +1 -1
  33. package/dist/runtime.js +1 -1
  34. package/dist/{s3-DtJFWC0G.js → s3-CsG2EWED.js} +1 -1
  35. package/dist/{s3-config-BTO12tCC.js → s3-config-CjcLyFV5.js} +4 -2
  36. package/dist/{s3-config-CSy3fx8H.js → s3-config-zz4GWh2T.js} +1 -1
  37. package/dist/server/desktop-host.js +28 -104
  38. package/dist/{server-discovery-Bey8IqXH.js → server-discovery-DeVTdcqK.js} +3 -3
  39. package/dist/sharing/index.d.ts +484 -74
  40. package/dist/sharing/index.js +1 -1
  41. package/dist/{src-DsM3jO8G.js → src-DYZLCUP_.js} +98 -18
  42. package/dist/{src-BMPuxWv4.js → src-DtfSndqS.js} +269 -227
  43. package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-B29lve9-.js} +318 -271
  44. package/dist/{thread-state-5pYr6_zM.js → thread-state-BPpvKFwQ.js} +12 -2
  45. package/dist/{tool-failure-C9MpbXJJ.js → tool-failure-CQhyLht3.js} +10 -3
  46. package/instructions/authoring.md +20 -5
  47. package/package.json +4 -4
  48. package/src/agent-selection.ts +6 -0
  49. package/src/ask/agents.ts +13 -1
  50. package/src/ask/checkout-files.ts +2 -5
  51. package/src/ask/thread-state.ts +4 -0
  52. package/src/ask/thread.ts +61 -31
  53. package/src/ask/threads.ts +24 -3
  54. package/src/cli-install.ts +13 -2
  55. package/src/cli-runner.ts +4 -1
  56. package/src/cli.ts +4 -1
  57. package/src/diff-selection-migration.ts +48 -34
  58. package/src/heading-anchors.ts +42 -0
  59. package/src/lens-selection.ts +38 -2
  60. package/src/markdown.ts +42 -0
  61. package/src/posthog-capture-client.ts +1 -1
  62. package/src/review-api/README.md +52 -13
  63. package/src/review-api/activity.ts +1 -4
  64. package/src/review-api/agent-cli.ts +9 -1
  65. package/src/review-api/anchor-quotes.ts +14 -7
  66. package/src/review-api/ask-history.ts +118 -51
  67. package/src/review-api/authoring-tools.ts +45 -30
  68. package/src/review-api/azure-devops-provider.ts +2 -20
  69. package/src/review-api/azure-remote.ts +21 -0
  70. package/src/review-api/blocks/definition.ts +1 -1
  71. package/src/review-api/blocks/markdown.ts +2 -2
  72. package/src/review-api/board-order.ts +144 -0
  73. package/src/review-api/checkout-groups.ts +586 -0
  74. package/src/review-api/document-headings.ts +58 -27
  75. package/src/review-api/document-text.ts +31 -2
  76. package/src/review-api/document.ts +362 -67
  77. package/src/review-api/foreign-references.ts +328 -0
  78. package/src/review-api/github-provider.ts +51 -13
  79. package/src/review-api/http.ts +734 -135
  80. package/src/review-api/instructions.ts +0 -19
  81. package/src/review-api/link-preview.ts +113 -0
  82. package/src/review-api/local-data.ts +310 -46
  83. package/src/review-api/mcp.ts +7 -13
  84. package/src/review-api/onboarding.ts +1 -1
  85. package/src/review-api/profile.ts +37 -6
  86. package/src/review-api/public-tools.ts +24 -2
  87. package/src/review-api/pull-request.ts +82 -49
  88. package/src/review-api/request-origin.ts +10 -0
  89. package/src/review-api/review-progress.ts +61 -27
  90. package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
  91. package/src/review-api/store-migrations.ts +355 -0
  92. package/src/review-api/store-schema.ts +38 -29
  93. package/src/review-api/store.ts +2176 -495
  94. package/src/review-api/worktree-events.ts +3 -5
  95. package/src/review-api/worktree-source.ts +3 -7
  96. package/src/review-api/worktree-structural.integration.ts +72 -0
  97. package/src/review-stack.ts +15 -23
  98. package/src/runtime.ts +4 -30
  99. package/src/server/desktop-host.ts +6 -1
  100. package/src/server/desktop-server.ts +1 -36
  101. package/src/server/headless-host.ts +0 -2
  102. package/src/server/hono-http.ts +2 -1
  103. package/src/server/review-gateway-list.ts +3 -5
  104. package/src/server/review-gateway-memory.ts +1 -1
  105. package/src/server/review-lifecycle-telemetry.ts +2 -2
  106. package/src/server/review-server-core.ts +1 -5
  107. package/src/server/structural-comparisons.ts +47 -0
  108. package/src/sharing/export.ts +36 -7
  109. package/src/sharing/host.ts +34 -14
  110. package/src/sharing/import.ts +156 -24
  111. package/src/sharing/repository.ts +143 -24
  112. package/src/startup-trace.ts +3 -3
  113. package/src/stored-document-migration.ts +110 -19
  114. package/src/ui-telemetry-events.ts +44 -18
  115. package/dist/agent-cli-C6SPSII0.js +0 -2
  116. package/dist/profile-CYPmvBoA.js +0 -2
  117. package/dist/runtime-CKiWhGrf.js +0 -52
  118. package/instructions/scratchpad.md +0 -23
  119. package/src/review-preferences.ts +0 -88
  120. package/src/server/legacy-tutorial-cleanup.ts +0 -38
@@ -0,0 +1,328 @@
1
+ import path from "node:path";
2
+
3
+ import { type JsonValue, isJsonObject, isStringValue } from "@dev.fast/json";
4
+ import { parseAnchor } from "@review/lens-selection.js";
5
+ import {
6
+ markdownNodes,
7
+ markdownText,
8
+ parseMarkdown,
9
+ } from "@review/markdown.js";
10
+
11
+ /** A file's text at a commit; undefined when it can't be read. */
12
+ export type ReadAtCommit = (
13
+ repositoryId: string,
14
+ commit: string,
15
+ file: string,
16
+ ) => string | undefined;
17
+
18
+ const LANGUAGES = new Map([
19
+ [".ts", "typescript"],
20
+ [".tsx", "tsx"],
21
+ [".js", "javascript"],
22
+ [".jsx", "jsx"],
23
+ [".mjs", "javascript"],
24
+ [".rs", "rust"],
25
+ [".go", "go"],
26
+ [".py", "python"],
27
+ [".rb", "ruby"],
28
+ [".java", "java"],
29
+ [".kt", "kotlin"],
30
+ [".swift", "swift"],
31
+ [".c", "c"],
32
+ [".h", "c"],
33
+ [".cc", "cpp"],
34
+ [".cpp", "cpp"],
35
+ [".css", "css"],
36
+ [".html", "html"],
37
+ [".json", "json"],
38
+ [".md", "markdown"],
39
+ [".sh", "bash"],
40
+ [".sql", "sql"],
41
+ [".toml", "toml"],
42
+ [".yaml", "yaml"],
43
+ [".yml", "yaml"],
44
+ ]);
45
+
46
+ /** A source link in Markdown, in either spelling. */
47
+ const SOURCE_LINK = /^(?:whiteboard:source\/|review-source:)/i;
48
+
49
+ /**
50
+ * Migrates the old global scratchpad, which quoted several repositories.
51
+ * A board may only quote its own, so each quote of another repository is
52
+ * replaced:
53
+ * - a code peek becomes a `code` block of those lines;
54
+ * - a source link becomes its text, then what it pointed at in code;
55
+ * - a diagram attachment leaves the diagram and its lines follow it as
56
+ * a `code` block;
57
+ * - a step keeps its label and takes the lines as its code;
58
+ * - a call stack or database lens becomes a line saying what it was,
59
+ * then a `code` block for each range it quoted, in any repository; the
60
+ * first takes its id. A lens that quotes only its own repository is
61
+ * kept.
62
+ * New blocks take ids from `allocate`.
63
+ */
64
+ export function snapshotForeignReferences(
65
+ document: JsonValue[],
66
+ own: string,
67
+ read: ReadAtCommit,
68
+ repositoryName: (repositoryId: string) => string,
69
+ allocate: (prefix: string) => string,
70
+ ): JsonValue[] {
71
+ const foreign = (pins: JsonValue | undefined) =>
72
+ isJsonObject(pins) &&
73
+ isStringValue(pins.repositoryId) &&
74
+ pins.repositoryId !== own
75
+ ? pins
76
+ : undefined;
77
+
78
+ const caption = (
79
+ pins: { repositoryId: string; head: string; base?: string },
80
+ anchor: string,
81
+ ) => {
82
+ const selection = parseAnchor(anchor);
83
+
84
+ const commit =
85
+ selection?.start.side === "base" ? (pins.base ?? pins.head) : pins.head;
86
+
87
+ const range = selection
88
+ ? `${selection.file}:${selection.start.line}-${selection.end.line}`
89
+ : anchor;
90
+
91
+ return `${repositoryName(pins.repositoryId)} · ${range} @ ${commit.slice(0, 7)}`;
92
+ };
93
+
94
+ const snapshot = (
95
+ id: JsonValue | undefined,
96
+ pins: { repositoryId: string; head: string; base?: string },
97
+ anchor: string,
98
+ ): JsonValue => {
99
+ const selection = parseAnchor(anchor);
100
+
101
+ const commit =
102
+ selection?.start.side === "base" ? (pins.base ?? pins.head) : pins.head;
103
+
104
+ const file = selection
105
+ ? read(pins.repositoryId, commit, selection.file)
106
+ : undefined;
107
+
108
+ const label = caption(pins, anchor);
109
+
110
+ const text =
111
+ file !== undefined && selection
112
+ ? file
113
+ .split("\n")
114
+ .slice(selection.start.line - 1, selection.end.line)
115
+ .join("\n")
116
+ : `${label}\n(source unavailable)`;
117
+
118
+ return {
119
+ type: "code",
120
+ ...(isStringValue(id) && { id }),
121
+ language: selection
122
+ ? (LANGUAGES.get(path.extname(selection.file).toLowerCase()) ?? "text")
123
+ : "text",
124
+ text,
125
+ caption: label,
126
+ };
127
+ };
128
+
129
+ const pinsIn = (pins: JsonValue | undefined) => {
130
+ return isJsonObject(pins) &&
131
+ isStringValue(pins.repositoryId) &&
132
+ isStringValue(pins.head)
133
+ ? {
134
+ repositoryId: pins.repositoryId,
135
+ head: pins.head,
136
+ ...(isStringValue(pins.base) && { base: pins.base }),
137
+ }
138
+ : undefined;
139
+ };
140
+
141
+ const pinsOf = (value: JsonValue | undefined) => pinsIn(foreign(value));
142
+
143
+ const peeks = (blocks: JsonValue[]): JsonValue[] =>
144
+ blocks.map((block) => {
145
+ if (!isJsonObject(block)) return block;
146
+
147
+ if (Array.isArray(block.children))
148
+ return { ...block, children: peeks(block.children) };
149
+
150
+ const pins = pinsOf(block.pins);
151
+
152
+ if (block.type === "code_peek" && pins && isStringValue(block.source))
153
+ return snapshot(block.id, pins, block.source);
154
+
155
+ return block;
156
+ });
157
+
158
+ const rest = (blocks: JsonValue[]): JsonValue[] =>
159
+ blocks.flatMap((block): JsonValue | JsonValue[] => {
160
+ if (!isJsonObject(block)) return block;
161
+
162
+ if (Array.isArray(block.children))
163
+ return { ...block, children: rest(block.children) };
164
+
165
+ const blockPins = pinsOf(block.pins);
166
+
167
+ if (
168
+ block.type === "markdown" &&
169
+ blockPins &&
170
+ isStringValue(block.markdown)
171
+ ) {
172
+ const { pins: _pins, ...kept } = block;
173
+
174
+ return {
175
+ ...kept,
176
+ markdown: unlinkSourceLinks(block.markdown, blockPins),
177
+ };
178
+ }
179
+
180
+ if (block.type === "sequence" && Array.isArray(block.steps))
181
+ return {
182
+ ...block,
183
+ steps: block.steps.map((step) => {
184
+ if (!isJsonObject(step) || !isStringValue(step.source)) return step;
185
+ const pins = pinsOf(step.pins ?? block.pins);
186
+
187
+ if (!pins) return step;
188
+ const { source, pins: _pins, ...kept } = step;
189
+ const quoted = snapshot(undefined, pins, source);
190
+
191
+ return isJsonObject(quoted)
192
+ ? {
193
+ ...kept,
194
+ code: { language: quoted.language, text: quoted.text },
195
+ }
196
+ : kept;
197
+ }),
198
+ };
199
+
200
+ if (block.type === "flow_diagram" && Array.isArray(block.nodes)) {
201
+ const moved: JsonValue[] = [];
202
+
203
+ const diagram = {
204
+ ...block,
205
+ nodes: block.nodes.map((node) =>
206
+ isJsonObject(node) && Array.isArray(node.attachments)
207
+ ? {
208
+ ...node,
209
+ attachments: node.attachments.filter((attachment) => {
210
+ if (
211
+ !isJsonObject(attachment) ||
212
+ !pinsOf(attachment.pins ?? block.pins)
213
+ )
214
+ return true;
215
+
216
+ // Its lines stay on the board, after the diagram.
217
+ for (const [pins, anchor] of quotes(
218
+ attachment,
219
+ pinsIn,
220
+ block.pins,
221
+ ))
222
+ moved.push(snapshot(allocate("block"), pins, anchor));
223
+
224
+ return false;
225
+ }),
226
+ }
227
+ : node,
228
+ ),
229
+ };
230
+
231
+ return [diagram, ...moved];
232
+ }
233
+
234
+ if (block.type === "call_stack_diff" || block.type === "database_lens") {
235
+ const foreignQuote = quotes(block, pinsOf)[0];
236
+
237
+ if (!foreignQuote) return block;
238
+
239
+ const what =
240
+ block.type === "call_stack_diff" ? "Call stack" : "Database lens";
241
+
242
+ const title = isStringValue(block.title) ? ` “${block.title}”` : "";
243
+
244
+ // Its own repository's ranges too, so none of it is lost.
245
+ return [
246
+ {
247
+ type: "markdown",
248
+ id: allocate("block"),
249
+ markdown: `${what}${title} (from ${repositoryName(foreignQuote[0].repositoryId)}), kept as snapshots below.`,
250
+ },
251
+ ...quotes(block, pinsIn).map(([pins, anchor], index) =>
252
+ snapshot(index === 0 ? block.id : allocate("block"), pins, anchor),
253
+ ),
254
+ ];
255
+ }
256
+
257
+ return block;
258
+ });
259
+
260
+ /** The ranges a block's frames or operations quote at the pins `read`
261
+ * accepts. */
262
+ function quotes(
263
+ block: JsonValue,
264
+ read: typeof pinsIn,
265
+ inherited?: JsonValue,
266
+ ): [{ repositoryId: string; head: string; base?: string }, string][] {
267
+ if (Array.isArray(block))
268
+ return block.flatMap((item) => quotes(item, read, inherited));
269
+
270
+ if (!isJsonObject(block)) return [];
271
+ const scope = block.pins ?? inherited;
272
+ const pins = read(scope);
273
+
274
+ const own: [
275
+ { repositoryId: string; head: string; base?: string },
276
+ string,
277
+ ][] = pins
278
+ ? [
279
+ block.source,
280
+ block.callSite,
281
+ ...(Array.isArray(block.contextSources) ? block.contextSources : []),
282
+ ...(Array.isArray(block.sources) ? block.sources : []),
283
+ ].flatMap((anchor): [typeof pins, string][] =>
284
+ isStringValue(anchor) ? [[pins, anchor]] : [],
285
+ )
286
+ : [];
287
+
288
+ return [
289
+ ...own,
290
+ ...Object.entries(block)
291
+ .filter(([key]) => key !== "pins")
292
+ .flatMap(([, value]) =>
293
+ Array.isArray(value) || isJsonObject(value)
294
+ ? quotes(value, read, scope)
295
+ : [],
296
+ ),
297
+ ];
298
+ }
299
+
300
+ function unlinkSourceLinks(
301
+ markdown: string,
302
+ pins: { repositoryId: string; head: string; base?: string },
303
+ ) {
304
+ let result = markdown;
305
+
306
+ // Rewrite from the end so earlier offsets stay valid.
307
+ const links = [...markdownNodes(parseMarkdown(markdown))]
308
+ .filter(
309
+ (node) =>
310
+ node.type === "link" &&
311
+ SOURCE_LINK.test(node.url ?? "") &&
312
+ node.position?.start.offset !== undefined &&
313
+ node.position.end.offset !== undefined,
314
+ )
315
+ .reverse();
316
+
317
+ // Its text, then where it pointed, so neither is lost.
318
+ for (const node of links)
319
+ result =
320
+ result.slice(0, node.position!.start.offset) +
321
+ `${markdownText(node)} (\`${caption(pins, node.url!.replace(SOURCE_LINK, ""))}\`)` +
322
+ result.slice(node.position!.end.offset);
323
+
324
+ return result;
325
+ }
326
+
327
+ return rest(peeks(document));
328
+ }
@@ -1,10 +1,11 @@
1
1
  import { parseGitRemote } from "@dev.fast/local-vcs";
2
+ import type { PullRequestState } from "@dev.fast/review-protocol";
2
3
  import { errorMessage } from "@dev.fast/trace-core";
3
4
  import { z } from "zod";
4
5
 
5
6
  import { ReviewInputError } from "./document.js";
6
7
  import type { PullRequestProvider } from "./provider.js";
7
- import { firstLine } from "./pull-request.js";
8
+ import { GitHubRefusedRead, firstLine } from "./pull-request.js";
8
9
 
9
10
  const GH_TIMEOUT_MS = 20_000;
10
11
 
@@ -20,8 +21,24 @@ const metadataSchema = z.object({
20
21
  baseRefOid: z.string().optional(),
21
22
  headRefName: z.string().min(1),
22
23
  isCrossRepository: z.boolean(),
24
+ state: z.enum(["OPEN", "CLOSED", "MERGED"]).optional(),
25
+ isDraft: z.boolean().optional(),
23
26
  });
24
27
 
28
+ /** gh's state and draft flag as one state. */
29
+ function ghState(
30
+ state: "OPEN" | "CLOSED" | "MERGED" | undefined,
31
+ isDraft: boolean | undefined,
32
+ ): PullRequestState | undefined {
33
+ if (state === "MERGED") return "merged";
34
+
35
+ if (state === "CLOSED") return "closed";
36
+
37
+ if (state === "OPEN") return isDraft ? "draft" : "open";
38
+
39
+ return undefined;
40
+ }
41
+
25
42
  function unreadable(url: string, ghFailure: string, fallback: string) {
26
43
  return new ReviewInputError(
27
44
  `Could not read ${url}: gh failed (${ghFailure}) and ${fallback}. Run \`gh auth status\` and sign in with an account that can read the repository, then retry.`,
@@ -54,24 +71,27 @@ export const githubProvider: PullRequestProvider = {
54
71
  [
55
72
  "pr",
56
73
  "view",
57
- String(number),
74
+ { value: String(number) },
58
75
  "--repo",
59
- `${host}/${slug}`,
76
+ { value: `${host}/${slug}` },
60
77
  "--json",
61
- "number,title,baseRefName,baseRefOid,headRefName,isCrossRepository",
78
+ "number,title,baseRefName,baseRefOid,headRefName,isCrossRepository,state,isDraft",
62
79
  ],
63
80
  { timeoutMs: GH_TIMEOUT_MS },
64
81
  );
65
82
 
66
- const { headRefName, isCrossRepository, ...metadata } =
83
+ const { headRefName, isCrossRepository, state, isDraft, ...metadata } =
67
84
  metadataSchema.parse(JSON.parse(stdout));
68
85
 
86
+ const pullRequestState = ghState(state, isDraft);
87
+
69
88
  return {
70
89
  host,
71
90
  slug,
72
91
  headRef,
73
92
  ...metadata,
74
93
  headBranch: isCrossRepository ? undefined : headRefName,
94
+ ...(pullRequestState && { state: pullRequestState }),
75
95
  };
76
96
  } catch (error) {
77
97
  ghFailure = firstLine(errorMessage(error));
@@ -110,15 +130,21 @@ export const githubProvider: PullRequestProvider = {
110
130
  404,
111
131
  );
112
132
 
113
- if (response.status === 403 || response.status === 429)
114
- throw unreadable(
115
- url,
116
- ghFailure,
117
- response.headers.get("x-ratelimit-remaining") === "0" ||
118
- response.status === 429
119
- ? "the unauthenticated GitHub API rate limit is exhausted"
120
- : "the GitHub API refused the request",
133
+ if (response.status === 403 || response.status === 429) {
134
+ const reset = Number(response.headers.get("x-ratelimit-reset"));
135
+
136
+ throw new GitHubRefusedRead(
137
+ unreadable(
138
+ url,
139
+ ghFailure,
140
+ response.headers.get("x-ratelimit-remaining") === "0" ||
141
+ response.status === 429
142
+ ? "the unauthenticated GitHub API rate limit is exhausted"
143
+ : "the GitHub API refused the request",
144
+ ).message,
145
+ reset > 0 ? reset * 1000 : Date.now() + 15 * 60_000,
121
146
  );
147
+ }
122
148
 
123
149
  if (!response.ok)
124
150
  throw unreadable(
@@ -140,6 +166,9 @@ export const githubProvider: PullRequestProvider = {
140
166
  ref: z.string().min(1),
141
167
  repo: z.object({ id: z.number() }).nullable(),
142
168
  }),
169
+ state: z.enum(["open", "closed"]).optional(),
170
+ draft: z.boolean().optional(),
171
+ merged_at: z.string().nullable().optional(),
143
172
  })
144
173
  .safeParse(await response.json().catch(() => undefined));
145
174
 
@@ -158,6 +187,15 @@ export const githubProvider: PullRequestProvider = {
158
187
  parsed.data.head.repo?.id === parsed.data.base.repo.id
159
188
  ? parsed.data.head.ref
160
189
  : undefined,
190
+ ...(parsed.data.state && {
191
+ state: parsed.data.merged_at
192
+ ? "merged"
193
+ : parsed.data.state === "closed"
194
+ ? "closed"
195
+ : parsed.data.draft
196
+ ? "draft"
197
+ : "open",
198
+ }),
161
199
  };
162
200
  },
163
201