@dev.fast/whiteboard 0.2.5-preview.20261013.113 → 0.2.5-preview.20261015.115
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/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
- package/dist/{agent-cli-04xbDj23.js → agent-cli-CR2PnA9G.js} +9 -12
- package/dist/agent-cli-CRPPmS3l.js +2 -0
- package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
- package/dist/{authoring-tools-fdA0RPIT.js → authoring-tools-BXKEwhfi.js} +5 -5
- package/dist/background-server-Cw7CcfaZ.js +156 -0
- package/dist/background-server-D2Rrr_e2.js +2 -0
- package/dist/build-info.json +1 -1
- package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
- package/dist/{cli-runner-C3men22A.js → cli-runner-PlGqBeMw.js} +368 -252
- package/dist/cli.js +3 -2
- package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
- package/dist/{process-error-telemetry-BCBYp2t4.js → headless-host-CVXTSIlx.js} +3285 -2585
- package/dist/headless-host-Cp37C-v0.js +2 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/{local-data-Dy1pNtLF.js → local-data-yfLp5PiJ.js} +113 -62
- package/dist/{mcp-B8kuRSXM.js → mcp-DBbDwG4y.js} +4 -4
- package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
- package/dist/profile-BSOkA6A-.js +2 -0
- package/dist/{profile-tbGWuWHu.js → profile-pAzg7fxC.js} +3 -3
- package/dist/remote-attach-BAt2eZx3.js +137 -0
- package/dist/remote-extensions-2Cxf3hMo.js +2 -0
- package/dist/remote-extensions-BiNx6NQK.js +323 -0
- package/dist/remote-language-server-DQgWUNeV.js +252 -0
- package/dist/remote-uninstall-DLtDG0o7.js +153 -0
- package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
- package/dist/review-app-launcher-D7I4DGoH.js +222 -0
- package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
- package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
- package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
- package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
- package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
- package/dist/server/desktop-host.js +1663 -266
- package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
- package/dist/sharing/index.d.ts +88 -32
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
- package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
- package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
- package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
- package/instructions/authoring.md +16 -12
- package/instructions/file-lenses.md +4 -2
- package/package.json +9 -14
- package/src/agent-connect.ts +507 -0
- package/src/agent-selection.ts +4 -0
- package/src/ask/agents.ts +59 -13
- package/src/ask/pi-mcp.ts +7 -3
- package/src/ask/thread-state.ts +9 -4
- package/src/ask/thread.ts +88 -50
- package/src/ask/threads.ts +53 -7
- package/src/cli-install.ts +3 -1
- package/src/cli-runner.ts +472 -81
- package/src/cli.ts +1 -1
- package/src/connect-agents.ts +285 -0
- package/src/connect-prompts.ts +60 -6
- package/src/desktop-discovery.ts +24 -0
- package/src/pinned-download.ts +118 -0
- package/src/remote-attach.ts +130 -0
- package/src/remote-extensions.ts +359 -0
- package/src/remote-language-server.ts +428 -0
- package/src/remote-toolchains.ts +107 -0
- package/src/remote-uninstall.ts +280 -0
- package/src/review-api/README.md +1 -3
- package/src/review-api/agent-cli.ts +4 -4
- package/src/review-api/agent-client.ts +23 -0
- package/src/review-api/ask-history.ts +31 -16
- package/src/review-api/authoring-tools.ts +1 -1
- package/src/review-api/blocks/index.ts +2 -4
- package/src/review-api/document.ts +1 -4
- package/src/review-api/http.ts +265 -237
- package/src/review-api/local-data.ts +18 -3
- package/src/review-api/onboarding.ts +478 -0
- package/src/review-api/origin.ts +7 -0
- package/src/review-api/recent-folders.ts +173 -0
- package/src/review-api/store.ts +56 -10
- package/src/review-api/workspaces.ts +56 -11
- package/src/review-app-launcher.ts +61 -3
- package/src/review-stack.ts +162 -36
- package/src/server/background-server.ts +250 -0
- package/src/server/desktop-host-shutdown.ts +24 -0
- package/src/server/desktop-host.ts +39 -24
- package/src/server/desktop-server.ts +68 -118
- package/src/server/global-verb-relay.ts +16 -0
- package/src/server/headless-host.ts +128 -3
- package/src/server/hono-http.ts +6 -1
- package/src/server/legacy-tutorial-cleanup.ts +38 -0
- package/src/server/review-gateway-hosts.ts +379 -0
- package/src/server/review-gateway-list.ts +109 -0
- package/src/server/review-gateway-memory.ts +130 -0
- package/src/server/review-gateway-pushes.ts +142 -0
- package/src/server/review-gateway-streams.ts +415 -0
- package/src/server/review-gateway-transport.ts +204 -0
- package/src/server/review-gateway.ts +433 -0
- package/src/server/review-server-core.ts +36 -6
- package/src/server-discovery.ts +11 -0
- package/src/sharing/import.ts +1 -0
- package/src/ui-telemetry-events.ts +5 -1
- package/src/vsix.ts +122 -0
- package/dist/agent-cli-BR_Jw5BL.js +0 -2
- package/dist/headless-host-BLLJ7Dyu.js +0 -95
- package/dist/profile-aMSw1oCM.js +0 -2
- package/dist/server-discovery-ZVi8Ay8J.js +0 -66
- package/src/fixtures/blocks/tutorial.json +0 -14
- package/src/review-api/blocks/tutorial.ts +0 -23
- package/src/server/tutorial-service.ts +0 -217
- package/tutorial/document.json +0 -216
- package/tutorial/lenses.json +0 -10
- package/tutorial/runtime-manifest.json +0 -11
- package/tutorial/sample-service/package.json +0 -9
- package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
- package/tutorial/sample-service/src/app.ts +0 -26
- package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
- package/tutorial/sample-service/src/database/schema.ts +0 -9
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
- package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
- package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
- package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
- package/tutorial/sample-service/src/orders/order-service.ts +0 -35
- package/tutorial/sample-service/src/orders/order.ts +0 -43
- package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
- package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
- package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
- package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
- package/tutorial/sample-service/tsconfig.json +0 -12
package/src/review-api/http.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
1
|
import { type JsonObject, isJsonObject } from "@dev.fast/json";
|
|
4
2
|
import {
|
|
5
3
|
REVIEW_CLIENT_HEADER,
|
|
6
4
|
REVIEW_CLIENT_REMOTE,
|
|
5
|
+
type ReviewRemoteLanguageEnvironment,
|
|
6
|
+
type ReviewRemoteNavigatorAnswer,
|
|
7
|
+
type ReviewStreamLine,
|
|
7
8
|
type ReviewStructuralDiffEvent,
|
|
8
9
|
} from "@dev.fast/review-protocol";
|
|
9
10
|
import { errorMessage } from "@dev.fast/trace-core";
|
|
@@ -12,7 +13,11 @@ import {
|
|
|
12
13
|
AgentSelectionSchema,
|
|
13
14
|
selectionMarkdown,
|
|
14
15
|
} from "@review/agent-selection.js";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type AskAgentStatus,
|
|
18
|
+
askAgents,
|
|
19
|
+
defaultAskPermissions,
|
|
20
|
+
} from "@review/ask/agents.js";
|
|
16
21
|
import { checkoutFiles, mentionableFiles } from "@review/ask/checkout-files.js";
|
|
17
22
|
import { parseFileRef, resolveFileRefs } from "@review/ask/file-refs.js";
|
|
18
23
|
import {
|
|
@@ -20,9 +25,11 @@ import {
|
|
|
20
25
|
type AskThreadState,
|
|
21
26
|
askAgentIds,
|
|
22
27
|
askChoiceKinds,
|
|
28
|
+
askPermissionsSchema,
|
|
23
29
|
askPicksSchema,
|
|
24
30
|
askQuestionSchema,
|
|
25
31
|
} from "@review/ask/thread-state.js";
|
|
32
|
+
import type { AskThreadHooks } from "@review/ask/thread.js";
|
|
26
33
|
import type { AskThreads } from "@review/ask/threads.js";
|
|
27
34
|
import { watchAskThreads } from "@review/ask/watch.js";
|
|
28
35
|
import { fuzzyRank } from "@review/fuzzy-match.js";
|
|
@@ -41,6 +48,7 @@ import { type Context, Hono, type MiddlewareHandler } from "hono";
|
|
|
41
48
|
import { z } from "zod";
|
|
42
49
|
|
|
43
50
|
import { anchorQuotes } from "./anchor-quotes.js";
|
|
51
|
+
import type { AskRecord } from "./ask-history.js";
|
|
44
52
|
import { authoringTools } from "./authoring-tools.js";
|
|
45
53
|
import { documentText } from "./document-text.js";
|
|
46
54
|
import { ReviewInputError } from "./document.js";
|
|
@@ -50,6 +58,7 @@ import {
|
|
|
50
58
|
scratchpadAvailable,
|
|
51
59
|
} from "./instructions.js";
|
|
52
60
|
import type { LocalReviewData } from "./local-data.js";
|
|
61
|
+
import { registerOnboardingRoutes } from "./onboarding.js";
|
|
53
62
|
import {
|
|
54
63
|
inspectQuerySchema,
|
|
55
64
|
queryAnchor,
|
|
@@ -86,10 +95,10 @@ const askStartSchema = z.strictObject({
|
|
|
86
95
|
question: askQuestionSchema,
|
|
87
96
|
selection: AgentSelectionSchema,
|
|
88
97
|
picks: askPicksSchema.optional(),
|
|
89
|
-
|
|
98
|
+
permissions: askPermissionsSchema.optional(),
|
|
90
99
|
});
|
|
91
100
|
|
|
92
|
-
const askPermitSchema = z.strictObject({
|
|
101
|
+
const askPermitSchema = z.strictObject({ permissions: askPermissionsSchema });
|
|
93
102
|
|
|
94
103
|
/** The threads one watch follows: those of a review's open Asks. A body,
|
|
95
104
|
* not the query, so there can be as many as are open. */
|
|
@@ -155,10 +164,6 @@ export interface ReviewApiHooks {
|
|
|
155
164
|
sharing?: SharingHostEvents;
|
|
156
165
|
}
|
|
157
166
|
|
|
158
|
-
/** A gateway forwarding from another machine; it gets no local paths. */
|
|
159
|
-
const remoteCaller = (context: Context) =>
|
|
160
|
-
context.req.header(REVIEW_CLIENT_HEADER) === REVIEW_CLIENT_REMOTE;
|
|
161
|
-
|
|
162
167
|
/** Both hosts mount this behind their token authentication. */
|
|
163
168
|
export function createReviewApi(
|
|
164
169
|
store: ReviewStore,
|
|
@@ -509,121 +514,123 @@ export function createReviewApi(
|
|
|
509
514
|
);
|
|
510
515
|
},
|
|
511
516
|
);
|
|
512
|
-
app.get("/watch",
|
|
513
|
-
|
|
517
|
+
app.get("/watch", (context) => {
|
|
518
|
+
let input: unknown;
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
input = JSON.parse(context.req.query("subscriptions") ?? "");
|
|
522
|
+
} catch {
|
|
523
|
+
throw new ReviewInputError("Invalid subscriptions.");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const subscriptions = z
|
|
527
|
+
.array(
|
|
528
|
+
z.strictObject({
|
|
529
|
+
reviewId: z.string().min(1).nullable(),
|
|
530
|
+
mode: coverageModeSchema,
|
|
531
|
+
}),
|
|
532
|
+
)
|
|
533
|
+
.parse(input);
|
|
534
|
+
|
|
535
|
+
const modes = new Set(
|
|
536
|
+
subscriptions.flatMap((item) =>
|
|
537
|
+
item.reviewId === null ? [item.mode] : [],
|
|
538
|
+
),
|
|
539
|
+
);
|
|
514
540
|
|
|
515
|
-
|
|
516
|
-
|
|
541
|
+
const reviewIds = new Set(
|
|
542
|
+
subscriptions.flatMap((item) => item.reviewId ?? []),
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
const dirtyModes = new Set(modes);
|
|
546
|
+
const dirtyReviews = new Set(reviewIds);
|
|
547
|
+
const sentLists = new Map<string, string>();
|
|
517
548
|
|
|
549
|
+
const readReviewLine = (reviewId: string): ReviewStreamLine<Snapshot> => {
|
|
518
550
|
try {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
551
|
+
return {
|
|
552
|
+
kind: "review",
|
|
553
|
+
reviewId,
|
|
554
|
+
value: {
|
|
555
|
+
...readReview(reviewId),
|
|
556
|
+
activity: store.activity.read(reviewId),
|
|
557
|
+
coverageRevision: data?.coverageRevision ?? 0,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
} catch (error) {
|
|
561
|
+
return {
|
|
562
|
+
kind: "review",
|
|
563
|
+
reviewId,
|
|
564
|
+
error:
|
|
565
|
+
error instanceof ReviewInputError
|
|
566
|
+
? error.message
|
|
567
|
+
: "Could not read review.",
|
|
568
|
+
};
|
|
522
569
|
}
|
|
570
|
+
};
|
|
523
571
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
z.strictObject({
|
|
527
|
-
reviewId: z.string().min(1).nullable(),
|
|
528
|
-
mode: coverageModeSchema,
|
|
529
|
-
}),
|
|
530
|
-
)
|
|
531
|
-
.parse(input);
|
|
572
|
+
const lines = () => {
|
|
573
|
+
const pending: string[] = [];
|
|
532
574
|
|
|
533
|
-
|
|
534
|
-
|
|
575
|
+
for (const mode of dirtyModes) {
|
|
576
|
+
const line = JSON.stringify({
|
|
577
|
+
kind: "list",
|
|
578
|
+
mode,
|
|
579
|
+
reviews: catalog(mode),
|
|
580
|
+
} satisfies ReviewStreamLine);
|
|
535
581
|
|
|
536
|
-
|
|
537
|
-
|
|
582
|
+
if (sentLists.get(mode) !== line) pending.push(line);
|
|
583
|
+
sentLists.set(mode, line);
|
|
584
|
+
}
|
|
538
585
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
dirty.add(index);
|
|
542
|
-
marked = true;
|
|
543
|
-
}
|
|
544
|
-
});
|
|
586
|
+
for (const reviewId of dirtyReviews)
|
|
587
|
+
pending.push(JSON.stringify(readReviewLine(reviewId)));
|
|
545
588
|
|
|
546
|
-
|
|
547
|
-
|
|
589
|
+
dirtyModes.clear();
|
|
590
|
+
dirtyReviews.clear();
|
|
548
591
|
|
|
549
|
-
return
|
|
550
|
-
|
|
551
|
-
subscriptions.map(({ reviewId, mode }, index) => {
|
|
552
|
-
if (!dirty.delete(index)) return null;
|
|
553
|
-
|
|
554
|
-
try {
|
|
555
|
-
return {
|
|
556
|
-
value:
|
|
557
|
-
reviewId === null
|
|
558
|
-
? catalog(mode)
|
|
559
|
-
: {
|
|
560
|
-
...readReview(reviewId),
|
|
561
|
-
activity: store.activity.read(reviewId),
|
|
562
|
-
coverageRevision: data?.coverageRevision ?? 0,
|
|
563
|
-
},
|
|
564
|
-
};
|
|
565
|
-
} catch (error) {
|
|
566
|
-
return {
|
|
567
|
-
error:
|
|
568
|
-
error instanceof ReviewInputError
|
|
569
|
-
? error.message
|
|
570
|
-
: "Could not read review.",
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
}),
|
|
574
|
-
(notify) => {
|
|
575
|
-
const stopRefresh = store.watchWorktrees();
|
|
576
|
-
|
|
577
|
-
const stops = [
|
|
578
|
-
stopRefresh,
|
|
579
|
-
data?.subscribeCoverage(() => {
|
|
580
|
-
subscriptions.forEach((item, index) => {
|
|
581
|
-
if (item.reviewId !== null) dirty.add(index);
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// Coverage never changes the catalog; don't send an all-null line.
|
|
585
|
-
if (dirty.size > 0) notify();
|
|
586
|
-
}) ?? (() => {}),
|
|
587
|
-
store.subscribe((result) => {
|
|
588
|
-
if (mark(result.reviewId)) notify();
|
|
589
|
-
}),
|
|
590
|
-
store.activity.subscribe((id) => {
|
|
591
|
-
if (mark(id)) notify();
|
|
592
|
-
}),
|
|
593
|
-
store.activity.subscribeWorking(() => {
|
|
594
|
-
if (mark(null)) notify();
|
|
595
|
-
}),
|
|
596
|
-
shared?.subscribe(() => {
|
|
597
|
-
if (mark(null)) notify();
|
|
598
|
-
}) ?? (() => {}),
|
|
599
|
-
store.subscribeCatalog(() => {
|
|
600
|
-
if (mark(null)) notify();
|
|
601
|
-
}),
|
|
602
|
-
];
|
|
592
|
+
return pending;
|
|
593
|
+
};
|
|
603
594
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
() => {},
|
|
608
|
-
);
|
|
609
|
-
}
|
|
595
|
+
const markReview = (reviewId: string) => {
|
|
596
|
+
if (!reviewIds.has(reviewId)) return false;
|
|
597
|
+
dirtyReviews.add(reviewId);
|
|
610
598
|
|
|
611
|
-
|
|
599
|
+
return true;
|
|
600
|
+
};
|
|
612
601
|
|
|
613
|
-
|
|
614
|
-
() =>
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
602
|
+
const markLists = () => {
|
|
603
|
+
modes.forEach((mode) => dirtyModes.add(mode));
|
|
604
|
+
|
|
605
|
+
return modes.size > 0;
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
return watch(lines, (notify) => {
|
|
609
|
+
const stops = [
|
|
610
|
+
store.watchWorktrees(),
|
|
611
|
+
data?.subscribeCoverage(() => {
|
|
612
|
+
reviewIds.forEach((id) => dirtyReviews.add(id));
|
|
613
|
+
notify();
|
|
614
|
+
}) ?? (() => {}),
|
|
615
|
+
store.subscribe((result) => {
|
|
616
|
+
if (markReview(result.reviewId)) notify();
|
|
617
|
+
}),
|
|
618
|
+
store.activity.subscribe((id) => {
|
|
619
|
+
if (markReview(id)) notify();
|
|
620
|
+
}),
|
|
621
|
+
store.activity.subscribeWorking(() => {
|
|
622
|
+
if (markLists()) notify();
|
|
623
|
+
}),
|
|
624
|
+
shared?.subscribe(() => {
|
|
625
|
+
if (markLists()) notify();
|
|
626
|
+
}) ?? (() => {}),
|
|
627
|
+
store.subscribeCatalog(() => {
|
|
628
|
+
if (markLists()) notify();
|
|
629
|
+
}),
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
return () => stops.forEach((stop) => stop());
|
|
633
|
+
});
|
|
627
634
|
});
|
|
628
635
|
|
|
629
636
|
/** Show a review in Desktop and start preparing its pinned checkouts. */
|
|
@@ -684,41 +691,6 @@ export function createReviewApi(
|
|
|
684
691
|
|
|
685
692
|
return context.json({ ok: true, ...(await openReview(readReview(id))) });
|
|
686
693
|
});
|
|
687
|
-
app.get("/:id/watch", (context) => {
|
|
688
|
-
const id = context.req.param("id");
|
|
689
|
-
|
|
690
|
-
// Activity changes every renewal; reload the document only when it changed.
|
|
691
|
-
let document: Snapshot | undefined;
|
|
692
|
-
|
|
693
|
-
return watch(
|
|
694
|
-
() => ({
|
|
695
|
-
...(document ??= readReview(id)),
|
|
696
|
-
activity: isShared(id)
|
|
697
|
-
? { workingCount: 0, expiresAt: null }
|
|
698
|
-
: store.activity.read(id),
|
|
699
|
-
}),
|
|
700
|
-
(notify) => {
|
|
701
|
-
const stopRefresh = store.watchWorktrees();
|
|
702
|
-
|
|
703
|
-
const stopDocument = store.subscribe((result) => {
|
|
704
|
-
if (result.reviewId === id) {
|
|
705
|
-
document = undefined;
|
|
706
|
-
notify();
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
const stopActivity = store.activity.subscribe((changed) => {
|
|
711
|
-
if (changed === id) notify();
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
return () => {
|
|
715
|
-
stopRefresh();
|
|
716
|
-
stopDocument();
|
|
717
|
-
stopActivity();
|
|
718
|
-
};
|
|
719
|
-
},
|
|
720
|
-
);
|
|
721
|
-
});
|
|
722
694
|
|
|
723
695
|
if (data) {
|
|
724
696
|
const traceQuery = readQuerySchemas.maps.extend({
|
|
@@ -768,12 +740,6 @@ export function createReviewApi(
|
|
|
768
740
|
return context.json(result);
|
|
769
741
|
});
|
|
770
742
|
app.post("/:id/navigator", reviewAccess("edit"), async (context) => {
|
|
771
|
-
if (remoteCaller(context))
|
|
772
|
-
throw new ReviewInputError(
|
|
773
|
-
"Source windows are not available for a review on another machine.",
|
|
774
|
-
409,
|
|
775
|
-
);
|
|
776
|
-
|
|
777
743
|
const input = readQuerySchemas.file
|
|
778
744
|
.extend({
|
|
779
745
|
side: z.enum(["base", "head"]).default("head"),
|
|
@@ -782,16 +748,24 @@ export function createReviewApi(
|
|
|
782
748
|
})
|
|
783
749
|
.parse(context.req.query());
|
|
784
750
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
},
|
|
793
|
-
),
|
|
751
|
+
const answer = await data.navigatorWorkspace(
|
|
752
|
+
readReview(context.req.param("id"), input.version),
|
|
753
|
+
{
|
|
754
|
+
...input,
|
|
755
|
+
empty: input.empty === "true",
|
|
756
|
+
anchor: queryAnchor(input),
|
|
757
|
+
},
|
|
794
758
|
);
|
|
759
|
+
|
|
760
|
+
if (context.req.header(REVIEW_CLIENT_HEADER) !== REVIEW_CLIENT_REMOTE)
|
|
761
|
+
return context.json(answer);
|
|
762
|
+
|
|
763
|
+
return context.json({
|
|
764
|
+
...(input.empty === "true"
|
|
765
|
+
? { workspacePath: answer.workspacePath, emptySide: true }
|
|
766
|
+
: answer),
|
|
767
|
+
serverId: store.serverId(),
|
|
768
|
+
} satisfies ReviewRemoteNavigatorAnswer);
|
|
795
769
|
});
|
|
796
770
|
app.get("/:id/tree", async (context) => {
|
|
797
771
|
const input = readQuerySchemas.tree.parse(context.req.query());
|
|
@@ -884,21 +858,17 @@ export function createReviewApi(
|
|
|
884
858
|
queryAnchor(input),
|
|
885
859
|
);
|
|
886
860
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}),
|
|
899
|
-
}
|
|
900
|
-
: environment,
|
|
901
|
-
);
|
|
861
|
+
if (context.req.header(REVIEW_CLIENT_HEADER) !== REVIEW_CLIENT_REMOTE)
|
|
862
|
+
return context.json(environment);
|
|
863
|
+
|
|
864
|
+
// The laptop opens a remote root on this server, so it needs the server id.
|
|
865
|
+
const { rootPath, ...rest } = environment;
|
|
866
|
+
|
|
867
|
+
return context.json({
|
|
868
|
+
...rest,
|
|
869
|
+
remoteRootPath: rootPath ?? null,
|
|
870
|
+
serverId: store.serverId(),
|
|
871
|
+
} satisfies ReviewRemoteLanguageEnvironment);
|
|
902
872
|
});
|
|
903
873
|
app.post("/:id/environment", reviewAccess("active"), async (context) => {
|
|
904
874
|
const input = z
|
|
@@ -972,7 +942,6 @@ export function createReviewApi(
|
|
|
972
942
|
}
|
|
973
943
|
|
|
974
944
|
const local =
|
|
975
|
-
!remoteCaller(context) &&
|
|
976
945
|
!input.commit &&
|
|
977
946
|
!anchor &&
|
|
978
947
|
input.side === "head" &&
|
|
@@ -1013,7 +982,7 @@ export function createReviewApi(
|
|
|
1013
982
|
} catch (error) {
|
|
1014
983
|
send({
|
|
1015
984
|
type: "error",
|
|
1016
|
-
message:
|
|
985
|
+
message: errorMessage(error),
|
|
1017
986
|
});
|
|
1018
987
|
} finally {
|
|
1019
988
|
if (!abort.signal.aborted) controller.close();
|
|
@@ -1131,7 +1100,9 @@ export function createReviewApi(
|
|
|
1131
1100
|
);
|
|
1132
1101
|
|
|
1133
1102
|
return [
|
|
1134
|
-
|
|
1103
|
+
target.kind === "review"
|
|
1104
|
+
? `Whiteboard: ${snapshot.title}`
|
|
1105
|
+
: `Selected ${target.kind === "text" ? "text" : "code"} from Whiteboard: ${snapshot.title}`,
|
|
1135
1106
|
`Session ID: ${snapshot.reviewId}`,
|
|
1136
1107
|
`Version: ${snapshot.version}`,
|
|
1137
1108
|
...(selection.apiSource?.commit
|
|
@@ -1171,20 +1142,80 @@ export function createReviewApi(
|
|
|
1171
1142
|
return thread;
|
|
1172
1143
|
};
|
|
1173
1144
|
|
|
1145
|
+
/** Where a thread in a review saves itself: its Ask history, recorded
|
|
1146
|
+
* once the agent has a session to reopen. */
|
|
1147
|
+
const historyHooks = (
|
|
1148
|
+
record: Omit<AskRecord, "sessionId">,
|
|
1149
|
+
): AskThreadHooks => ({
|
|
1150
|
+
onSession: (sessionId) =>
|
|
1151
|
+
store.askHistory.get(record.id)
|
|
1152
|
+
? store.askHistory.updateSession(record.id, sessionId)
|
|
1153
|
+
: store.askHistory.save({ ...record, sessionId }),
|
|
1154
|
+
onTurn: () => store.askHistory.touch(record.id),
|
|
1155
|
+
onSave: (entries) => store.askHistory.saveEntries(record.id, entries),
|
|
1156
|
+
onPermissions: (permissions) =>
|
|
1157
|
+
store.askHistory.setPermissions(record.id, permissions),
|
|
1158
|
+
onOffer: (offer) => store.askHistory.saveOffer(record.agent, offer),
|
|
1159
|
+
onTitle: (title) => store.askHistory.rename(record.id, title),
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
/** Threads started for a review before its canvas opened, for it to
|
|
1163
|
+
* show once it does. */
|
|
1164
|
+
const handedOff = new Map<string, string[]>();
|
|
1165
|
+
|
|
1166
|
+
const handOff = (reviewId: string, threadId: string) =>
|
|
1167
|
+
handedOff.set(reviewId, [...(handedOff.get(reviewId) ?? []), threadId]);
|
|
1168
|
+
|
|
1169
|
+
// The conversations started for this review since its canvas last asked,
|
|
1170
|
+
// to open in its Ask panel.
|
|
1171
|
+
app.post("/:id/ask/handoff", (context) => {
|
|
1172
|
+
const reviewId = context.req.param("id");
|
|
1173
|
+
const ids = handedOff.get(reviewId) ?? [];
|
|
1174
|
+
|
|
1175
|
+
handedOff.delete(reviewId);
|
|
1176
|
+
|
|
1177
|
+
return context.json({
|
|
1178
|
+
threads: ids.flatMap((id) => {
|
|
1179
|
+
const record = store.askHistory.get(id);
|
|
1180
|
+
|
|
1181
|
+
if (record)
|
|
1182
|
+
return [{ id, agent: record.agent, selection: record.selection }];
|
|
1183
|
+
|
|
1184
|
+
// Its agent has no session to save yet, but the thread is live.
|
|
1185
|
+
const live = ask.threads.get(id);
|
|
1186
|
+
|
|
1187
|
+
if (live?.reviewId !== reviewId) return [];
|
|
1188
|
+
const state = live.read();
|
|
1189
|
+
|
|
1190
|
+
return [
|
|
1191
|
+
{
|
|
1192
|
+
id,
|
|
1193
|
+
agent: state.agent,
|
|
1194
|
+
selection: {
|
|
1195
|
+
target: { kind: "review" as const },
|
|
1196
|
+
title: state.selection.title,
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
];
|
|
1200
|
+
}),
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1174
1204
|
/** What an agent reads before a session's first question: the review,
|
|
1175
1205
|
* the checkout, and the selection. */
|
|
1176
1206
|
const askContext = async (
|
|
1207
|
+
threadId: string,
|
|
1177
1208
|
agent: AskAgentId,
|
|
1178
1209
|
snapshot: Snapshot,
|
|
1179
1210
|
checkout: { head: string; live: boolean },
|
|
1180
1211
|
selection: AgentSelection,
|
|
1181
1212
|
version: number | undefined,
|
|
1182
1213
|
) => {
|
|
1183
|
-
const reach = await ask.threads.reach(agent);
|
|
1214
|
+
const reach = await ask.threads.reach(agent, threadId);
|
|
1184
1215
|
const { reviewId } = snapshot;
|
|
1185
1216
|
|
|
1186
1217
|
return [
|
|
1187
|
-
`A reviewer is reading "${snapshot.title}" in Whiteboard and has a question about a selection.`,
|
|
1218
|
+
`A reviewer is reading "${snapshot.title}" in Whiteboard and has a question about ${selection.target.kind === "review" ? "it" : "a selection"}.`,
|
|
1188
1219
|
checkout.live
|
|
1189
1220
|
? "Your working directory is the repository the review describes."
|
|
1190
1221
|
: `Your working directory is a checkout of the review's head commit, ${checkout.head}. Answer from this code, not from other branches.`,
|
|
@@ -1204,6 +1235,13 @@ export function createReviewApi(
|
|
|
1204
1235
|
].join("\n");
|
|
1205
1236
|
};
|
|
1206
1237
|
|
|
1238
|
+
registerOnboardingRoutes(app, store, ask, {
|
|
1239
|
+
register: (root) => data.register(root),
|
|
1240
|
+
historyHooks,
|
|
1241
|
+
handOff,
|
|
1242
|
+
open: openCreated,
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1207
1245
|
// The agents answering in any review, which quitting the app would stop.
|
|
1208
1246
|
app.get("/ask/working", (context) =>
|
|
1209
1247
|
context.json({ agents: ask.threads.working() }),
|
|
@@ -1291,7 +1329,11 @@ export function createReviewApi(
|
|
|
1291
1329
|
const id = crypto.randomUUID();
|
|
1292
1330
|
const createdAt = new Date().toISOString();
|
|
1293
1331
|
|
|
1332
|
+
const permissions =
|
|
1333
|
+
input.permissions ?? defaultAskPermissions(input.agent);
|
|
1334
|
+
|
|
1294
1335
|
const initialContext = await askContext(
|
|
1336
|
+
id,
|
|
1295
1337
|
input.agent,
|
|
1296
1338
|
snapshot,
|
|
1297
1339
|
checkout,
|
|
@@ -1307,30 +1349,21 @@ export function createReviewApi(
|
|
|
1307
1349
|
agent: input.agent,
|
|
1308
1350
|
// Saved once the agent has a session to reopen; a later one
|
|
1309
1351
|
// replaces a session the agent could not reopen.
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
createdAt,
|
|
1324
|
-
updatedAt: createdAt,
|
|
1325
|
-
bypass: input.bypass,
|
|
1326
|
-
}),
|
|
1327
|
-
onTurn: () => store.askHistory.touch(id),
|
|
1328
|
-
onSave: (entries) => store.askHistory.saveEntries(id, entries),
|
|
1352
|
+
...historyHooks({
|
|
1353
|
+
id,
|
|
1354
|
+
reviewId,
|
|
1355
|
+
agent: input.agent,
|
|
1356
|
+
version: snapshot.version,
|
|
1357
|
+
head: checkout.head,
|
|
1358
|
+
cwd: checkout.rootPath,
|
|
1359
|
+
selection: input.selection,
|
|
1360
|
+
title: input.question.text.slice(0, 200),
|
|
1361
|
+
createdAt,
|
|
1362
|
+
updatedAt: createdAt,
|
|
1363
|
+
permissions,
|
|
1364
|
+
}),
|
|
1329
1365
|
picks: input.picks,
|
|
1330
|
-
|
|
1331
|
-
onBypass: (bypass) => store.askHistory.setBypass(id, bypass),
|
|
1332
|
-
onOffer: (offer) => store.askHistory.saveOffer(input.agent, offer),
|
|
1333
|
-
onTitle: (title) => store.askHistory.rename(id, title),
|
|
1366
|
+
permissions,
|
|
1334
1367
|
cwd: checkout.rootPath,
|
|
1335
1368
|
head: checkout.head,
|
|
1336
1369
|
selection: {
|
|
@@ -1391,7 +1424,7 @@ export function createReviewApi(
|
|
|
1391
1424
|
agentName: askAgents[record.agent].name,
|
|
1392
1425
|
status: "idle",
|
|
1393
1426
|
readOnly: true,
|
|
1394
|
-
|
|
1427
|
+
permissions: record.permissions ?? "read-only",
|
|
1395
1428
|
head: record.head,
|
|
1396
1429
|
cwd: record.cwd,
|
|
1397
1430
|
title: record.title,
|
|
@@ -1428,6 +1461,7 @@ export function createReviewApi(
|
|
|
1428
1461
|
const target = record.selection.target;
|
|
1429
1462
|
|
|
1430
1463
|
const initialContext = await askContext(
|
|
1464
|
+
record.id,
|
|
1431
1465
|
record.agent,
|
|
1432
1466
|
snapshot,
|
|
1433
1467
|
checkout,
|
|
@@ -1449,15 +1483,9 @@ export function createReviewApi(
|
|
|
1449
1483
|
resume: { sessionId: record.sessionId, entries: record.entries },
|
|
1450
1484
|
// For a new session, should the agent no longer have this one.
|
|
1451
1485
|
context: initialContext,
|
|
1452
|
-
|
|
1453
|
-
store.askHistory.updateSession(record.id, sessionId),
|
|
1454
|
-
onTurn: () => store.askHistory.touch(record.id),
|
|
1455
|
-
onSave: (entries) => store.askHistory.saveEntries(record.id, entries),
|
|
1486
|
+
...historyHooks(record),
|
|
1456
1487
|
picks,
|
|
1457
|
-
|
|
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),
|
|
1488
|
+
permissions: record.permissions,
|
|
1461
1489
|
});
|
|
1462
1490
|
|
|
1463
1491
|
return context.json({ threadId: record.id });
|
|
@@ -1575,7 +1603,7 @@ export function createReviewApi(
|
|
|
1575
1603
|
},
|
|
1576
1604
|
);
|
|
1577
1605
|
|
|
1578
|
-
//
|
|
1606
|
+
// Changes what the agent may do from the next answer.
|
|
1579
1607
|
app.post(
|
|
1580
1608
|
"/:id/ask/:threadId/permissions",
|
|
1581
1609
|
reviewAccess("edit"),
|
|
@@ -1585,7 +1613,7 @@ export function createReviewApi(
|
|
|
1585
1613
|
context.req.param("threadId"),
|
|
1586
1614
|
);
|
|
1587
1615
|
|
|
1588
|
-
const {
|
|
1616
|
+
const { permissions } = askPermitSchema.parse(
|
|
1589
1617
|
await readBoundedRequestJson(context.req.raw),
|
|
1590
1618
|
);
|
|
1591
1619
|
|
|
@@ -1596,7 +1624,7 @@ export function createReviewApi(
|
|
|
1596
1624
|
);
|
|
1597
1625
|
|
|
1598
1626
|
try {
|
|
1599
|
-
await thread.permit(
|
|
1627
|
+
await thread.permit(permissions);
|
|
1600
1628
|
} catch (error) {
|
|
1601
1629
|
throw new ReviewInputError(errorMessage(error), 409);
|
|
1602
1630
|
}
|
|
@@ -1670,7 +1698,13 @@ export function createReviewApi(
|
|
|
1670
1698
|
const snapshot = readReview(id, query.version);
|
|
1671
1699
|
|
|
1672
1700
|
if (isShared(id) || !snapshot.pins) return context.json({ layers: [] });
|
|
1673
|
-
|
|
1701
|
+
|
|
1702
|
+
const layers = await resolveReviewStackLayers(snapshot, store.list(), {
|
|
1703
|
+
repositoryPath: store
|
|
1704
|
+
.repositories()
|
|
1705
|
+
.find((repository) => repository.id === snapshot.pins!.repositoryId)
|
|
1706
|
+
?.path,
|
|
1707
|
+
});
|
|
1674
1708
|
|
|
1675
1709
|
return context.json({ layers });
|
|
1676
1710
|
});
|
|
@@ -1962,35 +1996,23 @@ async function locateRepositories(
|
|
|
1962
1996
|
};
|
|
1963
1997
|
}
|
|
1964
1998
|
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
read: () => T,
|
|
1999
|
+
function watch(
|
|
2000
|
+
lines: () => string[],
|
|
1968
2001
|
subscribe: (notify: () => void) => () => void,
|
|
1969
|
-
probe: () => void = read,
|
|
1970
2002
|
) {
|
|
1971
|
-
probe(); // Return a normal 404 before opening the response.
|
|
1972
2003
|
let stop = () => {};
|
|
1973
2004
|
|
|
1974
|
-
let dirty = true;
|
|
1975
2005
|
const encoder = new TextEncoder();
|
|
1976
2006
|
|
|
1977
2007
|
const send = (controller: ReadableStreamDefaultController<Uint8Array>) => {
|
|
1978
|
-
if (
|
|
1979
|
-
!dirty ||
|
|
1980
|
-
controller.desiredSize === null ||
|
|
1981
|
-
controller.desiredSize <= 0
|
|
1982
|
-
)
|
|
1983
|
-
return;
|
|
2008
|
+
if (controller.desiredSize === null || controller.desiredSize <= 0) return;
|
|
1984
2009
|
|
|
1985
2010
|
try {
|
|
1986
|
-
const
|
|
2011
|
+
const pending = lines();
|
|
1987
2012
|
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
controller.enqueue(encoder.encode(line));
|
|
2013
|
+
if (pending.length)
|
|
2014
|
+
controller.enqueue(encoder.encode(pending.join("\n") + "\n"));
|
|
1991
2015
|
} catch (error) {
|
|
1992
|
-
// A review can be deleted while this stream is open. Do not throw into
|
|
1993
|
-
// the already-committed writer; close this reader and unsubscribe it.
|
|
1994
2016
|
stop();
|
|
1995
2017
|
controller.error(error);
|
|
1996
2018
|
}
|
|
@@ -1998,9 +2020,15 @@ function watch<T>(
|
|
|
1998
2020
|
|
|
1999
2021
|
const body = new ReadableStream<Uint8Array>({
|
|
2000
2022
|
start(controller) {
|
|
2023
|
+
let scheduled = false;
|
|
2024
|
+
|
|
2001
2025
|
stop = subscribe(() => {
|
|
2002
|
-
|
|
2003
|
-
|
|
2026
|
+
if (scheduled) return;
|
|
2027
|
+
scheduled = true;
|
|
2028
|
+
queueMicrotask(() => {
|
|
2029
|
+
scheduled = false;
|
|
2030
|
+
send(controller);
|
|
2031
|
+
});
|
|
2004
2032
|
});
|
|
2005
2033
|
send(controller);
|
|
2006
2034
|
},
|