@dev.fast/whiteboard 0.2.4-preview.20261009.108 → 0.2.4

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.
@@ -0,0 +1,2 @@
1
+ import { n as runReviewAgentCli } from "./agent-cli-BrFqhnzM.js";
2
+ export { runReviewAgentCli };
@@ -23,7 +23,7 @@ async function runReviewAgentCli(input) {
23
23
  [REVIEW_AGENT_HEADER]: reviewSessionAgent(env)
24
24
  };
25
25
  if (mode === "mcp") {
26
- const { serveReviewMcp } = await import("./mcp-BWp_kC0V.js");
26
+ const { serveReviewMcp } = await import("./mcp-BDy9Aybd.js");
27
27
  await serveReviewMcp(async (key, agentKind) => {
28
28
  const connected = await connectReviewInstance(key ? {
29
29
  ...env,
@@ -1,4 +1,4 @@
1
- import { g as activityUpdateSchema, h as activityEndSchema, i as commandSchema, m as activityBeginSchema, n as uploadSchema } from "./local-data-BfAaQmqC.js";
1
+ import { g as activityUpdateSchema, h as activityEndSchema, i as commandSchema, m as activityBeginSchema, n as uploadSchema } from "./local-data-Bm84Xdck.js";
2
2
  import { x as publishedEditSchema, y as pathTargetSchema } from "./stored-document-migration-BxZWQQjv.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
4
  import { t as findReviewPackageRoot } from "./package-paths-B6-zxvIO.js";
@@ -1 +1 @@
1
- {"version":"0.2.4-preview.20261009.108","commit":"449d5ddd42bb5bd075544be06a5dd3ff5826ab52","dirty":true,"builtAt":"2026-10-09T02:49:52.008Z"}
1
+ {"version":"0.2.4","commit":"5b00f0e66d034e187a29d270b3690a37b57ccf7b","dirty":true,"builtAt":"2026-10-09T06:23:07.418Z"}
@@ -13,7 +13,7 @@ import { t as ReviewTelemetry } from "./review-telemetry-DBy3eVCo.js";
13
13
  import { a as isOwnedShim, f as windowsInstallerCommand, m as connectPrompts, o as pathShimPath, p as scanLegacySkills, t as aliasInstallationToAccount } from "./account-alias-_1_7vROx.js";
14
14
  import { t as fuzzyRank } from "./fuzzy-match-BoAmcyak.js";
15
15
  import { n as readReviewServerDiscovery, o as reviewServerStateDir, r as readReviewServerHealth, s as serverNotReady } from "./server-discovery-Dbzn3w6Z.js";
16
- import { t as reviewAgentCliHelp } from "./agent-cli-BYMMEOlO.js";
16
+ import { t as reviewAgentCliHelp } from "./agent-cli-BrFqhnzM.js";
17
17
  import { closeSync, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readSync, renameSync, writeFileSync } from "node:fs";
18
18
  import { realpath } from "node:fs/promises";
19
19
  import path from "node:path";
@@ -4495,7 +4495,7 @@ async function runReviewCli(input) {
4495
4495
  process.once("SIGINT", stop);
4496
4496
  process.once("SIGTERM", stop);
4497
4497
  try {
4498
- const { runHeadlessServer } = await import("./headless-host-wMPUx0Aj.js");
4498
+ const { runHeadlessServer } = await import("./headless-host-DcWaxx9P.js");
4499
4499
  await runHeadlessServer({
4500
4500
  stateDir,
4501
4501
  port,
@@ -4546,7 +4546,7 @@ async function runReviewCli(input) {
4546
4546
  if (problem) throw new Error(`Cannot tell whether a Whiteboard Desktop is using ${stateDir}: ${problem.message}`);
4547
4547
  const inUse = () => /* @__PURE__ */ new Error(`A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`);
4548
4548
  if (desktops.instances.some(({ discovery }) => [discovery.appPid, discovery.serverPid].some((pid) => processIsAlive(pid)))) throw inUse();
4549
- const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-YLwWPRQp.js"), import("./headless-host-wMPUx0Aj.js")]);
4549
+ const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-BQGaX8aX.js"), import("./headless-host-DcWaxx9P.js")]);
4550
4550
  const reset = await withHeadlessServerLock(await realpath(stateDir), async () => {
4551
4551
  const local = await openReviewProfile(stateDir, { manageWorkspaces: false });
4552
4552
  try {
@@ -4774,7 +4774,7 @@ async function runReviewCli(input) {
4774
4774
  return commands;
4775
4775
  } });
4776
4776
  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) => {
4777
- const { runReviewAgentCli } = await import("./agent-cli-BJ_SxT1P.js");
4777
+ const { runReviewAgentCli } = await import("./agent-cli-BR2j-2n5.js");
4778
4778
  state.exitCode = await runReviewAgentCli({
4779
4779
  ...input,
4780
4780
  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-CyBLX8Oj.js");
20
+ const { runReviewCli } = await import("./cli-runner-Cjl8h376.js");
21
21
  return runReviewCli({
22
22
  argv,
23
23
  cliPaths: {
@@ -1,8 +1,8 @@
1
1
  import { p as isObjectValue } from "./src-X9phtB2j.js";
2
2
  import { O as withFileLock, P as writePrivateJsonAtomic } from "./s3-config-CAxhO9u_.js";
3
3
  import "./review-home-paths-6zZH-1c9.js";
4
- import { t as openReviewProfile } from "./profile-CviK33_F.js";
5
- import { i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, p as GlobalReviewDesktopVerbRelay, t as drainServerCrashReport } from "./process-error-telemetry-WjZ2FTnX.js";
4
+ import { t as openReviewProfile } from "./profile-RDEioF8b.js";
5
+ import { i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, p as GlobalReviewDesktopVerbRelay, t as drainServerCrashReport } from "./process-error-telemetry-Dzh5a3s_.js";
6
6
  import { i as reviewServerDiscoveryPath, t as headlessServerLockPath } from "./server-discovery-Dbzn3w6Z.js";
7
7
  import { randomBytes, randomUUID } from "node:crypto";
8
8
  import { mkdir, realpath, rm } from "node:fs/promises";
@@ -425,9 +425,9 @@ async function* structuralDiff(input) {
425
425
  case "merge-base": args.push(`${comparison.base}...${comparison.head}`);
426
426
  }
427
427
  args.push("--", ...input.paths ?? []);
428
- const idleAbort = new AbortController();
429
- const idle = setTimeout(() => idleAbort.abort(), 12e4);
430
- const signal = AbortSignal.any([idleAbort.signal, input.signal]);
428
+ const startupAbort = new AbortController();
429
+ const startup = setTimeout(() => startupAbort.abort(), 12e4);
430
+ const signal = AbortSignal.any([startupAbort.signal, input.signal]);
431
431
  console.info(`[Review] structural diff: ${executable} ${args.join(" ")}`);
432
432
  const child = spawn(executable, args, {
433
433
  cwd: input.repositoryPath,
@@ -461,7 +461,6 @@ async function* structuralDiff(input) {
461
461
  });
462
462
  try {
463
463
  for await (const line of lines) {
464
- idle.refresh();
465
464
  if (Buffer.byteLength(line) > 100663296) throw new Error("Structural diff record exceeded 96 MiB.");
466
465
  if (!line.trim()) continue;
467
466
  const event = decodeStructuralDiffEvent(line);
@@ -469,6 +468,7 @@ async function* structuralDiff(input) {
469
468
  if (!started) {
470
469
  if (event.type !== "start" || event.version !== 3) throw new Error("Unsupported diffr stream protocol.");
471
470
  started = true;
471
+ clearTimeout(startup);
472
472
  } else if (event.type === "complete") {
473
473
  completed = true;
474
474
  failed = event.failed;
@@ -484,7 +484,7 @@ async function* structuralDiff(input) {
484
484
  const code = await exited;
485
485
  if (code !== 0 && !(code === 2 && (failed > 0 || aborted !== void 0))) throw exitError(code);
486
486
  } finally {
487
- clearTimeout(idle);
487
+ clearTimeout(startup);
488
488
  lines.close();
489
489
  if (child.exitCode === null) child.kill();
490
490
  await exited.catch(() => {});
@@ -960,9 +960,9 @@ async function comparisonCoverage(data, reviewId, pins, mode, signal, publish) {
960
960
  const fileSources = /* @__PURE__ */ new Map();
961
961
  const alignments = /* @__PURE__ */ new Map();
962
962
  const files = [];
963
- if (mode === "textual") for (const file of await data.changes(pins)) {
963
+ for (const file of await data.changes(pins)) {
964
964
  signal.throwIfAborted();
965
- const patch = await data.changes(pins, file.path);
965
+ const patch = await data.changes(pins, file.path, file.previousPath);
966
966
  const changed = emptyCoverage();
967
967
  for (const hunk of parseUnifiedPatch(file.path, patch)) for (const line of hunk.lines) {
968
968
  if (line.kind === "add") changed.head.push([line.newLine - 1, line.newLine]);
@@ -1001,80 +1001,72 @@ async function comparisonCoverage(data, reviewId, pins, mode, signal, publish) {
1001
1001
  changed,
1002
1002
  viewed: emptyCoverage()
1003
1003
  });
1004
- publish?.({
1004
+ if (mode === "textual") publish?.({
1005
+ counted: false,
1005
1006
  files: [...files],
1006
1007
  fileSources: new Map(fileSources),
1007
1008
  alignments: new Map(alignments)
1008
1009
  });
1009
1010
  }
1010
- else {
1011
- const remaining = /* @__PURE__ */ new Set();
1012
- for await (const event of data.structuralChanges({
1013
- reviewId,
1014
- pins,
1015
- signal
1016
- })) {
1017
- if (event.type === "start") {
1018
- for (const entry of event.files) remaining.add((entry.file.rhs ?? entry.file.lhs).path);
1019
- if (!remaining.size) break;
1020
- continue;
1011
+ const coverage = {
1012
+ files,
1013
+ fileSources,
1014
+ alignments,
1015
+ counted: true
1016
+ };
1017
+ if (mode === "textual") return coverage;
1018
+ const publishCurrent = () => publish?.({
1019
+ ...coverage,
1020
+ files: [...coverage.files]
1021
+ });
1022
+ publishCurrent();
1023
+ const baseFiles = new Map(coverage.files.map((file, index) => [file.previousPath ?? file.path, index]));
1024
+ const headFiles = new Map(coverage.files.map((file, index) => [file.path, index]));
1025
+ const remaining = /* @__PURE__ */ new Set();
1026
+ for await (const event of data.structuralChanges({
1027
+ reviewId,
1028
+ pins,
1029
+ signal
1030
+ })) {
1031
+ if (event.type === "start") {
1032
+ for (const entry of event.files) remaining.add((entry.file.rhs ?? entry.file.lhs).path);
1033
+ if (!remaining.size) break;
1034
+ continue;
1035
+ }
1036
+ if (event.type === "complete" && (event.failed || event.aborted)) throw new Error(event.aborted?.message ?? "Structural coverage is incomplete.");
1037
+ if (event.type !== "file") continue;
1038
+ const path = (event.file.rhs ?? event.file.lhs).path;
1039
+ if (!remaining.delete(path)) throw new Error(`Unexpected structural result: ${path}`);
1040
+ if (event.error) {
1041
+ if (event.error.code !== "unsupported_file_type" && event.error.code !== "not_utf8") throw new Error(`Cannot count ${path}: ${event.error.message}`);
1042
+ } else if (event.diff.type === "text") {
1043
+ const diff = event.diff;
1044
+ const folded = foldedChanges(diff);
1045
+ for (const side of ["base", "head"]) {
1046
+ const source = side === "base" ? event.file.lhs : event.file.rhs;
1047
+ const index = source && (side === "base" ? baseFiles : headFiles).get(source.path);
1048
+ if (index === void 0) continue;
1049
+ const file = coverage.files[index];
1050
+ const visible = subtractIntervals(diff.structural_changes[side], folded[side]);
1051
+ coverage.files[index] = {
1052
+ ...file,
1053
+ folded: {
1054
+ ...file.folded ?? emptyCoverage(),
1055
+ [side]: subtractIntervals(file.changed[side], visible)
1056
+ }
1057
+ };
1021
1058
  }
1022
- if (event.type === "complete" && (event.failed || event.aborted)) throw new Error(event.aborted?.message ?? "Structural coverage is incomplete.");
1023
- if (event.type !== "file") continue;
1024
- const path = (event.file.rhs ?? event.file.lhs).path;
1025
- if (!remaining.delete(path)) throw new Error(`Unexpected structural result: ${path}`);
1026
- if (event.error) {
1027
- if (event.error.code !== "unsupported_file_type" && event.error.code !== "not_utf8") throw new Error(`Cannot count ${path}: ${event.error.message}`);
1028
- if (!remaining.size) break;
1029
- continue;
1059
+ const baseIndex = event.file.lhs && baseFiles.get(event.file.lhs.path);
1060
+ const index = (event.file.rhs && headFiles.get(event.file.rhs.path)) ?? baseIndex;
1061
+ if (index !== void 0) {
1062
+ if ((coverage.fileSources.get(coverage.files[index].path) ?? []).every((source) => source.file === (source.side === "base" ? event.file.lhs : event.file.rhs)?.path)) coverage.alignments = new Map(coverage.alignments).set(coverage.files[index].path, structuralRows(diff));
1030
1063
  }
1031
- const diff = event.diff;
1032
- const previousPath = event.file.lhs?.path !== path ? event.file.lhs?.path : void 0;
1033
- const base = diff.type === "text" ? diff.lhs?.text ?? null : null;
1034
- const head = diff.type === "text" ? diff.rhs?.text ?? null : null;
1035
- const fingerprint = hash([
1036
- pins.repositoryId,
1037
- base,
1038
- head,
1039
- ...diff.type === "binary" ? [event.file.lhs?.oid ?? null, event.file.rhs?.oid ?? null] : []
1040
- ]);
1041
- const sources = [];
1042
- if (base !== null) sources.push({
1043
- side: "base",
1044
- file: event.file.lhs.path,
1045
- fromLine: 1,
1046
- toLine: base.split("\n").length
1047
- });
1048
- if (head !== null) sources.push({
1049
- side: "head",
1050
- file: path,
1051
- fromLine: 1,
1052
- toLine: head.split("\n").length
1053
- });
1054
- fileSources.set(path, sources);
1055
- if (diff.type === "text") alignments.set(path, structuralRows(diff));
1056
- files.push({
1057
- path,
1058
- previousPath,
1059
- fingerprint,
1060
- changed: diff.type === "text" ? diff.structural_changes : emptyCoverage(),
1061
- folded: foldedChanges(diff),
1062
- viewed: emptyCoverage()
1063
- });
1064
- publish?.({
1065
- files: [...files],
1066
- fileSources: new Map(fileSources),
1067
- alignments: new Map(alignments)
1068
- });
1069
- if (!remaining.size) break;
1064
+ publishCurrent();
1070
1065
  }
1071
- if (remaining.size) throw new Error("Structural coverage is incomplete.");
1066
+ if (!remaining.size) break;
1072
1067
  }
1073
- return {
1074
- files,
1075
- fileSources,
1076
- alignments
1077
- };
1068
+ if (remaining.size) throw new Error("Structural coverage is incomplete.");
1069
+ return coverage;
1078
1070
  }
1079
1071
  /**
1080
1072
  * The changed lines diffr folds by default. A file it hides (lockfiles,
@@ -4221,6 +4213,7 @@ var LocalReviewData = class {
4221
4213
  return {
4222
4214
  pending: this.coveragePending(reviewId, pins, mode),
4223
4215
  comparison: this.coverageCache.get(JSON.stringify([pins, mode])).partial ?? {
4216
+ counted: false,
4224
4217
  files: [],
4225
4218
  fileSources: /* @__PURE__ */ new Map(),
4226
4219
  alignments: /* @__PURE__ */ new Map()
@@ -4272,10 +4265,11 @@ var LocalReviewData = class {
4272
4265
  }
4273
4266
  return entry.promise;
4274
4267
  }
4275
- async changes(pins, file) {
4268
+ async changes(pins, file, previousPath) {
4276
4269
  if (file === void 0) return this.summaries(pins);
4277
4270
  checkRelativePath(file);
4278
- return this.rawPatch(pins, { paths: [file] });
4271
+ if (previousPath) checkRelativePath(previousPath);
4272
+ return this.rawPatch(pins, { paths: previousPath ? [previousPath, file] : [file] });
4279
4273
  }
4280
4274
  async summaries(pins) {
4281
4275
  if (pins.worktreeRevision) return diffFileSummariesWorkingTree(await this.worktreeInput(pins));
@@ -1,6 +1,6 @@
1
1
  import { a as toolResultText, s as ReviewApiError } from "./agent-client-B_b1GtUc.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-CUJ_56bd.js";
3
+ import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-DvIO906l.js";
4
4
  import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-DJmTieh5.js";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -1,6 +1,6 @@
1
1
  import { c as jsonString, m as isStringValue, n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
2
2
  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-BFClfQLO.js";
3
- import { _ as invalidateStructuralComparisons, a as inspectSnapshot, c as askAgentIds, d as askQuestionSchema, f as AgentSelectionSchema, i as commandSchema, l as askChoiceKinds, p as selectionMarkdown, r as SCRATCHPAD_ID, s as applyAskChange, u as askPicksSchema, v as diffrExecutable, y as diffrMissingError } from "./local-data-BfAaQmqC.js";
3
+ import { _ as invalidateStructuralComparisons, a as inspectSnapshot, c as askAgentIds, d as askQuestionSchema, f as AgentSelectionSchema, i as commandSchema, l as askChoiceKinds, p as selectionMarkdown, r as SCRATCHPAD_ID, s as applyAskChange, u as askPicksSchema, v as diffrExecutable, y as diffrMissingError } from "./local-data-Bm84Xdck.js";
4
4
  import { d as REVIEW_CLIENT_REMOTE, i as parseReviewVerbRequest, r as parseReviewDesktopVerbResult, u as REVIEW_CLIENT_HEADER, v as reviewDiffrSummarizerInputSchema, y as decodeStructuralDiffEvent } from "./src-WCbabvdS.js";
5
5
  import { C as TraceStorageDeniedError, G as readStoreAuth, H as DEFAULT_STORE_ORIGIN, U as clearStoreAuth, W as openUrlInBrowser, _ as isTraceStorageConfigured, b as selectTraceStorage, o as listReviewTraceSessions, q as runStoreLogin, s as loadReviewAgentTrace, y as resolveTraceStorage } from "./review-agent-traces-DNJFYzc8.js";
6
6
  import { $ as unionIntervals, G as selectionKey, H as comparisonKey, I as fileLineRangeSchema, J as coverageSources, N as lensSelections, Q as subtractIntervals, T as selectionReferences, U as formatAnchor, V as anchorSelection, W as resolveDiffSelection, Y as emptyCoverage, Z as scopedCoverage, h as elements, k as UNCATEGORIZED_LENS_ID, u as anchorPins } from "./stored-document-migration-BxZWQQjv.js";
@@ -10,7 +10,7 @@ import { t as errorMessage } from "./error-message-OtiDonty.js";
10
10
  import { T as traceMachineEnabled } from "./review-home-paths-6zZH-1c9.js";
11
11
  import { i as readReviewPackageVersion, n as readBuildCommit } from "./package-paths-B6-zxvIO.js";
12
12
  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-DA4K1qWY.js";
13
- 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-CUJ_56bd.js";
13
+ 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-DvIO906l.js";
14
14
  import { t as fuzzyRank } from "./fuzzy-match-BoAmcyak.js";
15
15
  import { createRequire } from "node:module";
16
16
  import crypto$1, { createHash, randomUUID } from "node:crypto";
@@ -2909,10 +2909,12 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
2909
2909
  const marks = snapshot.shared ? /* @__PURE__ */ new Map() : store.viewedCoverage(snapshot.reviewId);
2910
2910
  const pins = snapshot.pins ? (await data.resolveSource(snapshot)).pins : void 0;
2911
2911
  const comparison = pins ? partial ?? await data.coverage(snapshot.reviewId, pins, mode) : {
2912
+ counted: true,
2912
2913
  files: [],
2913
2914
  fileSources: /* @__PURE__ */ new Map(),
2914
2915
  alignments: /* @__PURE__ */ new Map()
2915
2916
  };
2917
+ const counting = !comparison.counted;
2916
2918
  signal.throwIfAborted();
2917
2919
  const documentKey = JSON.stringify(pins);
2918
2920
  const comparisons = /* @__PURE__ */ new Map();
@@ -2922,7 +2924,8 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
2922
2924
  let loading = comparisons.get(key);
2923
2925
  if (loading) return loading;
2924
2926
  if (partial) {
2925
- if (data.coverageSnapshot(snapshot.reviewId, own, mode).pending) return void 0;
2927
+ const state = data.coverageSnapshot(snapshot.reviewId, own, mode);
2928
+ if (state.pending) return state.comparison.counted ? Promise.resolve(state.comparison) : void 0;
2926
2929
  }
2927
2930
  loading = data.coverage(snapshot.reviewId, own, mode);
2928
2931
  comparisons.set(key, loading);
@@ -2962,7 +2965,7 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
2962
2965
  if (file) group[file.path] ??= withViewed(file);
2963
2966
  }
2964
2967
  let rows = file && ownAlignments.get(file.path);
2965
- if (!file && partial && !own) {
2968
+ if (!file && counting && !own) {
2966
2969
  pendingSources.add(key);
2967
2970
  return [];
2968
2971
  }
@@ -2995,8 +2998,8 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
2995
2998
  return {
2996
2999
  ...lens,
2997
3000
  ...resolved,
2998
- pending: !!partial,
2999
- unavailable: partial || resolved.fileCount ? void 0 : "No files match these targets"
3001
+ pending: counting,
3002
+ unavailable: counting || resolved.fileCount ? void 0 : "No files match these targets"
3000
3003
  };
3001
3004
  } catch {
3002
3005
  return {
@@ -3011,18 +3014,18 @@ async function reviewProgress(store, data, snapshot, signal = new AbortControlle
3011
3014
  await resolveAvailable(source);
3012
3015
  } catch {}
3013
3016
  }));
3014
- const uncategorized = partial ? [] : uncategorizedSources(files, lenses.filter((lens) => !lens.unavailable).flatMap((lens) => lens.sources));
3017
+ const uncategorized = counting ? [] : uncategorizedSources(files, lenses.filter((lens) => !lens.unavailable).flatMap((lens) => lens.sources));
3015
3018
  lenses.push({
3016
3019
  id: UNCATEGORIZED_LENS_ID,
3017
3020
  title: "Uncategorized changes",
3018
3021
  sources: uncategorized,
3019
3022
  wholeFiles: false,
3020
3023
  fileCount: new Set(uncategorized.map((source) => files.find((file) => source.file === (source.side === "base" ? file.previousPath ?? file.path : file.path))?.path ?? source.file)).size,
3021
- pending: !!partial,
3022
- unavailable: partial || uncategorized.length ? void 0 : "All changed lines are covered by lenses"
3024
+ pending: counting,
3025
+ unavailable: counting || uncategorized.length ? void 0 : "All changed lines are covered by lenses"
3023
3026
  });
3024
3027
  const progress = {
3025
- complete: !partial,
3028
+ complete: !counting,
3026
3029
  files,
3027
3030
  referenceFiles,
3028
3031
  lenses,
@@ -0,0 +1,2 @@
1
+ import { t as openReviewProfile } from "./profile-RDEioF8b.js";
2
+ export { openReviewProfile };
@@ -1,4 +1,4 @@
1
- import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-BfAaQmqC.js";
1
+ import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-Bm84Xdck.js";
2
2
  import { O as withFileLock } from "./s3-config-CAxhO9u_.js";
3
3
  import "./review-home-paths-6zZH-1c9.js";
4
4
  import { access, mkdir } from "node:fs/promises";
@@ -9,12 +9,12 @@ import { A as devReviewHome, P as writePrivateJsonAtomic } from "../s3-config-CA
9
9
  import { U as shellQuote, i as reviewInstanceDiscoveryPath, o as reviewLegacyDiscoveryPath } from "../review-home-paths-6zZH-1c9.js";
10
10
  import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "../package-paths-B6-zxvIO.js";
11
11
  import { c as reviewInstanceIdentity, t as REVIEW_INSTANCE_ENV } from "../desktop-discovery-C-Qz54wO.js";
12
- import { t as openReviewProfile } from "../profile-CviK33_F.js";
12
+ import { t as openReviewProfile } from "../profile-RDEioF8b.js";
13
13
  import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-DBy3eVCo.js";
14
14
  import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-DA4K1qWY.js";
15
15
  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-_1_7vROx.js";
16
- import { _ as setDiffrConfigValue, a as serverJson, c as recordClientError, d as HttpJsonError, f as ReviewServerError, g as saveDiffrSummarizer, h as readDiffrConfig, i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, o as captureSanitizedUiTelemetry, p as GlobalReviewDesktopVerbRelay, r as serverCrashReportPath, s as clientErrorsForSession, t as drainServerCrashReport, u as readBoundedRequestJson, v as testDiffrSummarizer } from "../process-error-telemetry-WjZ2FTnX.js";
17
- import { c as readQuerySchemas } from "../authoring-tools-CUJ_56bd.js";
16
+ import { _ as setDiffrConfigValue, a as serverJson, c as recordClientError, d as HttpJsonError, f as ReviewServerError, g as saveDiffrSummarizer, h as readDiffrConfig, i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, o as captureSanitizedUiTelemetry, p as GlobalReviewDesktopVerbRelay, r as serverCrashReportPath, s as clientErrorsForSession, t as drainServerCrashReport, u as readBoundedRequestJson, v as testDiffrSummarizer } from "../process-error-telemetry-Dzh5a3s_.js";
17
+ import { c as readQuerySchemas } from "../authoring-tools-DvIO906l.js";
18
18
  import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
19
19
  import { z } from "zod";
20
20
  import fs, { existsSync } from "node:fs";
@@ -312,7 +312,7 @@ declare const fileLineRangeSchema: z.ZodObject<{
312
312
  type FileLineRange = z.infer<typeof fileLineRangeSchema>;
313
313
  //#endregion
314
314
  //#region src/lens-selection.d.ts
315
- type AlignmentRow$1 = readonly [number | null, number | null];
315
+ type AlignmentRow = readonly [number | null, number | null];
316
316
  //#endregion
317
317
  //#region src/viewed-coverage.d.ts
318
318
  /** Zero-based, half-open intervals; always normalized at the storage boundary. */
@@ -321,7 +321,7 @@ interface Coverage {
321
321
  base: LineInterval[];
322
322
  head: LineInterval[];
323
323
  }
324
- interface CoverageFile$1 {
324
+ interface CoverageFile {
325
325
  path: string;
326
326
  previousPath?: string;
327
327
  fingerprint: string;
@@ -1040,9 +1040,11 @@ interface WrittenComponent {
1040
1040
  //#region src/review-api/comparison-coverage.d.ts
1041
1041
  type CoverageMode = "structural" | "textual";
1042
1042
  interface ComparisonCoverage {
1043
- files: CoverageFile$1[];
1043
+ /** All Git changes are counted, even if folding is still loading. */
1044
+ counted: boolean;
1045
+ files: CoverageFile[];
1044
1046
  fileSources: Map<string, FileLineRange[]>;
1045
- alignments: Map<string, readonly AlignmentRow$1[]>;
1047
+ alignments: Map<string, readonly AlignmentRow[]>;
1046
1048
  }
1047
1049
  //#endregion
1048
1050
  //#region src/review-api/pull-request.d.ts
@@ -1696,6 +1698,7 @@ declare class LocalReviewData {
1696
1698
  coverageSnapshot(reviewId: string, pins: Pins, mode: CoverageMode): {
1697
1699
  pending: boolean;
1698
1700
  comparison: ComparisonCoverage | {
1701
+ counted: false;
1699
1702
  files: never[];
1700
1703
  fileSources: Map<any, any>;
1701
1704
  alignments: Map<any, any>;
@@ -1703,14 +1706,10 @@ declare class LocalReviewData {
1703
1706
  };
1704
1707
  private coverageNotification;
1705
1708
  private notifyCoverage;
1706
- coverage(reviewId: string, pins: Pins, mode: CoverageMode): Promise<{
1707
- files: CoverageFile[];
1708
- fileSources: Map<string, FileLineRange[]>;
1709
- alignments: Map<string, readonly AlignmentRow[]>;
1710
- }>;
1709
+ coverage(reviewId: string, pins: Pins, mode: CoverageMode): Promise<ComparisonCoverage>;
1711
1710
  changes(pins: Pins): Promise<LocalVcsDiffFileSummary[]>;
1712
- changes(pins: Pins, file: string): Promise<string>;
1713
- changes(pins: Pins, file?: string): Promise<LocalVcsDiffFileSummary[] | string>;
1711
+ changes(pins: Pins, file: string, previousPath?: string): Promise<string>;
1712
+ changes(pins: Pins, file?: string, previousPath?: string): Promise<LocalVcsDiffFileSummary[] | string>;
1714
1713
  private summaries;
1715
1714
  /** Raw Git patch text for exact filenames, or every change. */
1716
1715
  private rawPatch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.fast/whiteboard",
3
- "version": "0.2.4-preview.20261009.108",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -71,10 +71,10 @@
71
71
  "tsx": "4.21.0",
72
72
  "typescript": "6.0.3",
73
73
  "vitest": "4.1.11",
74
+ "@dev.fast/json": "0.0.0",
74
75
  "@dev.fast/review-protocol": "0.0.0",
75
76
  "@dev.fast/review-share-protocol": "0.1.2",
76
77
  "@dev.fast/trace-core": "0.0.0",
77
- "@dev.fast/json": "0.0.0",
78
78
  "@dev.fast/trace-protocol": "0.5.0"
79
79
  },
80
80
  "engines": {
@@ -85,7 +85,7 @@
85
85
  "repo": "devdotfast/review",
86
86
  "docs": "packages/review/instructions/authoring.md"
87
87
  },
88
- "gitHead": "449d5ddd42bb5bd075544be06a5dd3ff5826ab52",
88
+ "gitHead": "5b00f0e66d034e187a29d270b3690a37b57ccf7b",
89
89
  "scripts": {
90
90
  "prebuild": "pnpm run build:workspace-deps",
91
91
  "build": "node --max-old-space-size=8192 node_modules/tsdown/dist/run.mjs",
@@ -29,6 +29,8 @@ const hash = (parts: (string | null)[]) =>
29
29
  createHash("sha256").update(JSON.stringify(parts)).digest("hex");
30
30
 
31
31
  export interface ComparisonCoverage {
32
+ /** All Git changes are counted, even if folding is still loading. */
33
+ counted: boolean;
32
34
  files: CoverageFile[];
33
35
  fileSources: Map<string, FileLineRange[]>;
34
36
  alignments: Map<string, readonly AlignmentRow[]>;
@@ -42,201 +44,221 @@ export async function comparisonCoverage(
42
44
  mode: CoverageMode,
43
45
  signal: AbortSignal,
44
46
  publish?: (coverage: ComparisonCoverage) => void,
45
- ) {
47
+ ): Promise<ComparisonCoverage> {
46
48
  const fileSources = new Map<string, FileLineRange[]>();
47
49
  const alignments = new Map<string, readonly AlignmentRow[]>();
48
50
 
49
51
  const files: CoverageFile[] = [];
50
52
 
51
- if (mode === "textual") {
52
- for (const file of await data.changes(pins)) {
53
- signal.throwIfAborted();
54
- const patch = await data.changes(pins, file.path);
55
- const changed = emptyCoverage();
56
-
57
- for (const hunk of parseUnifiedPatch(file.path, patch))
58
- for (const line of hunk.lines) {
59
- if (line.kind === "add")
60
- changed.head.push([line.newLine! - 1, line.newLine!]);
61
-
62
- if (line.kind === "remove")
63
- changed.base.push([line.oldLine! - 1, line.oldLine!]);
64
- }
65
-
66
- changed.base = unionIntervals(changed.base);
67
- changed.head = unionIntervals(changed.head);
68
-
69
- const [base, head] = await Promise.all([
70
- file.status === "added"
71
- ? null
72
- : data
73
- .file(pins, "base", file.previousPath ?? file.path)
74
- .then((value) => value.text)
75
- .catch(() => null),
76
- file.status === "deleted"
77
- ? null
78
- : data
79
- .file(pins, "head", file.path)
80
- .then((value) => value.text)
81
- .catch(() => null),
82
- ]);
83
-
84
- alignments.set(
85
- file.path,
86
- textualRows(
87
- file.path,
88
- patch,
89
- base === null ? 0 : base.split("\n").length,
90
- head === null ? 0 : head.split("\n").length,
91
- ),
92
- );
53
+ for (const file of await data.changes(pins)) {
54
+ signal.throwIfAborted();
55
+ const patch = await data.changes(pins, file.path, file.previousPath);
56
+ const changed = emptyCoverage();
93
57
 
94
- const readable =
95
- (file.status === "added" || base !== null) &&
96
- (file.status === "deleted" || head !== null);
97
-
98
- const fingerprint = hash([
99
- pins.repositoryId,
100
- base,
101
- head,
102
- ...(readable ? [] : [pins.base, pins.head, patch]),
103
- ]);
104
-
105
- fileSources.set(file.path, [
106
- ...(base !== null
107
- ? [
108
- {
109
- side: "base" as const,
110
- file: file.previousPath ?? file.path,
111
- fromLine: 1,
112
- toLine: base.split("\n").length,
113
- },
114
- ]
115
- : []),
116
- ...(head !== null
117
- ? [
118
- {
119
- side: "head" as const,
120
- file: file.path,
121
- fromLine: 1,
122
- toLine: head.split("\n").length,
123
- },
124
- ]
125
- : []),
126
- ]);
127
- files.push({
128
- path: file.path,
129
- previousPath: file.previousPath,
130
- fingerprint,
131
- changed,
132
- viewed: emptyCoverage(),
133
- });
58
+ for (const hunk of parseUnifiedPatch(file.path, patch))
59
+ for (const line of hunk.lines) {
60
+ if (line.kind === "add")
61
+ changed.head.push([line.newLine! - 1, line.newLine!]);
62
+
63
+ if (line.kind === "remove")
64
+ changed.base.push([line.oldLine! - 1, line.oldLine!]);
65
+ }
66
+
67
+ changed.base = unionIntervals(changed.base);
68
+ changed.head = unionIntervals(changed.head);
69
+
70
+ const [base, head] = await Promise.all([
71
+ file.status === "added"
72
+ ? null
73
+ : data
74
+ .file(pins, "base", file.previousPath ?? file.path)
75
+ .then((value) => value.text)
76
+ .catch(() => null),
77
+ file.status === "deleted"
78
+ ? null
79
+ : data
80
+ .file(pins, "head", file.path)
81
+ .then((value) => value.text)
82
+ .catch(() => null),
83
+ ]);
84
+
85
+ alignments.set(
86
+ file.path,
87
+ textualRows(
88
+ file.path,
89
+ patch,
90
+ base === null ? 0 : base.split("\n").length,
91
+ head === null ? 0 : head.split("\n").length,
92
+ ),
93
+ );
94
+
95
+ const readable =
96
+ (file.status === "added" || base !== null) &&
97
+ (file.status === "deleted" || head !== null);
98
+
99
+ const fingerprint = hash([
100
+ pins.repositoryId,
101
+ base,
102
+ head,
103
+ ...(readable ? [] : [pins.base, pins.head, patch]),
104
+ ]);
105
+
106
+ fileSources.set(file.path, [
107
+ ...(base !== null
108
+ ? [
109
+ {
110
+ side: "base" as const,
111
+ file: file.previousPath ?? file.path,
112
+ fromLine: 1,
113
+ toLine: base.split("\n").length,
114
+ },
115
+ ]
116
+ : []),
117
+ ...(head !== null
118
+ ? [
119
+ {
120
+ side: "head" as const,
121
+ file: file.path,
122
+ fromLine: 1,
123
+ toLine: head.split("\n").length,
124
+ },
125
+ ]
126
+ : []),
127
+ ]);
128
+ files.push({
129
+ path: file.path,
130
+ previousPath: file.previousPath,
131
+ fingerprint,
132
+ changed,
133
+ viewed: emptyCoverage(),
134
+ });
135
+
136
+ if (mode === "textual")
134
137
  publish?.({
138
+ counted: false,
135
139
  files: [...files],
136
140
  fileSources: new Map(fileSources),
137
141
  alignments: new Map(alignments),
138
142
  });
143
+ }
144
+
145
+ const coverage: ComparisonCoverage = {
146
+ files,
147
+ fileSources,
148
+ alignments,
149
+ counted: true,
150
+ };
151
+
152
+ if (mode === "textual") return coverage;
153
+
154
+ // Keep Git's totals and file identities while structural results add folding.
155
+ const publishCurrent = () =>
156
+ publish?.({ ...coverage, files: [...coverage.files] });
157
+
158
+ publishCurrent();
159
+
160
+ const baseFiles = new Map(
161
+ coverage.files.map((file, index) => [
162
+ file.previousPath ?? file.path,
163
+ index,
164
+ ]),
165
+ );
166
+
167
+ const headFiles = new Map(
168
+ coverage.files.map((file, index) => [file.path, index]),
169
+ );
170
+
171
+ const remaining = new Set<string>();
172
+
173
+ for await (const event of data.structuralChanges({
174
+ reviewId,
175
+ pins,
176
+ signal,
177
+ })) {
178
+ if (event.type === "start") {
179
+ for (const entry of event.files)
180
+ remaining.add((entry.file.rhs ?? entry.file.lhs)!.path);
181
+
182
+ if (!remaining.size) break;
183
+ continue;
139
184
  }
140
- } else {
141
- const remaining = new Set<string>();
142
-
143
- for await (const event of data.structuralChanges({
144
- reviewId,
145
- pins,
146
- signal,
147
- })) {
148
- if (event.type === "start") {
149
- for (const entry of event.files)
150
- remaining.add((entry.file.rhs ?? entry.file.lhs)!.path);
151
-
152
- if (!remaining.size) break;
153
- continue;
154
- }
155
185
 
156
- if (event.type === "complete" && (event.failed || event.aborted))
157
- throw new Error(
158
- event.aborted?.message ?? "Structural coverage is incomplete.",
159
- );
186
+ if (event.type === "complete" && (event.failed || event.aborted))
187
+ throw new Error(
188
+ event.aborted?.message ?? "Structural coverage is incomplete.",
189
+ );
160
190
 
161
- if (event.type !== "file") continue;
191
+ if (event.type !== "file") continue;
162
192
 
163
- const path = (event.file.rhs ?? event.file.lhs)!.path;
193
+ const path = (event.file.rhs ?? event.file.lhs)!.path;
164
194
 
165
- if (!remaining.delete(path))
166
- throw new Error(`Unexpected structural result: ${path}`);
195
+ if (!remaining.delete(path))
196
+ throw new Error(`Unexpected structural result: ${path}`);
167
197
 
168
- if (event.error) {
169
- // Neither kind has line ranges to count. Keep the rest of the review
170
- // available when a repository contains non-UTF-8 fixtures or links.
171
- if (
172
- event.error.code !== "unsupported_file_type" &&
173
- event.error.code !== "not_utf8"
174
- )
175
- throw new Error(`Cannot count ${path}: ${event.error.message}`);
198
+ if (event.error) {
199
+ if (
200
+ event.error.code !== "unsupported_file_type" &&
201
+ event.error.code !== "not_utf8"
202
+ )
203
+ throw new Error(`Cannot count ${path}: ${event.error.message}`);
204
+ } else if (event.diff.type === "text") {
205
+ const diff = event.diff;
206
+ const folded = foldedChanges(diff);
207
+
208
+ // Match each side separately: diffr can split a Git rename into two files.
209
+ for (const side of ["base", "head"] as const) {
210
+ const source = side === "base" ? event.file.lhs : event.file.rhs;
211
+
212
+ const index =
213
+ source && (side === "base" ? baseFiles : headFiles).get(source.path);
214
+
215
+ if (index === undefined) continue;
216
+ const file = coverage.files[index];
176
217
 
177
- if (!remaining.size) break;
178
- continue;
218
+ const visible = subtractIntervals(
219
+ diff.structural_changes[side],
220
+ folded[side],
221
+ );
222
+
223
+ coverage.files[index] = {
224
+ ...file,
225
+ folded: {
226
+ ...(file.folded ?? emptyCoverage()),
227
+ [side]: subtractIntervals(file.changed[side], visible),
228
+ },
229
+ };
179
230
  }
180
231
 
181
- const diff = event.diff;
232
+ const baseIndex = event.file.lhs && baseFiles.get(event.file.lhs.path);
233
+ const headIndex = event.file.rhs && headFiles.get(event.file.rhs.path);
234
+ const index = headIndex ?? baseIndex;
182
235
 
183
- const previousPath =
184
- event.file.lhs?.path !== path ? event.file.lhs?.path : undefined;
185
-
186
- const base = diff.type === "text" ? (diff.lhs?.text ?? null) : null;
187
- const head = diff.type === "text" ? (diff.rhs?.text ?? null) : null;
188
-
189
- const fingerprint = hash([
190
- pins.repositoryId,
191
- base,
192
- head,
193
- ...(diff.type === "binary"
194
- ? [event.file.lhs?.oid ?? null, event.file.rhs?.oid ?? null]
195
- : []),
196
- ]);
197
-
198
- const sources: FileLineRange[] = [];
199
-
200
- if (base !== null)
201
- sources.push({
202
- side: "base",
203
- file: event.file.lhs!.path,
204
- fromLine: 1,
205
- toLine: base.split("\n").length,
206
- });
207
-
208
- if (head !== null)
209
- sources.push({
210
- side: "head",
211
- file: path,
212
- fromLine: 1,
213
- toLine: head.split("\n").length,
214
- });
215
- fileSources.set(path, sources);
216
-
217
- if (diff.type === "text") alignments.set(path, structuralRows(diff));
218
- files.push({
219
- path,
220
- previousPath,
221
- fingerprint,
222
- changed:
223
- diff.type === "text" ? diff.structural_changes : emptyCoverage(),
224
- folded: foldedChanges(diff),
225
- viewed: emptyCoverage(),
226
- });
227
- publish?.({
228
- files: [...files],
229
- fileSources: new Map(fileSources),
230
- alignments: new Map(alignments),
231
- });
236
+ if (index !== undefined) {
237
+ const sources =
238
+ coverage.fileSources.get(coverage.files[index].path) ?? [];
232
239
 
233
- if (!remaining.size) break;
240
+ if (
241
+ sources.every(
242
+ (source) =>
243
+ source.file ===
244
+ (source.side === "base" ? event.file.lhs : event.file.rhs)?.path,
245
+ )
246
+ )
247
+ coverage.alignments = new Map(coverage.alignments).set(
248
+ coverage.files[index].path,
249
+ structuralRows(diff),
250
+ );
251
+ }
252
+
253
+ publishCurrent();
234
254
  }
235
255
 
236
- if (remaining.size) throw new Error("Structural coverage is incomplete.");
256
+ if (!remaining.size) break;
237
257
  }
238
258
 
239
- return { files, fileSources, alignments };
259
+ if (remaining.size) throw new Error("Structural coverage is incomplete.");
260
+
261
+ return coverage;
240
262
  }
241
263
 
242
264
  /**
@@ -1409,6 +1409,7 @@ export class LocalReviewData {
1409
1409
  return {
1410
1410
  pending,
1411
1411
  comparison: entry.partial ?? {
1412
+ counted: false,
1412
1413
  files: [],
1413
1414
  fileSources: new Map(),
1414
1415
  alignments: new Map(),
@@ -1496,16 +1497,21 @@ export class LocalReviewData {
1496
1497
  }
1497
1498
 
1498
1499
  changes(pins: Pins): Promise<LocalVcsDiffFileSummary[]>;
1499
- changes(pins: Pins, file: string): Promise<string>;
1500
+ changes(pins: Pins, file: string, previousPath?: string): Promise<string>;
1500
1501
  changes(
1501
1502
  pins: Pins,
1502
1503
  file?: string,
1504
+ previousPath?: string,
1503
1505
  ): Promise<LocalVcsDiffFileSummary[] | string>;
1504
- async changes(pins: Pins, file?: string) {
1506
+ async changes(pins: Pins, file?: string, previousPath?: string) {
1505
1507
  if (file === undefined) return this.summaries(pins);
1506
1508
  checkRelativePath(file);
1507
1509
 
1508
- return this.rawPatch(pins, { paths: [file] });
1510
+ if (previousPath) checkRelativePath(previousPath);
1511
+
1512
+ return this.rawPatch(pins, {
1513
+ paths: previousPath ? [previousPath, file] : [file],
1514
+ });
1509
1515
  }
1510
1516
  private async summaries(pins: Pins) {
1511
1517
  if (pins.worktreeRevision) {
@@ -79,7 +79,14 @@ export async function reviewProgress(
79
79
  // each resolve against the comparison their own pins name.
80
80
  const comparison: ComparisonCoverage = pins
81
81
  ? (partial ?? (await data.coverage(snapshot.reviewId, pins, mode)))
82
- : { files: [], fileSources: new Map(), alignments: new Map() };
82
+ : {
83
+ counted: true,
84
+ files: [],
85
+ fileSources: new Map(),
86
+ alignments: new Map(),
87
+ };
88
+
89
+ const counting = !comparison.counted;
83
90
 
84
91
  signal.throwIfAborted();
85
92
 
@@ -98,7 +105,10 @@ export async function reviewProgress(
98
105
  if (partial) {
99
106
  const state = data.coverageSnapshot(snapshot.reviewId, own, mode);
100
107
 
101
- if (state.pending) return undefined;
108
+ if (state.pending)
109
+ return state.comparison.counted
110
+ ? Promise.resolve(state.comparison)
111
+ : undefined;
102
112
  }
103
113
 
104
114
  loading = data.coverage(snapshot.reviewId, own, mode);
@@ -162,7 +172,7 @@ export async function reviewProgress(
162
172
 
163
173
  // Context-only files are absent from the changed-file stream. Read both pins
164
174
  // and require equality rather than inventing correspondence for missing diffs.
165
- if (!file && partial && !own) {
175
+ if (!file && counting && !own) {
166
176
  pendingSources.add(key);
167
177
 
168
178
  return [];
@@ -228,9 +238,9 @@ export async function reviewProgress(
228
238
  return {
229
239
  ...lens,
230
240
  ...resolved,
231
- pending: !!partial,
241
+ pending: counting,
232
242
  unavailable:
233
- partial || resolved.fileCount
243
+ counting || resolved.fileCount
234
244
  ? undefined
235
245
  : "No files match these targets",
236
246
  };
@@ -257,7 +267,7 @@ export async function reviewProgress(
257
267
  ),
258
268
  );
259
269
 
260
- const uncategorized = partial
270
+ const uncategorized = counting
261
271
  ? []
262
272
  : uncategorizedSources(
263
273
  files,
@@ -283,15 +293,15 @@ export async function reviewProgress(
283
293
  )?.path ?? source.file,
284
294
  ),
285
295
  ).size,
286
- pending: !!partial,
296
+ pending: counting,
287
297
  unavailable:
288
- partial || uncategorized.length
298
+ counting || uncategorized.length
289
299
  ? undefined
290
300
  : "All changed lines are covered by lenses",
291
301
  });
292
302
 
293
303
  const progress: ReviewProgress = {
294
- complete: !partial,
304
+ complete: !counting,
295
305
  files,
296
306
  referenceFiles,
297
307
  lenses,
@@ -63,9 +63,11 @@ export async function* structuralDiff(
63
63
 
64
64
  args.push("--", ...(input.paths ?? []));
65
65
 
66
- const idleAbort = new AbortController();
67
- const idle = setTimeout(() => idleAbort.abort(), 120_000);
68
- const signal = AbortSignal.any([idleAbort.signal, input.signal]);
66
+ // Plugins can exceed two minutes between records. Apply this timeout only
67
+ // until the manifest arrives; plugins enforce their own request timeouts.
68
+ const startupAbort = new AbortController();
69
+ const startup = setTimeout(() => startupAbort.abort(), 120_000);
70
+ const signal = AbortSignal.any([startupAbort.signal, input.signal]);
69
71
 
70
72
  // The host inherits its own environment and runs from the repository, so
71
73
  // diffr reads the user's config and keys exactly as it would from a shell.
@@ -103,9 +105,6 @@ export async function* structuralDiff(
103
105
 
104
106
  try {
105
107
  for await (const line of lines) {
106
- // Large comparisons may take minutes while continuing to make progress.
107
- idle.refresh();
108
-
109
108
  // Bound individual records, not the entire streamed comparison: a
110
109
  // directory move can legitimately contain thousands of small files.
111
110
  // Generated parsers in diffr can exceed 64 MiB after JSON encoding.
@@ -126,6 +125,7 @@ export async function* structuralDiff(
126
125
  }
127
126
 
128
127
  started = true;
128
+ clearTimeout(startup);
129
129
  } else if (event.type === "complete") {
130
130
  completed = true;
131
131
 
@@ -151,7 +151,7 @@ export async function* structuralDiff(
151
151
  if (code !== 0 && !(code === 2 && (failed > 0 || aborted !== undefined)))
152
152
  throw exitError(code);
153
153
  } finally {
154
- clearTimeout(idle);
154
+ clearTimeout(startup);
155
155
  lines.close();
156
156
 
157
157
  if (child.exitCode === null) child.kill();
@@ -1,2 +0,0 @@
1
- import { n as runReviewAgentCli } from "./agent-cli-BYMMEOlO.js";
2
- export { runReviewAgentCli };
@@ -1,2 +0,0 @@
1
- import { t as openReviewProfile } from "./profile-CviK33_F.js";
2
- export { openReviewProfile };