@dev.fast/whiteboard 0.3.1-preview.20261016.122 → 0.3.2

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 (30) hide show
  1. package/dist/{account-alias-CwNaMOwU.js → account-alias-D_mqo_Jk.js} +2 -2
  2. package/dist/agent-cli-CoHvydp7.js +2 -0
  3. package/dist/{agent-cli-S0bt9I7V.js → agent-cli-DjLmorAW.js} +22 -3
  4. package/dist/{authoring-tools-CjFAGHBa.js → authoring-tools-ByMdW_S8.js} +1 -1
  5. package/dist/build-info.json +1 -1
  6. package/dist/{cli-runner-DeQ4IhcG.js → cli-runner-DEKbykD8.js} +7 -7
  7. package/dist/cli.js +1 -1
  8. package/dist/fs-utils-BMPLt0cr.js +22 -0
  9. package/dist/{fuzzy-match-DFPKdSLR.js → fuzzy-match-C9olHRCK.js} +1 -1
  10. package/dist/{headless-host-CoN2f0Sk.js → headless-host-CnCDrHTx.js} +6 -6
  11. package/dist/{headless-host-DHShakz_.js → headless-host-DRwHcAuF.js} +1 -1
  12. package/dist/{local-data-DN-A5p0K.js → local-data-DKmj2ygd.js} +5 -3
  13. package/dist/{mcp-BY4ExeMQ.js → mcp-D-7jkRTU.js} +1 -1
  14. package/dist/{profile-Dm6irjbg.js → profile-B58hz5n7.js} +1 -1
  15. package/dist/profile-D1rScZ-E.js +2 -0
  16. package/dist/{request-origin-D1RNVSeK.js → request-origin-BSwTNS8K.js} +93 -3
  17. package/dist/{review-telemetry-DnQNwgR1.js → review-telemetry-ClhP_5lW.js} +1 -1
  18. package/dist/server/desktop-host.js +47 -24
  19. package/dist/sharing/index.d.ts +1 -0
  20. package/dist/{thread-state-CaQ2lGdj.js → thread-state-DZihwhfu.js} +1 -21
  21. package/package.json +3 -3
  22. package/src/review-api/agent-cli.ts +2 -0
  23. package/src/review-api/install-update.ts +27 -0
  24. package/src/review-api/local-data.ts +3 -2
  25. package/src/server/desktop-server.ts +43 -16
  26. package/src/server/json-review-reporting.ts +24 -15
  27. package/src/server/review-gateway.ts +12 -1
  28. package/src/ui-telemetry-events.ts +87 -1
  29. package/dist/agent-cli-NWQPsL0W.js +0 -2
  30. package/dist/profile-BBw-jMW3.js +0 -2
@@ -3,8 +3,8 @@ import { w as ReviewCliInstallStampSchema } from "./src-DZG1xAqW.js";
3
3
  import { C as disableTraceMachine, E as traceMachineStatus, G as listTraceRepositoryRoots, K as traceRepositoryStatus, Q as removeAgentTraceHook, S as configureTraceMachine, T as traceMachineEnabled, U as disableAllTraceRepositories, W as enableTraceRepository, X as describeTraceHookOwners, Y as AGENT_TRACE_HOOK_AGENTS, Z as installHarnessHooks, dt as StoreClient, n as reviewDesktopStateDir, nt as traceScope, ot as readStoreAuth } from "./review-home-paths-Lc02WFuX.js";
4
4
  import { A as withFileLock, F as writeFileAtomicAsync, I as writePrivateJsonAtomic, M as devReviewHome } from "./s3-config-DDWWMh5x.js";
5
5
  import { i as readReviewPackageVersion } from "./package-paths-jMg6JxDP.js";
6
- import { f as isDirectory, p as isFile } from "./thread-state-CaQ2lGdj.js";
7
- import { s as findExecutable } from "./fuzzy-match-DFPKdSLR.js";
6
+ import { n as isFile, t as isDirectory } from "./fs-utils-BMPLt0cr.js";
7
+ import { s as findExecutable } from "./fuzzy-match-C9olHRCK.js";
8
8
  import { createHash } from "node:crypto";
9
9
  import { constants } from "node:fs";
10
10
  import { access, chmod, lstat, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
@@ -0,0 +1,2 @@
1
+ import { n as runReviewAgentCli } from "./agent-cli-DjLmorAW.js";
2
+ export { runReviewAgentCli };
@@ -2,10 +2,28 @@ import { n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
2
2
  import { T as traceMachineEnabled } from "./review-home-paths-Lc02WFuX.js";
3
3
  import { a as toolResultText, i as connectReviewInstance, s as ReviewApiError } from "./agent-client-DjXQy6CL.js";
4
4
  import { M as devReviewHome } from "./s3-config-DDWWMh5x.js";
5
+ import { n as readBuildCommit } from "./package-paths-jMg6JxDP.js";
5
6
  import { b as selectReviewInstance, u as REVIEW_INSTANCE_ENV } from "./server-discovery-DVqrcIDT.js";
6
- import { n as reviewSessionAgent } from "./review-telemetry-DnQNwgR1.js";
7
- import { n as REVIEW_VIA_HEADER, t as REVIEW_AGENT_HEADER } from "./request-origin-D1RNVSeK.js";
7
+ import { n as reviewSessionAgent } from "./review-telemetry-ClhP_5lW.js";
8
+ import { n as REVIEW_VIA_HEADER, t as REVIEW_AGENT_HEADER } from "./request-origin-BSwTNS8K.js";
8
9
  import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-CVJrfi5s.js";
10
+ import { existsSync } from "node:fs";
11
+ import path from "node:path";
12
+ //#region src/review-api/install-update.ts
13
+ /** Exits once an update replaces this install, or the Windows updater marks it. */
14
+ function exitOnInstallUpdate(stderr) {
15
+ const commit = readBuildCommit(import.meta.url);
16
+ if (!commit) return;
17
+ const marker = path.join(path.dirname(process.execPath), "applying_update");
18
+ const staleMarker = existsSync(marker);
19
+ const timer = setInterval(() => {
20
+ if (!staleMarker && existsSync(marker) || readBuildCommit(import.meta.url) !== commit) {
21
+ clearInterval(timer);
22
+ stderr.write("Whiteboard was updated. Restart its MCP server.\n", () => process.exit(75));
23
+ }
24
+ }, 1e3).unref();
25
+ }
26
+ //#endregion
9
27
  //#region src/review-api/agent-cli.ts
10
28
  const reviewAgentCliHelp = "whiteboard api tools (one line per tool)\nwhiteboard api tools <tool-name> (its description and input schema)\nwhiteboard api <tool-name> '<json>'\nwhiteboard api <tool-name> - (read JSON from stdin)\nwhiteboard mcp (stdio MCP adapter; Whiteboard Desktop or whiteboard server start must be running)\nSelect headless state with DEV_REVIEW_SERVER_DIR or whiteboard --state-dir <path> api/mcp.\n";
11
29
  async function runReviewAgentCli(input) {
@@ -20,7 +38,8 @@ async function runReviewAgentCli(input) {
20
38
  if (extra.length || mode === "mcp" && name) throw new Error("Unexpected arguments. Use whiteboard api --help.");
21
39
  const headers = Object.fromEntries([[REVIEW_VIA_HEADER, mode === "mcp" ? "mcp" : "api"], [REVIEW_AGENT_HEADER, reviewSessionAgent(env)]]);
22
40
  if (mode === "mcp") {
23
- const { serveReviewMcp } = await import("./mcp-BY4ExeMQ.js");
41
+ const { serveReviewMcp } = await import("./mcp-D-7jkRTU.js");
42
+ exitOnInstallUpdate(input.stderr);
24
43
  await serveReviewMcp(async (key, agentKind) => {
25
44
  const connected = await connectReviewInstance(key ? {
26
45
  ...env,
@@ -1,4 +1,4 @@
1
- import { c as activityEndSchema, i as commandSchema, l as activityUpdateSchema, n as uploadSchema, s as activityBeginSchema } from "./local-data-DN-A5p0K.js";
1
+ import { c as activityEndSchema, i as commandSchema, l as activityUpdateSchema, n as uploadSchema, s as activityBeginSchema } from "./local-data-DKmj2ygd.js";
2
2
  import { S as publishedEditSchema, y as pathTargetSchema } from "./stored-document-migration-DLBhkSfp.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
4
  import { t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
@@ -1 +1 @@
1
- {"version":"0.3.1-preview.20261016.122","commit":"7cef417b896162d45b18541ced02e6d250cdc535","dirty":true,"builtAt":"2026-10-16T18:47:14.922Z"}
1
+ {"version":"0.3.2","commit":"38a6dad3049993f987d9b82ca3c5fee424b28f6b","dirty":true,"builtAt":"2026-10-16T21:59:03.536Z"}
@@ -8,11 +8,11 @@ import { C as s3ProfileSchema, E as writeTraceConfigFile, M as devReviewHome, N
8
8
  import { t as errorMessage } from "./error-message-OtiDonty.js";
9
9
  import { i as readReviewPackageVersion, r as readBuildInfo, t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
10
10
  import { a as readReviewServerHealth, b as selectReviewInstance, c as reviewServerStateDir, d as ReviewInstanceUnavailableError, f as clearDefaultReviewInstance, g as requireHealthyReviewDesktop, h as readDefaultReviewInstance, i as readReviewServerDiscovery, l as serverNotReady, m as healthyReviewInstance, p as desktopUsesHome, t as DESKTOP_TAKEOVER_SIGNAL, u as REVIEW_INSTANCE_ENV, x as writeDefaultReviewInstance, y as reviewInstanceUnavailable } from "./server-discovery-DVqrcIDT.js";
11
- import { t as ReviewTelemetry } from "./review-telemetry-DnQNwgR1.js";
12
- import { t as fuzzyRank } from "./fuzzy-match-DFPKdSLR.js";
13
- import { _ as isInstallTarget, a as isOwnedShim, f as windowsInstallerCommand, g as ALL_INSTALL_TARGETS, h as detectConnectAgents, m as connectAgent, o as pathShimPath, p as scanLegacySkills, t as aliasInstallationToAccount, v as connectPrompts } from "./account-alias-CwNaMOwU.js";
11
+ import { t as ReviewTelemetry } from "./review-telemetry-ClhP_5lW.js";
12
+ import { t as fuzzyRank } from "./fuzzy-match-C9olHRCK.js";
13
+ import { _ as isInstallTarget, a as isOwnedShim, f as windowsInstallerCommand, g as ALL_INSTALL_TARGETS, h as detectConnectAgents, m as connectAgent, o as pathShimPath, p as scanLegacySkills, t as aliasInstallationToAccount, v as connectPrompts } from "./account-alias-D_mqo_Jk.js";
14
14
  import { n as focusReviewDesktop, r as runReviewAppLaunch } from "./review-app-launcher-i1hmxOZA.js";
15
- import { t as reviewAgentCliHelp } from "./agent-cli-S0bt9I7V.js";
15
+ import { t as reviewAgentCliHelp } from "./agent-cli-DjLmorAW.js";
16
16
  import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
17
17
  import { realpath } from "node:fs/promises";
18
18
  import path from "node:path";
@@ -4319,7 +4319,7 @@ async function runReviewCli(input) {
4319
4319
  process.once("SIGINT", stop);
4320
4320
  process.once("SIGTERM", stop);
4321
4321
  process.once(DESKTOP_TAKEOVER_SIGNAL, takeover);
4322
- const { HeadlessServerBusyError, runHeadlessServer } = await import("./headless-host-DHShakz_.js");
4322
+ const { HeadlessServerBusyError, runHeadlessServer } = await import("./headless-host-DRwHcAuF.js");
4323
4323
  try {
4324
4324
  await runHeadlessServer({
4325
4325
  stateDir,
@@ -4384,7 +4384,7 @@ async function runReviewCli(input) {
4384
4384
  if (!existsSync(path.join(stateDir, "review-api.db"))) throw new Error(`No saved reviews in ${stateDir}, so there is no server id to reset. Check --state-dir.`);
4385
4385
  const inUse = `A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`;
4386
4386
  if (await desktopUsesHome(stateDir, env)) return refuse(inUse, options.json);
4387
- const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-BBw-jMW3.js"), import("./headless-host-DHShakz_.js")]);
4387
+ const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-D1rScZ-E.js"), import("./headless-host-DRwHcAuF.js")]);
4388
4388
  const reset = await withHeadlessServerLock(await realpath(stateDir), async () => {
4389
4389
  const local = await openReviewProfile(stateDir, { manageWorkspaces: false });
4390
4390
  try {
@@ -4758,7 +4758,7 @@ async function runReviewCli(input) {
4758
4758
  return commands;
4759
4759
  } });
4760
4760
  for (const [name, description] of [["api", "Call a JSON review authoring tool on the running server"], ["mcp", "Serve the JSON review authoring tools over stdio MCP"]]) configureOutput(program.command(name).description(description).argument("[args...]", "tool name and JSON input").allowUnknownOption().allowExcessArguments().helpOption(false).passThroughOptions().addHelpText("after", `\n${reviewAgentCliHelp}`), "plain").action(async (args) => {
4761
- const { runReviewAgentCli } = await import("./agent-cli-NWQPsL0W.js");
4761
+ const { runReviewAgentCli } = await import("./agent-cli-CoHvydp7.js");
4762
4762
  state.exitCode = await runReviewAgentCli({
4763
4763
  ...input,
4764
4764
  env: authoringEnv(),
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ async function runCli(effectivePath) {
17
17
  `);
18
18
  return 1;
19
19
  }
20
- const { runReviewCli } = await import("./cli-runner-DeQ4IhcG.js");
20
+ const { runReviewCli } = await import("./cli-runner-DEKbykD8.js");
21
21
  return runReviewCli({
22
22
  argv,
23
23
  cliPaths: {
@@ -0,0 +1,22 @@
1
+ import { stat } from "node:fs/promises";
2
+ //#region src/fs-utils.ts
3
+ /** Whether a thrown filesystem error reports a missing file (ENOENT). */
4
+ function isMissingFileError(cause) {
5
+ return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
6
+ }
7
+ async function isDirectory(target) {
8
+ try {
9
+ return (await stat(target)).isDirectory();
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+ async function isFile(target) {
15
+ try {
16
+ return (await stat(target)).isFile();
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ //#endregion
22
+ export { isFile as n, isMissingFileError as r, isDirectory as t };
@@ -1,6 +1,6 @@
1
1
  import "./review-home-paths-Lc02WFuX.js";
2
2
  import { M as devReviewHome } from "./s3-config-DDWWMh5x.js";
3
- import { n as askAgentIds } from "./thread-state-CaQ2lGdj.js";
3
+ import { n as askAgentIds } from "./thread-state-DZihwhfu.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { constants } from "node:fs";
6
6
  import { access, chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
@@ -1,7 +1,7 @@
1
1
  import { d as git, r as defaultBranch } from "./src-BMPuxWv4.js";
2
2
  import { c as jsonString, m as isStringValue, n as isJsonObject, p as isObjectValue, u as parseJsonText } from "./src-X9phtB2j.js";
3
3
  import { a as sharedReviewId, c as verifyShareRepository, d as StreamLimitError, f as readBoundedStream, l as exportShare, m as shareIdSchema, n as ShareClient, p as parseShareLink, r as SharePreflightError, s as readShareRepository, t as ShareAuthError, u as validateShareSources } from "./client-CLqKkzOc.js";
4
- import { a as inspectSnapshot, d as diffrExecutable, f as diffrMissingError, i as commandSchema, r as SCRATCHPAD_ID, u as invalidateStructuralComparisons } from "./local-data-DN-A5p0K.js";
4
+ import { a as inspectSnapshot, d as invalidateStructuralComparisons, f as diffrExecutable, i as commandSchema, p as diffrMissingError, r as SCRATCHPAD_ID } from "./local-data-DKmj2ygd.js";
5
5
  import { A as reviewDiffrSummarizerInputSchema, D as ReviewOnboardingRunRequestSchema, a as parseReviewVerbRequest, g as REVIEW_HOST_HEADER, i as parseReviewDesktopVerbResult, j as decodeStructuralDiffEvent } from "./src-DZG1xAqW.js";
6
6
  import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as openUrlInBrowser, et as shellQuote, it as clearStoreAuth, ot as readStoreAuth, rt as DEFAULT_STORE_ORIGIN, st as runStoreLogin, z as selectTraceStorage } from "./review-home-paths-Lc02WFuX.js";
7
7
  import { E as selectionReferences, G as comparisonKey, I as lensSelections, J as selectionKey, K as formatAnchor, M as UNCATEGORIZED_LENS_ID, Q as emptyCoverage, W as anchorSelection, Z as coverageSources, et as scopedCoverage, h as elements, j as stepSources, nt as unionIntervals, q as resolveDiffSelection, tt as subtractIntervals, u as anchorPins, z as fileLineRangeSchema } from "./stored-document-migration-DLBhkSfp.js";
@@ -10,11 +10,11 @@ import { A as withFileLock, I as writePrivateJsonAtomic, M as devReviewHome, h a
10
10
  import { t as errorMessage } from "./error-message-OtiDonty.js";
11
11
  import { i as readReviewPackageVersion, n as readBuildCommit, t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
12
12
  import { i as readReviewServerDiscovery, n as desktopOwnsHome, o as reviewServerDiscoveryPath, p as desktopUsesHome, r as headlessServerLockPath, t as DESKTOP_TAKEOVER_SIGNAL } from "./server-discovery-DVqrcIDT.js";
13
- import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-CaQ2lGdj.js";
14
- import { t as openReviewProfile } from "./profile-Dm6irjbg.js";
15
- import { a as REVIEW_APP_SESSION_ID_HEADER, c as sanitizeUiTelemetryEvent, i as BUNDLE_FRAME_PATTERN, l as cleanTelemetryText, r as reviewRequestOrigin, s as isReportableCleanedMessage } from "./request-origin-D1RNVSeK.js";
16
- import { a as defaultAskPermissions, c as launchAskAgent, i as askAgents, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-DFPKdSLR.js";
17
- import { a as scratchpadAvailable, c as readQuerySchemas, i as renderInstructions, o as inspectQuerySchema, r as instructionsQuerySchema, s as queryAnchor, t as authoringTools } from "./authoring-tools-CjFAGHBa.js";
13
+ import { c as askPicksSchema, d as selectionMarkdown, l as askQuestionSchema, n as askAgentIds, r as askChoiceKinds, s as askPermissionsSchema, t as applyAskChange, u as AgentSelectionSchema } from "./thread-state-DZihwhfu.js";
14
+ import { t as openReviewProfile } from "./profile-B58hz5n7.js";
15
+ import { a as REVIEW_APP_SESSION_ID_HEADER, c as sanitizeUiTelemetryEvent, i as BUNDLE_FRAME_PATTERN, l as cleanTelemetryText, r as reviewRequestOrigin, s as isReportableCleanedMessage } from "./request-origin-BSwTNS8K.js";
16
+ import { a as defaultAskPermissions, c as launchAskAgent, i as askAgents, n as askAgentSettings, o as detectAskAgents, r as askAgentTakesMcp, t as fuzzyRank } from "./fuzzy-match-C9olHRCK.js";
17
+ import { a as scratchpadAvailable, c as readQuerySchemas, i as renderInstructions, o as inspectQuerySchema, r as instructionsQuerySchema, s as queryAnchor, t as authoringTools } from "./authoring-tools-ByMdW_S8.js";
18
18
  import { a as headlessServerOwner, c as stopBackgroundServer } from "./background-server-ONy-xKJ1.js";
19
19
  import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
20
20
  import { z } from "zod";
@@ -1,2 +1,2 @@
1
- import { i as withHeadlessServerLock, n as runHeadlessServer, r as takeOverServerHome, t as HeadlessServerBusyError } from "./headless-host-CoN2f0Sk.js";
1
+ import { i as withHeadlessServerLock, n as runHeadlessServer, r as takeOverServerHome, t as HeadlessServerBusyError } from "./headless-host-CnCDrHTx.js";
2
2
  export { HeadlessServerBusyError, runHeadlessServer, takeOverServerHome, withHeadlessServerLock };
@@ -6,7 +6,8 @@ import { $ as intersectIntervals, A as documentAdvice, B as requireVisibleSource
6
6
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
7
7
  import { A as withFileLock, I as writePrivateJsonAtomic, O as processIsAlive } from "./s3-config-DDWWMh5x.js";
8
8
  import { t as errorMessage } from "./error-message-OtiDonty.js";
9
- import { a as askHistoryEntrySchema, i as askEntrySchema, m as isMissingFileError, o as askOfferSchema, s as askPermissionsSchema } from "./thread-state-CaQ2lGdj.js";
9
+ import { r as isMissingFileError } from "./fs-utils-BMPLt0cr.js";
10
+ import { a as askHistoryEntrySchema, i as askEntrySchema, o as askOfferSchema, s as askPermissionsSchema } from "./thread-state-DZihwhfu.js";
10
11
  import { createRequire } from "node:module";
11
12
  import crypto, { createHash, randomUUID } from "node:crypto";
12
13
  import { z } from "zod";
@@ -3736,7 +3737,8 @@ var LocalReviewData = class {
3736
3737
  }
3737
3738
  return {
3738
3739
  workspacePath,
3739
- filePath
3740
+ filePath,
3741
+ rootPath
3740
3742
  };
3741
3743
  }
3742
3744
  /** A document read that needs default pins; 409 when the document has none. */
@@ -4592,4 +4594,4 @@ async function readWorkspace(file) {
4592
4594
  }
4593
4595
  }
4594
4596
  //#endregion
4595
- export { inspectSnapshot as a, activityEndSchema as c, diffrExecutable as d, diffrMissingError as f, commandSchema as i, activityUpdateSchema as l, uploadSchema as n, initializeReviewStoreSchema as o, SCRATCHPAD_ID as r, activityBeginSchema as s, openLocalReviewStore as t, invalidateStructuralComparisons as u };
4597
+ export { inspectSnapshot as a, activityEndSchema as c, invalidateStructuralComparisons as d, diffrExecutable as f, commandSchema as i, activityUpdateSchema as l, uploadSchema as n, initializeReviewStoreSchema as o, diffrMissingError as p, SCRATCHPAD_ID as r, activityBeginSchema as s, openLocalReviewStore as t, pullRequestProvider as u };
@@ -1,6 +1,6 @@
1
1
  import { a as toolResultText, s as ReviewApiError } from "./agent-client-DjXQy6CL.js";
2
2
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
3
- import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-CjFAGHBa.js";
3
+ import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-ByMdW_S8.js";
4
4
  import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-CVJrfi5s.js";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -1,4 +1,4 @@
1
- import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-DN-A5p0K.js";
1
+ import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-DKmj2ygd.js";
2
2
  import "./review-home-paths-Lc02WFuX.js";
3
3
  import { A as withFileLock } from "./s3-config-DDWWMh5x.js";
4
4
  import { access, mkdir } from "node:fs/promises";
@@ -0,0 +1,2 @@
1
+ import { t as openReviewProfile } from "./profile-B58hz5n7.js";
2
+ export { openReviewProfile };
@@ -1,5 +1,6 @@
1
1
  import { a as jsonNumber, c as jsonString, i as jsonBoolean, n as isJsonObject } from "./src-X9phtB2j.js";
2
2
  import { S as REVIEW_THEME_CHOICES, h as REVIEW_DIFF_LAYOUT_SCOPES, m as REVIEW_DIFF_LAYOUTS } from "./src-DZG1xAqW.js";
3
+ import { n as askAgentIds } from "./thread-state-DZihwhfu.js";
3
4
  import { z } from "zod";
4
5
  //#region src/telemetry-clean-text.ts
5
6
  const USER_DATA_REGEXES = [
@@ -150,7 +151,8 @@ const PEEK_VIA = [
150
151
  "diagram",
151
152
  "map",
152
153
  "db_lens",
153
- "call_stack_frame"
154
+ "call_stack_frame",
155
+ "diff_definition"
154
156
  ];
155
157
  const PEEK_ROOT_KIND = ["range"];
156
158
  const LSP_FEATURE = [
@@ -226,6 +228,7 @@ const SETTING_NAME = [
226
228
  "diffr_config",
227
229
  "theme",
228
230
  "ctrl_tab",
231
+ "open_files_in",
229
232
  "ready_notification",
230
233
  "document_width",
231
234
  "code_font_size"
@@ -322,10 +325,39 @@ const INSTALL_LOCATION = [
322
325
  const DIFF_OPENED_VIA = [
323
326
  "topbar",
324
327
  "lens",
325
- "locate"
328
+ "locate",
329
+ "file_picker"
326
330
  ];
327
331
  /** Which diff opened: a commit's, or a file diff drawn plain or structural. */
328
332
  const DIFF_OPENED_KIND = ["commit", "structural"];
333
+ /** GitHub Enterprise is any GitHub host but github.com. */
334
+ const PR_PROVIDER = [
335
+ "github",
336
+ "github_enterprise",
337
+ "azure_devops"
338
+ ];
339
+ const REMOTE_HOST_PROBLEM = [
340
+ "unreachable",
341
+ "not_installed",
342
+ "auth_failed",
343
+ "unsupported",
344
+ "incompatible"
345
+ ];
346
+ const REMOTE_HOST_TARGET = ["linux-x64", "linux-arm64"];
347
+ const ASK_SELECTION = [
348
+ "text",
349
+ "code",
350
+ "diff",
351
+ "review"
352
+ ];
353
+ const ONBOARDING_STEP = [
354
+ "welcome",
355
+ "install",
356
+ "run"
357
+ ];
358
+ const RUN_MODE = ["draw", "copy"];
359
+ const NEW_WHITEBOARD_VIA = ["button", "command"];
360
+ const FIND_SURFACE = ["review", "diff"];
329
361
  const DISCORD_VIA = [
330
362
  "topbar",
331
363
  "dialog",
@@ -586,7 +618,9 @@ const UI_TELEMETRY_EVENTS = {
586
618
  event: "review_session_started",
587
619
  properties: {
588
620
  source_kind: SESSION_SOURCE_KIND,
589
- agent_kind: SESSION_AGENT_KIND
621
+ agent_kind: SESSION_AGENT_KIND,
622
+ remote: "boolean",
623
+ pr_provider: PR_PROVIDER
590
624
  }
591
625
  },
592
626
  session_ended: {
@@ -674,6 +708,62 @@ const UI_TELEMETRY_EVENTS = {
674
708
  review_shared: {
675
709
  event: "review_review_shared",
676
710
  properties: {}
711
+ },
712
+ find_opened: {
713
+ event: "review_find_opened",
714
+ properties: { surface: FIND_SURFACE }
715
+ },
716
+ ask_sent: {
717
+ event: "review_ask_sent",
718
+ properties: {
719
+ agent: askAgentIds,
720
+ follow_up: "boolean",
721
+ selection: ASK_SELECTION,
722
+ remote: "boolean"
723
+ }
724
+ },
725
+ onboarding_step_viewed: {
726
+ event: "review_onboarding_step_viewed",
727
+ properties: { step: ONBOARDING_STEP }
728
+ },
729
+ onboarding_skipped: {
730
+ event: "review_onboarding_skipped",
731
+ properties: { step: ONBOARDING_STEP }
732
+ },
733
+ onboarding_run_started: {
734
+ event: "review_onboarding_run_started",
735
+ properties: {
736
+ mode: RUN_MODE,
737
+ agent: askAgentIds
738
+ }
739
+ },
740
+ new_whiteboard_opened: {
741
+ event: "review_new_whiteboard_opened",
742
+ properties: { via: NEW_WHITEBOARD_VIA }
743
+ },
744
+ new_whiteboard_started: {
745
+ event: "review_new_whiteboard_started",
746
+ properties: {
747
+ mode: RUN_MODE,
748
+ embedded: "boolean",
749
+ agent: askAgentIds
750
+ }
751
+ },
752
+ remote_host_installed: {
753
+ event: "review_remote_host_installed",
754
+ properties: {
755
+ ok: "boolean",
756
+ target: REMOTE_HOST_TARGET,
757
+ duration_ms: "number"
758
+ }
759
+ },
760
+ remote_host_connected: {
761
+ event: "review_remote_host_connected",
762
+ properties: {
763
+ ok: "boolean",
764
+ problem: REMOTE_HOST_PROBLEM,
765
+ direct: "boolean"
766
+ }
677
767
  }
678
768
  };
679
769
  const MAX_FREE_STRING_LENGTH = 40;
@@ -3,7 +3,7 @@ import "./src-DZG1xAqW.js";
3
3
  import "./review-home-paths-Lc02WFuX.js";
4
4
  import { A as withFileLock, M as devReviewHome, O as processIsAlive, P as writeFileAtomic } from "./s3-config-DDWWMh5x.js";
5
5
  import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
6
- import "./request-origin-D1RNVSeK.js";
6
+ import "./request-origin-BSwTNS8K.js";
7
7
  import { createHmac, randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
9
9
  import { existsSync, readFileSync, rmSync } from "node:fs";
@@ -2,6 +2,7 @@
2
2
  import { S as splitGitPatchFiles, a as diff, o as diffFileSummaries } from "../src-BMPuxWv4.js";
3
3
  import { c as jsonString, i as jsonBoolean, n as isJsonObject, p as isObjectValue, s as jsonProperty, u as parseJsonText } from "../src-X9phtB2j.js";
4
4
  import { d as StreamLimitError, f as readBoundedStream, i as SharedReviewStore } from "../client-CLqKkzOc.js";
5
+ import { u as pullRequestProvider } from "../local-data-DKmj2ygd.js";
5
6
  import { A as reviewDiffrSummarizerInputSchema, E as ReviewGatewayHostSchema, T as ReviewConnectRequestSchema, c as ReviewBugReportMetaV2Schema, d as REVIEW_CLIENT_HEADER, f as REVIEW_CLIENT_REMOTE, g as REVIEW_HOST_HEADER, l as ReviewBugReportRequestSchema, r as parseReviewDesktopVerbFrame, s as parseZod, t as parseReviewCliInstallApplyRequest, u as parseReviewBugReportResponse } from "../src-DZG1xAqW.js";
6
7
  import { i as reviewInstanceDiscoveryPath, o as reviewLegacyDiscoveryPath } from "../review-home-paths-Lc02WFuX.js";
7
8
  import { w as resourceReferences } from "../stored-document-migration-DLBhkSfp.js";
@@ -10,12 +11,12 @@ import { I as writePrivateJsonAtomic, M as devReviewHome } from "../s3-config-DD
10
11
  import { t as errorMessage } from "../error-message-OtiDonty.js";
11
12
  import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "../package-paths-jMg6JxDP.js";
12
13
  import { _ as reviewInstanceIdentity, u as REVIEW_INSTANCE_ENV } from "../server-discovery-DVqrcIDT.js";
13
- import { t as openReviewProfile } from "../profile-Dm6irjbg.js";
14
- import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-DnQNwgR1.js";
15
- import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-D1RNVSeK.js";
16
- import { c as removeLegacyReviewSkills, d as skipCliInstall, h as detectConnectAgents, i as finishCliInstallUpdate, l as resetCliInstall, m as connectAgent, n as applyCliInstall, o as pathShimPath, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-CwNaMOwU.js";
17
- import { C as saveDiffrSummarizer, E as cliAskTools, S as readDiffrConfig, T as testDiffrSummarizer, _ as DEFAULT_MAX_REQUEST_BYTES, a as drainServerCrashReport, b as GlobalReviewDesktopVerbRelay, c as answerErrorsAsJson, d as legacyModeSchema, f as captureSanitizedUiTelemetry, g as readBoundedRequestJson, h as createNodeRequestListener, l as createWhiteboardCore, m as recordClientError, o as installProcessErrorTelemetry, p as clientErrorsForSession, r as takeOverServerHome, s as serverCrashReportPath, u as serverJson, v as HttpJsonError, w as setDiffrConfigValue, x as migrateDiffrConfig, y as ReviewServerError } from "../headless-host-CoN2f0Sk.js";
18
- import { c as readQuerySchemas } from "../authoring-tools-CjFAGHBa.js";
14
+ import { t as openReviewProfile } from "../profile-B58hz5n7.js";
15
+ import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-ClhP_5lW.js";
16
+ import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-BSwTNS8K.js";
17
+ import { c as removeLegacyReviewSkills, d as skipCliInstall, h as detectConnectAgents, i as finishCliInstallUpdate, l as resetCliInstall, m as connectAgent, n as applyCliInstall, o as pathShimPath, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-D_mqo_Jk.js";
18
+ import { C as saveDiffrSummarizer, E as cliAskTools, S as readDiffrConfig, T as testDiffrSummarizer, _ as DEFAULT_MAX_REQUEST_BYTES, a as drainServerCrashReport, b as GlobalReviewDesktopVerbRelay, c as answerErrorsAsJson, d as legacyModeSchema, f as captureSanitizedUiTelemetry, g as readBoundedRequestJson, h as createNodeRequestListener, l as createWhiteboardCore, m as recordClientError, o as installProcessErrorTelemetry, p as clientErrorsForSession, r as takeOverServerHome, s as serverCrashReportPath, u as serverJson, v as HttpJsonError, w as setDiffrConfigValue, x as migrateDiffrConfig, y as ReviewServerError } from "../headless-host-CnCDrHTx.js";
19
+ import { c as readQuerySchemas } from "../authoring-tools-ByMdW_S8.js";
19
20
  import crypto, { createHash, randomBytes } from "node:crypto";
20
21
  import { z } from "zod";
21
22
  import fs, { existsSync, readFileSync } from "node:fs";
@@ -741,6 +742,16 @@ function jsonReviewBugReportSource(store, snapshot) {
741
742
  }
742
743
  };
743
744
  }
745
+ /** The path id wins over any review id in the payload. */
746
+ async function captureReviewUiEvent(telemetry, request, reviewId) {
747
+ const body = await readBoundedRequestJson(request, void 0, {});
748
+ const payload = isJsonObject(body) ? body : {};
749
+ const rawContext = isJsonObject(payload.context) ? payload.context : {};
750
+ await captureSanitizedUiTelemetry(telemetry, request, payload.name, payload.properties, recordClientError, payload.error, {
751
+ ...rawContext,
752
+ reviewUuid: reviewId
753
+ }, payload.occurredAt);
754
+ }
744
755
  /** Telemetry and report uploads for a pinned native review. */
745
756
  function createJsonReviewReporting(store, telemetry, options = {}) {
746
757
  const { submit = submitReviewBugReport, shared } = options;
@@ -758,13 +769,7 @@ function createJsonReviewReporting(store, telemetry, options = {}) {
758
769
  await next();
759
770
  });
760
771
  app.post("/:id/telemetry/event", async (c) => {
761
- const body = await readBoundedRequestJson(c.req.raw, void 0, {});
762
- const payload = isJsonObject(body) ? body : {};
763
- const rawContext = isJsonObject(payload.context) ? payload.context : {};
764
- await captureSanitizedUiTelemetry(telemetry, c.req.raw, payload.name, payload.properties, recordClientError, payload.error, {
765
- ...rawContext,
766
- reviewUuid: c.req.param("id")
767
- }, payload.occurredAt);
772
+ await captureReviewUiEvent(telemetry, c.req.raw, c.req.param("id"));
768
773
  return c.json({ ok: true });
769
774
  });
770
775
  app.post("/:id/telemetry/tab", async (context) => {
@@ -1902,7 +1907,8 @@ function createReviewGateway(input) {
1902
1907
  if (owner === void 0 || owner === "local") return input.local(request);
1903
1908
  const alias = "remote" in owner ? owner.remote.alias : owner.down.alias;
1904
1909
  if (route === "telemetry") {
1905
- await request.body?.cancel();
1910
+ if (input.remoteEvent && new URL(request.url).pathname.endsWith("/telemetry/event")) await input.remoteEvent(request, reviewId);
1911
+ else await request.body?.cancel();
1906
1912
  return answer(alias, 200, { ok: true });
1907
1913
  }
1908
1914
  if (!route) return answer(alias, 404, {
@@ -1946,6 +1952,10 @@ function createReviewGateway(input) {
1946
1952
  fetch: (request) => app.fetch(request),
1947
1953
  setHosts: (list) => hosts.set(list),
1948
1954
  hosts: () => hosts.states(),
1955
+ isRemote(reviewId) {
1956
+ const owner = locate(reviewId);
1957
+ return owner !== void 0 && owner !== "local";
1958
+ },
1949
1959
  async close() {
1950
1960
  closed = true;
1951
1961
  stopWatchingWindows();
@@ -2115,6 +2125,7 @@ function createGlobalReviewServer(input) {
2115
2125
  home: devReviewHome(),
2116
2126
  relay,
2117
2127
  restarted: (alias) => input.onRemoteHostRestarted?.(alias),
2128
+ remoteEvent: (request, reviewId) => captureReviewUiEvent(telemetry, request, reviewId),
2118
2129
  log: input.log
2119
2130
  });
2120
2131
  app.all("/reviews-api", (context) => gateway.fetch(context.req.raw));
@@ -2150,8 +2161,9 @@ function createGlobalReviewServer(input) {
2150
2161
  delete eventProperties.source_kind;
2151
2162
  delete eventProperties.agent_kind;
2152
2163
  const rawContext = isJsonObject(payload.context) ? payload.context : {};
2153
- const sourceKind = sessionStartedSourceKind(reviewStore, jsonString(rawContext.reviewUuid));
2154
- if (sourceKind) eventProperties.source_kind = sourceKind;
2164
+ const reviewUuid = jsonString(rawContext.reviewUuid);
2165
+ Object.assign(eventProperties, sessionStartedSource(reviewStore, reviewUuid));
2166
+ if (reviewUuid) eventProperties.remote = gateway.isRemote(reviewUuid);
2155
2167
  }
2156
2168
  await captureSanitizedUiTelemetry(telemetry, context.req.raw, payload.name, eventProperties, (event, eventContext) => {
2157
2169
  flushBeforeOptOut = event.event === "review_setting_changed" && event.properties.setting === "telemetry_enabled" && event.properties.enabled === false;
@@ -2286,20 +2298,31 @@ function watchSessionOpen(watchdog, event, context) {
2286
2298
  else if (event === "review_review_presented" || event === "review_session_ended") watchdog.presented(presentationSessionId);
2287
2299
  }
2288
2300
  /**
2289
- * `session_started`'s `source_kind`: the opened review's target kind, or
2290
- * `scratchpad` for the one scratchpad. Undefined when the review is gone (a
2291
- * shared review this store never had, or one deleted between open and the
2292
- * event arriving) or was left without a target.
2301
+ * `session_started`'s `source_kind` and `pr_provider`. Empty when the review
2302
+ * is not in this store: a shared or remote review, or one deleted between open
2303
+ * and the event arriving.
2293
2304
  */
2294
- function sessionStartedSourceKind(reviewStore, reviewUuid) {
2295
- if (!reviewUuid) return void 0;
2305
+ function sessionStartedSource(reviewStore, reviewUuid) {
2306
+ if (!reviewUuid) return {};
2296
2307
  try {
2297
2308
  const snapshot = reviewStore.read(reviewUuid);
2298
- return snapshot.kind === "scratchpad" ? snapshot.kind : snapshot.target?.kind;
2309
+ const sourceKind = snapshot.kind === "scratchpad" ? snapshot.kind : snapshot.target?.kind;
2310
+ const pullRequestUrl = snapshot.origin?.pullRequestUrl;
2311
+ const prProvider = pullRequestUrl && prProviderOf(pullRequestUrl);
2312
+ return {
2313
+ ...sourceKind && { source_kind: sourceKind },
2314
+ ...prProvider && { pr_provider: prProvider }
2315
+ };
2299
2316
  } catch {
2300
- return;
2317
+ return {};
2301
2318
  }
2302
2319
  }
2320
+ function prProviderOf(url) {
2321
+ const resolved = pullRequestProvider(url);
2322
+ if (!resolved) return void 0;
2323
+ if (resolved.provider.id === "azure-devops") return "azure_devops";
2324
+ return resolved.address.host === "github.com" ? "github" : "github_enterprise";
2325
+ }
2303
2326
  function listen(server, port) {
2304
2327
  return new Promise((resolve, reject) => {
2305
2328
  server.once("error", reject);
@@ -1644,6 +1644,7 @@ declare class LocalReviewData {
1644
1644
  }): Promise<{
1645
1645
  workspacePath: string;
1646
1646
  filePath?: string;
1647
+ rootPath: string;
1647
1648
  }>;
1648
1649
  /** A document read that needs default pins; 409 when the document has none. */
1649
1650
  documentPins(snapshot: Snapshot): Promise<Pins>;
@@ -1,25 +1,5 @@
1
1
  import { C as ReviewApiSelectionSourceSchema, O as ReviewSelectedDiffSchema } from "./src-DZG1xAqW.js";
2
2
  import { z } from "zod";
3
- import { stat } from "node:fs/promises";
4
- //#region src/fs-utils.ts
5
- /** Whether a thrown filesystem error reports a missing file (ENOENT). */
6
- function isMissingFileError(cause) {
7
- return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
8
- }
9
- async function isDirectory(target) {
10
- try {
11
- return (await stat(target)).isDirectory();
12
- } catch {
13
- return false;
14
- }
15
- }
16
- async function isFile(target) {
17
- try {
18
- return (await stat(target)).isFile();
19
- } catch {
20
- return false;
21
- }
22
- }
23
3
  /** A semantic selection, independent of comment/thread creation. */
24
4
  const AgentSelectionSchema = z.strictObject({
25
5
  target: z.discriminatedUnion("kind", [
@@ -397,4 +377,4 @@ const askHistoryEntrySchema = z.object({
397
377
  updatedAt: z.string()
398
378
  });
399
379
  //#endregion
400
- export { askHistoryEntrySchema as a, askPicksSchema as c, selectionMarkdown as d, isDirectory as f, askEntrySchema as i, askQuestionSchema as l, isMissingFileError as m, askAgentIds as n, askOfferSchema as o, isFile as p, askChoiceKinds as r, askPermissionsSchema as s, applyAskChange as t, AgentSelectionSchema as u };
380
+ export { askHistoryEntrySchema as a, askPicksSchema as c, selectionMarkdown as d, askEntrySchema as i, askQuestionSchema as l, askAgentIds as n, askOfferSchema as o, askChoiceKinds as r, askPermissionsSchema as s, applyAskChange as t, AgentSelectionSchema as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.fast/whiteboard",
3
- "version": "0.3.1-preview.20261016.122",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -76,8 +76,8 @@
76
76
  "vitest": "4.1.11",
77
77
  "@dev.fast/json": "0.0.0",
78
78
  "@dev.fast/review-protocol": "0.0.0",
79
- "@dev.fast/review-share-protocol": "0.1.2",
80
79
  "@dev.fast/trace-core": "0.0.0",
80
+ "@dev.fast/review-share-protocol": "0.1.2",
81
81
  "@dev.fast/trace-protocol": "0.5.0"
82
82
  },
83
83
  "engines": {
@@ -88,7 +88,7 @@
88
88
  "repo": "devdotfast/review",
89
89
  "docs": "packages/review/instructions/authoring.md"
90
90
  },
91
- "gitHead": "7cef417b896162d45b18541ced02e6d250cdc535",
91
+ "gitHead": "38a6dad3049993f987d9b82ca3c5fee424b28f6b",
92
92
  "scripts": {
93
93
  "prebuild": "pnpm run build:workspace-deps",
94
94
  "build": "node --max-old-space-size=8192 node_modules/tsdown/dist/run.mjs",
@@ -17,6 +17,7 @@ import {
17
17
  toolResultText,
18
18
  } from "./agent-client.js";
19
19
  import { type ReviewApiClient, ReviewApiError } from "./client.js";
20
+ import { exitOnInstallUpdate } from "./install-update.js";
20
21
  import { callPublicTool, publicTool } from "./public-tools.js";
21
22
  import { RECOVERY } from "./recovery.js";
22
23
  import { REVIEW_AGENT_HEADER, REVIEW_VIA_HEADER } from "./request-origin.js";
@@ -70,6 +71,7 @@ export async function runReviewAgentCli(input: AgentCliInput): Promise<number> {
70
71
 
71
72
  if (mode === "mcp") {
72
73
  const { serveReviewMcp } = await import("./mcp.js");
74
+ exitOnInstallUpdate(input.stderr);
73
75
  await serveReviewMcp(
74
76
  async (key, agentKind) => {
75
77
  const connected = await connectReviewInstance(
@@ -0,0 +1,27 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { Writable } from "node:stream";
4
+
5
+ import { readBuildCommit } from "@review/package-paths.js";
6
+
7
+ /** Exits once an update replaces this install, or the Windows updater marks it. */
8
+ export function exitOnInstallUpdate(stderr: Writable): void {
9
+ const commit = readBuildCommit(import.meta.url);
10
+
11
+ if (!commit) return;
12
+
13
+ const marker = path.join(path.dirname(process.execPath), "applying_update");
14
+ const staleMarker = existsSync(marker);
15
+
16
+ const timer = setInterval(() => {
17
+ if (
18
+ (!staleMarker && existsSync(marker)) ||
19
+ readBuildCommit(import.meta.url) !== commit
20
+ ) {
21
+ clearInterval(timer);
22
+ stderr.write("Whiteboard was updated. Restart its MCP server.\n", () =>
23
+ process.exit(75),
24
+ );
25
+ }
26
+ }, 1000).unref();
27
+ }
@@ -382,7 +382,7 @@ export class LocalReviewData {
382
382
  commit?: string;
383
383
  anchor?: SourcePins;
384
384
  } = {},
385
- ): Promise<{ workspacePath: string; filePath?: string }> {
385
+ ): Promise<{ workspacePath: string; filePath?: string; rootPath: string }> {
386
386
  const { pins } = await this.resolveSource(
387
387
  snapshot,
388
388
  source.commit,
@@ -515,7 +515,8 @@ export class LocalReviewData {
515
515
  }
516
516
  }
517
517
 
518
- return { workspacePath, filePath };
518
+ // External editors open the checkout as their workspace beside the file.
519
+ return { workspacePath, filePath, rootPath };
519
520
  }
520
521
 
521
522
  /** A document read that needs default pins; 409 when the document has none. */
@@ -40,6 +40,7 @@ import {
40
40
  } from "@review/desktop-discovery";
41
41
  import { readReviewPackageVersion } from "@review/package-paths";
42
42
  import type { LocalReviewData } from "@review/review-api/local-data.js";
43
+ import { pullRequestProvider } from "@review/review-api/provider.js";
43
44
  import type { ReviewStore } from "@review/review-api/store.js";
44
45
  import {
45
46
  devReviewHome,
@@ -77,7 +78,10 @@ import {
77
78
  readBoundedRequestJson,
78
79
  } from "./hono-http";
79
80
  import { ReviewServerError } from "./http-json";
80
- import { createJsonReviewReporting } from "./json-review-reporting";
81
+ import {
82
+ captureReviewUiEvent,
83
+ createJsonReviewReporting,
84
+ } from "./json-review-reporting";
81
85
  import { removeLegacyTutorial } from "./legacy-tutorial-cleanup";
82
86
  import { createReviewGateway } from "./review-gateway";
83
87
  import { reviewLifecycleTelemetry } from "./review-lifecycle-telemetry";
@@ -253,6 +257,8 @@ export function createGlobalReviewServer(
253
257
  home: devReviewHome(),
254
258
  relay,
255
259
  restarted: (alias) => input.onRemoteHostRestarted?.(alias),
260
+ remoteEvent: (request, reviewId) =>
261
+ captureReviewUiEvent(telemetry, request, reviewId),
256
262
  log: input.log,
257
263
  });
258
264
 
@@ -314,12 +320,14 @@ export function createGlobalReviewServer(
314
320
 
315
321
  const rawContext = isJsonObject(payload.context) ? payload.context : {};
316
322
 
317
- const sourceKind = sessionStartedSourceKind(
318
- reviewStore,
319
- jsonString(rawContext.reviewUuid),
323
+ const reviewUuid = jsonString(rawContext.reviewUuid);
324
+
325
+ Object.assign(
326
+ eventProperties,
327
+ sessionStartedSource(reviewStore, reviewUuid),
320
328
  );
321
329
 
322
- if (sourceKind) eventProperties.source_kind = sourceKind;
330
+ if (reviewUuid) eventProperties.remote = gateway.isRemote(reviewUuid);
323
331
  }
324
332
 
325
333
  await captureSanitizedUiTelemetry(
@@ -562,28 +570,47 @@ function watchSessionOpen(
562
570
  }
563
571
 
564
572
  /**
565
- * `session_started`'s `source_kind`: the opened review's target kind, or
566
- * `scratchpad` for the one scratchpad. Undefined when the review is gone (a
567
- * shared review this store never had, or one deleted between open and the
568
- * event arriving) or was left without a target.
573
+ * `session_started`'s `source_kind` and `pr_provider`. Empty when the review
574
+ * is not in this store: a shared or remote review, or one deleted between open
575
+ * and the event arriving.
569
576
  */
570
- export function sessionStartedSourceKind(
577
+ export function sessionStartedSource(
571
578
  reviewStore: ReviewStore,
572
579
  reviewUuid: string | undefined,
573
- ): string | undefined {
574
- if (!reviewUuid) return undefined;
580
+ ) {
581
+ if (!reviewUuid) return {};
575
582
 
576
583
  try {
577
584
  const snapshot = reviewStore.read(reviewUuid);
578
585
 
579
- return snapshot.kind === "scratchpad"
580
- ? snapshot.kind
581
- : snapshot.target?.kind;
586
+ const sourceKind =
587
+ snapshot.kind === "scratchpad" ? snapshot.kind : snapshot.target?.kind;
588
+
589
+ const pullRequestUrl = snapshot.origin?.pullRequestUrl;
590
+
591
+ const prProvider = pullRequestUrl && prProviderOf(pullRequestUrl);
592
+
593
+ return {
594
+ ...(sourceKind && { source_kind: sourceKind }),
595
+ ...(prProvider && { pr_provider: prProvider }),
596
+ };
582
597
  } catch {
583
- return undefined;
598
+ return {};
584
599
  }
585
600
  }
586
601
 
602
+ function prProviderOf(url: string) {
603
+ const resolved = pullRequestProvider(url);
604
+
605
+ if (!resolved) return undefined;
606
+
607
+ if (resolved.provider.id === "azure-devops") return "azure_devops";
608
+
609
+ return resolved.address.host === "github.com"
610
+ ? "github"
611
+ : "github_enterprise";
612
+ }
613
+
587
614
  function listen(server: Server, port: number): Promise<number> {
588
615
  return new Promise((resolve, reject) => {
589
616
  server.once("error", reject);
@@ -23,6 +23,7 @@ import {
23
23
  requestJsonErrorStatus,
24
24
  } from "./review-api-parsers";
25
25
  import {
26
+ type ReviewTelemetryCapture,
26
27
  captureSanitizedUiTelemetry,
27
28
  clientErrorsForSession,
28
29
  recordClientError,
@@ -69,6 +70,27 @@ export function jsonReviewBugReportSource(
69
70
  };
70
71
  }
71
72
 
73
+ /** The path id wins over any review id in the payload. */
74
+ export async function captureReviewUiEvent(
75
+ telemetry: ReviewTelemetryCapture,
76
+ request: Request,
77
+ reviewId: string,
78
+ ) {
79
+ const body = await readBoundedRequestJson(request, undefined, {});
80
+ const payload = isJsonObject(body) ? body : {};
81
+ const rawContext = isJsonObject(payload.context) ? payload.context : {};
82
+ await captureSanitizedUiTelemetry(
83
+ telemetry,
84
+ request,
85
+ payload.name,
86
+ payload.properties,
87
+ recordClientError,
88
+ payload.error,
89
+ { ...rawContext, reviewUuid: reviewId },
90
+ payload.occurredAt,
91
+ );
92
+ }
93
+
72
94
  /** Telemetry and report uploads for a pinned native review. */
73
95
  export function createJsonReviewReporting(
74
96
  store: ReviewStore,
@@ -105,21 +127,8 @@ export function createJsonReviewReporting(
105
127
  await next();
106
128
  });
107
129
  app.post("/:id/telemetry/event", async (c) => {
108
- const body = await readBoundedRequestJson(c.req.raw, undefined, {});
109
- const payload = isJsonObject(body) ? body : {};
110
- const rawContext = isJsonObject(payload.context) ? payload.context : {};
111
- await captureSanitizedUiTelemetry(
112
- telemetry,
113
- c.req.raw,
114
- payload.name,
115
- payload.properties,
116
- recordClientError,
117
- payload.error,
118
- // The path id wins: the middleware above already asserted it exists,
119
- // so a payload trying to assert a different review id is overridden.
120
- { ...rawContext, reviewUuid: c.req.param("id") },
121
- payload.occurredAt,
122
- );
130
+ // The middleware above already asserted the review exists.
131
+ await captureReviewUiEvent(telemetry, c.req.raw, c.req.param("id"));
123
132
 
124
133
  return c.json({ ok: true });
125
134
  });
@@ -129,6 +129,7 @@ export function createReviewGateway(input: {
129
129
  firstByteMs?: number;
130
130
  slowRouteMs?: number;
131
131
  restarted?(alias: string): void;
132
+ remoteEvent?(request: Request, reviewId: string): Promise<void>;
132
133
  log?(message: string): void;
133
134
  }) {
134
135
  const log = input.log ?? (() => {});
@@ -352,7 +353,12 @@ export function createReviewGateway(input: {
352
353
  const alias = "remote" in owner ? owner.remote.alias : owner.down.alias;
353
354
 
354
355
  if (route === "telemetry") {
355
- await request.body?.cancel();
356
+ if (
357
+ input.remoteEvent &&
358
+ new URL(request.url).pathname.endsWith("/telemetry/event")
359
+ )
360
+ await input.remoteEvent(request, reviewId);
361
+ else await request.body?.cancel();
356
362
 
357
363
  return answer(alias, 200, { ok: true });
358
364
  }
@@ -420,6 +426,11 @@ export function createReviewGateway(input: {
420
426
  fetch: (request: Request) => app.fetch(request),
421
427
  setHosts: (list: ReviewGatewayHost[]) => hosts.set(list),
422
428
  hosts: () => hosts.states(),
429
+ isRemote(reviewId: string) {
430
+ const owner = locate(reviewId);
431
+
432
+ return owner !== undefined && owner !== "local";
433
+ },
423
434
  async close() {
424
435
  closed = true;
425
436
  stopWatchingWindows();
@@ -28,6 +28,7 @@ import {
28
28
  } from "@dev.fast/review-protocol";
29
29
  import { z } from "zod";
30
30
 
31
+ import { askAgentIds } from "./ask/thread-state";
31
32
  import { containsFilePath, hasPossibleUserInfo } from "./telemetry-clean-text";
32
33
 
33
34
  /** Property value validators: an enum of allowed strings, or a scalar type. */
@@ -64,6 +65,7 @@ const PEEK_VIA = [
64
65
  "map",
65
66
  "db_lens",
66
67
  "call_stack_frame",
68
+ "diff_definition",
67
69
  ] as const;
68
70
 
69
71
  export const PEEK_ROOT_KIND = ["range"] as const;
@@ -139,6 +141,7 @@ export const SETTING_NAME = [
139
141
  "diffr_config",
140
142
  "theme",
141
143
  "ctrl_tab",
144
+ "open_files_in",
142
145
  "ready_notification",
143
146
  "document_width",
144
147
  "code_font_size",
@@ -241,11 +244,43 @@ export const INSTALL_LOCATION = [
241
244
  "other",
242
245
  ] as const;
243
246
 
244
- export const DIFF_OPENED_VIA = ["topbar", "lens", "locate"] as const;
247
+ export const DIFF_OPENED_VIA = [
248
+ "topbar",
249
+ "lens",
250
+ "locate",
251
+ "file_picker",
252
+ ] as const;
245
253
 
246
254
  /** Which diff opened: a commit's, or a file diff drawn plain or structural. */
247
255
  export const DIFF_OPENED_KIND = ["commit", "structural"] as const;
248
256
 
257
+ /** GitHub Enterprise is any GitHub host but github.com. */
258
+ export const PR_PROVIDER = [
259
+ "github",
260
+ "github_enterprise",
261
+ "azure_devops",
262
+ ] as const;
263
+
264
+ export const REMOTE_HOST_PROBLEM = [
265
+ "unreachable",
266
+ "not_installed",
267
+ "auth_failed",
268
+ "unsupported",
269
+ "incompatible",
270
+ ] as const;
271
+
272
+ export const REMOTE_HOST_TARGET = ["linux-x64", "linux-arm64"] as const;
273
+
274
+ export const ASK_SELECTION = ["text", "code", "diff", "review"] as const;
275
+
276
+ export const ONBOARDING_STEP = ["welcome", "install", "run"] as const;
277
+
278
+ export const RUN_MODE = ["draw", "copy"] as const;
279
+
280
+ export const NEW_WHITEBOARD_VIA = ["button", "command"] as const;
281
+
282
+ export const FIND_SURFACE = ["review", "diff"] as const;
283
+
249
284
  export const DISCORD_VIA = ["topbar", "dialog", "docs"] as const;
250
285
 
251
286
  const REVIEW_DELETED_VIA = ["home"] as const;
@@ -514,6 +549,8 @@ export const UI_TELEMETRY_EVENTS = {
514
549
  properties: {
515
550
  source_kind: SESSION_SOURCE_KIND,
516
551
  agent_kind: SESSION_AGENT_KIND,
552
+ remote: "boolean",
553
+ pr_provider: PR_PROVIDER,
517
554
  },
518
555
  },
519
556
  session_ended: {
@@ -584,6 +621,55 @@ export const UI_TELEMETRY_EVENTS = {
584
621
  },
585
622
  // The reader copied the review's share link.
586
623
  review_shared: { event: "review_review_shared", properties: {} },
624
+ find_opened: {
625
+ event: "review_find_opened",
626
+ properties: { surface: FIND_SURFACE },
627
+ },
628
+ ask_sent: {
629
+ event: "review_ask_sent",
630
+ properties: {
631
+ agent: askAgentIds,
632
+ follow_up: "boolean",
633
+ selection: ASK_SELECTION,
634
+ remote: "boolean",
635
+ },
636
+ },
637
+ onboarding_step_viewed: {
638
+ event: "review_onboarding_step_viewed",
639
+ properties: { step: ONBOARDING_STEP },
640
+ },
641
+ onboarding_skipped: {
642
+ event: "review_onboarding_skipped",
643
+ properties: { step: ONBOARDING_STEP },
644
+ },
645
+ onboarding_run_started: {
646
+ event: "review_onboarding_run_started",
647
+ properties: { mode: RUN_MODE, agent: askAgentIds },
648
+ },
649
+ new_whiteboard_opened: {
650
+ event: "review_new_whiteboard_opened",
651
+ properties: { via: NEW_WHITEBOARD_VIA },
652
+ },
653
+ new_whiteboard_started: {
654
+ event: "review_new_whiteboard_started",
655
+ properties: { mode: RUN_MODE, embedded: "boolean", agent: askAgentIds },
656
+ },
657
+ remote_host_installed: {
658
+ event: "review_remote_host_installed",
659
+ properties: {
660
+ ok: "boolean",
661
+ target: REMOTE_HOST_TARGET,
662
+ duration_ms: "number",
663
+ },
664
+ },
665
+ remote_host_connected: {
666
+ event: "review_remote_host_connected",
667
+ properties: {
668
+ ok: "boolean",
669
+ problem: REMOTE_HOST_PROBLEM,
670
+ direct: "boolean",
671
+ },
672
+ },
587
673
  } as const satisfies Record<
588
674
  string,
589
675
  { event: string; properties: Record<string, UiTelemetryPropertySpec> }
@@ -1,2 +0,0 @@
1
- import { n as runReviewAgentCli } from "./agent-cli-S0bt9I7V.js";
2
- export { runReviewAgentCli };
@@ -1,2 +0,0 @@
1
- import { t as openReviewProfile } from "./profile-Dm6irjbg.js";
2
- export { openReviewProfile };