@dev.fast/whiteboard 0.2.4 → 0.2.5-preview.20261013.109
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.
- package/dist/{account-alias-_1_7vROx.js → account-alias-D0hF0sKk.js} +2 -3
- package/dist/{agent-cli-BrFqhnzM.js → agent-cli-BBPxwUOd.js} +6 -6
- package/dist/agent-cli-YSuhE5X8.js +2 -0
- package/dist/{agent-client-B_b1GtUc.js → agent-client-P3J4y8Zn.js} +3 -3
- package/dist/{authoring-tools-DvIO906l.js → authoring-tools-D8WXsm_g.js} +11 -10
- package/dist/build-info.json +1 -1
- package/dist/{cli-gRGxkwc0.js → cli-C0ziP6hg.js} +1 -1
- package/dist/{cli-runner-Cjl8h376.js → cli-runner-C1gY-HAI.js} +16 -17
- package/dist/cli.js +1 -1
- package/dist/{client-BFClfQLO.js → client-DzcSt6pV.js} +63 -13
- package/dist/{desktop-discovery-C-Qz54wO.js → desktop-discovery-Ba3dD5Cn.js} +1 -1
- package/dist/{headless-host-DcWaxx9P.js → headless-host-BHD_Vg5F.js} +4 -4
- package/dist/{local-data-Bm84Xdck.js → local-data-BlERtVm2.js} +281 -75
- package/dist/{mcp-BDy9Aybd.js → mcp-t7VN9R3k.js} +4 -4
- package/dist/{package-paths-B6-zxvIO.js → package-paths-B9PDoAl6.js} +1 -1
- package/dist/{process-error-telemetry-Dzh5a3s_.js → process-error-telemetry-C0swByfp.js} +158 -57
- package/dist/{profile-RDEioF8b.js → profile-BsYDBr7t.js} +2 -2
- package/dist/profile-j97aRTS7.js +2 -0
- package/dist/{review-agent-traces-DNJFYzc8.js → review-home-paths-Y9mYWkCI.js} +2600 -62
- package/dist/{review-telemetry-DBy3eVCo.js → review-telemetry-CmM6tCbj.js} +2 -2
- package/dist/server/desktop-host.js +35 -46
- package/dist/{server-discovery-Dbzn3w6Z.js → server-discovery-ZVi8Ay8J.js} +1 -1
- package/dist/sharing/index.d.ts +37 -49
- package/dist/sharing/index.js +1 -1
- package/dist/{src-CmdiBL20.js → src-DiHP11l3.js} +11 -1
- package/dist/{stored-document-migration-BxZWQQjv.js → stored-document-migration-u4L_1VZa.js} +15 -36
- package/dist/{tool-failure-DJmTieh5.js → tool-failure-CJHBCHMh.js} +2 -2
- package/instructions/scratchpad.md +1 -1
- package/package.json +5 -4
- package/src/ask/agents.ts +6 -0
- package/src/ask/thread-state.ts +17 -1
- package/src/ask/thread.ts +116 -22
- package/src/ask/threads.ts +8 -2
- package/src/error-telemetry.ts +13 -9
- package/src/fixtures/blocks/tutorial.json +3 -40
- package/src/review-api/README.md +10 -7
- package/src/review-api/ask-history.ts +37 -0
- package/src/review-api/authoring-tools.ts +8 -5
- package/src/review-api/blocks/database_lens.ts +5 -0
- package/src/review-api/blocks/tutorial.ts +2 -18
- package/src/review-api/comparison-coverage.ts +16 -9
- package/src/review-api/component-reference.ts +1 -1
- package/src/review-api/document.ts +24 -10
- package/src/review-api/http.ts +267 -189
- package/src/review-api/local-data.ts +126 -37
- package/src/review-api/store-schema.ts +25 -1
- package/src/review-api/store.ts +165 -33
- package/src/review-api/workspaces.ts +14 -14
- package/src/review-api/worktree-events.ts +46 -0
- package/src/review-app.ts +1 -1
- package/src/review-info.ts +1 -1
- package/src/review-stack.ts +2 -2
- package/src/server/review-server-core.ts +25 -1
- package/src/server/tutorial-service.ts +22 -46
- package/src/sharing/export.ts +1 -0
- package/src/sharing/import.ts +111 -26
- package/tutorial/document.json +27 -86
- package/tutorial/lenses.json +10 -0
- package/tutorial/runtime-manifest.json +2 -3
- package/tutorial/sample-service/src/database/drizzle-orm.d.ts +16 -0
- package/tutorial/sample-service/src/database/schema.ts +9 -10
- package/tutorial/sample-service/src/inventory/forklift-mood.ts +4 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +18 -1
- package/tutorial/sample-service/src/inventory/warehouse-lore.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +3 -2
- package/tutorial/sample-service/src/orders/order.ts +27 -5
- package/tutorial/sample-service/src/payments/fraud-vibes.ts +4 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +19 -2
- package/tutorial/sample-service/src/payments/raccoon-ledger.ts +9 -0
- package/dist/agent-cli-BR2j-2n5.js +0 -2
- package/dist/input-error-OLgB_h31.js +0 -11
- package/dist/profile-BQGaX8aX.js +0 -2
- package/dist/review-home-paths-6zZH-1c9.js +0 -2560
- package/dist/tutorial-trace-DrBvkaeL.js +0 -43
- package/src/tutorial-conversation.ts +0 -18
- package/tutorial/software-map.json +0 -139
- package/tutorial/trace.json +0 -20
|
@@ -353,18 +353,32 @@ function documentReferences(
|
|
|
353
353
|
});
|
|
354
354
|
|
|
355
355
|
if (element.type === "database_lens")
|
|
356
|
-
return
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
356
|
+
return [
|
|
357
|
+
...Object.entries(element.stores).flatMap(([storeKey, store]) =>
|
|
358
|
+
Object.entries(store.collections).flatMap(([key, collection]) =>
|
|
359
|
+
collection.source
|
|
360
|
+
? select(collection.source, element.pins).map((source) => ({
|
|
361
|
+
id: `${element.id}:schema:${storeKey}:${key}`,
|
|
362
|
+
source,
|
|
363
|
+
label: collection.label,
|
|
364
|
+
peek: true,
|
|
365
|
+
}))
|
|
366
|
+
: [],
|
|
365
367
|
),
|
|
366
368
|
),
|
|
367
|
-
|
|
369
|
+
...element.useCases.flatMap((useCase) =>
|
|
370
|
+
useCase.operations.flatMap((operation) =>
|
|
371
|
+
select(operation.source, operation.pins ?? element.pins).map(
|
|
372
|
+
(source) => ({
|
|
373
|
+
id: operation.id!,
|
|
374
|
+
source,
|
|
375
|
+
label: operation.label,
|
|
376
|
+
peek: true,
|
|
377
|
+
}),
|
|
378
|
+
),
|
|
379
|
+
),
|
|
380
|
+
),
|
|
381
|
+
];
|
|
368
382
|
|
|
369
383
|
return [];
|
|
370
384
|
});
|
package/src/review-api/http.ts
CHANGED
|
@@ -226,6 +226,23 @@ export function createReviewApi(
|
|
|
226
226
|
const sharedData = shared ? new SharedReviewData(shared) : undefined;
|
|
227
227
|
const isShared = (id: string) => id.startsWith("shared-");
|
|
228
228
|
|
|
229
|
+
const assertWritable = (id: string) => {
|
|
230
|
+
if (isShared(id)) shared?.assertWritable(id);
|
|
231
|
+
else store.assertWritable(id);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const reviewAccess =
|
|
235
|
+
(access: "read" | "active" | "edit"): MiddlewareHandler =>
|
|
236
|
+
async (context, next) => {
|
|
237
|
+
const id = context.req.param("id")!;
|
|
238
|
+
|
|
239
|
+
if (access !== "read") assertWritable(id);
|
|
240
|
+
|
|
241
|
+
if (access === "edit" && isShared(id))
|
|
242
|
+
throw new ReviewInputError("Shared reviews are read-only.", 409);
|
|
243
|
+
await next();
|
|
244
|
+
};
|
|
245
|
+
|
|
229
246
|
const sharedCommandSchema = z.object({
|
|
230
247
|
operation: z.object({ reviewId: z.string().optional() }),
|
|
231
248
|
});
|
|
@@ -257,13 +274,6 @@ export function createReviewApi(
|
|
|
257
274
|
|
|
258
275
|
readReview(id, query.version);
|
|
259
276
|
|
|
260
|
-
if (
|
|
261
|
-
context.req.method !== "GET" &&
|
|
262
|
-
!/\/(open|source|copy-context|environment)$/.test(context.req.path) &&
|
|
263
|
-
!/\/workspaces\/[^/]+\/retry$/.test(context.req.path)
|
|
264
|
-
)
|
|
265
|
-
throw new ReviewInputError("Shared reviews are read-only.", 409);
|
|
266
|
-
|
|
267
277
|
return next();
|
|
268
278
|
};
|
|
269
279
|
|
|
@@ -378,7 +388,7 @@ export function createReviewApi(
|
|
|
378
388
|
),
|
|
379
389
|
);
|
|
380
390
|
});
|
|
381
|
-
app.post("/:id/progress", async (context) => {
|
|
391
|
+
app.post("/:id/progress", reviewAccess("edit"), async (context) => {
|
|
382
392
|
if (!data) throw new ReviewInputError("Source data is unavailable.", 409);
|
|
383
393
|
|
|
384
394
|
const input = progressUpdateSchema.parse(
|
|
@@ -483,18 +493,22 @@ export function createReviewApi(
|
|
|
483
493
|
|
|
484
494
|
// One route per agent tool; the path names the action.
|
|
485
495
|
for (const action of ["begin", "update", "end"] as const)
|
|
486
|
-
app.post(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
496
|
+
app.post(
|
|
497
|
+
`/:id/activity/${action}`,
|
|
498
|
+
reviewAccess("edit"),
|
|
499
|
+
async (context) => {
|
|
500
|
+
const input = await readBoundedRequestJson(context.req.raw);
|
|
501
|
+
const id = context.req.param("id");
|
|
502
|
+
store.assertExists(id);
|
|
490
503
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
504
|
+
return context.json(
|
|
505
|
+
store.activity.update(
|
|
506
|
+
id,
|
|
507
|
+
isJsonObject(input) ? { ...input, action } : input,
|
|
508
|
+
),
|
|
509
|
+
);
|
|
510
|
+
},
|
|
511
|
+
);
|
|
498
512
|
app.get("/watch", async (context) => {
|
|
499
513
|
const query = context.req.query("subscriptions");
|
|
500
514
|
|
|
@@ -663,7 +677,7 @@ export function createReviewApi(
|
|
|
663
677
|
}
|
|
664
678
|
};
|
|
665
679
|
|
|
666
|
-
app.post("/:id/open", async (context) => {
|
|
680
|
+
app.post("/:id/open", reviewAccess("active"), async (context) => {
|
|
667
681
|
const id = context.req.param("id");
|
|
668
682
|
|
|
669
683
|
if (isShared(id)) await shared?.assertReady(id);
|
|
@@ -753,7 +767,7 @@ export function createReviewApi(
|
|
|
753
767
|
|
|
754
768
|
return context.json(result);
|
|
755
769
|
});
|
|
756
|
-
app.post("/:id/navigator", async (context) => {
|
|
770
|
+
app.post("/:id/navigator", reviewAccess("edit"), async (context) => {
|
|
757
771
|
if (remoteCaller(context))
|
|
758
772
|
throw new ReviewInputError(
|
|
759
773
|
"Source windows are not available for a review on another machine.",
|
|
@@ -886,7 +900,7 @@ export function createReviewApi(
|
|
|
886
900
|
: environment,
|
|
887
901
|
);
|
|
888
902
|
});
|
|
889
|
-
app.post("/:id/environment", async (context) => {
|
|
903
|
+
app.post("/:id/environment", reviewAccess("active"), async (context) => {
|
|
890
904
|
const input = z
|
|
891
905
|
.strictObject({ retry: z.boolean().optional() })
|
|
892
906
|
.parse(await readBoundedRequestJson(context.req.raw));
|
|
@@ -913,14 +927,18 @@ export function createReviewApi(
|
|
|
913
927
|
|
|
914
928
|
return context.json(data.workspaces.list(context.req.param("id")));
|
|
915
929
|
});
|
|
916
|
-
app.post(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
930
|
+
app.post(
|
|
931
|
+
"/:id/workspaces/:workspaceId/retry",
|
|
932
|
+
reviewAccess("active"),
|
|
933
|
+
async (context) => {
|
|
934
|
+
return context.json(
|
|
935
|
+
await data.workspaces.retry(
|
|
936
|
+
context.req.param("id"),
|
|
937
|
+
context.req.param("workspaceId"),
|
|
938
|
+
),
|
|
939
|
+
);
|
|
940
|
+
},
|
|
941
|
+
);
|
|
924
942
|
app.get("/:id/file", async (context) => {
|
|
925
943
|
// Browsing can describe binaries; authoring reads still require text.
|
|
926
944
|
const input = readQuerySchemas.file
|
|
@@ -1039,7 +1057,7 @@ export function createReviewApi(
|
|
|
1039
1057
|
});
|
|
1040
1058
|
}
|
|
1041
1059
|
|
|
1042
|
-
app.post("/:id/copy-context", async (context) => {
|
|
1060
|
+
app.post("/:id/copy-context", reviewAccess("read"), async (context) => {
|
|
1043
1061
|
const query = readQuerySchemas.get
|
|
1044
1062
|
.pick({ version: true })
|
|
1045
1063
|
.extend({ mode: coverageModeSchema })
|
|
@@ -1256,7 +1274,7 @@ export function createReviewApi(
|
|
|
1256
1274
|
return context.json({ paths: files.slice(0, MENTION_LIMIT) });
|
|
1257
1275
|
});
|
|
1258
1276
|
|
|
1259
|
-
app.post("/:id/ask", async (context) => {
|
|
1277
|
+
app.post("/:id/ask", reviewAccess("edit"), async (context) => {
|
|
1260
1278
|
const reviewId = context.req.param("id");
|
|
1261
1279
|
|
|
1262
1280
|
const { version } = readQuerySchemas.get
|
|
@@ -1273,6 +1291,16 @@ export function createReviewApi(
|
|
|
1273
1291
|
const id = crypto.randomUUID();
|
|
1274
1292
|
const createdAt = new Date().toISOString();
|
|
1275
1293
|
|
|
1294
|
+
const initialContext = await askContext(
|
|
1295
|
+
input.agent,
|
|
1296
|
+
snapshot,
|
|
1297
|
+
checkout,
|
|
1298
|
+
input.selection,
|
|
1299
|
+
version,
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
assertWritable(reviewId);
|
|
1303
|
+
|
|
1276
1304
|
const thread = ask.threads.open({
|
|
1277
1305
|
id,
|
|
1278
1306
|
reviewId,
|
|
@@ -1309,13 +1337,7 @@ export function createReviewApi(
|
|
|
1309
1337
|
title: input.selection.title,
|
|
1310
1338
|
quote: target.kind === "text" ? target.quote : undefined,
|
|
1311
1339
|
},
|
|
1312
|
-
context:
|
|
1313
|
-
input.agent,
|
|
1314
|
-
snapshot,
|
|
1315
|
-
checkout,
|
|
1316
|
-
input.selection,
|
|
1317
|
-
version,
|
|
1318
|
-
),
|
|
1340
|
+
context: initialContext,
|
|
1319
1341
|
question: input.question,
|
|
1320
1342
|
});
|
|
1321
1343
|
|
|
@@ -1332,7 +1354,7 @@ export function createReviewApi(
|
|
|
1332
1354
|
|
|
1333
1355
|
// The threads of every open Ask in a review, over one connection. Before
|
|
1334
1356
|
// the route for one thread, which would read `watch` as its id.
|
|
1335
|
-
app.post("/:id/ask/watch", async (context) => {
|
|
1357
|
+
app.post("/:id/ask/watch", reviewAccess("read"), async (context) => {
|
|
1336
1358
|
const reviewId = context.req.param("id");
|
|
1337
1359
|
|
|
1338
1360
|
readReview(reviewId);
|
|
@@ -1383,182 +1405,209 @@ export function createReviewApi(
|
|
|
1383
1405
|
|
|
1384
1406
|
// A saved conversation: attach to it if it is still running, else start
|
|
1385
1407
|
// the agent and load it, at the commit it was asked about.
|
|
1386
|
-
app.post(
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1408
|
+
app.post(
|
|
1409
|
+
"/:id/ask/:threadId/open",
|
|
1410
|
+
reviewAccess("edit"),
|
|
1411
|
+
async (context) => {
|
|
1412
|
+
const reviewId = context.req.param("id");
|
|
1413
|
+
const threadId = context.req.param("threadId");
|
|
1414
|
+
const live = ask.threads.get(threadId);
|
|
1415
|
+
|
|
1416
|
+
if (live?.reviewId === reviewId) return context.json({ threadId });
|
|
1417
|
+
const record = store.askHistory.get(threadId);
|
|
1418
|
+
|
|
1419
|
+
const { picks } = askOpenSchema.parse(
|
|
1420
|
+
await readBoundedRequestJson(context.req.raw, undefined, {}),
|
|
1421
|
+
);
|
|
1397
1422
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1423
|
+
if (record?.reviewId !== reviewId)
|
|
1424
|
+
throw new ReviewInputError("This conversation was not found.", 404);
|
|
1400
1425
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1426
|
+
const snapshot = readReview(reviewId, record.version);
|
|
1427
|
+
const checkout = await data.agentCheckout(snapshot);
|
|
1428
|
+
const target = record.selection.target;
|
|
1404
1429
|
|
|
1405
|
-
|
|
1406
|
-
id: record.id,
|
|
1407
|
-
reviewId,
|
|
1408
|
-
agent: record.agent,
|
|
1409
|
-
cwd: checkout.rootPath,
|
|
1410
|
-
head: checkout.head,
|
|
1411
|
-
selection: {
|
|
1412
|
-
title: record.selection.title,
|
|
1413
|
-
quote: target.kind === "text" ? target.quote : undefined,
|
|
1414
|
-
},
|
|
1415
|
-
resume: { sessionId: record.sessionId, entries: record.entries },
|
|
1416
|
-
// For a new session, should the agent no longer have this one.
|
|
1417
|
-
context: await askContext(
|
|
1430
|
+
const initialContext = await askContext(
|
|
1418
1431
|
record.agent,
|
|
1419
1432
|
snapshot,
|
|
1420
1433
|
checkout,
|
|
1421
1434
|
record.selection,
|
|
1422
1435
|
record.version,
|
|
1423
|
-
)
|
|
1424
|
-
onSession: (sessionId) =>
|
|
1425
|
-
store.askHistory.updateSession(record.id, sessionId),
|
|
1426
|
-
onTurn: () => store.askHistory.touch(record.id),
|
|
1427
|
-
onSave: (entries) => store.askHistory.saveEntries(record.id, entries),
|
|
1428
|
-
picks,
|
|
1429
|
-
bypass: record.bypass,
|
|
1430
|
-
onBypass: (bypass) => store.askHistory.setBypass(record.id, bypass),
|
|
1431
|
-
onOffer: (offer) => store.askHistory.saveOffer(record.agent, offer),
|
|
1432
|
-
onTitle: (title) => store.askHistory.rename(record.id, title),
|
|
1433
|
-
});
|
|
1436
|
+
);
|
|
1434
1437
|
|
|
1435
|
-
|
|
1436
|
-
|
|
1438
|
+
assertWritable(reviewId);
|
|
1439
|
+
ask.threads.open({
|
|
1440
|
+
id: record.id,
|
|
1441
|
+
reviewId,
|
|
1442
|
+
agent: record.agent,
|
|
1443
|
+
cwd: checkout.rootPath,
|
|
1444
|
+
head: checkout.head,
|
|
1445
|
+
selection: {
|
|
1446
|
+
title: record.selection.title,
|
|
1447
|
+
quote: target.kind === "text" ? target.quote : undefined,
|
|
1448
|
+
},
|
|
1449
|
+
resume: { sessionId: record.sessionId, entries: record.entries },
|
|
1450
|
+
// For a new session, should the agent no longer have this one.
|
|
1451
|
+
context: initialContext,
|
|
1452
|
+
onSession: (sessionId) =>
|
|
1453
|
+
store.askHistory.updateSession(record.id, sessionId),
|
|
1454
|
+
onTurn: () => store.askHistory.touch(record.id),
|
|
1455
|
+
onSave: (entries) => store.askHistory.saveEntries(record.id, entries),
|
|
1456
|
+
picks,
|
|
1457
|
+
bypass: record.bypass,
|
|
1458
|
+
onBypass: (bypass) => store.askHistory.setBypass(record.id, bypass),
|
|
1459
|
+
onOffer: (offer) => store.askHistory.saveOffer(record.agent, offer),
|
|
1460
|
+
onTitle: (title) => store.askHistory.rename(record.id, title),
|
|
1461
|
+
});
|
|
1437
1462
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
context.req.param("threadId"),
|
|
1442
|
-
);
|
|
1463
|
+
return context.json({ threadId: record.id });
|
|
1464
|
+
},
|
|
1465
|
+
);
|
|
1443
1466
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
)
|
|
1467
|
+
app.post(
|
|
1468
|
+
"/:id/ask/:threadId/prompt",
|
|
1469
|
+
reviewAccess("edit"),
|
|
1470
|
+
async (context) => {
|
|
1471
|
+
const thread = readThread(
|
|
1472
|
+
context.req.param("id"),
|
|
1473
|
+
context.req.param("threadId"),
|
|
1474
|
+
);
|
|
1447
1475
|
|
|
1448
|
-
|
|
1476
|
+
const { question } = askFollowUpSchema.parse(
|
|
1477
|
+
await readBoundedRequestJson(context.req.raw, ASK_REQUEST_MAX_BYTES),
|
|
1478
|
+
);
|
|
1449
1479
|
|
|
1450
|
-
|
|
1480
|
+
const refusal = thread.askRefusal();
|
|
1451
1481
|
|
|
1452
|
-
|
|
1482
|
+
if (refusal) throw new ReviewInputError(refusal, 409);
|
|
1453
1483
|
|
|
1454
|
-
|
|
1455
|
-
});
|
|
1484
|
+
void thread.ask(question);
|
|
1456
1485
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
context.req.param("threadId"),
|
|
1461
|
-
);
|
|
1486
|
+
return context.json({ ok: true });
|
|
1487
|
+
},
|
|
1488
|
+
);
|
|
1462
1489
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
)
|
|
1490
|
+
app.post(
|
|
1491
|
+
"/:id/ask/:threadId/permission",
|
|
1492
|
+
reviewAccess("edit"),
|
|
1493
|
+
async (context) => {
|
|
1494
|
+
const thread = readThread(
|
|
1495
|
+
context.req.param("id"),
|
|
1496
|
+
context.req.param("threadId"),
|
|
1497
|
+
);
|
|
1466
1498
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1499
|
+
const decision = askDecisionSchema.parse(
|
|
1500
|
+
await readBoundedRequestJson(context.req.raw),
|
|
1501
|
+
);
|
|
1469
1502
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1503
|
+
if (!thread.decide(decision.permissionId, decision.optionId))
|
|
1504
|
+
throw new ReviewInputError("This request was already answered.", 409);
|
|
1505
|
+
|
|
1506
|
+
return context.json({ ok: true });
|
|
1507
|
+
},
|
|
1508
|
+
);
|
|
1472
1509
|
|
|
1473
1510
|
// The files an answer names, as the checkout's own paths, so the panel
|
|
1474
1511
|
// can open them.
|
|
1475
|
-
app.post(
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1512
|
+
app.post(
|
|
1513
|
+
"/:id/ask/:threadId/files",
|
|
1514
|
+
reviewAccess("edit"),
|
|
1515
|
+
async (context) => {
|
|
1516
|
+
const thread = readThread(
|
|
1517
|
+
context.req.param("id"),
|
|
1518
|
+
context.req.param("threadId"),
|
|
1519
|
+
);
|
|
1480
1520
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1521
|
+
const { paths } = askFilesSchema.parse(
|
|
1522
|
+
await readBoundedRequestJson(context.req.raw),
|
|
1523
|
+
);
|
|
1484
1524
|
|
|
1485
|
-
|
|
1525
|
+
const { cwd, entries } = thread.read();
|
|
1486
1526
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1527
|
+
const touched = entries.flatMap((entry) =>
|
|
1528
|
+
entry.kind === "tool"
|
|
1529
|
+
? `${entry.title} ${entry.input ?? ""}`
|
|
1530
|
+
.split(/\s+/)
|
|
1531
|
+
.flatMap((token) => parseFileRef(token)?.path ?? [])
|
|
1532
|
+
: [],
|
|
1533
|
+
);
|
|
1494
1534
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1535
|
+
const files = resolveFileRefs(
|
|
1536
|
+
cwd,
|
|
1537
|
+
await checkoutFiles(cwd),
|
|
1538
|
+
paths,
|
|
1539
|
+
touched,
|
|
1540
|
+
);
|
|
1501
1541
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1542
|
+
return context.json({
|
|
1543
|
+
files: [...files].map(([path, file]) => ({ path, file })),
|
|
1544
|
+
});
|
|
1545
|
+
},
|
|
1546
|
+
);
|
|
1506
1547
|
|
|
1507
1548
|
// Another model or effort for the next answer.
|
|
1508
|
-
app.post(
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
);
|
|
1549
|
+
app.post(
|
|
1550
|
+
"/:id/ask/:threadId/choice",
|
|
1551
|
+
reviewAccess("edit"),
|
|
1552
|
+
async (context) => {
|
|
1553
|
+
const thread = readThread(
|
|
1554
|
+
context.req.param("id"),
|
|
1555
|
+
context.req.param("threadId"),
|
|
1556
|
+
);
|
|
1517
1557
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
"Settings can change once the agent finishes answering.",
|
|
1521
|
-
409,
|
|
1558
|
+
const { kind, value } = askChoiceSchema.parse(
|
|
1559
|
+
await readBoundedRequestJson(context.req.raw),
|
|
1522
1560
|
);
|
|
1523
1561
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1562
|
+
if (thread.read().status !== "idle")
|
|
1563
|
+
throw new ReviewInputError(
|
|
1564
|
+
"Settings can change once the agent finishes answering.",
|
|
1565
|
+
409,
|
|
1566
|
+
);
|
|
1529
1567
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1568
|
+
try {
|
|
1569
|
+
await thread.choose(kind, value);
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
throw new ReviewInputError(errorMessage(error), 409);
|
|
1572
|
+
}
|
|
1532
1573
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
context.req.param("id"),
|
|
1537
|
-
context.req.param("threadId"),
|
|
1538
|
-
);
|
|
1574
|
+
return context.json({ ok: true });
|
|
1575
|
+
},
|
|
1576
|
+
);
|
|
1539
1577
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1578
|
+
// Bypasses permissions from the next answer, or stops.
|
|
1579
|
+
app.post(
|
|
1580
|
+
"/:id/ask/:threadId/permissions",
|
|
1581
|
+
reviewAccess("edit"),
|
|
1582
|
+
async (context) => {
|
|
1583
|
+
const thread = readThread(
|
|
1584
|
+
context.req.param("id"),
|
|
1585
|
+
context.req.param("threadId"),
|
|
1586
|
+
);
|
|
1543
1587
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
"Settings can change once the agent finishes answering.",
|
|
1547
|
-
409,
|
|
1588
|
+
const { bypass } = askPermitSchema.parse(
|
|
1589
|
+
await readBoundedRequestJson(context.req.raw),
|
|
1548
1590
|
);
|
|
1549
1591
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1592
|
+
if (thread.read().status !== "idle")
|
|
1593
|
+
throw new ReviewInputError(
|
|
1594
|
+
"Settings can change once the agent finishes answering.",
|
|
1595
|
+
409,
|
|
1596
|
+
);
|
|
1555
1597
|
|
|
1556
|
-
|
|
1557
|
-
|
|
1598
|
+
try {
|
|
1599
|
+
await thread.permit(bypass);
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
throw new ReviewInputError(errorMessage(error), 409);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
return context.json({ ok: true });
|
|
1605
|
+
},
|
|
1606
|
+
);
|
|
1558
1607
|
|
|
1559
1608
|
// Starts a failed agent again, as after signing it back in, and asks
|
|
1560
1609
|
// again what it did not answer. The state reports how that goes.
|
|
1561
|
-
app.post("/:id/ask/:threadId/retry", (context) => {
|
|
1610
|
+
app.post("/:id/ask/:threadId/retry", reviewAccess("edit"), (context) => {
|
|
1562
1611
|
const thread = readThread(
|
|
1563
1612
|
context.req.param("id"),
|
|
1564
1613
|
context.req.param("threadId"),
|
|
@@ -1574,26 +1623,33 @@ export function createReviewApi(
|
|
|
1574
1623
|
return context.json({ ok: true });
|
|
1575
1624
|
});
|
|
1576
1625
|
|
|
1577
|
-
app.post(
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1626
|
+
app.post(
|
|
1627
|
+
"/:id/ask/:threadId/cancel",
|
|
1628
|
+
reviewAccess("read"),
|
|
1629
|
+
async (context) => {
|
|
1630
|
+
await readThread(
|
|
1631
|
+
context.req.param("id"),
|
|
1632
|
+
context.req.param("threadId"),
|
|
1633
|
+
).cancel();
|
|
1582
1634
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1635
|
+
return context.json({ ok: true });
|
|
1636
|
+
},
|
|
1637
|
+
);
|
|
1585
1638
|
|
|
1586
1639
|
// Closing an Ask ends its agent; the conversation stays saved. A tab
|
|
1587
1640
|
// switched away from or a reload leaves it running, to come back to.
|
|
1588
|
-
app.post("/:id/ask/:threadId/close", (context) => {
|
|
1641
|
+
app.post("/:id/ask/:threadId/close", reviewAccess("read"), (context) => {
|
|
1589
1642
|
readThread(context.req.param("id"), context.req.param("threadId"));
|
|
1590
|
-
ask.threads.close(
|
|
1643
|
+
ask.threads.close(
|
|
1644
|
+
context.req.param("threadId"),
|
|
1645
|
+
!readReview(context.req.param("id")).trashedAt,
|
|
1646
|
+
);
|
|
1591
1647
|
|
|
1592
1648
|
return context.json({ ok: true });
|
|
1593
1649
|
});
|
|
1594
1650
|
|
|
1595
1651
|
// Forgets a saved conversation. The agent keeps its own transcript.
|
|
1596
|
-
app.delete("/:id/ask/:threadId", (context) => {
|
|
1652
|
+
app.delete("/:id/ask/:threadId", reviewAccess("edit"), (context) => {
|
|
1597
1653
|
const reviewId = context.req.param("id");
|
|
1598
1654
|
const threadId = context.req.param("threadId");
|
|
1599
1655
|
|
|
@@ -1733,6 +1789,11 @@ export function createReviewApi(
|
|
|
1733
1789
|
const parsed = commandSchema.parse(input);
|
|
1734
1790
|
|
|
1735
1791
|
if (shared && parsed.operation.type === "delete") {
|
|
1792
|
+
if (!shared.get(parsed.operation.reviewId).snapshot.trashedAt)
|
|
1793
|
+
throw new ReviewInputError(
|
|
1794
|
+
"Move this whiteboard to Trash before deleting it.",
|
|
1795
|
+
409,
|
|
1796
|
+
);
|
|
1736
1797
|
await shared.removeLocal(parsed.operation.reviewId);
|
|
1737
1798
|
|
|
1738
1799
|
return context.json({
|
|
@@ -1742,11 +1803,22 @@ export function createReviewApi(
|
|
|
1742
1803
|
});
|
|
1743
1804
|
}
|
|
1744
1805
|
|
|
1745
|
-
if (
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
parsed.operation.
|
|
1749
|
-
|
|
1806
|
+
if (
|
|
1807
|
+
shared &&
|
|
1808
|
+
(parsed.operation.type === "attention" ||
|
|
1809
|
+
parsed.operation.type === "lifecycle")
|
|
1810
|
+
) {
|
|
1811
|
+
if (parsed.operation.type === "lifecycle")
|
|
1812
|
+
await shared.setLifecycle(
|
|
1813
|
+
parsed.operation.reviewId,
|
|
1814
|
+
parsed.operation.state,
|
|
1815
|
+
);
|
|
1816
|
+
else await shared.setAttention(parsed.operation.reviewId);
|
|
1817
|
+
|
|
1818
|
+
const reviewId = parsed.operation.reviewId;
|
|
1819
|
+
|
|
1820
|
+
if (shared.get(reviewId).snapshot.trashedAt)
|
|
1821
|
+
ask?.threads.closeDeletedReviews((id) => id === reviewId);
|
|
1750
1822
|
|
|
1751
1823
|
return context.json({
|
|
1752
1824
|
reviewId: parsed.operation.reviewId,
|
|
@@ -1760,6 +1832,12 @@ export function createReviewApi(
|
|
|
1760
1832
|
|
|
1761
1833
|
const result = await store.execute(input);
|
|
1762
1834
|
|
|
1835
|
+
if (
|
|
1836
|
+
result.deleted ||
|
|
1837
|
+
(result.lifecycle && store.read(result.reviewId).trashedAt)
|
|
1838
|
+
)
|
|
1839
|
+
ask?.threads.closeDeletedReviews((id) => id === result.reviewId);
|
|
1840
|
+
|
|
1763
1841
|
if (input.operation.type === "edit" && data && !result.deleted) {
|
|
1764
1842
|
const { quotes, unquoted } = await anchorQuotes(
|
|
1765
1843
|
result.version > 0
|