@dev.fast/whiteboard 0.0.0-stage → 0.2.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.
Files changed (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/THIRD_PARTY_NOTICES.md +93 -0
  4. package/dist/account-alias-DOHN1RSH.js +973 -0
  5. package/dist/agent-cli-ChOq0ZuO.js +117 -0
  6. package/dist/agent-cli-DzWfZr6G.js +2 -0
  7. package/dist/agent-client-CPQo7iTI.js +305 -0
  8. package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
  9. package/dist/build-info.json +1 -0
  10. package/dist/cli-hVgHcdsv.js +47 -0
  11. package/dist/cli-runner-D8y2_luU.js +5095 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +128 -0
  14. package/dist/client-DFoh50W3.js +950 -0
  15. package/dist/desktop-discovery-DBO-PM2V.js +164 -0
  16. package/dist/error-message-OtiDonty.js +6 -0
  17. package/dist/fs-utils-BMPLt0cr.js +22 -0
  18. package/dist/fuzzy-match-BoAmcyak.js +30 -0
  19. package/dist/headless-host-BJxf4dUU.js +94 -0
  20. package/dist/input-error-OLgB_h31.js +11 -0
  21. package/dist/local-data-CxLk75rx.js +4491 -0
  22. package/dist/mcp-QeF8vkdf.js +140 -0
  23. package/dist/package-paths-B6-zxvIO.js +47 -0
  24. package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
  25. package/dist/profile-4ry2f3AM.js +76 -0
  26. package/dist/profile-Bh5FIrmf.js +2 -0
  27. package/dist/request-origin-D_4QIdoN.js +811 -0
  28. package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
  29. package/dist/review-home-paths-6zZH-1c9.js +2560 -0
  30. package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
  31. package/dist/runtime-CKiWhGrf.js +52 -0
  32. package/dist/runtime.d.ts +9 -0
  33. package/dist/runtime.js +2 -0
  34. package/dist/s3-SzLpeCG6.js +345 -0
  35. package/dist/s3-config-BgxkSoOy.js +2 -0
  36. package/dist/s3-config-CAxhO9u_.js +676 -0
  37. package/dist/server/desktop-host.d.ts +4 -0
  38. package/dist/server/desktop-host.js +1702 -0
  39. package/dist/server-discovery-Dbzn3w6Z.js +66 -0
  40. package/dist/sharing/index.d.ts +1977 -0
  41. package/dist/sharing/index.js +2 -0
  42. package/dist/src-CGP5ytbV.js +283 -0
  43. package/dist/src-CmdiBL20.js +3676 -0
  44. package/dist/src-DnwdaQ2r.js +865 -0
  45. package/dist/src-X9phtB2j.js +68 -0
  46. package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
  47. package/dist/tool-failure-C8zB73HV.js +98 -0
  48. package/dist/tutorial-trace-DrBvkaeL.js +43 -0
  49. package/instructions/authoring.md +36 -0
  50. package/instructions/file-lenses.md +13 -0
  51. package/instructions/scratchpad.md +23 -0
  52. package/instructions/trace-archaeology.md +113 -0
  53. package/onboarding.md +8 -0
  54. package/package.json +106 -3
  55. package/src/agent-selection.ts +100 -0
  56. package/src/agent-session-ref.ts +80 -0
  57. package/src/ask/agents.ts +351 -0
  58. package/src/ask/checkout-files.ts +47 -0
  59. package/src/ask/file-refs.ts +111 -0
  60. package/src/ask/pi-mcp.ts +154 -0
  61. package/src/ask/protocol.ts +244 -0
  62. package/src/ask/thread-state.ts +350 -0
  63. package/src/ask/thread.ts +1400 -0
  64. package/src/ask/threads.ts +219 -0
  65. package/src/ask/watch.ts +79 -0
  66. package/src/cli-install.ts +1041 -0
  67. package/src/cli-runner.ts +1461 -0
  68. package/src/cli-runtime-info.ts +35 -0
  69. package/src/cli.ts +222 -0
  70. package/src/connect-prompts.ts +248 -0
  71. package/src/cursor-deeplink.ts +11 -0
  72. package/src/desktop-discovery.ts +355 -0
  73. package/src/diff-selection-migration.ts +63 -0
  74. package/src/embedded-posthog-key.ts +6 -0
  75. package/src/error-telemetry.ts +247 -0
  76. package/src/evidence.ts +14 -0
  77. package/src/exception-telemetry.ts +126 -0
  78. package/src/fixtures/blocks/call_stack_diff.json +27 -0
  79. package/src/fixtures/blocks/callout.json +11 -0
  80. package/src/fixtures/blocks/code.json +9 -0
  81. package/src/fixtures/blocks/code_peek.json +8 -0
  82. package/src/fixtures/blocks/database_lens.json +51 -0
  83. package/src/fixtures/blocks/divider.json +1 -0
  84. package/src/fixtures/blocks/fixtures.ts +33 -0
  85. package/src/fixtures/blocks/flow_diagram.json +36 -0
  86. package/src/fixtures/blocks/ids.ts +8 -0
  87. package/src/fixtures/blocks/image.json +9 -0
  88. package/src/fixtures/blocks/markdown.json +7 -0
  89. package/src/fixtures/blocks/section.json +11 -0
  90. package/src/fixtures/blocks/sequence.json +31 -0
  91. package/src/fixtures/blocks/software_map.json +7 -0
  92. package/src/fixtures/blocks/trace_quote.json +9 -0
  93. package/src/fixtures/blocks/tutorial.json +51 -0
  94. package/src/fs-utils.ts +32 -0
  95. package/src/fuzzy-match.ts +94 -0
  96. package/src/install.ts +29 -0
  97. package/src/legacy-skills.ts +133 -0
  98. package/src/lens-selection.ts +230 -0
  99. package/src/markdown-latex-math.ts +230 -0
  100. package/src/markdown.ts +81 -0
  101. package/src/package-paths.ts +53 -0
  102. package/src/posthog-capture-client.ts +610 -0
  103. package/src/review-api/README.md +295 -0
  104. package/src/review-api/activity.ts +340 -0
  105. package/src/review-api/agent-cli.ts +219 -0
  106. package/src/review-api/agent-client.ts +188 -0
  107. package/src/review-api/anchor-quotes.ts +88 -0
  108. package/src/review-api/ask-history.ts +242 -0
  109. package/src/review-api/authoring-tools.ts +211 -0
  110. package/src/review-api/blocks/call_stack_diff.ts +75 -0
  111. package/src/review-api/blocks/callout.ts +24 -0
  112. package/src/review-api/blocks/code.ts +9 -0
  113. package/src/review-api/blocks/code_peek.ts +13 -0
  114. package/src/review-api/blocks/database_lens.ts +150 -0
  115. package/src/review-api/blocks/definition.ts +40 -0
  116. package/src/review-api/blocks/divider.ts +6 -0
  117. package/src/review-api/blocks/flow_diagram.ts +111 -0
  118. package/src/review-api/blocks/image.ts +10 -0
  119. package/src/review-api/blocks/index.ts +90 -0
  120. package/src/review-api/blocks/markdown.ts +17 -0
  121. package/src/review-api/blocks/section.ts +24 -0
  122. package/src/review-api/blocks/sequence.ts +56 -0
  123. package/src/review-api/blocks/software_map.ts +9 -0
  124. package/src/review-api/blocks/trace_quote.ts +10 -0
  125. package/src/review-api/blocks/tutorial.ts +39 -0
  126. package/src/review-api/checkout-fs.ts +14 -0
  127. package/src/review-api/client.ts +1 -0
  128. package/src/review-api/comparison-coverage.ts +304 -0
  129. package/src/review-api/component-reference.ts +30 -0
  130. package/src/review-api/diff-lenses.ts +175 -0
  131. package/src/review-api/document-headings.ts +51 -0
  132. package/src/review-api/document-text.ts +200 -0
  133. package/src/review-api/document.ts +906 -0
  134. package/src/review-api/file-lenses.ts +100 -0
  135. package/src/review-api/http.ts +1877 -0
  136. package/src/review-api/image-decode.ts +29 -0
  137. package/src/review-api/input-error.ts +9 -0
  138. package/src/review-api/instructions.ts +89 -0
  139. package/src/review-api/lens-alignment.ts +52 -0
  140. package/src/review-api/local-data.ts +1837 -0
  141. package/src/review-api/map-input.ts +154 -0
  142. package/src/review-api/mcp-client-agent.ts +32 -0
  143. package/src/review-api/mcp.ts +240 -0
  144. package/src/review-api/origin.ts +43 -0
  145. package/src/review-api/profile.ts +146 -0
  146. package/src/review-api/public-tools.ts +102 -0
  147. package/src/review-api/pull-request.ts +389 -0
  148. package/src/review-api/read-schemas.ts +85 -0
  149. package/src/review-api/recovery.ts +2 -0
  150. package/src/review-api/request-origin.ts +33 -0
  151. package/src/review-api/review-progress.ts +382 -0
  152. package/src/review-api/status-tool.ts +10 -0
  153. package/src/review-api/store-schema.ts +22 -0
  154. package/src/review-api/store.ts +1647 -0
  155. package/src/review-api/tool-failure.ts +64 -0
  156. package/src/review-api/trace-schema.ts +13 -0
  157. package/src/review-api/traces.ts +119 -0
  158. package/src/review-api/unsupported-files.integration.ts +91 -0
  159. package/src/review-api/workspaces.ts +692 -0
  160. package/src/review-api/worktree-source.ts +170 -0
  161. package/src/review-api/worktree-structural.integration.ts +320 -0
  162. package/src/review-app-launcher.ts +432 -0
  163. package/src/review-app-picker.ts +168 -0
  164. package/src/review-app.ts +134 -0
  165. package/src/review-bundled-tools.ts +249 -0
  166. package/src/review-checkout-paths.ts +37 -0
  167. package/src/review-diff-files.ts +89 -0
  168. package/src/review-head-checkout.ts +300 -0
  169. package/src/review-home-paths.ts +73 -0
  170. package/src/review-info.ts +59 -0
  171. package/src/review-instances.ts +113 -0
  172. package/src/review-logger.ts +183 -0
  173. package/src/review-preferences.ts +88 -0
  174. package/src/review-prepare.ts +291 -0
  175. package/src/review-stack.ts +112 -0
  176. package/src/review-telemetry.ts +1066 -0
  177. package/src/runtime.ts +74 -0
  178. package/src/server/account-alias.ts +30 -0
  179. package/src/server/bounded-stream.ts +34 -0
  180. package/src/server/bug-report.ts +301 -0
  181. package/src/server/client-error-budget.ts +45 -0
  182. package/src/server/crash-report.ts +273 -0
  183. package/src/server/desktop-host-shutdown.ts +64 -0
  184. package/src/server/desktop-host.ts +190 -0
  185. package/src/server/desktop-server.ts +703 -0
  186. package/src/server/diffr-config.ts +509 -0
  187. package/src/server/diffr-languages.ts +97 -0
  188. package/src/server/global-verb-relay.ts +190 -0
  189. package/src/server/headless-host.ts +149 -0
  190. package/src/server/hono-http.ts +162 -0
  191. package/src/server/http-json.ts +24 -0
  192. package/src/server/json-review-reporting.ts +174 -0
  193. package/src/server/process-error-telemetry.ts +174 -0
  194. package/src/server/review-api-parsers.ts +90 -0
  195. package/src/server/review-lifecycle-telemetry.ts +73 -0
  196. package/src/server/review-open-watchdog.ts +46 -0
  197. package/src/server/review-server-core.ts +290 -0
  198. package/src/server/structural-comparisons.ts +153 -0
  199. package/src/server/structural-diff.ts +186 -0
  200. package/src/server/tutorial-service.ts +241 -0
  201. package/src/server/ui-telemetry.ts +198 -0
  202. package/src/server-discovery.ts +95 -0
  203. package/src/session-markers.ts +132 -0
  204. package/src/sharing/auth.ts +41 -0
  205. package/src/sharing/cli.ts +83 -0
  206. package/src/sharing/client.ts +334 -0
  207. package/src/sharing/export.ts +195 -0
  208. package/src/sharing/host.ts +316 -0
  209. package/src/sharing/import.ts +787 -0
  210. package/src/sharing/index.ts +16 -0
  211. package/src/sharing/repository.ts +145 -0
  212. package/src/sharing/routes.ts +34 -0
  213. package/src/slug.ts +23 -0
  214. package/src/software-map-diff-counts.ts +517 -0
  215. package/src/software-map-model.ts +1147 -0
  216. package/src/software-map-topology-diff.ts +260 -0
  217. package/src/source.ts +92 -0
  218. package/src/startup-trace.ts +232 -0
  219. package/src/stored-document-migration.ts +151 -0
  220. package/src/telemetry-clean-text.ts +257 -0
  221. package/src/telemetry-config.ts +298 -0
  222. package/src/telemetry-debug-sink.ts +38 -0
  223. package/src/telemetry.ts +13 -0
  224. package/src/trace-cli.ts +156 -0
  225. package/src/trace-storage-cli.ts +509 -0
  226. package/src/tutorial-conversation.ts +18 -0
  227. package/src/ui-telemetry-events.ts +765 -0
  228. package/src/unified-diff.ts +71 -0
  229. package/src/viewed-coverage.ts +259 -0
  230. package/src/windows-cli.ts +125 -0
  231. package/tutorial/document.json +275 -0
  232. package/tutorial/runtime-manifest.json +12 -0
  233. package/tutorial/sample-service/package.json +9 -0
  234. package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
  235. package/tutorial/sample-service/src/app.ts +26 -0
  236. package/tutorial/sample-service/src/database/schema.ts +10 -0
  237. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
  238. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
  239. package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
  240. package/tutorial/sample-service/src/orders/order-service.ts +34 -0
  241. package/tutorial/sample-service/src/orders/order.ts +21 -0
  242. package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
  243. package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
  244. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
  245. package/tutorial/sample-service/tsconfig.json +12 -0
  246. package/tutorial/software-map.json +139 -0
  247. package/tutorial/trace.json +20 -0
@@ -0,0 +1,1702 @@
1
+ #!/usr/bin/env node
2
+ import { a as diffFileSummaries, b as resolveRevision, i as diff, x as splitGitPatchFiles } from "../src-CmdiBL20.js";
3
+ import { c as jsonString, i as jsonBoolean, m as isStringValue, n as isJsonObject, p as isObjectValue, s as jsonProperty, u as parseJsonText } from "../src-X9phtB2j.js";
4
+ import { i as SharedReviewStore } from "../client-DFoh50W3.js";
5
+ import { c as ReviewBugReportRequestSchema, l as parseReviewBugReportResponse, o as parseZod, s as ReviewBugReportMetaV2Schema, t as parseReviewCliInstallApplyRequest, v as reviewDiffrSummarizerInputSchema, y as decodeStructuralDiffEvent } from "../src-DnwdaQ2r.js";
6
+ import { _ as invalidateStructuralComparisons, b as installFullDiffr, v as diffrExecutable, y as diffrMissingError } from "../local-data-CxLk75rx.js";
7
+ import { C as resourceReferences, p as documentSchema } from "../stored-document-migration-DHeMpLMb.js";
8
+ import { t as ReviewInputError } from "../input-error-OLgB_h31.js";
9
+ import { A as devReviewHome, P as writePrivateJsonAtomic } from "../s3-config-CAxhO9u_.js";
10
+ import { U as shellQuote, i as reviewInstanceDiscoveryPath, o as reviewLegacyDiscoveryPath } from "../review-home-paths-6zZH-1c9.js";
11
+ import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "../package-paths-B6-zxvIO.js";
12
+ import { c as reviewInstanceIdentity, t as REVIEW_INSTANCE_ENV } from "../desktop-discovery-DBO-PM2V.js";
13
+ import { t as openReviewProfile } from "../profile-4ry2f3AM.js";
14
+ import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-DEFnQsQ2.js";
15
+ import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-D_4QIdoN.js";
16
+ import { c as removeLegacyReviewSkills, d as skipCliInstall, i as finishCliInstallUpdate, l as resetCliInstall, n as applyCliInstall, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-DOHN1RSH.js";
17
+ import { a as serverJson, c as recordClientError, d as HttpJsonError, f as ReviewServerError, i as createWhiteboardCore, l as createNodeRequestListener, n as installProcessErrorTelemetry, o as captureSanitizedUiTelemetry, p as GlobalReviewDesktopVerbRelay, r as serverCrashReportPath, s as clientErrorsForSession, t as drainServerCrashReport, u as readBoundedRequestJson } from "../process-error-telemetry-v9e5h6D7.js";
18
+ import { c as readQuerySchemas } from "../authoring-tools-Cr6Kpjsx.js";
19
+ import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
20
+ import { z } from "zod";
21
+ import fs, { existsSync } from "node:fs";
22
+ import { chmod, copyFile, cp, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
23
+ import path, { join } from "node:path";
24
+ import { tmpdir } from "node:os";
25
+ import { execFile } from "node:child_process";
26
+ import { promisify } from "node:util";
27
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
28
+ import { pathToFileURL } from "node:url";
29
+ import { Writable } from "node:stream";
30
+ import { pipeline } from "node:stream/promises";
31
+ import { gzip, gzipSync } from "node:zlib";
32
+ import { createServer } from "node:http";
33
+ import { stringify } from "smol-toml";
34
+ import { Hono } from "hono";
35
+ //#region src/review-bundled-tools.ts
36
+ function reviewToolsRoot(env = process.env) {
37
+ return path.join(devReviewHome(env), "review-tools");
38
+ }
39
+ async function ensureBundledTool(input) {
40
+ if (!/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/.test(input.tool)) throw new Error(`Invalid bundled tool name: ${input.tool}`);
41
+ const sourcePath = input.sourcePath?.trim();
42
+ if (!sourcePath) return "no-source";
43
+ const source = path.resolve(sourcePath);
44
+ if (!await isExecutableFile(source)) return "no-source";
45
+ const env = input.env ?? process.env;
46
+ const platform = reviewToolPlatform();
47
+ const executable = stagedToolPath(env, input.tool, platform);
48
+ const destination = path.dirname(executable);
49
+ const sha256 = await hashFile(source);
50
+ const identity = {
51
+ tool: input.tool,
52
+ platform,
53
+ sha256
54
+ };
55
+ if (installedToolMatches(destination, executable, identity)) return "fresh";
56
+ await mkdir(path.dirname(destination), {
57
+ recursive: true,
58
+ mode: 448
59
+ });
60
+ return await withInstallLock(destination, async () => {
61
+ if (installedToolMatches(destination, executable, identity)) return "fresh";
62
+ const staging = `${destination}.staging-${process.pid}-${randomBytes(6).toString("hex")}`;
63
+ await rm(staging, {
64
+ recursive: true,
65
+ force: true
66
+ });
67
+ await mkdir(staging, {
68
+ recursive: true,
69
+ mode: 448
70
+ });
71
+ try {
72
+ const stagedExecutable = path.join(staging, executableName(input.tool, platform));
73
+ await copyFile(source, stagedExecutable);
74
+ if (await hashFile(stagedExecutable) !== sha256) throw new Error(`The bundled ${input.tool} source changed while Whiteboard staged it.`);
75
+ await chmod(stagedExecutable, 493);
76
+ await writeToolStamp(staging, identity);
77
+ await rm(destination, {
78
+ recursive: true,
79
+ force: true
80
+ });
81
+ await rename(staging, destination);
82
+ return "staged";
83
+ } finally {
84
+ await rm(staging, {
85
+ recursive: true,
86
+ force: true
87
+ });
88
+ }
89
+ });
90
+ }
91
+ async function ensureBundledRustAnalyzer(input = {}) {
92
+ const env = input.env ?? process.env;
93
+ return await ensureBundledTool({
94
+ tool: "rust-analyzer",
95
+ sourcePath: env.DEV_FAST_REVIEW_RUST_ANALYZER,
96
+ env
97
+ });
98
+ }
99
+ function stagedToolPath(env, tool, platform = reviewToolPlatform()) {
100
+ return path.join(reviewToolsRoot(env), tool, "bundled", platform, executableName(tool, platform));
101
+ }
102
+ async function withInstallLock(destination, operation) {
103
+ const lockPath = `${destination}.install-lock`;
104
+ const deadline = Date.now() + 12e4;
105
+ while (true) try {
106
+ await mkdir(lockPath, { mode: 448 });
107
+ break;
108
+ } catch (error) {
109
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
110
+ if (await stat(lockPath).then((value) => Date.now() - value.mtimeMs).catch(() => 0) > 6e5) {
111
+ await rm(lockPath, {
112
+ recursive: true,
113
+ force: true
114
+ });
115
+ continue;
116
+ }
117
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for another Whiteboard process to install ${path.basename(destination)}.`);
118
+ await setTimeout$1(100);
119
+ }
120
+ try {
121
+ return await operation();
122
+ } finally {
123
+ await rm(lockPath, {
124
+ recursive: true,
125
+ force: true
126
+ });
127
+ }
128
+ }
129
+ function installedToolMatches(destination, executable, identity) {
130
+ if (!isExecutable(executable)) return false;
131
+ try {
132
+ const stamp = parseJsonText(fs.readFileSync(path.join(destination, "review-tool.json"), "utf8"));
133
+ return isJsonObject(stamp) && stamp.tool === identity.tool && stamp.platform === identity.platform && stamp.sha256 === identity.sha256;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+ async function writeToolStamp(destination, identity) {
139
+ await writeFile(path.join(destination, "review-tool.json"), `${JSON.stringify(identity, null, 2)}\n`, {
140
+ encoding: "utf8",
141
+ mode: 384
142
+ });
143
+ }
144
+ function reviewToolPlatform() {
145
+ return `${process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform}-${process.arch}`;
146
+ }
147
+ function executableName(name, platform) {
148
+ return platform.startsWith("win32-") ? `${name}.exe` : name;
149
+ }
150
+ function isExecutable(filePath) {
151
+ try {
152
+ fs.accessSync(filePath, fs.constants.X_OK);
153
+ return true;
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+ async function isExecutableFile(filePath) {
159
+ const metadata = await stat(filePath).catch(() => void 0);
160
+ return Boolean(metadata?.isFile() && isExecutable(filePath));
161
+ }
162
+ async function hashFile(filePath) {
163
+ const hash = createHash("sha256");
164
+ await pipeline(fs.createReadStream(filePath), new Writable({ write(chunk, _encoding, callback) {
165
+ hash.update(chunk);
166
+ callback();
167
+ } }));
168
+ return hash.digest("hex");
169
+ }
170
+ //#endregion
171
+ //#region src/server/desktop-host-shutdown.ts
172
+ function listenForDesktopHostShutdown(hostProcess, onShutdown, onTelemetrySetting, onStageRustAnalyzer) {
173
+ const handleMessage = (message) => {
174
+ if (!isJsonObject(message)) return;
175
+ const type = jsonProperty(message, "type");
176
+ if (type === "shutdown") {
177
+ onShutdown();
178
+ return;
179
+ }
180
+ if (type === "telemetry-setting") {
181
+ const enabled = jsonBoolean(jsonProperty(message, "enabled"));
182
+ if (enabled !== void 0) onTelemetrySetting?.(enabled);
183
+ return;
184
+ }
185
+ if (type === "stage-rust-analyzer") {
186
+ const path = jsonString(jsonProperty(message, "path"));
187
+ if (path) onStageRustAnalyzer?.(path);
188
+ }
189
+ };
190
+ if (hostProcess.parentPort) {
191
+ const handleParentMessage = (event) => {
192
+ handleMessage(event.data);
193
+ };
194
+ hostProcess.parentPort.on("message", handleParentMessage);
195
+ return () => hostProcess.parentPort?.off("message", handleParentMessage);
196
+ }
197
+ hostProcess.on("message", handleMessage);
198
+ return () => hostProcess.off("message", handleMessage);
199
+ }
200
+ //#endregion
201
+ //#region src/review-preferences.ts
202
+ const DEFAULT_REVIEW_PREFERENCES = { scratchpadEnabled: false };
203
+ function reviewPreferencesPath(devHome = devReviewHome()) {
204
+ return path.join(devHome, "preferences.json");
205
+ }
206
+ /** A missing or unreadable file falls back to the defaults; it never throws. */
207
+ async function readReviewPreferences(devHome) {
208
+ try {
209
+ return { scratchpadEnabled: parseScratchpadEnabled(parseJsonText(await readFile(reviewPreferencesPath(devHome), "utf8"))) };
210
+ } catch {
211
+ return { ...DEFAULT_REVIEW_PREFERENCES };
212
+ }
213
+ }
214
+ async function writeReviewPreferences(preferences, devHome) {
215
+ const next = { scratchpadEnabled: preferences.scratchpadEnabled === true };
216
+ await writePrivateJsonAtomic(reviewPreferencesPath(devHome), next);
217
+ return next;
218
+ }
219
+ /** Reads the scratchpad preference; a missing file means off. */
220
+ async function readScratchpadEnabled(devHome) {
221
+ return (await readReviewPreferences(devHome)).scratchpadEnabled;
222
+ }
223
+ /** Sets the scratchpad preference, keeping the other preferences as they are. */
224
+ async function writeScratchpadEnabled(enabled, devHome) {
225
+ return (await writeReviewPreferences({
226
+ ...await readReviewPreferences(devHome),
227
+ scratchpadEnabled: enabled
228
+ }, devHome)).scratchpadEnabled;
229
+ }
230
+ function parseScratchpadEnabled(raw) {
231
+ return isJsonObject(raw) && raw.scratchpadEnabled === true;
232
+ }
233
+ //#endregion
234
+ //#region src/server/crash-report.ts
235
+ const CRASH_REPORT_URL = "https://bug.dev.fast/api/v2/crashes";
236
+ const gzipAsync = promisify(gzip);
237
+ /** The Worker's limit on the gzip dump it stores. */
238
+ const DEFAULT_MAX_DUMP_BYTES = 5e7;
239
+ /** Reading a dump this large into memory is not worth it; it can never fit. */
240
+ const MAX_RAW_DUMP_BYTES = 536870912;
241
+ const DUMP_TOO_LARGE = {
242
+ ok: false,
243
+ status: 413,
244
+ error: "Crash dump is too large."
245
+ };
246
+ const UPSTREAM_TIMEOUT_MS$1 = 6e4;
247
+ const version = z.string().regex(/^[0-9A-Za-z.+_-]{1,64}$/);
248
+ /**
249
+ * The envelope keys the Worker's strict crash meta accepts, with the values it
250
+ * accepts. Any other key is dropped, and a value that would not pass is left
251
+ * out rather than failing the whole upload.
252
+ */
253
+ const CrashEnvelopeSchema = z.object({
254
+ app_version: version.optional().catch(void 0),
255
+ cli_version: version.optional().catch(void 0),
256
+ channel: z.enum([
257
+ "stable",
258
+ "preview",
259
+ "dev"
260
+ ]).optional().catch(void 0),
261
+ environment: z.enum([
262
+ "production",
263
+ "ci",
264
+ "internal",
265
+ "e2e",
266
+ "smoke"
267
+ ]).optional().catch(void 0),
268
+ surface: z.enum([
269
+ "desktop",
270
+ "cli",
271
+ "headless",
272
+ "mcp",
273
+ "api"
274
+ ]).optional().catch(void 0),
275
+ platform: ReviewBugReportMetaV2Schema.shape.platform.optional().catch(void 0),
276
+ arch: z.enum([
277
+ "arm",
278
+ "arm64",
279
+ "ia32",
280
+ "loong64",
281
+ "mips",
282
+ "mipsel",
283
+ "ppc",
284
+ "ppc64",
285
+ "riscv64",
286
+ "s390",
287
+ "s390x",
288
+ "x64"
289
+ ]).optional().catch(void 0),
290
+ os_version: version.optional().catch(void 0),
291
+ node_major: z.number().int().optional().catch(void 0),
292
+ ci: z.boolean().optional().catch(void 0),
293
+ internal: z.boolean().optional().catch(void 0),
294
+ app_session_id: z.uuid().optional().catch(void 0)
295
+ });
296
+ /** The launch a dump was written during, from Electron main's dump ledger. */
297
+ const CrashLaunchSchema = CrashEnvelopeSchema.pick({
298
+ app_session_id: true,
299
+ app_version: true,
300
+ cli_version: true
301
+ });
302
+ /** The body Electron main posts to `/crash-reports`. */
303
+ const CrashReportRequestSchema = z.object({
304
+ dump_path: z.string().min(1),
305
+ crashed_at: z.number().int().nonnegative(),
306
+ covered: z.boolean(),
307
+ launch: CrashLaunchSchema.optional()
308
+ });
309
+ /**
310
+ * A dump belongs to the launch that wrote it, not to this one: its session id
311
+ * and versions override the current envelope. Without a matching launch the
312
+ * crashed session is unknown, so its id is left out; the versions stay the
313
+ * current launch's, the best guess there is.
314
+ */
315
+ function crashedLaunchProperties(launch) {
316
+ const properties = { app_session_id: launch?.app_session_id };
317
+ if (!launch) return properties;
318
+ return {
319
+ ...properties,
320
+ app_version: launch.app_version,
321
+ cli_version: launch.cli_version
322
+ };
323
+ }
324
+ /**
325
+ * Upload one Crashpad minidump with the telemetry envelope as metadata. The
326
+ * caller has already checked the opt-out; this only packs and posts.
327
+ */
328
+ async function uploadCrashDump(input) {
329
+ const maxDumpBytes = input.maxDumpBytes ?? DEFAULT_MAX_DUMP_BYTES;
330
+ if ((await stat(input.dumpPath)).size > MAX_RAW_DUMP_BYTES) return DUMP_TOO_LARGE;
331
+ const dump = await gzipAsync(await readFile(input.dumpPath), { level: 6 });
332
+ if (dump.byteLength > maxDumpBytes) return DUMP_TOO_LARGE;
333
+ const meta = {
334
+ schema_version: 1,
335
+ distinct_id: input.distinctId,
336
+ crashed_at: input.crashedAt,
337
+ covered: input.covered,
338
+ dump_bytes: dump.byteLength,
339
+ dump_sha256: createHash("sha256").update(dump).digest("hex"),
340
+ ...CrashEnvelopeSchema.parse(input.envelope)
341
+ };
342
+ const form = new FormData();
343
+ form.append("meta", JSON.stringify(meta));
344
+ form.append("dump", new Blob([Uint8Array.from(dump)], { type: "application/gzip" }), "minidump.dmp.gz");
345
+ try {
346
+ const response = await (input.fetchImpl ?? fetch)(CRASH_REPORT_URL, {
347
+ method: "POST",
348
+ body: form,
349
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS$1)
350
+ });
351
+ if (!response.ok) return {
352
+ ok: false,
353
+ status: response.status,
354
+ error: "Crash report service failed."
355
+ };
356
+ return { ok: true };
357
+ } catch (error) {
358
+ return {
359
+ ok: false,
360
+ status: 502,
361
+ error: error instanceof Error ? error.message : "Crash report service failed."
362
+ };
363
+ }
364
+ }
365
+ /**
366
+ * `POST /crash-reports` from Electron main. The server token also reaches the
367
+ * canvas, so only a `.dmp` file whose real path sits inside the Review dump
368
+ * directory is ever read. A dump no live listener covered is counted here as
369
+ * `review_crash`, stamped with its own launch, which the `/telemetry/event`
370
+ * route cannot do: that route stamps the sender's session. Nothing is uploaded
371
+ * when telemetry is off or only printed (the debug sink).
372
+ */
373
+ async function reportCrashDump(telemetry, request, crashDumpsDir, fetchImpl) {
374
+ const dumpPath = await dumpInside(request.dump_path, crashDumpsDir);
375
+ if (!dumpPath) return {
376
+ status: 403,
377
+ body: {
378
+ ok: false,
379
+ error: "Not a Whiteboard crash dump."
380
+ }
381
+ };
382
+ const launch = crashedLaunchProperties(request.launch);
383
+ const counted = !request.covered;
384
+ if (counted) await telemetry.captureEvent("review_crash", {
385
+ process: "unknown",
386
+ reason: "minidump",
387
+ source: "minidump",
388
+ ...launch
389
+ });
390
+ if (!await telemetry.sendsEvents()) return {
391
+ status: 200,
392
+ body: {
393
+ ok: true,
394
+ counted,
395
+ skipped: "telemetry_disabled"
396
+ }
397
+ };
398
+ const result = await uploadCrashDump({
399
+ dumpPath,
400
+ crashedAt: request.crashed_at,
401
+ covered: request.covered,
402
+ distinctId: await telemetry.getInstallationId(),
403
+ envelope: {
404
+ ...await telemetry.envelope(),
405
+ ...launch
406
+ },
407
+ fetchImpl
408
+ });
409
+ return {
410
+ status: result.ok ? 200 : result.status ?? 502,
411
+ body: {
412
+ ...result,
413
+ counted
414
+ }
415
+ };
416
+ }
417
+ async function dumpInside(file, directory) {
418
+ if (!directory) return void 0;
419
+ try {
420
+ const [realFile, realDirectory] = await Promise.all([realpath(file), realpath(directory)]);
421
+ const relative = path.relative(realDirectory, realFile);
422
+ if (!realFile.endsWith(".dmp") || !relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return void 0;
423
+ return realFile;
424
+ } catch {
425
+ return;
426
+ }
427
+ }
428
+ //#endregion
429
+ //#region src/server/diffr-config.ts
430
+ const execFileAsync = promisify(execFile);
431
+ let writes = Promise.resolve();
432
+ let testing = false;
433
+ function serialized(operation) {
434
+ const next = writes.then(operation, operation);
435
+ writes = next.catch(() => {});
436
+ return next;
437
+ }
438
+ async function diffr(args, rootPath, options = {}) {
439
+ try {
440
+ const { stdout } = await execFileAsync(diffrExecutable(), args, {
441
+ cwd: rootPath,
442
+ maxBuffer: 16777216,
443
+ signal: options.signal ?? AbortSignal.timeout(3e4),
444
+ env: options.env ?? process.env
445
+ });
446
+ return stdout;
447
+ } catch (error) {
448
+ const failure = error;
449
+ if (failure.code === "ENOENT") throw diffrMissingError();
450
+ if (failure.name === "AbortError") throw new Error("diffr timed out or was cancelled.");
451
+ throw new Error("diffr could not complete the operation. Check its configuration and credentials.");
452
+ }
453
+ }
454
+ async function values(rootPath, reveal = false, signal) {
455
+ return json(await diffr([
456
+ "config",
457
+ "show",
458
+ "--json",
459
+ ...reveal ? ["--reveal"] : []
460
+ ], rootPath, { signal }));
461
+ }
462
+ function json(output) {
463
+ try {
464
+ const parsed = parseJsonText(output);
465
+ if (isJsonObject(parsed)) return parsed;
466
+ } catch {}
467
+ throw new Error("diffr configuration response is malformed.");
468
+ }
469
+ function valueAt(object, key) {
470
+ let value = object;
471
+ for (const segment of key.split(".")) value = isJsonObject(value) ? value[segment] : void 0;
472
+ return value;
473
+ }
474
+ const prefix = "plugins.bundled.summarize";
475
+ function savedProvider(config) {
476
+ const value = valueAt(config, `${prefix}.provider`);
477
+ return isStringValue(value) ? value : "";
478
+ }
479
+ function savedEndpoint(config) {
480
+ const value = valueAt(config, `${prefix}.endpoint`);
481
+ return isStringValue(value) ? value : "";
482
+ }
483
+ /** Whether a saved key belongs to a different destination than the draft's. */
484
+ function movesKey(config, draft) {
485
+ return draft.provider !== savedProvider(config) || draft.endpoint !== savedEndpoint(config);
486
+ }
487
+ function environmentKey(providers, id) {
488
+ return providers.find((provider) => provider.id === id)?.keyVariables.map((name) => process.env[name]).find((key) => !!key);
489
+ }
490
+ function keyOptional(providers, id, endpoint) {
491
+ return endpoint !== "" && !!providers.find((provider) => provider.id === id)?.keylessCustomEndpoint;
492
+ }
493
+ function strings(value) {
494
+ return Array.isArray(value) ? value.filter(isStringValue) : [];
495
+ }
496
+ /**
497
+ * What diffr's schema says about summaries: each provider's defaults, and
498
+ * the default prompt with the link its description gives. Nothing when
499
+ * diffr cannot describe its schema: the rest of Settings still works.
500
+ */
501
+ async function summaryDefaults(rootPath) {
502
+ let schema;
503
+ try {
504
+ schema = json(await diffr(["config", "schema"], rootPath));
505
+ } catch {
506
+ return {};
507
+ }
508
+ const options = valueAt(schema, "properties.plugins.properties.bundled.properties.summarize.properties");
509
+ if (!isJsonObject(options)) return {};
510
+ const option = (name) => isJsonObject(options[name]) ? options[name] : {};
511
+ const provider = option("provider");
512
+ const titles = strings(provider["x-enum-titles"]);
513
+ const models = valueAt(option("model"), "x-default-by.values");
514
+ const details = valueAt(option("provider_details"), "x-default-by.values");
515
+ const providers = strings(provider.enum).map((id, index) => {
516
+ const detail = isJsonObject(details) ? details[id] : void 0;
517
+ const model = isJsonObject(models) ? models[id] : void 0;
518
+ const endpoint = isJsonObject(detail) ? detail.endpoint : void 0;
519
+ return {
520
+ id,
521
+ title: titles[index] ?? id,
522
+ model: isStringValue(model) ? model : "",
523
+ endpoint: isStringValue(endpoint) ? endpoint : "",
524
+ keyVariables: isJsonObject(detail) ? strings(detail.key_variables) : [],
525
+ keylessCustomEndpoint: isJsonObject(detail) && detail.keyless_custom_endpoint === true
526
+ };
527
+ });
528
+ const prompt = option("system_prompt");
529
+ return {
530
+ providers,
531
+ defaultPrompt: isStringValue(prompt.default) ? prompt.default : void 0
532
+ };
533
+ }
534
+ async function read(rootPath) {
535
+ const config = await values(rootPath);
536
+ const defaults = await summaryDefaults(rootPath);
537
+ const saved = valueAt(config, `${prefix}.api_key`);
538
+ const credentialSource = isStringValue(saved) && saved.length > 0 ? "config" : environmentKey(defaults.providers ?? [], savedProvider(config)) ? "environment" : "missing";
539
+ const plugins = config.plugins;
540
+ if (isJsonObject(plugins)) for (const namespace of Object.values(plugins)) {
541
+ if (!isJsonObject(namespace)) continue;
542
+ for (const plugin of Object.values(namespace)) if (isJsonObject(plugin)) delete plugin.api_key;
543
+ }
544
+ return {
545
+ values: config,
546
+ credentialSource,
547
+ ...defaults
548
+ };
549
+ }
550
+ function readDiffrConfig(rootPath) {
551
+ return serialized(() => read(rootPath));
552
+ }
553
+ function diffrConfigValueText(value) {
554
+ return isStringValue(value) ? value : JSON.stringify(value);
555
+ }
556
+ async function writeSettings(entries, rootPath) {
557
+ let changed = false;
558
+ let error;
559
+ let current = await values(rootPath);
560
+ try {
561
+ for (const [key, value] of entries) {
562
+ if (valueAt(current, key) === value) continue;
563
+ await diffr([
564
+ "config",
565
+ "set",
566
+ key,
567
+ diffrConfigValueText(value)
568
+ ], rootPath);
569
+ changed = true;
570
+ current = await values(rootPath);
571
+ }
572
+ } catch {
573
+ error = changed ? "Some settings were saved before diffr rejected a change. Check the current values and try again." : "diffr could not save the setting. Check its configuration and the entered value.";
574
+ } finally {
575
+ if (changed) invalidateStructuralComparisons();
576
+ }
577
+ return {
578
+ ...await read(rootPath),
579
+ changed,
580
+ error
581
+ };
582
+ }
583
+ function setDiffrConfigValue(key, value, rootPath) {
584
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_-]*(\.[A-Za-z0-9_][A-Za-z0-9_-]*)*$/.test(key)) throw new Error("Invalid diffr config key.");
585
+ return serialized(() => writeSettings([[key, value]], rootPath));
586
+ }
587
+ function saveDiffrSummarizer(input, rootPath) {
588
+ const parsed = reviewDiffrSummarizerInputSchema.safeParse(input);
589
+ if (!parsed.success) throw new Error("Invalid summary settings.");
590
+ const draft = parsed.data;
591
+ return serialized(async () => {
592
+ const current = await read(rootPath);
593
+ const providers = current.providers ?? [];
594
+ const moving = movesKey(current.values, draft);
595
+ const savedKey = current.credentialSource === "config";
596
+ if (draft.enabled && !draft.apiKey && !(savedKey && !moving) && !environmentKey(providers, draft.provider) && !keyOptional(providers, draft.provider, draft.endpoint)) return {
597
+ ...current,
598
+ changed: false,
599
+ error: "Add an API key before enabling summaries."
600
+ };
601
+ const entries = [];
602
+ if (!draft.enabled) entries.push([`${prefix}.enabled`, false]);
603
+ if (moving && savedKey) entries.push([`${prefix}.api_key`, ""]);
604
+ entries.push([`${prefix}.provider`, draft.provider]);
605
+ if (draft.endpoint !== savedEndpoint(current.values) || draft.endpoint && draft.provider !== savedProvider(current.values)) entries.push([`${prefix}.endpoint`, draft.endpoint]);
606
+ if (draft.apiKey) entries.push([`${prefix}.api_key`, draft.apiKey]);
607
+ if (draft.systemPrompt.trim() && draft.systemPrompt !== valueAt(current.values, `${prefix}.system_prompt`)) entries.push([`${prefix}.system_prompt`, draft.systemPrompt]);
608
+ entries.push([`${prefix}.model`, draft.model], [`${prefix}.tests`, draft.tests]);
609
+ if (draft.enabled) entries.push([`${prefix}.enabled`, true]);
610
+ return writeSettings(entries, rootPath);
611
+ });
612
+ }
613
+ async function testDiffrSummarizer(input, rootPath, signal) {
614
+ const parsed = reviewDiffrSummarizerInputSchema.safeParse(input);
615
+ if (!parsed.success) throw new Error("Invalid summary settings.");
616
+ if (testing) throw new Error("A summary test is already running.");
617
+ testing = true;
618
+ const timeout = AbortSignal.timeout(6e4);
619
+ const deadline = signal ? AbortSignal.any([signal, timeout]) : timeout;
620
+ let directory;
621
+ try {
622
+ const config = await serialized(() => values(rootPath, true, deadline));
623
+ const saved = valueAt(config, prefix);
624
+ if (!isJsonObject(saved)) throw new Error("The summarizer is not available in this configuration.");
625
+ const { provider, endpoint, model, systemPrompt } = parsed.data;
626
+ const providers = (await summaryDefaults(rootPath)).providers ?? [];
627
+ const variables = providers.find((known) => known.id === provider)?.keyVariables ?? [];
628
+ const apiKey = parsed.data.apiKey || (!movesKey(config, parsed.data) && isStringValue(saved.api_key) ? saved.api_key : "") || environmentKey(providers, provider);
629
+ if (!apiKey && !keyOptional(providers, provider, endpoint)) throw new Error("Add an API key to test summaries.");
630
+ directory = await mkdtemp(join(tmpdir(), "review-summary-"));
631
+ const options = {
632
+ ...saved,
633
+ enabled: true,
634
+ provider,
635
+ model,
636
+ ...systemPrompt.trim() && { system_prompt: systemPrompt },
637
+ min_lines: 1,
638
+ retries: 0,
639
+ request_timeout_ms: 45e3
640
+ };
641
+ delete options.api_key;
642
+ delete options.provider_details;
643
+ if (endpoint) options.endpoint = endpoint;
644
+ else delete options.endpoint;
645
+ await mkdir(join(directory, "diffr"));
646
+ await writeFile(join(directory, "diffr", "config.toml"), stringify({
647
+ version: 1,
648
+ plugins: {
649
+ order: ["bundled.summarize"],
650
+ bundled: { summarize: options }
651
+ }
652
+ }), { mode: 384 });
653
+ const before = join(directory, "before.rs"), after = join(directory, "after.rs");
654
+ await writeFile(before, "");
655
+ await writeFile(after, SUMMARY_SAMPLE);
656
+ const output = await diffr([
657
+ "--no-index",
658
+ "--format",
659
+ "ndjson",
660
+ "--stream-annotations",
661
+ "--",
662
+ before,
663
+ after
664
+ ], directory, {
665
+ env: {
666
+ ...process.env,
667
+ ...Object.fromEntries(providers.flatMap((known) => known.keyVariables).map((name) => [name, ""])),
668
+ ...apiKey && variables[0] && { [variables[0]]: apiKey },
669
+ XDG_CONFIG_HOME: directory
670
+ },
671
+ signal: deadline
672
+ });
673
+ try {
674
+ const events = output.trim().split("\n").map(decodeStructuralDiffEvent);
675
+ const complete = events.at(-1);
676
+ const annotation = events.flatMap((event) => event.type === "annotations" ? event.annotations : []).find((item) => item.label.trim());
677
+ if (complete?.type === "complete" && complete.failed === 0 && !complete.aborted && !events.some((event) => (event.type === "annotations" || event.type === "file") && event.error) && annotation) return apiKey ? annotation.label.split(apiKey).join("[redacted]") : annotation.label;
678
+ } catch {}
679
+ throw new Error("No summary was produced. Check the API key, model, and network connection.");
680
+ } finally {
681
+ testing = false;
682
+ if (directory) await rm(directory, {
683
+ recursive: true,
684
+ force: true
685
+ });
686
+ }
687
+ }
688
+ const SUMMARY_SAMPLE = `pub fn count_values(values: &[i32]) -> (i32, i32) {
689
+ let mut total = 0;
690
+ let mut count = 0;
691
+ for value in values {
692
+ if *value > 0 {
693
+ total += value;
694
+ count += 1;
695
+ }
696
+ }
697
+ let average = if count > 0 {
698
+ total / count
699
+ } else {
700
+ 0
701
+ };
702
+ (count, average)
703
+ }
704
+ `;
705
+ //#endregion
706
+ //#region src/review-diff-files.ts
707
+ async function resolveReviewDiffFiles(input) {
708
+ const baseRef = input.baseRef?.trim();
709
+ if (!baseRef) return { files: [] };
710
+ const headRef = input.headRef?.trim() || void 0;
711
+ const paths = normalizeDiffPaths(input.paths);
712
+ if (input.includePatch === false) return {
713
+ baseRef,
714
+ headRef,
715
+ files: await diffFileSummaries({
716
+ rootPath: input.rootPath,
717
+ baseRef,
718
+ headRef,
719
+ paths
720
+ })
721
+ };
722
+ const stdout = await diff({
723
+ rootPath: input.rootPath,
724
+ baseRef,
725
+ headRef,
726
+ paths,
727
+ contextLines: input.contextLines
728
+ });
729
+ return {
730
+ baseRef,
731
+ headRef,
732
+ files: splitGitPatchFiles(stdout).map(({ file, patch }) => ({
733
+ path: file.path,
734
+ previousPath: file.previousPath,
735
+ status: file.status,
736
+ additions: file.additions,
737
+ deletions: file.deletions,
738
+ patch
739
+ })).filter((file) => matchesDiffPath(file, paths))
740
+ };
741
+ }
742
+ function normalizeDiffPaths(paths) {
743
+ return [...new Set((paths ?? []).map((path) => path.trim()).filter((path) => path.length > 0))].sort();
744
+ }
745
+ function matchesDiffPath(file, paths) {
746
+ if (paths.length === 0) return true;
747
+ const candidates = new Set([file.path, file.previousPath].filter(Boolean));
748
+ return paths.some((path) => candidates.has(path));
749
+ }
750
+ //#endregion
751
+ //#region src/server/bug-report.ts
752
+ const BUG_REPORT_URL = "https://bug.dev.fast/api/v2/reports";
753
+ const MAX_PAYLOAD_BYTES = 10485760;
754
+ const UPSTREAM_TIMEOUT_MS = 2e4;
755
+ var BugReportUpstreamError = class extends Error {
756
+ status;
757
+ constructor(status, message) {
758
+ super(message);
759
+ this.status = status;
760
+ this.name = "BugReportUpstreamError";
761
+ }
762
+ };
763
+ async function submitReviewBugReport(input) {
764
+ const cliVersion = readReviewPackageVersion();
765
+ const attachmentErrors = [];
766
+ const payload = {
767
+ schema_version: 4,
768
+ description: input.report.description,
769
+ diagnostics: {
770
+ app_version: input.report.app_version,
771
+ cli_version: cliVersion,
772
+ platform: process.platform,
773
+ app_session_id: input.report.app_session_id,
774
+ client_error_names: input.clientErrorNames.slice(-20)
775
+ }
776
+ };
777
+ if (input.telemetryEnvelope) payload.diagnostics.telemetry = Object.fromEntries(Object.entries(input.telemetryEnvelope).filter((entry) => entry[1] !== void 0));
778
+ if (input.report.screenshot) payload.screenshot = input.report.screenshot;
779
+ let reviewSource;
780
+ let omittedReviewFiles;
781
+ let mapSource;
782
+ let changedFileDiffs;
783
+ const tasks = [];
784
+ if (input.report.include_review) tasks.push(input.source.review().then((result) => {
785
+ reviewSource = result.files;
786
+ if (result.omitted.length > 0) omittedReviewFiles = result.omitted;
787
+ }, () => {
788
+ attachmentErrors.push(unavailable("review"));
789
+ }));
790
+ if (input.report.include_map) tasks.push(input.source.map().then((source) => {
791
+ if (source !== null) mapSource = source;
792
+ }, () => {
793
+ attachmentErrors.push(unavailable("map"));
794
+ }));
795
+ if (input.report.include_diff) tasks.push(input.source.diff().then((diff) => {
796
+ changedFileDiffs = diff;
797
+ }, () => {
798
+ attachmentErrors.push(unavailable("diff"));
799
+ }));
800
+ await Promise.all(tasks);
801
+ if (reviewSource !== void 0) payload.review = reviewSource;
802
+ if (omittedReviewFiles !== void 0) payload.diagnostics.review_omitted_files = omittedReviewFiles;
803
+ if (mapSource !== void 0) payload.map = mapSource;
804
+ if (changedFileDiffs !== void 0) payload.diff = changedFileDiffs;
805
+ if (attachmentErrors.length > 0) payload.diagnostics.attachment_errors = attachmentErrors.sort(byAttachment);
806
+ const response = await (input.fetchImpl ?? fetch)(BUG_REPORT_URL, {
807
+ method: "POST",
808
+ body: buildBugReportRequest(payload, {
809
+ appVersion: input.report.app_version,
810
+ cliVersion
811
+ }),
812
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
813
+ }).catch((error) => {
814
+ throw new BugReportUpstreamError(502, error instanceof Error ? error.message : "Bug report service failed.");
815
+ });
816
+ const responseBody = await response.json().catch(() => null);
817
+ if (!response.ok) throw new BugReportUpstreamError(response.status === 429 || response.status === 413 ? response.status : 502, response.status === 429 ? "Too many reports. Try again later." : response.status === 413 ? "Bug report is too large." : "Bug report service failed.");
818
+ const result = parseReviewBugReportResponse(responseBody);
819
+ if (!result.ok) throw new BugReportUpstreamError(502, result.error);
820
+ return result;
821
+ }
822
+ function buildBugReportRequest(payload, input) {
823
+ const maxPayloadBytes = input.maxPayloadBytes ?? MAX_PAYLOAD_BYTES;
824
+ let truncatedDiff = false;
825
+ let truncatedMap = false;
826
+ let truncatedScreenshot = false;
827
+ let payloadBytes = gzipPayload(payload);
828
+ if (payloadBytes.byteLength > maxPayloadBytes && payload.diff) {
829
+ delete payload.diff;
830
+ truncatedDiff = true;
831
+ payloadBytes = gzipPayload(payload);
832
+ }
833
+ if (payloadBytes.byteLength > maxPayloadBytes && payload.map) {
834
+ delete payload.map;
835
+ truncatedMap = true;
836
+ payloadBytes = gzipPayload(payload);
837
+ }
838
+ if (payloadBytes.byteLength > maxPayloadBytes && payload.screenshot) {
839
+ delete payload.screenshot;
840
+ truncatedScreenshot = true;
841
+ payloadBytes = gzipPayload(payload);
842
+ }
843
+ if (payloadBytes.byteLength > maxPayloadBytes && payload.review) {
844
+ delete payload.review;
845
+ payloadBytes = gzipPayload(payload);
846
+ }
847
+ if (payloadBytes.byteLength > maxPayloadBytes) throw new BugReportUpstreamError(413, "Bug report is too large.");
848
+ const meta = {
849
+ schema_version: 2,
850
+ description_length: Buffer.byteLength(payload.description),
851
+ has_review: payload.review !== void 0,
852
+ has_map: payload.map !== void 0,
853
+ has_diff: payload.diff !== void 0,
854
+ has_screenshot: payload.screenshot !== void 0,
855
+ has_trace: false,
856
+ payload_bytes: payloadBytes.byteLength,
857
+ app_version: input.appVersion,
858
+ cli_version: input.cliVersion,
859
+ platform: process.platform,
860
+ truncated_diff: truncatedDiff,
861
+ truncated_map: truncatedMap,
862
+ truncated_screenshot: truncatedScreenshot,
863
+ truncated_trace: false,
864
+ parts: [{
865
+ field: "payload",
866
+ filename: "payload.json.gz",
867
+ bytes: payloadBytes.byteLength,
868
+ sha256: sha256Bytes(payloadBytes)
869
+ }]
870
+ };
871
+ if (payload.description.trim().length > 0) meta.description = payload.description;
872
+ const form = new FormData();
873
+ form.append("meta", JSON.stringify(meta));
874
+ form.append("payload", new Blob([Uint8Array.from(payloadBytes)], { type: "application/gzip" }), "payload.json.gz");
875
+ return form;
876
+ }
877
+ function gzipPayload(payload) {
878
+ return gzipSync(Buffer.from(JSON.stringify(payload)), { level: 9 });
879
+ }
880
+ function sha256Bytes(bytes) {
881
+ return createHash("sha256").update(bytes).digest("hex");
882
+ }
883
+ function unavailable(attachment) {
884
+ return {
885
+ attachment,
886
+ error: "unavailable"
887
+ };
888
+ }
889
+ function byAttachment(left, right) {
890
+ return left.attachment.localeCompare(right.attachment);
891
+ }
892
+ //#endregion
893
+ //#region src/server/review-api-parsers.ts
894
+ const MIN_REVIEW_TAB_DWELL_MS = 250;
895
+ const MAX_REVIEW_TAB_DWELL_MS = 144e5;
896
+ const APP_SESSION_ID_PATTERN = /^[A-Za-z0-9_-][A-Za-z0-9_.-]{15,127}$/;
897
+ const MAX_BUG_REPORT_DESCRIPTION_BYTES = 65536;
898
+ const MAX_BUG_REPORT_SCREENSHOT_BYTES = 3145728;
899
+ function parseReviewBugReportInput(value) {
900
+ const parsed = ReviewBugReportRequestSchema.parse(value);
901
+ if (Buffer.byteLength(parsed.description, "utf8") > MAX_BUG_REPORT_DESCRIPTION_BYTES) throw new HttpJsonError(`description must not exceed ${MAX_BUG_REPORT_DESCRIPTION_BYTES} UTF-8 bytes`, 413);
902
+ if (parsed.screenshot) {
903
+ const screenshot = Buffer.from(parsed.screenshot.base64, "base64");
904
+ if (screenshot.toString("base64") !== parsed.screenshot.base64) throw new HttpJsonError("screenshot.base64 must be valid base64", 400);
905
+ if (screenshot.byteLength > MAX_BUG_REPORT_SCREENSHOT_BYTES) throw new HttpJsonError(`screenshot must not exceed ${MAX_BUG_REPORT_SCREENSHOT_BYTES} decoded bytes`, 413);
906
+ }
907
+ return parsed;
908
+ }
909
+ const ReviewTabTelemetryInputSchema = z.strictObject({
910
+ tab: z.enum(REVIEW_TELEMETRY_TABS, { error: "must be review, commits, map, files, or trace" }),
911
+ reason: z.enum([
912
+ "tab_change",
913
+ "visibility_hidden",
914
+ "pagehide",
915
+ "unmount"
916
+ ], { error: "must be tab_change, visibility_hidden, pagehide, or unmount" }),
917
+ duration_ms: z.number().int("must be an integer from 250ms to 4h").min(MIN_REVIEW_TAB_DWELL_MS, "must be an integer from 250ms to 4h").max(MAX_REVIEW_TAB_DWELL_MS, "must be an integer from 250ms to 4h"),
918
+ app_session_id: z.string().regex(APP_SESSION_ID_PATTERN, "must be a valid session id"),
919
+ document: z.unknown().optional()
920
+ }).transform((event) => ({
921
+ tab: event.tab,
922
+ reason: event.reason,
923
+ durationMs: event.duration_ms,
924
+ appSessionId: event.app_session_id
925
+ }));
926
+ function requestJsonErrorStatus(cause) {
927
+ return cause instanceof HttpJsonError ? cause.statusCode : 400;
928
+ }
929
+ function parseReviewTabTelemetryInput(value) {
930
+ return parseZod(ReviewTabTelemetryInputSchema, value, "Review tab telemetry event");
931
+ }
932
+ //#endregion
933
+ //#region src/server/json-review-reporting.ts
934
+ function jsonReviewBugReportSource(store, snapshot) {
935
+ return {
936
+ async review() {
937
+ return {
938
+ files: { "review.json": JSON.stringify(snapshot, null, 2) },
939
+ omitted: []
940
+ };
941
+ },
942
+ async map() {
943
+ const ids = [...new Set(resourceReferences(snapshot.document).flatMap((block) => block.type === "software_map" ? [block.mapVersionId] : []))];
944
+ return ids.length ? JSON.stringify(ids.map((id) => JSON.parse(Buffer.from(store.resource(id).data).toString()))) : null;
945
+ },
946
+ async diff() {
947
+ if (!snapshot.pins) return { files: [] };
948
+ return resolveReviewDiffFiles({
949
+ rootPath: store.repositoryPath(snapshot.pins.repositoryId),
950
+ baseRef: snapshot.pins.base,
951
+ headRef: snapshot.pins.head,
952
+ includePatch: true
953
+ });
954
+ }
955
+ };
956
+ }
957
+ /** Telemetry and report uploads for a pinned native review. */
958
+ function createJsonReviewReporting(store, telemetry, options = {}) {
959
+ const { submit = submitReviewBugReport, shared } = options;
960
+ const app = new Hono();
961
+ app.onError((error, context) => context.json({
962
+ ok: false,
963
+ error: error.message
964
+ }, error instanceof BugReportUpstreamError ? error.status : error instanceof ReviewInputError ? error.status : requestJsonErrorStatus(error)));
965
+ app.use("/:id/telemetry/*", async (context, next) => {
966
+ const id = context.req.param("id");
967
+ if (id.startsWith("shared-")) {
968
+ if (!shared) throw new ReviewInputError("Shared review is not available.", 404);
969
+ shared.get(id);
970
+ } else store.assertExists(id);
971
+ await next();
972
+ });
973
+ app.post("/:id/telemetry/event", async (c) => {
974
+ const body = await readBoundedRequestJson(c.req.raw, void 0, {});
975
+ const payload = isJsonObject(body) ? body : {};
976
+ const rawContext = isJsonObject(payload.context) ? payload.context : {};
977
+ await captureSanitizedUiTelemetry(telemetry, c.req.raw, payload.name, payload.properties, recordClientError, payload.error, {
978
+ ...rawContext,
979
+ reviewUuid: c.req.param("id")
980
+ }, payload.occurredAt);
981
+ return c.json({ ok: true });
982
+ });
983
+ app.post("/:id/telemetry/tab", async (context) => {
984
+ const input = await readBoundedRequestJson(context.req.raw, void 0, {}, { allowTextPlain: true });
985
+ await telemetry.captureTabViewed(parseReviewTabTelemetryInput(input), { reviewUuid: context.req.param("id") });
986
+ return context.json({ ok: true });
987
+ });
988
+ app.post("/:id/telemetry/bug-report", async (context) => {
989
+ const query = readQuerySchemas.get.parse({ version: context.req.query("version") });
990
+ const id = context.req.param("id");
991
+ const imported = id.startsWith("shared-") ? shared?.get(id) : void 0;
992
+ const snapshot = imported?.snapshot ?? store.read(id, query.version);
993
+ const report = parseReviewBugReportInput(await readBoundedRequestJson(context.req.raw, 6291456, {}));
994
+ return context.json(await submit({
995
+ report,
996
+ source: imported ? {
997
+ review: async () => ({
998
+ files: { "review.json": JSON.stringify(snapshot) },
999
+ omitted: []
1000
+ }),
1001
+ map: async () => JSON.stringify(imported.presentation.maps),
1002
+ diff: () => jsonReviewBugReportSource(store, snapshot).diff()
1003
+ } : jsonReviewBugReportSource(store, snapshot),
1004
+ clientErrorNames: clientErrorsForSession(report.app_session_id),
1005
+ telemetryEnvelope: await telemetry.envelope().catch(() => void 0)
1006
+ }));
1007
+ });
1008
+ return app;
1009
+ }
1010
+ //#endregion
1011
+ //#region src/server/review-lifecycle-telemetry.ts
1012
+ /**
1013
+ * Review created, published and revoked, and agent authoring completed, as
1014
+ * server events. Authoring is complete at the first publish of a review an
1015
+ * agent created through `whiteboard api` or `whiteboard mcp`, timed from the review's
1016
+ * creation. Which reviews an agent created is known only to this process, so
1017
+ * a server restart between create and publish loses that one completion.
1018
+ * Sign-in reports its funnel, and a success calls `onLoggedIn`.
1019
+ */
1020
+ function reviewLifecycleTelemetry(telemetry, firstCreatedAt, onLoggedIn, now = Date.now) {
1021
+ const reported = /* @__PURE__ */ new Set();
1022
+ const authoring = /* @__PURE__ */ new Map();
1023
+ const capture = (event, properties, reviewUuid) => void telemetry.captureEvent(event, properties, reviewUuid ? { reviewUuid } : void 0);
1024
+ return {
1025
+ onReviewCreated: ({ reviewId, kind, blocks, via, agentKind }) => {
1026
+ if (reported.has(reviewId)) return;
1027
+ reported.add(reviewId);
1028
+ if (via === "api" || via === "mcp") authoring.set(reviewId, agentKind);
1029
+ const properties = {
1030
+ kind,
1031
+ blocks,
1032
+ via
1033
+ };
1034
+ if (agentKind) properties.agent_kind = agentKind;
1035
+ capture("review_review_created", properties, reviewId);
1036
+ },
1037
+ sharing: {
1038
+ onPublished: ({ reviewId, version }) => {
1039
+ capture("review_review_published", { version }, reviewId);
1040
+ if (!authoring.has(reviewId)) return;
1041
+ const agentKind = authoring.get(reviewId);
1042
+ authoring.delete(reviewId);
1043
+ const createdAt = Date.parse(firstCreatedAt(reviewId) ?? "");
1044
+ if (Number.isNaN(createdAt)) return;
1045
+ const properties = { duration_ms: Math.max(0, now() - createdAt) };
1046
+ if (agentKind) properties.agent_kind = agentKind;
1047
+ capture("review_authoring_completed", properties, reviewId);
1048
+ },
1049
+ onRevoked: () => capture("review_review_revoked"),
1050
+ onLogin: (outcome, reason) => {
1051
+ capture(`review_login_${outcome}`, reason ? { reason } : {});
1052
+ if (outcome === "succeeded") onLoggedIn();
1053
+ }
1054
+ }
1055
+ };
1056
+ }
1057
+ //#endregion
1058
+ //#region src/server/review-open-watchdog.ts
1059
+ const OPEN_TIMEOUT_MS = 3e4;
1060
+ /**
1061
+ * "Opened but never presented" as an explicit event: one timer per
1062
+ * presentation from its session start, cleared when the canvas reports it
1063
+ * presented or the session ends. Keyed by presentation because a resume starts
1064
+ * a new session for the same review.
1065
+ */
1066
+ var ReviewOpenWatchdog = class {
1067
+ options;
1068
+ pending = /* @__PURE__ */ new Map();
1069
+ constructor(options) {
1070
+ this.options = options;
1071
+ }
1072
+ started(context) {
1073
+ const id = context.presentationSessionId;
1074
+ this.presented(id);
1075
+ const startedAt = Date.now();
1076
+ const timer = setTimeout(() => {
1077
+ this.pending.delete(id);
1078
+ this.options.onTimeout(context, Date.now() - startedAt);
1079
+ }, OPEN_TIMEOUT_MS);
1080
+ timer.unref?.();
1081
+ this.pending.set(id, timer);
1082
+ }
1083
+ presented(presentationSessionId) {
1084
+ clearTimeout(this.pending.get(presentationSessionId));
1085
+ this.pending.delete(presentationSessionId);
1086
+ }
1087
+ dispose() {
1088
+ for (const timer of this.pending.values()) clearTimeout(timer);
1089
+ this.pending.clear();
1090
+ }
1091
+ };
1092
+ //#endregion
1093
+ //#region src/server/tutorial-service.ts
1094
+ const authoredSchema = z.strictObject({
1095
+ title: z.string().min(1),
1096
+ document: documentSchema
1097
+ });
1098
+ const pinsSchema = z.strictObject({
1099
+ base: z.string().regex(/^[a-f0-9]{40}$/),
1100
+ head: z.string().regex(/^[a-f0-9]{40}$/)
1101
+ });
1102
+ const stampSchema = z.object({
1103
+ version: z.literal(10),
1104
+ reviewUuid: z.string(),
1105
+ contentHash: z.string()
1106
+ });
1107
+ async function readTutorialAssets(assetsRoot) {
1108
+ const bytes = await Promise.all([
1109
+ "document.json",
1110
+ "trace.json",
1111
+ "software-map.json",
1112
+ "pins.json"
1113
+ ].map((name) => readFile(path.join(assetsRoot, name), "utf8")));
1114
+ return {
1115
+ authored: authoredSchema.parse(JSON.parse(bytes[0])),
1116
+ trace: JSON.parse(bytes[1]),
1117
+ model: JSON.parse(bytes[2]),
1118
+ pins: pinsSchema.parse(JSON.parse(bytes[3])),
1119
+ contentHash: createHash("sha256").update(JSON.stringify(bytes)).digest("hex")
1120
+ };
1121
+ }
1122
+ /** Create the shipped document through the same strict validation as authored reviews. */
1123
+ async function createNativeTutorial(input) {
1124
+ const assets = await readTutorialAssets(input.assetsRoot);
1125
+ const repository = await input.data.register(input.sampleRoot);
1126
+ const pins = await input.data.resolvePins(repository.id, assets.pins.base, assets.pins.head);
1127
+ const aliases = /* @__PURE__ */ new Map();
1128
+ const traceId = randomUUID();
1129
+ await input.data.upload({
1130
+ id: traceId,
1131
+ repositoryId: repository.id,
1132
+ kind: "trace",
1133
+ trace: assets.trace
1134
+ });
1135
+ aliases.set("tutorial-trace", traceId);
1136
+ for (const side of ["base", "head"]) {
1137
+ const id = randomUUID();
1138
+ await input.data.upload({
1139
+ id,
1140
+ repositoryId: repository.id,
1141
+ kind: "map",
1142
+ pins,
1143
+ side,
1144
+ model: assets.model
1145
+ });
1146
+ aliases.set(`tutorial-map-${side}`, id);
1147
+ }
1148
+ for (const block of resourceReferences(assets.authored.document)) {
1149
+ if (block.type === "trace_quote") block.traceId = aliases.get(block.traceId) ?? block.traceId;
1150
+ if (block.type === "software_map") block.mapVersionId = aliases.get(block.mapVersionId) ?? block.mapVersionId;
1151
+ }
1152
+ const result = await input.store.execute({ operation: {
1153
+ type: "create",
1154
+ title: assets.authored.title,
1155
+ target: {
1156
+ kind: "commits",
1157
+ ...pins
1158
+ }
1159
+ } }, {
1160
+ document: assets.authored.document,
1161
+ origin: { tutorial: true }
1162
+ });
1163
+ return {
1164
+ snapshot: input.store.read(result.reviewId),
1165
+ contentHash: assets.contentHash
1166
+ };
1167
+ }
1168
+ function createTutorialService(input) {
1169
+ const tutorialRoot = path.join(devReviewHome(), "tutorial");
1170
+ const sampleRoot = path.join(tutorialRoot, "sample-service");
1171
+ const stampPath = path.join(tutorialRoot, "stamp.json");
1172
+ const assetsRoot = path.join(input.packageRoot, "tutorial");
1173
+ const readStamp = async () => stampSchema.safeParse(await readFile(stampPath, "utf8").then((value) => JSON.parse(value)).catch(() => null)).data;
1174
+ async function find() {
1175
+ try {
1176
+ const stamp = await readStamp();
1177
+ if (!stamp || !input.store.has(stamp.reviewUuid)) return null;
1178
+ const snapshot = input.store.read(stamp.reviewUuid);
1179
+ const assets = await readTutorialAssets(assetsRoot);
1180
+ const [head, base] = await Promise.all([resolveRevision(sampleRoot, "HEAD"), resolveRevision(sampleRoot, "HEAD^")]);
1181
+ return snapshot.origin?.tutorial && stamp.contentHash === assets.contentHash && head && base && snapshot.pins?.head === head.commit && snapshot.pins?.base === base.commit && head.commit === assets.pins.head && base.commit === assets.pins.base ? snapshot : null;
1182
+ } catch {
1183
+ return null;
1184
+ }
1185
+ }
1186
+ async function cleanup() {
1187
+ for (const reviewId of input.store.tutorialIds()) await input.store.execute({ operation: {
1188
+ type: "delete",
1189
+ reviewId
1190
+ } });
1191
+ for (const repository of input.store.repositories()) {
1192
+ const relative = path.relative(tutorialRoot, repository.path);
1193
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) await input.data.forgetRepository(repository.id);
1194
+ }
1195
+ await rm(tutorialRoot, {
1196
+ recursive: true,
1197
+ force: true,
1198
+ maxRetries: 10
1199
+ });
1200
+ }
1201
+ return {
1202
+ async status() {
1203
+ return {
1204
+ version: 1,
1205
+ reviewUuid: (await find())?.reviewId ?? null
1206
+ };
1207
+ },
1208
+ async referencesReview(id) {
1209
+ return input.store.has(id) && input.store.read(id).origin?.tutorial === true;
1210
+ },
1211
+ async prepare(options) {
1212
+ const current = await find();
1213
+ if (current) return current;
1214
+ await readTutorialAssets(assetsRoot);
1215
+ await options?.beforeReset();
1216
+ await cleanup();
1217
+ await mkdir(tutorialRoot, {
1218
+ recursive: true,
1219
+ mode: 448
1220
+ });
1221
+ const temporaryRoot = path.join(tutorialRoot, `.sample-service-${randomUUID()}`);
1222
+ try {
1223
+ await cp(path.join(assetsRoot, "sample-service"), temporaryRoot, { recursive: true });
1224
+ await cp(path.join(assetsRoot, "git-stub"), path.join(temporaryRoot, ".git"), { recursive: true });
1225
+ await rename(temporaryRoot, sampleRoot);
1226
+ const { snapshot, contentHash } = await createNativeTutorial({
1227
+ assetsRoot,
1228
+ sampleRoot,
1229
+ store: input.store,
1230
+ data: input.data
1231
+ });
1232
+ await writePrivateJsonAtomic(stampPath, {
1233
+ version: 10,
1234
+ reviewUuid: snapshot.reviewId,
1235
+ contentHash
1236
+ });
1237
+ return snapshot;
1238
+ } finally {
1239
+ await rm(temporaryRoot, {
1240
+ recursive: true,
1241
+ force: true
1242
+ });
1243
+ }
1244
+ },
1245
+ cleanup
1246
+ };
1247
+ }
1248
+ //#endregion
1249
+ //#region src/server/desktop-server.ts
1250
+ const TUTORIAL_LIFECYCLE_LOCK_KEY = "tutorial-lifecycle";
1251
+ function createGlobalReviewServer(input) {
1252
+ const instanceId = input.instanceId ?? crypto.randomUUID();
1253
+ const token = input.token ?? crypto.randomBytes(32).toString("base64url");
1254
+ let boundPort = input.port;
1255
+ const urlForBoundPort = () => `http://127.0.0.1:${boundPort}`;
1256
+ const identity = input.identity ?? {
1257
+ key: "stable",
1258
+ channel: "stable"
1259
+ };
1260
+ const discoveryPaths = input.discoveryPath ? [input.discoveryPath] : identity.key === "stable" ? [reviewInstanceDiscoveryPath("stable"), reviewLegacyDiscoveryPath()] : [reviewInstanceDiscoveryPath(identity.key)];
1261
+ const telemetry = input.telemetry ?? ReviewTelemetry.fromEnv();
1262
+ const relay = input.relay ?? new GlobalReviewDesktopVerbRelay({ maxClients: 1 });
1263
+ const askCliEnv = () => [
1264
+ {
1265
+ name: REVIEW_INSTANCE_ENV,
1266
+ value: identity.key
1267
+ },
1268
+ ...process.versions.electron ? [{
1269
+ name: "ELECTRON_RUN_AS_NODE",
1270
+ value: "1"
1271
+ }] : [],
1272
+ ...process.env.DEV_REVIEW_HOME ? [{
1273
+ name: "DEV_REVIEW_HOME",
1274
+ value: process.env.DEV_REVIEW_HOME
1275
+ }] : []
1276
+ ];
1277
+ const askTools = {
1278
+ mcpServers: () => discovery.cliPath ? [{
1279
+ name: "whiteboard",
1280
+ command: process.execPath,
1281
+ args: [discovery.cliPath, "mcp"],
1282
+ env: askCliEnv()
1283
+ }] : [],
1284
+ cli: () => discovery.cliPath && [
1285
+ ...askCliEnv().map(({ name, value }) => `${name}=${shellQuote(value)}`),
1286
+ shellQuote(process.execPath),
1287
+ shellQuote(discovery.cliPath)
1288
+ ].join(" ")
1289
+ };
1290
+ const reviewStore = input.reviewStore;
1291
+ const reviewLocks = /* @__PURE__ */ new Map();
1292
+ let appReadyReported = false;
1293
+ const openWatchdog = new ReviewOpenWatchdog({ onTimeout: (context, elapsedMs) => void telemetry.captureEvent("review_open_timeout", { elapsed_ms: elapsedMs }, context) });
1294
+ const tutorial = createTutorialService({
1295
+ packageRoot: input.packageRoot,
1296
+ store: reviewStore,
1297
+ data: input.reviewData
1298
+ });
1299
+ let closing = false;
1300
+ const cliPath = path.join(input.packageRoot, "dist", "cli.js");
1301
+ let scratchpadEnabled = false;
1302
+ const discovery = {
1303
+ version: 3,
1304
+ instanceId,
1305
+ url: urlForBoundPort(),
1306
+ appPid: input.appPid,
1307
+ serverPid: process.pid,
1308
+ token,
1309
+ startedAt: Date.now(),
1310
+ ...identity
1311
+ };
1312
+ if (existsSync(cliPath)) {
1313
+ discovery.cliPath = cliPath;
1314
+ discovery.cliVersion = readReviewPackageVersion(pathToFileURL(cliPath).href);
1315
+ if (input.cliRuntimePath && existsSync(input.cliRuntimePath)) discovery.cliRuntimePath = input.cliRuntimePath;
1316
+ }
1317
+ const core = createWhiteboardCore({
1318
+ profile: {
1319
+ store: reviewStore,
1320
+ data: input.reviewData,
1321
+ shared: input.sharedReviews
1322
+ },
1323
+ relay,
1324
+ token,
1325
+ instanceId,
1326
+ scratchpad: () => scratchpadEnabled,
1327
+ status: () => {
1328
+ const { key, channel, checkout, appVersion, cliVersion, instanceId } = discovery;
1329
+ return {
1330
+ key: key ?? null,
1331
+ channel: channel ?? null,
1332
+ checkout: checkout ?? null,
1333
+ appVersion: appVersion ?? null,
1334
+ cliVersion: cliVersion ?? null,
1335
+ instanceId,
1336
+ url: urlForBoundPort(),
1337
+ home: devReviewHome()
1338
+ };
1339
+ },
1340
+ hooks: reviewLifecycleTelemetry(telemetry, (reviewId) => reviewStore.summary(reviewId)?.firstCreatedAt, () => aliasInstallationToAccount(telemetry)),
1341
+ ask: { tools: askTools }
1342
+ });
1343
+ const { app } = core;
1344
+ app.route("/reviews-api", createJsonReviewReporting(input.reviewStore, telemetry, { shared: input.sharedReviews }));
1345
+ app.route("/reviews-api", core.api);
1346
+ app.get("/preferences/scratchpad", () => serverJson(200, { enabled: scratchpadEnabled }));
1347
+ app.put("/preferences/scratchpad", async (context) => {
1348
+ const request = z.object({ enabled: z.boolean() }).safeParse(await readBoundedRequestJson(context.req.raw));
1349
+ if (!request.success) throw new ReviewServerError("enabled must be a boolean.", 400);
1350
+ scratchpadEnabled = await writeScratchpadEnabled(request.data.enabled);
1351
+ if (scratchpadEnabled) await reviewStore.ensureScratchpad();
1352
+ reviewStore.invalidateCatalog();
1353
+ return serverJson(200, { enabled: scratchpadEnabled });
1354
+ });
1355
+ app.post("/app/focus", async () => {
1356
+ const result = await relay.dispatch({
1357
+ name: "focusWindow",
1358
+ args: {}
1359
+ });
1360
+ return serverJson(result.ok ? 200 : 409, result);
1361
+ });
1362
+ app.post("/telemetry/event", async (context) => {
1363
+ try {
1364
+ const body = await readBoundedRequestJson(context.req.raw, void 0, {});
1365
+ const payload = isJsonObject(body) ? body : {};
1366
+ let flushBeforeOptOut = false;
1367
+ if (payload.name === "app_ready") {
1368
+ if (appReadyReported) return serverJson(200, { ok: true });
1369
+ appReadyReported = true;
1370
+ }
1371
+ const eventProperties = isJsonObject(payload.properties) ? { ...payload.properties } : {};
1372
+ if (payload.name === "session_started") {
1373
+ delete eventProperties.source_kind;
1374
+ delete eventProperties.agent_kind;
1375
+ const rawContext = isJsonObject(payload.context) ? payload.context : {};
1376
+ const sourceKind = sessionStartedSourceKind(reviewStore, jsonString(rawContext.reviewUuid));
1377
+ if (sourceKind) eventProperties.source_kind = sourceKind;
1378
+ }
1379
+ await captureSanitizedUiTelemetry(telemetry, context.req.raw, payload.name, eventProperties, (event, eventContext) => {
1380
+ flushBeforeOptOut = event.event === "review_setting_changed" && event.properties.setting === "telemetry_enabled" && event.properties.enabled === false;
1381
+ watchSessionOpen(openWatchdog, event.event, eventContext);
1382
+ }, payload.error, payload.context, payload.occurredAt);
1383
+ if (flushBeforeOptOut) await telemetry.flush(500);
1384
+ } catch (error) {
1385
+ console.error(error);
1386
+ }
1387
+ return serverJson(200, { ok: true });
1388
+ });
1389
+ app.post("/crash-reports", async (context) => {
1390
+ const body = CrashReportRequestSchema.safeParse(await readBoundedRequestJson(context.req.raw));
1391
+ if (!body.success) throw new ReviewServerError("dump_path, crashed_at and covered are required.", 400);
1392
+ const result = await reportCrashDump(telemetry, body.data, input.crashDumpsDir);
1393
+ return serverJson(result.status, result.body);
1394
+ });
1395
+ app.get("/tutorial/status", async () => serverJson(200, await tutorial.status()));
1396
+ app.post("/tutorial/prepare", async () => {
1397
+ const prepared = await withReviewLock(TUTORIAL_LIFECYCLE_LOCK_KEY, prepareTutorialLocked);
1398
+ return serverJson(200, {
1399
+ ok: true,
1400
+ reviewUuid: prepared.reviewId
1401
+ });
1402
+ });
1403
+ app.post("/tutorial/open", async () => {
1404
+ return serverJson(200, await withReviewLock(TUTORIAL_LIFECYCLE_LOCK_KEY, openTutorialLocked));
1405
+ });
1406
+ app.delete("/tutorial", async () => {
1407
+ await withReviewLock(TUTORIAL_LIFECYCLE_LOCK_KEY, deleteTutorialLocked);
1408
+ return serverJson(200, { ok: true });
1409
+ });
1410
+ app.post("/diffr-languages/install", async () => {
1411
+ try {
1412
+ await installFullDiffr();
1413
+ invalidateStructuralComparisons();
1414
+ return serverJson(200, { installed: true });
1415
+ } catch (error) {
1416
+ return serverJson(503, { error: error instanceof Error ? error.message : String(error) });
1417
+ }
1418
+ });
1419
+ app.get("/diffr-config", async () => serverJson(200, await readDiffrConfig()));
1420
+ app.put("/diffr-config", async (context) => {
1421
+ const body = await readBoundedRequestJson(context.req.raw);
1422
+ const key = isJsonObject(body) ? jsonString(body.key) : void 0;
1423
+ const value = isJsonObject(body) ? body.value : void 0;
1424
+ if (key === void 0 || value === void 0) throw new ReviewServerError("key and value are required.", 400);
1425
+ return serverJson(200, await setDiffrConfigValue(key, value));
1426
+ });
1427
+ app.put("/diffr-config/summarizer", async (context) => {
1428
+ const input = reviewDiffrSummarizerInputSchema.safeParse(await readBoundedRequestJson(context.req.raw));
1429
+ if (!input.success) throw new ReviewServerError("Invalid summary settings.", 400);
1430
+ return serverJson(200, await saveDiffrSummarizer(input.data));
1431
+ });
1432
+ app.post("/diffr-config/summarizer/test", async (context) => {
1433
+ const input = reviewDiffrSummarizerInputSchema.safeParse(await readBoundedRequestJson(context.req.raw));
1434
+ if (!input.success) throw new ReviewServerError("Invalid summary settings.", 400);
1435
+ return serverJson(200, { summary: await testDiffrSummarizer(input.data, void 0, context.req.raw.signal) });
1436
+ });
1437
+ app.get("/install/status", async () => serverJson(200, await resolveCliInstallStatus({ packageRoot: input.packageRoot })));
1438
+ app.post("/install/apply", async (context) => {
1439
+ const request = parseReviewCliInstallApplyRequest(await readBoundedRequestJson(context.req.raw));
1440
+ const applyInput = { packageRoot: input.packageRoot };
1441
+ if (request.shim !== void 0) applyInput.shim = request.shim;
1442
+ if (request.autoUpdate) applyInput.autoUpdate = true;
1443
+ if (request.trace !== void 0) applyInput.trace = request.trace;
1444
+ if (discovery.cliPath) applyInput.cliPath = discovery.cliPath;
1445
+ if (discovery.cliRuntimePath) applyInput.cliRuntimePath = discovery.cliRuntimePath;
1446
+ const result = await applyCliInstall(applyInput);
1447
+ const body = {
1448
+ ok: result.code === 0,
1449
+ output: result.output
1450
+ };
1451
+ if (result.shimPath) body.shimPath = result.shimPath;
1452
+ return serverJson(result.code === 0 ? 200 : 500, body);
1453
+ });
1454
+ app.post("/install/remove", async (context) => {
1455
+ const request = parseReviewCliInstallApplyRequest(await readBoundedRequestJson(context.req.raw));
1456
+ const removeInput = {};
1457
+ if (request.shim) removeInput.shim = true;
1458
+ if (request.trace) removeInput.trace = true;
1459
+ const result = await removeCliInstall(removeInput);
1460
+ return serverJson(200, {
1461
+ ok: true,
1462
+ output: result.output
1463
+ });
1464
+ });
1465
+ app.post("/install/legacy-skills/remove", async () => {
1466
+ const { removed } = await removeLegacyReviewSkills();
1467
+ return serverJson(200, {
1468
+ ok: true,
1469
+ removed
1470
+ });
1471
+ });
1472
+ app.post("/install/finish-update", async () => {
1473
+ await finishCliInstallUpdate();
1474
+ return serverJson(200, { ok: true });
1475
+ });
1476
+ app.post("/install/decline", async () => {
1477
+ await declineCliInstall();
1478
+ return serverJson(200, { ok: true });
1479
+ });
1480
+ app.post("/install/skip", async () => {
1481
+ await skipCliInstall();
1482
+ return serverJson(200, { ok: true });
1483
+ });
1484
+ app.post("/install/reset", async () => {
1485
+ await resetCliInstall();
1486
+ return serverJson(200, { ok: true });
1487
+ });
1488
+ const httpServer = createServer(createNodeRequestListener(app));
1489
+ async function prepareTutorialLocked() {
1490
+ return tutorial.prepare();
1491
+ }
1492
+ async function openTutorialLocked() {
1493
+ const snapshot = await prepareTutorialLocked();
1494
+ return {
1495
+ kind: "api",
1496
+ reviewUuid: snapshot.reviewId,
1497
+ title: snapshot.title
1498
+ };
1499
+ }
1500
+ async function deleteTutorialLocked() {
1501
+ await tutorial.cleanup();
1502
+ }
1503
+ async function withReviewLock(reviewUuid, operation) {
1504
+ const previous = reviewLocks.get(reviewUuid) ?? Promise.resolve();
1505
+ let release = () => {};
1506
+ const current = new Promise((resolve) => {
1507
+ release = resolve;
1508
+ });
1509
+ const chain = previous.then(() => current);
1510
+ reviewLocks.set(reviewUuid, chain);
1511
+ await previous;
1512
+ try {
1513
+ return await operation();
1514
+ } finally {
1515
+ release();
1516
+ if (reviewLocks.get(reviewUuid) === chain) reviewLocks.delete(reviewUuid);
1517
+ }
1518
+ }
1519
+ return {
1520
+ discovery,
1521
+ get url() {
1522
+ return urlForBoundPort();
1523
+ },
1524
+ listen: async () => {
1525
+ scratchpadEnabled = await readScratchpadEnabled();
1526
+ boundPort = await listen(httpServer, input.port);
1527
+ discovery.url = urlForBoundPort();
1528
+ for (const discoveryPath of discoveryPaths) await writePrivateJsonAtomic(discoveryPath, discovery);
1529
+ },
1530
+ close: async () => {
1531
+ if (closing) return;
1532
+ closing = true;
1533
+ core.close();
1534
+ for (const discoveryPath of discoveryPaths) await removeMatchingDiscovery(discoveryPath, discovery);
1535
+ relay.close();
1536
+ openWatchdog.dispose();
1537
+ await closeHttpServer(httpServer);
1538
+ await telemetry.shutdown(1500);
1539
+ }
1540
+ };
1541
+ }
1542
+ function watchSessionOpen(watchdog, event, context) {
1543
+ const reviewUuid = context?.reviewUuid;
1544
+ const presentationSessionId = context?.presentationSessionId;
1545
+ if (!reviewUuid || !presentationSessionId) return;
1546
+ if (event === "review_session_started") watchdog.started({
1547
+ reviewUuid,
1548
+ presentationSessionId
1549
+ });
1550
+ else if (event === "review_review_presented" || event === "review_session_ended") watchdog.presented(presentationSessionId);
1551
+ }
1552
+ /**
1553
+ * `session_started`'s `source_kind`: the opened review's target kind, or
1554
+ * `scratchpad` for the one scratchpad. Undefined when the review is gone (a
1555
+ * shared review this store never had, or one deleted between open and the
1556
+ * event arriving) or was left without a target.
1557
+ */
1558
+ function sessionStartedSourceKind(reviewStore, reviewUuid) {
1559
+ if (!reviewUuid) return void 0;
1560
+ try {
1561
+ const snapshot = reviewStore.read(reviewUuid);
1562
+ return snapshot.kind === "scratchpad" ? snapshot.kind : snapshot.target?.kind;
1563
+ } catch {
1564
+ return;
1565
+ }
1566
+ }
1567
+ function listen(server, port) {
1568
+ return new Promise((resolve, reject) => {
1569
+ server.once("error", reject);
1570
+ server.listen(port, "127.0.0.1", () => {
1571
+ server.off("error", reject);
1572
+ const address = server.address();
1573
+ if (!isTcpAddress(address)) {
1574
+ reject(/* @__PURE__ */ new Error("The Whiteboard server did not bind a TCP port."));
1575
+ return;
1576
+ }
1577
+ resolve(address.port);
1578
+ });
1579
+ });
1580
+ }
1581
+ function closeHttpServer(server) {
1582
+ return new Promise((resolve, reject) => {
1583
+ if (!server.listening) {
1584
+ resolve();
1585
+ return;
1586
+ }
1587
+ server.close((error) => error ? reject(error) : resolve());
1588
+ });
1589
+ }
1590
+ async function removeMatchingDiscovery(filePath, discovery) {
1591
+ try {
1592
+ const current = JSON.parse(await readFile(filePath, "utf8"));
1593
+ if (isJsonObject(current) && current.instanceId === discovery.instanceId && current.appPid === discovery.appPid) await rm(filePath, { force: true });
1594
+ } catch (error) {
1595
+ if (error.code !== "ENOENT") throw error;
1596
+ }
1597
+ }
1598
+ /** `server.address()` is a string for pipe and socket listeners. */
1599
+ function isTcpAddress(address) {
1600
+ return isObjectValue(address);
1601
+ }
1602
+ //#endregion
1603
+ //#region src/server/desktop-host.ts
1604
+ async function runDesktopHost(env = process.env) {
1605
+ const port = requiredPort(env.DEV_FAST_REVIEW_SERVER_PORT);
1606
+ const appPid = requiredPositiveInteger(env.DEV_FAST_REVIEW_APP_PID, "DEV_FAST_REVIEW_APP_PID");
1607
+ const packageRoot = findReviewPackageRoot(import.meta.url);
1608
+ const toolingRoot = env.DEV_FAST_REVIEW_TOOLING_ROOT || packageRoot;
1609
+ const telemetryEnv = { ...env };
1610
+ delete telemetryEnv.DEV_FAST_REVIEW_TELEMETRY_DISABLED;
1611
+ const telemetry = ReviewTelemetry.fromEnv(telemetryEnv, { surface: "desktop" });
1612
+ installProcessErrorTelemetry(telemetry, {
1613
+ appSessionId: env.DEV_FAST_REVIEW_APP_SESSION_ID,
1614
+ crashReportPath: serverCrashReportPath(env)
1615
+ });
1616
+ await telemetry.setEnabled(!isEnabledEnvValue(env.DEV_FAST_REVIEW_TELEMETRY_DISABLED));
1617
+ if (reviewTelemetryChannel(env) === "dev") await telemetry.setInternal(true);
1618
+ await telemetry.captureInstallationCreated().catch(() => void 0);
1619
+ await telemetry.reconcileOpenSessions().catch(() => void 0);
1620
+ await drainServerCrashReport(telemetry, serverCrashReportPath(env));
1621
+ const installationId = await telemetry.getInstallationId();
1622
+ delete env.DEV_FAST_REVIEW_TELEMETRY_DISABLED;
1623
+ const serverInput = {
1624
+ appPid,
1625
+ packageRoot,
1626
+ toolingRoot,
1627
+ port,
1628
+ token: env.DEV_FAST_REVIEW_SERVER_TOKEN,
1629
+ instanceId: env.DEV_FAST_REVIEW_INSTANCE_ID,
1630
+ identity: reviewInstanceIdentity(env),
1631
+ telemetry
1632
+ };
1633
+ const home = devReviewHome(env);
1634
+ await mkdir(home, { recursive: true });
1635
+ const local = await openReviewProfile(home, { manageWorkspaces: true });
1636
+ const shared = new SharedReviewStore(path.join(home, "shared-reviews"));
1637
+ shared.connect(local.store, local.data);
1638
+ await shared.load();
1639
+ const server = createGlobalReviewServer({
1640
+ ...serverInput,
1641
+ sharedReviews: shared,
1642
+ reviewStore: local.store,
1643
+ reviewData: local.data,
1644
+ cliRuntimePath: env.DEV_FAST_REVIEW_CLI_RUNTIME,
1645
+ crashDumpsDir: env.DEV_FAST_REVIEW_CRASH_DUMPS_DIR
1646
+ });
1647
+ try {
1648
+ await server.listen();
1649
+ } catch (error) {
1650
+ await local?.data.close();
1651
+ await local?.store.close();
1652
+ throw error;
1653
+ }
1654
+ process.stdout.write(`${JSON.stringify({
1655
+ event: "ready",
1656
+ ...server.discovery,
1657
+ installationId
1658
+ })}\n`);
1659
+ const stageRustAnalyzer = () => ensureBundledRustAnalyzer({ env }).catch((error) => {
1660
+ const reason = error instanceof Error ? error.message : String(error);
1661
+ process.stderr.write(`[Review tools] Could not stage bundled rust-analyzer: ${reason}\n`);
1662
+ });
1663
+ stageRustAnalyzer();
1664
+ let stopping = null;
1665
+ const stop = () => {
1666
+ if (!stopping) stopping = server.close("app-exit").finally(() => shared.close()).finally(() => local?.data.close()).finally(() => local?.store.close());
1667
+ return stopping;
1668
+ };
1669
+ listenForDesktopHostShutdown(process, () => {
1670
+ stop().then(() => process.exit(0));
1671
+ }, (enabled) => {
1672
+ telemetry.setEnabled(enabled).catch(() => void 0);
1673
+ }, (sourcePath) => {
1674
+ env.DEV_FAST_REVIEW_RUST_ANALYZER = sourcePath;
1675
+ stageRustAnalyzer();
1676
+ });
1677
+ process.once("SIGINT", () => {
1678
+ stop().then(() => process.exit(0));
1679
+ });
1680
+ process.once("SIGTERM", () => {
1681
+ stop().then(() => process.exit(0));
1682
+ });
1683
+ }
1684
+ function isEnabledEnvValue(value) {
1685
+ return value === "1" || value?.toLowerCase() === "true";
1686
+ }
1687
+ function requiredPort(value) {
1688
+ const parsed = Number(value);
1689
+ if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 65535) throw new Error("DEV_FAST_REVIEW_SERVER_PORT must be a port between 0 and 65535.");
1690
+ return parsed;
1691
+ }
1692
+ function requiredPositiveInteger(value, name) {
1693
+ const parsed = Number(value);
1694
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer.`);
1695
+ return parsed;
1696
+ }
1697
+ if (process.env.DEV_FAST_REVIEW_DESKTOP_HOST_AUTOSTART !== "0") runDesktopHost().catch((error) => {
1698
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
1699
+ process.exitCode = 1;
1700
+ });
1701
+ //#endregion
1702
+ export { runDesktopHost };