@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.
- 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 +4 -3
- 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
package/src/ask/thread.ts
CHANGED
|
@@ -74,6 +74,17 @@ const piGreetingSchema = z.object({
|
|
|
74
74
|
|
|
75
75
|
const cursorTodosSchema = z.object({ todos: z.unknown() }).loose();
|
|
76
76
|
|
|
77
|
+
/** The steering extension Claude's adapter takes: a follow-up joins the
|
|
78
|
+
* turn under way rather than waiting for it to end. */
|
|
79
|
+
const STEER_METHOD = "_session/steering";
|
|
80
|
+
|
|
81
|
+
/** How an agent says, in its `initialize` response, that it takes steering. */
|
|
82
|
+
const steeringSchema = z.object({
|
|
83
|
+
steering: z.object({ supported: z.literal(true) }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const steerOutcomeSchema = z.object({ outcome: z.string() });
|
|
87
|
+
|
|
77
88
|
/** MCP servers the reviewer's own agent may bring whose tools only read,
|
|
78
89
|
* so their calls run without asking, like the agent reading files. */
|
|
79
90
|
const READ_ONLY_MCP_SERVERS = new Set([
|
|
@@ -203,6 +214,10 @@ export class AskThread {
|
|
|
203
214
|
/** What Pi greets a new session with, its version and skills, which is no
|
|
204
215
|
* part of the answer. */
|
|
205
216
|
private greeting?: string;
|
|
217
|
+
/** The agent takes follow-ups into the turn under way. */
|
|
218
|
+
private steerable = false;
|
|
219
|
+
/** Follow-ups waiting for the turn under way to end, oldest first. */
|
|
220
|
+
private queue: { id: string; question: AskQuestion }[] = [];
|
|
206
221
|
|
|
207
222
|
constructor(
|
|
208
223
|
private readonly launch: AskAgentLauncher,
|
|
@@ -283,20 +298,29 @@ export class AskThread {
|
|
|
283
298
|
askRefusal(): string | undefined {
|
|
284
299
|
switch (this.state.status) {
|
|
285
300
|
case "idle":
|
|
301
|
+
case "running":
|
|
302
|
+
case "waiting":
|
|
286
303
|
return undefined;
|
|
287
304
|
case "failed":
|
|
288
305
|
return `${this.state.agentName} stopped. Try again first.`;
|
|
289
306
|
default:
|
|
290
|
-
return
|
|
307
|
+
return `${this.state.agentName} is still starting.`;
|
|
291
308
|
}
|
|
292
309
|
}
|
|
293
310
|
|
|
294
|
-
/** Asks a follow-up, starting the agent again if it has stopped since.
|
|
311
|
+
/** Asks a follow-up, starting the agent again if it has stopped since.
|
|
312
|
+
* While the agent answers, it joins that turn or waits for it to end. */
|
|
295
313
|
async ask(question: AskQuestion) {
|
|
296
314
|
const refusal = this.askRefusal();
|
|
297
315
|
|
|
298
316
|
if (refusal) throw new Error(refusal);
|
|
299
317
|
|
|
318
|
+
if (this.state.status !== "idle") {
|
|
319
|
+
await this.steer(question);
|
|
320
|
+
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
300
324
|
this.asking = { id: this.addUser(question), question };
|
|
301
325
|
await this.attempt(async () => {
|
|
302
326
|
await this.reconnect();
|
|
@@ -340,6 +364,60 @@ export class AskThread {
|
|
|
340
364
|
});
|
|
341
365
|
}
|
|
342
366
|
|
|
367
|
+
/** Gives the turn under way a follow-up when the agent takes one, else
|
|
368
|
+
* keeps it for when the turn ends. */
|
|
369
|
+
private async steer(question: AskQuestion) {
|
|
370
|
+
const { connection, sessionId } = this;
|
|
371
|
+
|
|
372
|
+
if (this.steerable && connection && sessionId) {
|
|
373
|
+
const id = this.addUser(question);
|
|
374
|
+
|
|
375
|
+
const outcome = await connection.agent
|
|
376
|
+
.request(STEER_METHOD, {
|
|
377
|
+
sessionId,
|
|
378
|
+
prompt: [
|
|
379
|
+
{ type: "text", text: question.text },
|
|
380
|
+
...this.attachments(question),
|
|
381
|
+
],
|
|
382
|
+
// A turn that ended meanwhile leaves the follow-up to Whiteboard,
|
|
383
|
+
// rather than the agent starting a turn Whiteboard does not see end.
|
|
384
|
+
_meta: { steering: { idleBehavior: "promptRequired" } },
|
|
385
|
+
})
|
|
386
|
+
.then(
|
|
387
|
+
(response) => steerOutcomeSchema.safeParse(response).data?.outcome,
|
|
388
|
+
)
|
|
389
|
+
.catch(() => undefined);
|
|
390
|
+
|
|
391
|
+
if (outcome === "injected") return;
|
|
392
|
+
this.emit({ type: "remove", ids: [id] });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this.queue.push({ id: randomUUID(), question });
|
|
396
|
+
this.showQueue();
|
|
397
|
+
// The turn may have ended while the agent was asked.
|
|
398
|
+
this.askQueued();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Asks the oldest waiting follow-up, once no turn is under way. */
|
|
402
|
+
private askQueued() {
|
|
403
|
+
if (this.closed || this.state.status !== "idle") return;
|
|
404
|
+
const next = this.queue.shift();
|
|
405
|
+
|
|
406
|
+
if (!next) return;
|
|
407
|
+
this.showQueue();
|
|
408
|
+
void this.ask(next.question);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private showQueue() {
|
|
412
|
+
this.emit({
|
|
413
|
+
type: "set",
|
|
414
|
+
queued: this.queue.map(({ id, question }) => ({
|
|
415
|
+
id,
|
|
416
|
+
text: question.text,
|
|
417
|
+
})),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
343
421
|
decide(permissionId: string, optionId: string) {
|
|
344
422
|
const resolve = this.decisions.get(permissionId);
|
|
345
423
|
|
|
@@ -352,6 +430,12 @@ export class AskThread {
|
|
|
352
430
|
/** Stops the turn, or the start, in progress. A turn the agent does not
|
|
353
431
|
* end soon after is ended by stopping the agent. */
|
|
354
432
|
async cancel() {
|
|
433
|
+
// Stop drops the follow-ups waiting on the turn as well.
|
|
434
|
+
if (this.queue.length) {
|
|
435
|
+
this.queue = [];
|
|
436
|
+
this.showQueue();
|
|
437
|
+
}
|
|
438
|
+
|
|
355
439
|
// ACP: the Client answers every pending permission request as cancelled.
|
|
356
440
|
for (const resolve of this.decisions.values())
|
|
357
441
|
resolve({ outcome: { outcome: "cancelled" } });
|
|
@@ -379,31 +463,33 @@ export class AskThread {
|
|
|
379
463
|
.catch(() => {});
|
|
380
464
|
}
|
|
381
465
|
|
|
382
|
-
close() {
|
|
466
|
+
close(save = true) {
|
|
383
467
|
if (this.closed) return;
|
|
384
468
|
this.closed = true;
|
|
385
469
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
470
|
+
try {
|
|
471
|
+
// Starting and failed conversations must not replace saved history.
|
|
472
|
+
if (
|
|
473
|
+
save &&
|
|
474
|
+
this.sessionId &&
|
|
475
|
+
this.state.status !== "starting" &&
|
|
476
|
+
this.state.status !== "failed"
|
|
477
|
+
)
|
|
478
|
+
this.start.onSave?.(settled(this.state.entries));
|
|
479
|
+
} finally {
|
|
480
|
+
for (const resolve of this.decisions.values())
|
|
481
|
+
resolve({ outcome: { outcome: "cancelled" } });
|
|
397
482
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
483
|
+
if (this.connection && this.sessionId)
|
|
484
|
+
void this.connection.agent
|
|
485
|
+
.notify(methods.agent.session.cancel, { sessionId: this.sessionId })
|
|
486
|
+
.catch(() => {});
|
|
487
|
+
this.disconnect();
|
|
488
|
+
this.listeners.clear();
|
|
404
489
|
|
|
405
|
-
|
|
406
|
-
|
|
490
|
+
for (const closed of this.closers) closed();
|
|
491
|
+
this.closers.clear();
|
|
492
|
+
}
|
|
407
493
|
}
|
|
408
494
|
|
|
409
495
|
/** Runs when the thread closes, at once if it already has. */
|
|
@@ -556,6 +642,9 @@ export class AskThread {
|
|
|
556
642
|
clientInfo: { name: "whiteboard", title: "Whiteboard", version: "1" },
|
|
557
643
|
});
|
|
558
644
|
|
|
645
|
+
this.steerable =
|
|
646
|
+
askAgents[this.start.agent].steers === true &&
|
|
647
|
+
steeringSchema.safeParse(initialized._meta).success;
|
|
559
648
|
this.emit({
|
|
560
649
|
type: "set",
|
|
561
650
|
accepts: {
|
|
@@ -793,6 +882,8 @@ export class AskThread {
|
|
|
793
882
|
_meta: this.sessionMeta(),
|
|
794
883
|
});
|
|
795
884
|
|
|
885
|
+
if (this.closed) throw new Error("This conversation has ended.");
|
|
886
|
+
|
|
796
887
|
this.sessionId = response.sessionId;
|
|
797
888
|
start.onSession?.(response.sessionId);
|
|
798
889
|
|
|
@@ -895,6 +986,8 @@ export class AskThread {
|
|
|
895
986
|
clearTimeout(this.stopTimer);
|
|
896
987
|
}
|
|
897
988
|
|
|
989
|
+
if (this.closed) return;
|
|
990
|
+
|
|
898
991
|
if (withSelection) this.needsContext = false;
|
|
899
992
|
|
|
900
993
|
if (withPermission) this.permitted = false;
|
|
@@ -936,6 +1029,7 @@ export class AskThread {
|
|
|
936
1029
|
});
|
|
937
1030
|
this.start.onTurn?.();
|
|
938
1031
|
this.start.onSave?.(this.state.entries);
|
|
1032
|
+
this.askQueued();
|
|
939
1033
|
}
|
|
940
1034
|
|
|
941
1035
|
private async requestPermission(
|
package/src/ask/threads.ts
CHANGED
|
@@ -265,10 +265,16 @@ export class AskThreads {
|
|
|
265
265
|
return offer;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
close(id: string) {
|
|
269
|
-
this.threads.get(id)
|
|
268
|
+
close(id: string, save = true) {
|
|
269
|
+
const thread = this.threads.get(id);
|
|
270
270
|
this.threads.delete(id);
|
|
271
271
|
this.unwatchedSince.delete(id);
|
|
272
|
+
thread?.close(save);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
closeDeletedReviews(isDeleted: (reviewId: string) => boolean) {
|
|
276
|
+
for (const [id, thread] of this.threads)
|
|
277
|
+
if (isDeleted(thread.reviewId)) this.close(id, false);
|
|
272
278
|
}
|
|
273
279
|
|
|
274
280
|
closeAll() {
|
package/src/error-telemetry.ts
CHANGED
|
@@ -95,17 +95,21 @@ export function deriveErrorTelemetryProperties(
|
|
|
95
95
|
const message = jsonString(cause.message);
|
|
96
96
|
|
|
97
97
|
if (message !== undefined && message.length > 0) {
|
|
98
|
+
const cleaned = cleanTelemetryText(message, NO_DELETED_DIRECTORIES);
|
|
99
|
+
|
|
98
100
|
// The digest goes on every report, cleaned message or not. It is what
|
|
99
101
|
// groups the reports whose message does not survive the checks below.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
// Cleaned, so an error that names a file has one digest for all files.
|
|
103
|
+
derived.message_hash = hashErrorMessage(cleaned);
|
|
104
|
+
|
|
105
|
+
// Ask the allowlist's own check before sending. Failing here rather
|
|
106
|
+
// than there keeps a message that the cleaner could not finish out of
|
|
107
|
+
// the payload entirely, instead of relying on the later gate.
|
|
108
|
+
if (
|
|
109
|
+
!SCHEMA_ERROR_NAMES.has(derived.error_name ?? "") &&
|
|
110
|
+
isReportableCleanedMessage(cleaned)
|
|
111
|
+
) {
|
|
112
|
+
derived.message = cleaned;
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
@@ -4,48 +4,11 @@
|
|
|
4
4
|
"kind": "keymap",
|
|
5
5
|
"id": "block-1"
|
|
6
6
|
},
|
|
7
|
-
{
|
|
8
|
-
"type": "tutorial",
|
|
9
|
-
"kind": "conversation",
|
|
10
|
-
"conversation": {
|
|
11
|
-
"version": 1,
|
|
12
|
-
"title": "How an agent authors a review",
|
|
13
|
-
"messages": [
|
|
14
|
-
{
|
|
15
|
-
"role": "user",
|
|
16
|
-
"body": "Explain this change."
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"role": "assistant",
|
|
20
|
-
"body": "Here is a review."
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
},
|
|
24
|
-
"id": "block-2"
|
|
25
|
-
},
|
|
26
7
|
{
|
|
27
8
|
"type": "tutorial",
|
|
28
9
|
"kind": "view",
|
|
29
|
-
"view": "
|
|
30
|
-
"label": "
|
|
31
|
-
"id": "block-
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"type": "tutorial",
|
|
35
|
-
"kind": "feature",
|
|
36
|
-
"feature": "softwareMap",
|
|
37
|
-
"children": [
|
|
38
|
-
{
|
|
39
|
-
"type": "markdown",
|
|
40
|
-
"markdown": "Explore the map.",
|
|
41
|
-
"id": "block-5"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"id": "block-6",
|
|
45
|
-
"type": "software_map",
|
|
46
|
-
"mapVersionId": "33333333-3333-4333-8333-333333333333"
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
"id": "block-4"
|
|
10
|
+
"view": "diff",
|
|
11
|
+
"label": "Open the diff",
|
|
12
|
+
"id": "block-2"
|
|
50
13
|
}
|
|
51
14
|
]
|
package/src/review-api/README.md
CHANGED
|
@@ -16,7 +16,7 @@ catalog. Tests inject the native store and source-data provider.
|
|
|
16
16
|
- `authoring_presences`: one row per agent working on a review (focus, color slot, where it last wrote, expiry).
|
|
17
17
|
- `repositories`: server-only local paths; clients receive an ID and display name.
|
|
18
18
|
- `resources`: immutable image, trace and software-map bytes, scoped to a repository.
|
|
19
|
-
- `review_attention`: viewed/
|
|
19
|
+
- `review_attention`: viewed/trashed timestamps, separate from document history.
|
|
20
20
|
|
|
21
21
|
One host-owned store serializes writes, including asynchronous validation.
|
|
22
22
|
Presences show who is working across database connections and never block a
|
|
@@ -96,12 +96,15 @@ Commands: `create {title,target,pullRequestUrl?,reuseExisting?}` or `create {pul
|
|
|
96
96
|
Agents name the checkout by `repositoryPath` in a target, or on a create from a PR alone; `/commands` registers the path and passes the store its id. Retargeting preserves content and component IDs. Restore restores title, target, PR identity, and content. Live targets still read the current checkout.
|
|
97
97
|
PR URLs must be canonical `https://github.com/owner/repository/pull/123` URLs. The PR number is derived from the URL; identity is metadata alongside immutable pins, not a moving source reference. A `create` with a target keeps that target and treats the URL as identity only. A `create` with the URL and no target resolves the PR when accepted: it reads the PR with `gh pr view` (falling back to the public GitHub API), picks the registered checkout named by `repositoryId`, else the checkout of the PR's existing review, else the first registered checkout with a remote whose configured URL is `github.com/owner/repository`, and fetches `refs/pull/N/head` and the base branch from that remote into `refs/review/github/owner/repository/pull/N/*`, never moving branches or bookmarks (a jj repository indexes the commits through a transient tag). It pins that head and its merge base with the base branch; for a PR absorbed by a merge commit it uses the merge base with GitHub's frozen base commit instead. The title defaults to the PR title. A repeat resolves the PR again, so `headMoved` compares the existing review with the PR's current head. `set_target` preserves the document and component IDs, including when source commits change. Its response reports retained source ranges to verify, those in files the new commits changed, and resources that no longer match the pins; agents repair these with `edit`. Existing versions keep their original pins and content. It preserves omitted PR identity within one repository, clears it when switching repositories, and accepts an explicit URL or null.
|
|
98
98
|
|
|
99
|
-
`attention {reviewId,action:"view"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`
|
|
99
|
+
`attention {reviewId,action:"view"}` records viewing without changing lifecycle or document history.
|
|
100
|
+
`lifecycle {reviewId,state:"active"|"trash"}` moves a whiteboard to Trash or restores it.
|
|
101
|
+
Trash retains whiteboards for 30 days; repeated moves to Trash keep the original deadline.
|
|
102
|
+
`delete {reviewId}` permanently removes a whiteboard and its history, but only from Trash.
|
|
103
|
+
The agent tools are `session_attention`, `session_lifecycle`, and `session_delete`.
|
|
104
|
+
`session_restore_version` restores saved content; it does not move a whiteboard out of Trash.
|
|
105
|
+
Home summaries include the repository name, `viewedAt`, and `trashedAt`.
|
|
106
|
+
The list stream is separate from document streams; moving to Trash closes the native tab.
|
|
107
|
+
Old command
|
|
105
108
|
inputs are erased but their IDs remain, so delayed retries cannot resurrect content.
|
|
106
109
|
Repository resources remain shared. Home and the canvas open a pinned, read-only
|
|
107
110
|
source tree. Each source tab names its review version; files opened from it use
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from "@review/ask/thread-state.js";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
|
|
14
|
+
import { ReviewInputError } from "./document.js";
|
|
15
|
+
|
|
14
16
|
/** A saved Ask conversation: its history entry, and how to reopen it. */
|
|
15
17
|
export const askRecordSchema = askHistoryEntrySchema.extend({
|
|
16
18
|
reviewId: z.string(),
|
|
@@ -64,6 +66,10 @@ const rowSchema = z
|
|
|
64
66
|
/** Saved Ask conversations, per review. Shared reviews are not in
|
|
65
67
|
* `reviews`, so there is no foreign key; deleting a review deletes its rows. */
|
|
66
68
|
export class AskHistory {
|
|
69
|
+
private externalWriteGuard?: (reviewId: string) => void;
|
|
70
|
+
setExternalWriteGuard(guard: (reviewId: string) => void) {
|
|
71
|
+
this.externalWriteGuard = guard;
|
|
72
|
+
}
|
|
67
73
|
constructor(private readonly db: DatabaseSync) {
|
|
68
74
|
db.exec(`CREATE TABLE IF NOT EXISTS ask_conversations(
|
|
69
75
|
id TEXT PRIMARY KEY,
|
|
@@ -104,7 +110,26 @@ export class AskHistory {
|
|
|
104
110
|
);
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
private assertWritable(reviewId: string) {
|
|
114
|
+
if (reviewId.startsWith("shared-")) this.externalWriteGuard?.(reviewId);
|
|
115
|
+
|
|
116
|
+
const row = this.db
|
|
117
|
+
.prepare("SELECT deleted_at FROM review_attention WHERE review_id=?")
|
|
118
|
+
.get(reviewId);
|
|
119
|
+
|
|
120
|
+
if (row?.deleted_at)
|
|
121
|
+
throw new ReviewInputError(
|
|
122
|
+
`This whiteboard was moved to Trash on ${new Date(String(row.deleted_at)).toLocaleDateString("en-US", { month: "short", day: "numeric" })}. Restore it with session_lifecycle({"sessionId":"${reviewId}","state":"active"}) to continue.`,
|
|
123
|
+
409,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
private assertThreadWritable(id: string) {
|
|
127
|
+
const record = this.get(id);
|
|
128
|
+
|
|
129
|
+
if (record) this.assertWritable(record.reviewId);
|
|
130
|
+
}
|
|
107
131
|
save(record: AskRecord) {
|
|
132
|
+
this.assertWritable(record.reviewId);
|
|
108
133
|
this.db
|
|
109
134
|
.prepare(
|
|
110
135
|
`INSERT OR REPLACE INTO ask_conversations(id,review_id,agent,session_id,version,head,cwd,selection,title,created_at,updated_at,bypass)
|
|
@@ -128,6 +153,7 @@ export class AskHistory {
|
|
|
128
153
|
|
|
129
154
|
/** Whether reopening the conversation bypasses permissions. */
|
|
130
155
|
setBypass(id: string, bypass: boolean) {
|
|
156
|
+
this.assertThreadWritable(id);
|
|
131
157
|
this.db
|
|
132
158
|
.prepare("UPDATE ask_conversations SET bypass=? WHERE id=?")
|
|
133
159
|
.run(bypass ? 1 : 0, id);
|
|
@@ -136,6 +162,7 @@ export class AskHistory {
|
|
|
136
162
|
/** Points a conversation at a new session, when its agent could not
|
|
137
163
|
* reopen the one it had. */
|
|
138
164
|
updateSession(id: string, sessionId: string) {
|
|
165
|
+
this.assertThreadWritable(id);
|
|
139
166
|
this.db
|
|
140
167
|
.prepare("UPDATE ask_conversations SET session_id=? WHERE id=?")
|
|
141
168
|
.run(sessionId, id);
|
|
@@ -143,6 +170,7 @@ export class AskHistory {
|
|
|
143
170
|
|
|
144
171
|
/** Keeps what the panel shows, so a reopen need not wait on the agent. */
|
|
145
172
|
saveEntries(id: string, entries: AskEntry[]) {
|
|
173
|
+
this.assertThreadWritable(id);
|
|
146
174
|
this.db
|
|
147
175
|
.prepare("UPDATE ask_conversations SET entries=? WHERE id=?")
|
|
148
176
|
.run(JSON.stringify(entries), id);
|
|
@@ -150,6 +178,7 @@ export class AskHistory {
|
|
|
150
178
|
|
|
151
179
|
/** The agent's name for the conversation, which the list shows. */
|
|
152
180
|
rename(id: string, title: string) {
|
|
181
|
+
this.assertThreadWritable(id);
|
|
153
182
|
this.db
|
|
154
183
|
.prepare("UPDATE ask_conversations SET title=? WHERE id=?")
|
|
155
184
|
.run(title.slice(0, 200), id);
|
|
@@ -197,6 +226,7 @@ export class AskHistory {
|
|
|
197
226
|
}
|
|
198
227
|
|
|
199
228
|
touch(id: string, at = new Date().toISOString()) {
|
|
229
|
+
this.assertThreadWritable(id);
|
|
200
230
|
this.db
|
|
201
231
|
.prepare("UPDATE ask_conversations SET updated_at=? WHERE id=?")
|
|
202
232
|
.run(at, id);
|
|
@@ -236,7 +266,14 @@ export class AskHistory {
|
|
|
236
266
|
return row ? rowSchema.parse(row) : undefined;
|
|
237
267
|
}
|
|
238
268
|
|
|
269
|
+
removeReview(reviewId: string) {
|
|
270
|
+
this.db
|
|
271
|
+
.prepare("DELETE FROM ask_conversations WHERE review_id=?")
|
|
272
|
+
.run(reviewId);
|
|
273
|
+
}
|
|
274
|
+
|
|
239
275
|
delete(id: string) {
|
|
276
|
+
this.assertThreadWritable(id);
|
|
240
277
|
this.db.prepare("DELETE FROM ask_conversations WHERE id=?").run(id);
|
|
241
278
|
}
|
|
242
279
|
}
|
|
@@ -36,7 +36,7 @@ export function authoringTools(
|
|
|
36
36
|
|
|
37
37
|
const descriptions = {
|
|
38
38
|
create:
|
|
39
|
-
'Create a review of saved working files, immutable commits or a GitHub PR. Revisions are resolved on acceptance. A worktree target reviews the saved files in its checkout, uncommitted ones included and untracked ones left out (git add -N a new file to include it), against base: the branch to compare against, by default the default branch of the repository. The diff starts at the merge base of base and HEAD, which follows rebases. Omitted commits base means source at head with no diff; supply the parent to review introduced changes. For a GitHub PR, pullRequestUrl alone is enough: target and title become optional, and the host fetches the PR into a registered checkout of its repository and pins the current PR head and GitHub diff base, titled from the PR. When
|
|
39
|
+
'Create a review of saved working files, immutable commits or a GitHub PR. Revisions are resolved on acceptance. A worktree target reviews the saved files in its checkout, uncommitted ones included and untracked ones left out (git add -N a new file to include it), against base: the branch to compare against, by default the default branch of the repository. The diff starts at the merge base of base and HEAD, which follows rebases. Omitted commits base means source at head with no diff; supply the parent to review introduced changes. For a GitHub PR, pullRequestUrl alone is enough: target and title become optional, and the host fetches the PR into a registered checkout of its repository and pins the current PR head and GitHub diff base, titled from the PR. When the existing review for that PR is in Trash, creation reports its ID; restore it with review_lifecycle state:"active" before continuing. When an active review for that PR exists, it is returned instead, reporting whether its head moved and whether an agent is working on it; update it in place, move its target with review_set_target, or create a separate review with reuseExisting. kind:"scratchpad" names the one scratchpad, which the host creates itself. The result carries review, the review as review_list shows it: its target with resolved commits, origin (its PR), repositoryName and repositoryPath, so no follow-up read is needed before diffing. When Desktop is available the review opens there and the result reports opened, softwareMapEnabled and environmentIssues, as review_open does; set open:false to author in the background without taking over Desktop.',
|
|
40
40
|
set_target:
|
|
41
41
|
"Move the review to a new target, such as the commits after a rebase or amend, preserving the document and component IDs. Revisions are resolved on acceptance. Returns warnings for retained source ranges in files the new commits changed, to verify, and resources that no longer match; fix them with review_edit. Earlier versions keep their source. Omitted pullRequestUrl keeps the PR within the same repository; changing repositories clears it. Supply a URL to replace it or null to detach.",
|
|
42
42
|
edit: [
|
|
@@ -52,8 +52,11 @@ export function authoringTools(
|
|
|
52
52
|
restore:
|
|
53
53
|
"Restore title, source pins, PR identity and content from a saved version.",
|
|
54
54
|
attention:
|
|
55
|
-
"Mark
|
|
56
|
-
|
|
55
|
+
"Mark an active review viewed without changing its content or lifecycle. Restore a review from Trash before marking it viewed.",
|
|
56
|
+
lifecycle:
|
|
57
|
+
"Move a review between active and trash. Trash retains the review for 30 days before automatic deletion. Moving it back to active restores it; moving it to trash again does not restart that period.",
|
|
58
|
+
delete:
|
|
59
|
+
"Permanently delete a review and its history. The review must be in Trash. This cannot be undone.",
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
const tool = (
|
|
@@ -64,7 +67,7 @@ export function authoringTools(
|
|
|
64
67
|
path: string,
|
|
65
68
|
commandType?: string,
|
|
66
69
|
) => ({
|
|
67
|
-
name: `review_${name}`,
|
|
70
|
+
name: `review_${name === "restore" ? "restore_version" : name}`,
|
|
68
71
|
description,
|
|
69
72
|
inputSchema: {
|
|
70
73
|
...z.toJSONSchema(schema, { io: "input" }),
|
|
@@ -174,7 +177,7 @@ export function authoringTools(
|
|
|
174
177
|
),
|
|
175
178
|
tool(
|
|
176
179
|
"open",
|
|
177
|
-
"Show an
|
|
180
|
+
"Show an active review immediately; restore it from Trash before opening it. Prepare current pinned checkouts in the background. Returns softwareMapEnabled and any already-recorded environmentIssues. Missing optional setup is not an issue; use review_environment to recheck.",
|
|
178
181
|
z.strictObject(review),
|
|
179
182
|
"POST",
|
|
180
183
|
"/:reviewId/open",
|
|
@@ -57,6 +57,11 @@ export const storeSchema = z.strictObject({
|
|
|
57
57
|
label,
|
|
58
58
|
key: label.optional(),
|
|
59
59
|
fields: z.record(text, fieldSchema),
|
|
60
|
+
source: anchorSchema
|
|
61
|
+
.optional()
|
|
62
|
+
.describe(
|
|
63
|
+
"Where the table or collection is declared, such as its schema definition. Its use cases' tours start here.",
|
|
64
|
+
),
|
|
60
65
|
}),
|
|
61
66
|
),
|
|
62
67
|
});
|
|
@@ -1,36 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type TutorialAuthoringConversation,
|
|
3
|
-
tutorialAuthoringConversationSchema,
|
|
4
|
-
} from "@review/tutorial-conversation.js";
|
|
5
1
|
import { z } from "zod";
|
|
6
2
|
|
|
7
3
|
import { type BlockDefinition, defineBlock } from "./definition.js";
|
|
8
|
-
import { type Block, blockSchema } from "./index.js";
|
|
9
4
|
|
|
10
5
|
type TutorialContent =
|
|
11
6
|
| { kind: "keymap" }
|
|
12
|
-
| { kind: "
|
|
13
|
-
| { kind: "view"; view: "review" | "commits" | "diff" | "map"; label: string }
|
|
14
|
-
| { kind: "feature"; feature: "softwareMap"; children: Block[] };
|
|
7
|
+
| { kind: "view"; view: "diff"; label: string };
|
|
15
8
|
|
|
16
9
|
export type TutorialBlock = { id?: string; type: "tutorial" } & TutorialContent;
|
|
17
10
|
|
|
18
11
|
export const tutorialSchema = z.discriminatedUnion("kind", [
|
|
19
12
|
defineBlock("tutorial", { kind: z.literal("keymap") }),
|
|
20
|
-
defineBlock("tutorial", {
|
|
21
|
-
kind: z.literal("conversation"),
|
|
22
|
-
conversation: tutorialAuthoringConversationSchema,
|
|
23
|
-
}),
|
|
24
13
|
defineBlock("tutorial", {
|
|
25
14
|
kind: z.literal("view"),
|
|
26
|
-
view: z.
|
|
15
|
+
view: z.literal("diff"),
|
|
27
16
|
label: z.string(),
|
|
28
17
|
}),
|
|
29
|
-
defineBlock("tutorial", {
|
|
30
|
-
kind: z.literal("feature"),
|
|
31
|
-
feature: z.literal("softwareMap"),
|
|
32
|
-
children: z.array(z.lazy((): z.ZodType<Block> => blockSchema)),
|
|
33
|
-
}),
|
|
34
18
|
]);
|
|
35
19
|
|
|
36
20
|
export const tutorial: BlockDefinition<TutorialBlock> = {
|
|
@@ -151,11 +151,14 @@ export async function comparisonCoverage(
|
|
|
151
151
|
|
|
152
152
|
if (mode === "textual") return coverage;
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
const publishCurrent = () =>
|
|
156
|
-
publish?.({ ...coverage, files: [...coverage.files] });
|
|
154
|
+
const unsupported = new Set<string>();
|
|
157
155
|
|
|
158
|
-
|
|
156
|
+
const current = () => ({
|
|
157
|
+
...coverage,
|
|
158
|
+
files: coverage.files.filter((file) => !unsupported.has(file.path)),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
publish?.(current());
|
|
159
162
|
|
|
160
163
|
const baseFiles = new Map(
|
|
161
164
|
coverage.files.map((file, index) => [
|
|
@@ -195,12 +198,18 @@ export async function comparisonCoverage(
|
|
|
195
198
|
if (!remaining.delete(path))
|
|
196
199
|
throw new Error(`Unexpected structural result: ${path}`);
|
|
197
200
|
|
|
201
|
+
const baseIndex = event.file.lhs && baseFiles.get(event.file.lhs.path);
|
|
202
|
+
const headIndex = event.file.rhs && headFiles.get(event.file.rhs.path);
|
|
203
|
+
|
|
198
204
|
if (event.error) {
|
|
199
205
|
if (
|
|
200
206
|
event.error.code !== "unsupported_file_type" &&
|
|
201
207
|
event.error.code !== "not_utf8"
|
|
202
208
|
)
|
|
203
209
|
throw new Error(`Cannot count ${path}: ${event.error.message}`);
|
|
210
|
+
|
|
211
|
+
for (const index of [baseIndex, headIndex])
|
|
212
|
+
if (index !== undefined) unsupported.add(coverage.files[index].path);
|
|
204
213
|
} else if (event.diff.type === "text") {
|
|
205
214
|
const diff = event.diff;
|
|
206
215
|
const folded = foldedChanges(diff);
|
|
@@ -229,8 +238,6 @@ export async function comparisonCoverage(
|
|
|
229
238
|
};
|
|
230
239
|
}
|
|
231
240
|
|
|
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
241
|
const index = headIndex ?? baseIndex;
|
|
235
242
|
|
|
236
243
|
if (index !== undefined) {
|
|
@@ -249,16 +256,16 @@ export async function comparisonCoverage(
|
|
|
249
256
|
structuralRows(diff),
|
|
250
257
|
);
|
|
251
258
|
}
|
|
252
|
-
|
|
253
|
-
publishCurrent();
|
|
254
259
|
}
|
|
255
260
|
|
|
261
|
+
publish?.(current());
|
|
262
|
+
|
|
256
263
|
if (!remaining.size) break;
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
if (remaining.size) throw new Error("Structural coverage is incomplete.");
|
|
260
267
|
|
|
261
|
-
return
|
|
268
|
+
return current();
|
|
262
269
|
}
|
|
263
270
|
|
|
264
271
|
/**
|
|
@@ -22,7 +22,7 @@ export const COMPONENT_REFERENCE = [
|
|
|
22
22
|
"- call_stack_diff {title, base: frame[], head: frame[], pins?}; either side may be empty",
|
|
23
23
|
"- frame {source: anchor, label?, key?, parentKey? (an earlier frame on that side), callSite?: anchor, contextSources?: anchor[], via?: {kind: call|queue|callback|rpc, reason}, pins?}",
|
|
24
24
|
"- database_lens {title, actors: {key: label}, stores: {key: store}, useCases: useCase[] (1 or more), pins?}",
|
|
25
|
-
"- store {label, storage: relational|document, collections: {key: {label, fields: {key: field}}}, dataStoreKind?: database|objectStore|bucket|artifactStore|fileStore}",
|
|
25
|
+
"- store {label, storage: relational|document, collections: {key: {label, fields: {key: field}, source?: anchor of its schema declaration}}, dataStoreKind?: database|objectStore|bucket|artifactStore|fileStore}",
|
|
26
26
|
"- field {label, dataType, nullable?, primaryKey?, references?: {store, collection, field}, fields?: {key: field}}",
|
|
27
27
|
"- useCase {label, summary?, operations: operation[] (1 or more)}; operation {kind: read|write, actor, store, collection, label, source: anchor, field?, detail?, pins?}, naming declared keys",
|
|
28
28
|
"",
|