@dev.fast/whiteboard 0.2.4 → 0.2.5-preview.20261013.110

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 (77) hide show
  1. package/dist/{account-alias-_1_7vROx.js → account-alias-D0hF0sKk.js} +2 -3
  2. package/dist/{agent-cli-BrFqhnzM.js → agent-cli-BBPxwUOd.js} +6 -6
  3. package/dist/agent-cli-YSuhE5X8.js +2 -0
  4. package/dist/{agent-client-B_b1GtUc.js → agent-client-P3J4y8Zn.js} +3 -3
  5. package/dist/{authoring-tools-DvIO906l.js → authoring-tools-D8WXsm_g.js} +11 -10
  6. package/dist/build-info.json +1 -1
  7. package/dist/{cli-gRGxkwc0.js → cli-C0ziP6hg.js} +1 -1
  8. package/dist/{cli-runner-Cjl8h376.js → cli-runner-C1gY-HAI.js} +16 -17
  9. package/dist/cli.js +1 -1
  10. package/dist/{client-BFClfQLO.js → client-DzcSt6pV.js} +63 -13
  11. package/dist/{desktop-discovery-C-Qz54wO.js → desktop-discovery-Ba3dD5Cn.js} +1 -1
  12. package/dist/{headless-host-DcWaxx9P.js → headless-host-BHD_Vg5F.js} +4 -4
  13. package/dist/{local-data-Bm84Xdck.js → local-data-BlERtVm2.js} +281 -75
  14. package/dist/{mcp-BDy9Aybd.js → mcp-t7VN9R3k.js} +4 -4
  15. package/dist/{package-paths-B6-zxvIO.js → package-paths-B9PDoAl6.js} +1 -1
  16. package/dist/{process-error-telemetry-Dzh5a3s_.js → process-error-telemetry-C0swByfp.js} +158 -57
  17. package/dist/{profile-RDEioF8b.js → profile-BsYDBr7t.js} +2 -2
  18. package/dist/profile-j97aRTS7.js +2 -0
  19. package/dist/{review-agent-traces-DNJFYzc8.js → review-home-paths-Y9mYWkCI.js} +2600 -62
  20. package/dist/{review-telemetry-DBy3eVCo.js → review-telemetry-CmM6tCbj.js} +2 -2
  21. package/dist/server/desktop-host.js +35 -46
  22. package/dist/{server-discovery-Dbzn3w6Z.js → server-discovery-ZVi8Ay8J.js} +1 -1
  23. package/dist/sharing/index.d.ts +37 -49
  24. package/dist/sharing/index.js +1 -1
  25. package/dist/{src-CmdiBL20.js → src-DiHP11l3.js} +11 -1
  26. package/dist/{stored-document-migration-BxZWQQjv.js → stored-document-migration-u4L_1VZa.js} +15 -36
  27. package/dist/{tool-failure-DJmTieh5.js → tool-failure-CJHBCHMh.js} +2 -2
  28. package/instructions/scratchpad.md +1 -1
  29. package/package.json +4 -3
  30. package/src/ask/agents.ts +6 -0
  31. package/src/ask/thread-state.ts +17 -1
  32. package/src/ask/thread.ts +116 -22
  33. package/src/ask/threads.ts +8 -2
  34. package/src/error-telemetry.ts +13 -9
  35. package/src/fixtures/blocks/tutorial.json +3 -40
  36. package/src/review-api/README.md +10 -7
  37. package/src/review-api/ask-history.ts +37 -0
  38. package/src/review-api/authoring-tools.ts +8 -5
  39. package/src/review-api/blocks/database_lens.ts +5 -0
  40. package/src/review-api/blocks/tutorial.ts +2 -18
  41. package/src/review-api/comparison-coverage.ts +16 -9
  42. package/src/review-api/component-reference.ts +1 -1
  43. package/src/review-api/document.ts +24 -10
  44. package/src/review-api/http.ts +267 -189
  45. package/src/review-api/local-data.ts +126 -37
  46. package/src/review-api/store-schema.ts +25 -1
  47. package/src/review-api/store.ts +165 -33
  48. package/src/review-api/workspaces.ts +14 -14
  49. package/src/review-api/worktree-events.ts +46 -0
  50. package/src/review-app.ts +1 -1
  51. package/src/review-info.ts +1 -1
  52. package/src/review-stack.ts +2 -2
  53. package/src/server/review-server-core.ts +25 -1
  54. package/src/server/tutorial-service.ts +22 -46
  55. package/src/sharing/export.ts +1 -0
  56. package/src/sharing/import.ts +111 -26
  57. package/tutorial/document.json +27 -86
  58. package/tutorial/lenses.json +10 -0
  59. package/tutorial/runtime-manifest.json +2 -3
  60. package/tutorial/sample-service/src/database/drizzle-orm.d.ts +16 -0
  61. package/tutorial/sample-service/src/database/schema.ts +9 -10
  62. package/tutorial/sample-service/src/inventory/forklift-mood.ts +4 -0
  63. package/tutorial/sample-service/src/inventory/inventory-service.ts +18 -1
  64. package/tutorial/sample-service/src/inventory/warehouse-lore.ts +11 -0
  65. package/tutorial/sample-service/src/orders/order-service.ts +3 -2
  66. package/tutorial/sample-service/src/orders/order.ts +27 -5
  67. package/tutorial/sample-service/src/payments/fraud-vibes.ts +4 -0
  68. package/tutorial/sample-service/src/payments/payment-gateway.ts +19 -2
  69. package/tutorial/sample-service/src/payments/raccoon-ledger.ts +9 -0
  70. package/dist/agent-cli-BR2j-2n5.js +0 -2
  71. package/dist/input-error-OLgB_h31.js +0 -11
  72. package/dist/profile-BQGaX8aX.js +0 -2
  73. package/dist/review-home-paths-6zZH-1c9.js +0 -2560
  74. package/dist/tutorial-trace-DrBvkaeL.js +0 -43
  75. package/src/tutorial-conversation.ts +0 -18
  76. package/tutorial/software-map.json +0 -139
  77. package/tutorial/trace.json +0 -20
@@ -1,7 +1,7 @@
1
- import { a as toolResultText, s as ReviewApiError } from "./agent-client-B_b1GtUc.js";
2
- import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
3
- import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-DvIO906l.js";
4
- import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-DJmTieh5.js";
1
+ import { C as ReviewInputError } from "./src-DiHP11l3.js";
2
+ import { a as toolResultText, s as ReviewApiError } from "./agent-client-P3J4y8Zn.js";
3
+ import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-D8WXsm_g.js";
4
+ import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-CJHBCHMh.js";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -1,5 +1,5 @@
1
1
  import { c as jsonString, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
2
- import "./review-home-paths-6zZH-1c9.js";
2
+ import "./review-home-paths-Y9mYWkCI.js";
3
3
  import { readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -1,16 +1,15 @@
1
+ import { C as ReviewInputError } from "./src-DiHP11l3.js";
1
2
  import { c as jsonString, m as isStringValue, n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
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-Bm84Xdck.js";
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-DzcSt6pV.js";
4
+ 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-BlERtVm2.js";
4
5
  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
- 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
- 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";
7
- import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
6
+ import { B as TraceStorageDeniedError, D as listReviewTraceSessions, I as isTraceStorageConfigured, O as loadReviewAgentTrace, R as resolveTraceStorage, T as traceMachineEnabled, at as openUrlInBrowser, it as clearStoreAuth, ot as readStoreAuth, rt as DEFAULT_STORE_ORIGIN, st as runStoreLogin, z as selectTraceStorage } from "./review-home-paths-Y9mYWkCI.js";
7
+ 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-u4L_1VZa.js";
8
8
  import { A as devReviewHome, h as TraceConfigurationError, i as isS3MockMode } from "./s3-config-CAxhO9u_.js";
9
9
  import { t as errorMessage } from "./error-message-OtiDonty.js";
10
- import { T as traceMachineEnabled } from "./review-home-paths-6zZH-1c9.js";
11
- import { i as readReviewPackageVersion, n as readBuildCommit } from "./package-paths-B6-zxvIO.js";
10
+ import { i as readReviewPackageVersion, n as readBuildCommit } from "./package-paths-B9PDoAl6.js";
12
11
  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-DvIO906l.js";
12
+ 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-D8WXsm_g.js";
14
13
  import { t as fuzzyRank } from "./fuzzy-match-BoAmcyak.js";
15
14
  import { createRequire } from "node:module";
16
15
  import crypto$1, { createHash, randomUUID } from "node:crypto";
@@ -571,11 +570,9 @@ function deriveErrorTelemetryProperties(cause) {
571
570
  if (name !== void 0 && name.length > 0 && name.length <= MAX_ERROR_NAME_LENGTH && ERROR_NAME_PATTERN.test(name)) derived.error_name = name;
572
571
  const message = jsonString(cause.message);
573
572
  if (message !== void 0 && message.length > 0) {
574
- derived.message_hash = hashErrorMessage(message);
575
- if (!SCHEMA_ERROR_NAMES.has(derived.error_name ?? "")) {
576
- const cleaned = cleanTelemetryText(message, NO_DELETED_DIRECTORIES);
577
- if (isReportableCleanedMessage(cleaned)) derived.message = cleaned;
578
- }
573
+ const cleaned = cleanTelemetryText(message, NO_DELETED_DIRECTORIES);
574
+ derived.message_hash = hashErrorMessage(cleaned);
575
+ if (!SCHEMA_ERROR_NAMES.has(derived.error_name ?? "") && isReportableCleanedMessage(cleaned)) derived.message = cleaned;
579
576
  }
580
577
  const frames = packBundleFrames(cause.stack);
581
578
  if (frames) derived.frames = frames;
@@ -901,6 +898,7 @@ const askAgents = {
901
898
  executableEnv: "CLAUDE_CODE_EXECUTABLE"
902
899
  },
903
900
  signIn: "claude auth login",
901
+ steers: true,
904
902
  readOnlyMode: "default",
905
903
  sessionMeta: { claudeCode: { options: {
906
904
  disallowedTools: [
@@ -1221,6 +1219,12 @@ const REFUSED_KINDS = /* @__PURE__ */ new Set([
1221
1219
  ]);
1222
1220
  const piGreetingSchema = z.object({ piAcp: z.object({ startupInfo: z.string() }) });
1223
1221
  const cursorTodosSchema = z.object({ todos: z.unknown() }).loose();
1222
+ /** The steering extension Claude's adapter takes: a follow-up joins the
1223
+ * turn under way rather than waiting for it to end. */
1224
+ const STEER_METHOD = "_session/steering";
1225
+ /** How an agent says, in its `initialize` response, that it takes steering. */
1226
+ const steeringSchema = z.object({ steering: z.object({ supported: z.literal(true) }) });
1227
+ const steerOutcomeSchema = z.object({ outcome: z.string() });
1224
1228
  /** MCP servers the reviewer's own agent may bring whose tools only read,
1225
1229
  * so their calls run without asking, like the agent reading files. */
1226
1230
  const READ_ONLY_MCP_SERVERS = /* @__PURE__ */ new Set(["fff"]);
@@ -1282,6 +1286,10 @@ var AskThread = class {
1282
1286
  /** What Pi greets a new session with, its version and skills, which is no
1283
1287
  * part of the answer. */
1284
1288
  greeting;
1289
+ /** The agent takes follow-ups into the turn under way. */
1290
+ steerable = false;
1291
+ /** Follow-ups waiting for the turn under way to end, oldest first. */
1292
+ queue = [];
1285
1293
  constructor(launch, start, mcpServers = () => [], limits = askThreadLimits) {
1286
1294
  this.launch = launch;
1287
1295
  this.start = start;
@@ -1352,15 +1360,22 @@ var AskThread = class {
1352
1360
  /** Why a question cannot be asked now, if it cannot. */
1353
1361
  askRefusal() {
1354
1362
  switch (this.state.status) {
1355
- case "idle": return;
1363
+ case "idle":
1364
+ case "running":
1365
+ case "waiting": return;
1356
1366
  case "failed": return `${this.state.agentName} stopped. Try again first.`;
1357
- default: return "The agent is still answering.";
1367
+ default: return `${this.state.agentName} is still starting.`;
1358
1368
  }
1359
1369
  }
1360
- /** Asks a follow-up, starting the agent again if it has stopped since. */
1370
+ /** Asks a follow-up, starting the agent again if it has stopped since.
1371
+ * While the agent answers, it joins that turn or waits for it to end. */
1361
1372
  async ask(question) {
1362
1373
  const refusal = this.askRefusal();
1363
1374
  if (refusal) throw new Error(refusal);
1375
+ if (this.state.status !== "idle") {
1376
+ await this.steer(question);
1377
+ return;
1378
+ }
1364
1379
  this.asking = {
1365
1380
  id: this.addUser(question),
1366
1381
  question
@@ -1402,6 +1417,49 @@ var AskThread = class {
1402
1417
  });
1403
1418
  });
1404
1419
  }
1420
+ /** Gives the turn under way a follow-up when the agent takes one, else
1421
+ * keeps it for when the turn ends. */
1422
+ async steer(question) {
1423
+ const { connection, sessionId } = this;
1424
+ if (this.steerable && connection && sessionId) {
1425
+ const id = this.addUser(question);
1426
+ if (await connection.agent.request(STEER_METHOD, {
1427
+ sessionId,
1428
+ prompt: [{
1429
+ type: "text",
1430
+ text: question.text
1431
+ }, ...this.attachments(question)],
1432
+ _meta: { steering: { idleBehavior: "promptRequired" } }
1433
+ }).then((response) => steerOutcomeSchema.safeParse(response).data?.outcome).catch(() => void 0) === "injected") return;
1434
+ this.emit({
1435
+ type: "remove",
1436
+ ids: [id]
1437
+ });
1438
+ }
1439
+ this.queue.push({
1440
+ id: randomUUID(),
1441
+ question
1442
+ });
1443
+ this.showQueue();
1444
+ this.askQueued();
1445
+ }
1446
+ /** Asks the oldest waiting follow-up, once no turn is under way. */
1447
+ askQueued() {
1448
+ if (this.closed || this.state.status !== "idle") return;
1449
+ const next = this.queue.shift();
1450
+ if (!next) return;
1451
+ this.showQueue();
1452
+ this.ask(next.question);
1453
+ }
1454
+ showQueue() {
1455
+ this.emit({
1456
+ type: "set",
1457
+ queued: this.queue.map(({ id, question }) => ({
1458
+ id,
1459
+ text: question.text
1460
+ }))
1461
+ });
1462
+ }
1405
1463
  decide(permissionId, optionId) {
1406
1464
  const resolve = this.decisions.get(permissionId);
1407
1465
  if (!resolve) return false;
@@ -1414,6 +1472,10 @@ var AskThread = class {
1414
1472
  /** Stops the turn, or the start, in progress. A turn the agent does not
1415
1473
  * end soon after is ended by stopping the agent. */
1416
1474
  async cancel() {
1475
+ if (this.queue.length) {
1476
+ this.queue = [];
1477
+ this.showQueue();
1478
+ }
1417
1479
  for (const resolve of this.decisions.values()) resolve({ outcome: { outcome: "cancelled" } });
1418
1480
  const { status } = this.state;
1419
1481
  if (status === "starting") {
@@ -1428,16 +1490,19 @@ var AskThread = class {
1428
1490
  }, this.limits.stopGraceMs);
1429
1491
  if (sessionId) await connection.agent.notify(methods.agent.session.cancel, { sessionId }).catch(() => {});
1430
1492
  }
1431
- close() {
1493
+ close(save = true) {
1432
1494
  if (this.closed) return;
1433
1495
  this.closed = true;
1434
- if (this.sessionId && this.state.status !== "starting" && this.state.status !== "failed") this.start.onSave?.(settled(this.state.entries));
1435
- for (const resolve of this.decisions.values()) resolve({ outcome: { outcome: "cancelled" } });
1436
- if (this.connection && this.sessionId) this.connection.agent.notify(methods.agent.session.cancel, { sessionId: this.sessionId }).catch(() => {});
1437
- this.disconnect();
1438
- this.listeners.clear();
1439
- for (const closed of this.closers) closed();
1440
- this.closers.clear();
1496
+ try {
1497
+ if (save && this.sessionId && this.state.status !== "starting" && this.state.status !== "failed") this.start.onSave?.(settled(this.state.entries));
1498
+ } finally {
1499
+ for (const resolve of this.decisions.values()) resolve({ outcome: { outcome: "cancelled" } });
1500
+ if (this.connection && this.sessionId) this.connection.agent.notify(methods.agent.session.cancel, { sessionId: this.sessionId }).catch(() => {});
1501
+ this.disconnect();
1502
+ this.listeners.clear();
1503
+ for (const closed of this.closers) closed();
1504
+ this.closers.clear();
1505
+ }
1441
1506
  }
1442
1507
  /** Runs when the thread closes, at once if it already has. */
1443
1508
  onClose(closed) {
@@ -1546,6 +1611,7 @@ var AskThread = class {
1546
1611
  version: "1"
1547
1612
  }
1548
1613
  });
1614
+ this.steerable = askAgents[this.start.agent].steers === true && steeringSchema.safeParse(initialized._meta).success;
1549
1615
  this.emit({
1550
1616
  type: "set",
1551
1617
  accepts: { image: initialized.agentCapabilities?.promptCapabilities?.image === true }
@@ -1700,6 +1766,7 @@ var AskThread = class {
1700
1766
  mcpServers: this.mcpServers,
1701
1767
  _meta: this.sessionMeta()
1702
1768
  });
1769
+ if (this.closed) throw new Error("This conversation has ended.");
1703
1770
  this.sessionId = response.sessionId;
1704
1771
  start.onSession?.(response.sessionId);
1705
1772
  this.greeting = piGreetingSchema.safeParse(response._meta).data?.piAcp.startupInfo;
@@ -1763,6 +1830,7 @@ var AskThread = class {
1763
1830
  } finally {
1764
1831
  clearTimeout(this.stopTimer);
1765
1832
  }
1833
+ if (this.closed) return;
1766
1834
  if (withSelection) this.needsContext = false;
1767
1835
  if (withPermission) this.permitted = false;
1768
1836
  this.asking = void 0;
@@ -1786,6 +1854,7 @@ var AskThread = class {
1786
1854
  });
1787
1855
  this.start.onTurn?.();
1788
1856
  this.start.onSave?.(this.state.entries);
1857
+ this.askQueued();
1789
1858
  }
1790
1859
  async requestPermission(request, signal) {
1791
1860
  const id = request.toolCall.toolCallId;
@@ -2265,10 +2334,14 @@ var AskThreads = class {
2265
2334
  }
2266
2335
  return offer;
2267
2336
  }
2268
- close(id) {
2269
- this.threads.get(id)?.close();
2337
+ close(id, save = true) {
2338
+ const thread = this.threads.get(id);
2270
2339
  this.threads.delete(id);
2271
2340
  this.unwatchedSince.delete(id);
2341
+ thread?.close(save);
2342
+ }
2343
+ closeDeletedReviews(isDeleted) {
2344
+ for (const [id, thread] of this.threads) if (isDeleted(thread.reviewId)) this.close(id, false);
2272
2345
  }
2273
2346
  closeAll() {
2274
2347
  for (const id of [...this.threads.keys()]) this.close(id);
@@ -2478,7 +2551,7 @@ async function resolveReviewStackLayers(subject, reviews, runGitHubApi = default
2478
2551
  const matches = reviews.filter((candidate) => {
2479
2552
  return (candidate.origin?.pullRequestUrl?.replace(/\/pull\/\d+.*$/, "") ?? candidate.pins?.repositoryId) === repoKey && candidate.origin?.pullRequestNumber === pr.number;
2480
2553
  });
2481
- const review = matches.find((candidate) => candidate.reviewId === subject.reviewId) ?? matches.findLast((candidate) => !candidate.dismissedAt) ?? matches.at(-1);
2554
+ const review = matches.find((candidate) => candidate.reviewId === subject.reviewId) ?? matches.findLast((candidate) => !candidate.trashedAt) ?? matches.at(-1);
2482
2555
  return {
2483
2556
  branch: pr.head.ref,
2484
2557
  pullRequestNumber: pr.number,
@@ -3228,6 +3301,16 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3228
3301
  });
3229
3302
  const sharedData = shared ? new SharedReviewData(shared) : void 0;
3230
3303
  const isShared = (id) => id.startsWith("shared-");
3304
+ const assertWritable = (id) => {
3305
+ if (isShared(id)) shared?.assertWritable(id);
3306
+ else store.assertWritable(id);
3307
+ };
3308
+ const reviewAccess = (access) => async (context, next) => {
3309
+ const id = context.req.param("id");
3310
+ if (access !== "read") assertWritable(id);
3311
+ if (access === "edit" && isShared(id)) throw new ReviewInputError("Shared reviews are read-only.", 409);
3312
+ await next();
3313
+ };
3231
3314
  const sharedCommandSchema = z.object({ operation: z.object({ reviewId: z.string().optional() }) });
3232
3315
  const ensureScratchpad = async (id) => {
3233
3316
  if (open && scratchpadEnabled() && (!id || id === SCRATCHPAD_ID)) await store.ensureScratchpad();
@@ -3242,7 +3325,6 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3242
3325
  if (!id || !isShared(id)) return next();
3243
3326
  const query = readQuerySchemas.get.parse({ version: context.req.query("version") });
3244
3327
  readReview(id, query.version);
3245
- if (context.req.method !== "GET" && !/\/(open|source|copy-context|environment)$/.test(context.req.path) && !/\/workspaces\/[^/]+\/retry$/.test(context.req.path)) throw new ReviewInputError("Shared reviews are read-only.", 409);
3246
3328
  return next();
3247
3329
  };
3248
3330
  app.use("/:id", sharedGuard);
@@ -3292,7 +3374,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3292
3374
  }
3293
3375
  return context.json(await reviewProgress(store, data, snapshot, context.req.raw.signal, query.mode));
3294
3376
  });
3295
- app.post("/:id/progress", async (context) => {
3377
+ app.post("/:id/progress", reviewAccess("edit"), async (context) => {
3296
3378
  if (!data) throw new ReviewInputError("Source data is unavailable.", 409);
3297
3379
  const input = progressUpdateSchema.parse(await readBoundedRequestJson(context.req.raw));
3298
3380
  const id = context.req.param("id");
@@ -3342,7 +3424,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3342
3424
  "begin",
3343
3425
  "update",
3344
3426
  "end"
3345
- ]) app.post(`/:id/activity/${action}`, async (context) => {
3427
+ ]) app.post(`/:id/activity/${action}`, reviewAccess("edit"), async (context) => {
3346
3428
  const input = await readBoundedRequestJson(context.req.raw);
3347
3429
  const id = context.req.param("id");
3348
3430
  store.assertExists(id);
@@ -3375,7 +3457,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3375
3457
  });
3376
3458
  return marked;
3377
3459
  };
3378
- return watch$1(() => subscriptions.map(({ reviewId, mode }, index) => {
3460
+ return watch(() => subscriptions.map(({ reviewId, mode }, index) => {
3379
3461
  if (!dirty.delete(index)) return null;
3380
3462
  try {
3381
3463
  return { value: reviewId === null ? catalog(mode) : {
@@ -3415,7 +3497,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3415
3497
  }, () => {});
3416
3498
  }
3417
3499
  await ensureScratchpad();
3418
- return watch$1(() => catalog(coverageModeSchema.parse(context.req.query("mode"))), (notify) => {
3500
+ return watch(() => catalog(coverageModeSchema.parse(context.req.query("mode"))), (notify) => {
3419
3501
  const local = store.subscribeCatalog(notify);
3420
3502
  const activity = store.activity.subscribeWorking(notify);
3421
3503
  const imported = shared?.subscribe(notify);
@@ -3463,7 +3545,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3463
3545
  };
3464
3546
  }
3465
3547
  };
3466
- app.post("/:id/open", async (context) => {
3548
+ app.post("/:id/open", reviewAccess("active"), async (context) => {
3467
3549
  const id = context.req.param("id");
3468
3550
  if (isShared(id)) await shared?.assertReady(id);
3469
3551
  return context.json({
@@ -3474,7 +3556,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3474
3556
  app.get("/:id/watch", (context) => {
3475
3557
  const id = context.req.param("id");
3476
3558
  let document;
3477
- return watch$1(() => ({
3559
+ return watch(() => ({
3478
3560
  ...document ??= readReview(id),
3479
3561
  activity: isShared(id) ? {
3480
3562
  workingCount: 0,
@@ -3523,7 +3605,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3523
3605
  }, result.status);
3524
3606
  return context.json(result);
3525
3607
  });
3526
- app.post("/:id/navigator", async (context) => {
3608
+ app.post("/:id/navigator", reviewAccess("edit"), async (context) => {
3527
3609
  if (remoteCaller(context)) throw new ReviewInputError("Source windows are not available for a review on another machine.", 409);
3528
3610
  const input = readQuerySchemas.file.extend({
3529
3611
  side: z.enum(["base", "head"]).default("head"),
@@ -3580,7 +3662,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3580
3662
  ...environment.issue && { issue: "The checkout for language features is not available on the remote machine." }
3581
3663
  } : environment);
3582
3664
  });
3583
- app.post("/:id/environment", async (context) => {
3665
+ app.post("/:id/environment", reviewAccess("active"), async (context) => {
3584
3666
  const input = z.strictObject({ retry: z.boolean().optional() }).parse(await readBoundedRequestJson(context.req.raw));
3585
3667
  return context.json({ issues: await data.environmentIssues(readReview(context.req.param("id")), input.retry) });
3586
3668
  });
@@ -3593,7 +3675,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3593
3675
  readReview(context.req.param("id"));
3594
3676
  return context.json(data.workspaces.list(context.req.param("id")));
3595
3677
  });
3596
- app.post("/:id/workspaces/:workspaceId/retry", async (context) => {
3678
+ app.post("/:id/workspaces/:workspaceId/retry", reviewAccess("active"), async (context) => {
3597
3679
  return context.json(await data.workspaces.retry(context.req.param("id"), context.req.param("workspaceId")));
3598
3680
  });
3599
3681
  app.get("/:id/file", async (context) => {
@@ -3660,7 +3742,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3660
3742
  return context.json(await data.commits((await data.resolveSource(readReview(context.req.param("id"), input.version))).pins));
3661
3743
  });
3662
3744
  }
3663
- app.post("/:id/copy-context", async (context) => {
3745
+ app.post("/:id/copy-context", reviewAccess("read"), async (context) => {
3664
3746
  const query = readQuerySchemas.get.pick({ version: true }).extend({ mode: coverageModeSchema }).parse(context.req.query());
3665
3747
  const selection = AgentSelectionSchema.parse(await readBoundedRequestJson(context.req.raw));
3666
3748
  return context.json({ text: await selectionContext(context.req.param("id"), selection, query.version) });
@@ -3753,7 +3835,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3753
3835
  const files = fuzzyRank(query, await mentionableFiles(cwd), (file) => [file, file.slice(file.lastIndexOf("/") + 1)]);
3754
3836
  return context.json({ paths: files.slice(0, MENTION_LIMIT) });
3755
3837
  });
3756
- app.post("/:id/ask", async (context) => {
3838
+ app.post("/:id/ask", reviewAccess("edit"), async (context) => {
3757
3839
  const reviewId = context.req.param("id");
3758
3840
  const { version } = readQuerySchemas.get.pick({ version: true }).parse(context.req.query());
3759
3841
  const input = askStartSchema.parse(await readBoundedRequestJson(context.req.raw, ASK_REQUEST_MAX_BYTES));
@@ -3762,6 +3844,8 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3762
3844
  const target = input.selection.target;
3763
3845
  const id = crypto.randomUUID();
3764
3846
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
3847
+ const initialContext = await askContext(input.agent, snapshot, checkout, input.selection, version);
3848
+ assertWritable(reviewId);
3765
3849
  const thread = ask.threads.open({
3766
3850
  id,
3767
3851
  reviewId,
@@ -3793,7 +3877,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3793
3877
  title: input.selection.title,
3794
3878
  quote: target.kind === "text" ? target.quote : void 0
3795
3879
  },
3796
- context: await askContext(input.agent, snapshot, checkout, input.selection, version),
3880
+ context: initialContext,
3797
3881
  question: input.question
3798
3882
  });
3799
3883
  return context.json({ threadId: thread.id });
@@ -3803,7 +3887,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3803
3887
  readReview(reviewId);
3804
3888
  return context.json({ threads: store.askHistory.list(reviewId) });
3805
3889
  });
3806
- app.post("/:id/ask/watch", async (context) => {
3890
+ app.post("/:id/ask/watch", reviewAccess("read"), async (context) => {
3807
3891
  const reviewId = context.req.param("id");
3808
3892
  readReview(reviewId);
3809
3893
  const { threads } = askWatchSchema.parse(await readBoundedRequestJson(context.req.raw));
@@ -3833,7 +3917,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3833
3917
  entries: record.entries ?? []
3834
3918
  });
3835
3919
  });
3836
- app.post("/:id/ask/:threadId/open", async (context) => {
3920
+ app.post("/:id/ask/:threadId/open", reviewAccess("edit"), async (context) => {
3837
3921
  const reviewId = context.req.param("id");
3838
3922
  const threadId = context.req.param("threadId");
3839
3923
  if (ask.threads.get(threadId)?.reviewId === reviewId) return context.json({ threadId });
@@ -3843,6 +3927,8 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3843
3927
  const snapshot = readReview(reviewId, record.version);
3844
3928
  const checkout = await data.agentCheckout(snapshot);
3845
3929
  const target = record.selection.target;
3930
+ const initialContext = await askContext(record.agent, snapshot, checkout, record.selection, record.version);
3931
+ assertWritable(reviewId);
3846
3932
  ask.threads.open({
3847
3933
  id: record.id,
3848
3934
  reviewId,
@@ -3857,7 +3943,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3857
3943
  sessionId: record.sessionId,
3858
3944
  entries: record.entries
3859
3945
  },
3860
- context: await askContext(record.agent, snapshot, checkout, record.selection, record.version),
3946
+ context: initialContext,
3861
3947
  onSession: (sessionId) => store.askHistory.updateSession(record.id, sessionId),
3862
3948
  onTurn: () => store.askHistory.touch(record.id),
3863
3949
  onSave: (entries) => store.askHistory.saveEntries(record.id, entries),
@@ -3869,7 +3955,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3869
3955
  });
3870
3956
  return context.json({ threadId: record.id });
3871
3957
  });
3872
- app.post("/:id/ask/:threadId/prompt", async (context) => {
3958
+ app.post("/:id/ask/:threadId/prompt", reviewAccess("edit"), async (context) => {
3873
3959
  const thread = readThread(context.req.param("id"), context.req.param("threadId"));
3874
3960
  const { question } = askFollowUpSchema.parse(await readBoundedRequestJson(context.req.raw, ASK_REQUEST_MAX_BYTES));
3875
3961
  const refusal = thread.askRefusal();
@@ -3877,13 +3963,13 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3877
3963
  thread.ask(question);
3878
3964
  return context.json({ ok: true });
3879
3965
  });
3880
- app.post("/:id/ask/:threadId/permission", async (context) => {
3966
+ app.post("/:id/ask/:threadId/permission", reviewAccess("edit"), async (context) => {
3881
3967
  const thread = readThread(context.req.param("id"), context.req.param("threadId"));
3882
3968
  const decision = askDecisionSchema.parse(await readBoundedRequestJson(context.req.raw));
3883
3969
  if (!thread.decide(decision.permissionId, decision.optionId)) throw new ReviewInputError("This request was already answered.", 409);
3884
3970
  return context.json({ ok: true });
3885
3971
  });
3886
- app.post("/:id/ask/:threadId/files", async (context) => {
3972
+ app.post("/:id/ask/:threadId/files", reviewAccess("edit"), async (context) => {
3887
3973
  const thread = readThread(context.req.param("id"), context.req.param("threadId"));
3888
3974
  const { paths } = askFilesSchema.parse(await readBoundedRequestJson(context.req.raw));
3889
3975
  const { cwd, entries } = thread.read();
@@ -3894,7 +3980,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3894
3980
  file
3895
3981
  })) });
3896
3982
  });
3897
- app.post("/:id/ask/:threadId/choice", async (context) => {
3983
+ app.post("/:id/ask/:threadId/choice", reviewAccess("edit"), async (context) => {
3898
3984
  const thread = readThread(context.req.param("id"), context.req.param("threadId"));
3899
3985
  const { kind, value } = askChoiceSchema.parse(await readBoundedRequestJson(context.req.raw));
3900
3986
  if (thread.read().status !== "idle") throw new ReviewInputError("Settings can change once the agent finishes answering.", 409);
@@ -3905,7 +3991,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3905
3991
  }
3906
3992
  return context.json({ ok: true });
3907
3993
  });
3908
- app.post("/:id/ask/:threadId/permissions", async (context) => {
3994
+ app.post("/:id/ask/:threadId/permissions", reviewAccess("edit"), async (context) => {
3909
3995
  const thread = readThread(context.req.param("id"), context.req.param("threadId"));
3910
3996
  const { bypass } = askPermitSchema.parse(await readBoundedRequestJson(context.req.raw));
3911
3997
  if (thread.read().status !== "idle") throw new ReviewInputError("Settings can change once the agent finishes answering.", 409);
@@ -3916,22 +4002,22 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
3916
4002
  }
3917
4003
  return context.json({ ok: true });
3918
4004
  });
3919
- app.post("/:id/ask/:threadId/retry", (context) => {
4005
+ app.post("/:id/ask/:threadId/retry", reviewAccess("edit"), (context) => {
3920
4006
  const thread = readThread(context.req.param("id"), context.req.param("threadId"));
3921
4007
  if (thread.read().status !== "failed") throw new ReviewInputError("Only a conversation that failed can try again.", 409);
3922
4008
  thread.retry();
3923
4009
  return context.json({ ok: true });
3924
4010
  });
3925
- app.post("/:id/ask/:threadId/cancel", async (context) => {
4011
+ app.post("/:id/ask/:threadId/cancel", reviewAccess("read"), async (context) => {
3926
4012
  await readThread(context.req.param("id"), context.req.param("threadId")).cancel();
3927
4013
  return context.json({ ok: true });
3928
4014
  });
3929
- app.post("/:id/ask/:threadId/close", (context) => {
4015
+ app.post("/:id/ask/:threadId/close", reviewAccess("read"), (context) => {
3930
4016
  readThread(context.req.param("id"), context.req.param("threadId"));
3931
- ask.threads.close(context.req.param("threadId"));
4017
+ ask.threads.close(context.req.param("threadId"), !readReview(context.req.param("id")).trashedAt);
3932
4018
  return context.json({ ok: true });
3933
4019
  });
3934
- app.delete("/:id/ask/:threadId", (context) => {
4020
+ app.delete("/:id/ask/:threadId", reviewAccess("edit"), (context) => {
3935
4021
  const reviewId = context.req.param("id");
3936
4022
  const threadId = context.req.param("threadId");
3937
4023
  if (store.askHistory.get(threadId)?.reviewId !== reviewId) throw new ReviewInputError("This conversation was not found.", 404);
@@ -4003,6 +4089,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4003
4089
  if (command.success && command.data.operation.reviewId?.startsWith("shared-")) {
4004
4090
  const parsed = commandSchema.parse(input);
4005
4091
  if (shared && parsed.operation.type === "delete") {
4092
+ if (!shared.get(parsed.operation.reviewId).snapshot.trashedAt) throw new ReviewInputError("Move this whiteboard to Trash before deleting it.", 409);
4006
4093
  await shared.removeLocal(parsed.operation.reviewId);
4007
4094
  return context.json({
4008
4095
  reviewId: parsed.operation.reviewId,
@@ -4010,8 +4097,11 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4010
4097
  deleted: true
4011
4098
  });
4012
4099
  }
4013
- if (shared && parsed.operation.type === "attention") {
4014
- await shared.setAttention(parsed.operation.reviewId, parsed.operation.action);
4100
+ if (shared && (parsed.operation.type === "attention" || parsed.operation.type === "lifecycle")) {
4101
+ if (parsed.operation.type === "lifecycle") await shared.setLifecycle(parsed.operation.reviewId, parsed.operation.state);
4102
+ else await shared.setAttention(parsed.operation.reviewId);
4103
+ const reviewId = parsed.operation.reviewId;
4104
+ if (shared.get(reviewId).snapshot.trashedAt) ask?.threads.closeDeletedReviews((id) => id === reviewId);
4015
4105
  return context.json({
4016
4106
  reviewId: parsed.operation.reviewId,
4017
4107
  version: shared.get(parsed.operation.reviewId).snapshot.version,
@@ -4021,6 +4111,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
4021
4111
  throw new ReviewInputError("Shared reviews are read-only.", 409);
4022
4112
  }
4023
4113
  const result = await store.execute(input);
4114
+ if (result.deleted || result.lifecycle && store.read(result.reviewId).trashedAt) ask?.threads.closeDeletedReviews((id) => id === result.reviewId);
4024
4115
  if (input.operation.type === "edit" && data && !result.deleted) {
4025
4116
  const { quotes, unquoted } = await anchorQuotes(result.version > 0 ? store.read(result.reviewId, result.version - 1) : void 0, store.read(result.reviewId, result.version), async (pins, side, file) => (await data.file(pins, side, file)).text);
4026
4117
  return context.json({
@@ -4105,7 +4196,7 @@ async function locateRepositories(body, register) {
4105
4196
  };
4106
4197
  }
4107
4198
  /** Send committed state, coalescing updates when the reader falls behind. */
4108
- function watch$1(read, subscribe, probe = read) {
4199
+ function watch(read, subscribe, probe = read) {
4109
4200
  probe();
4110
4201
  let stop = () => {};
4111
4202
  let dirty = true;
@@ -4209,6 +4300,11 @@ function createWhiteboardCore(input) {
4209
4300
  });
4210
4301
  const callbacks = relayReviewCallbacks(input.relay, input.softwareMapEnabled);
4211
4302
  const askThreads = input.ask && new AskThreads(launchAskAgent, input.ask.tools);
4303
+ const closeDeletedThreads = () => askThreads?.closeDeletedReviews((id) => id.startsWith("shared-") ? !shared?.has(id) || Boolean(shared.get(id).snapshot.trashedAt) : !store.has(id) || Boolean(store.read(id).trashedAt));
4304
+ const stopLocal = askThreads && store.subscribe((result) => {
4305
+ if (result.deleted || result.lifecycle) closeDeletedThreads();
4306
+ });
4307
+ const stopShared = askThreads && shared?.subscribe(closeDeletedThreads);
4212
4308
  const api = createReviewApi(store, data, callbacks.open, shared, callbacks.capabilities, input.scratchpad, () => traceMachineEnabled(), input.status, input.hooks, askThreads && {
4213
4309
  threads: askThreads,
4214
4310
  agents: () => detectAskAgents()
@@ -4217,7 +4313,12 @@ function createWhiteboardCore(input) {
4217
4313
  return {
4218
4314
  app,
4219
4315
  api,
4220
- close: () => askThreads?.closeAll()
4316
+ close: () => {
4317
+ stopLocal?.();
4318
+ stopShared?.();
4319
+ closeDeletedThreads();
4320
+ askThreads?.closeAll();
4321
+ }
4221
4322
  };
4222
4323
  }
4223
4324
  /** The Desktop callbacks `createReviewApi` takes, answered over the relay. */
@@ -1,6 +1,6 @@
1
- import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-Bm84Xdck.js";
1
+ import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-BlERtVm2.js";
2
+ import "./review-home-paths-Y9mYWkCI.js";
2
3
  import { O as withFileLock } from "./s3-config-CAxhO9u_.js";
3
- import "./review-home-paths-6zZH-1c9.js";
4
4
  import { access, mkdir } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { DatabaseSync } from "node:sqlite";
@@ -0,0 +1,2 @@
1
+ import { t as openReviewProfile } from "./profile-BsYDBr7t.js";
2
+ export { openReviewProfile };