@dev.fast/whiteboard 0.3.4-preview.20261019.127 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-alias-B5-HYi1D.js → account-alias-CprZojXd.js} +8 -13
- package/dist/agent-cli-C6SPSII0.js +2 -0
- package/dist/{agent-cli-CohHf6Ua.js → agent-cli-CThN7Q59.js} +10 -15
- package/dist/{agent-client-Dtc-Kn_w.js → agent-client-BOOi7EUR.js} +3 -13
- package/dist/{authoring-tools-B7KU04f7.js → authoring-tools-BNwuMnz6.js} +27 -34
- package/dist/{background-server-C03RKxKS.js → background-server-CkvxA-6O.js} +5 -5
- package/dist/{background-server-CB9C4uoc.js → background-server-DLSDogkI.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-CJaEb7zC.js → cli-R4yfa5DT.js} +1 -1
- package/dist/{cli-runner-DGDIqh3V.js → cli-runner-Cuc9FSzu.js} +26 -26
- package/dist/cli.js +3 -4
- package/dist/{client-lKVF_Elo.js → client-BN_FDxFr.js} +47 -253
- package/dist/{fuzzy-match-BmVJJm9I.js → fuzzy-match-BO9bK37N.js} +4 -6
- package/dist/{headless-host-DRtjf9WA.js → headless-host-CVBOQyRS.js} +1 -1
- package/dist/{headless-host-XhL1X4vX.js → headless-host-Cog6SpS9.js} +140 -513
- package/dist/{local-data-Cpl4ENbJ.js → local-data-D_9tvdsp.js} +323 -2419
- package/dist/{mcp-ovmiUE_x.js → mcp-D1hziANa.js} +9 -5
- package/dist/{package-paths-BWxO-UEL.js → package-paths-C--__jEv.js} +1 -1
- package/dist/profile-CYPmvBoA.js +2 -0
- package/dist/{profile-BadSkCkI.js → profile-DokFw782.js} +8 -20
- package/dist/{remote-attach-Dd-YobM6.js → remote-attach-CEJ-kvDY.js} +4 -4
- package/dist/{remote-extensions-CNXwqK0D.js → remote-extensions-Blcjr9Ze.js} +3 -3
- package/dist/{remote-extensions-VP-kfq0C.js → remote-extensions-GN8Gf_qG.js} +1 -1
- package/dist/{remote-language-server-BXk87KCr.js → remote-language-server-BTKBQ7x4.js} +5 -5
- package/dist/{remote-uninstall-DuOzsvaD.js → remote-uninstall-DW7Gdryh.js} +6 -6
- package/dist/{request-origin-B9qCPiVB.js → request-origin-Zcn_CjX4.js} +12 -45
- package/dist/{review-app-launcher-C7el2Uoy.js → review-app-launcher-CkwLclzR.js} +2 -2
- package/dist/{review-home-paths-D8AoCdd2.js → review-home-paths-j2BRAaNv.js} +17 -19
- package/dist/{review-telemetry-BsoRMC2w.js → review-telemetry-BLNwYnLV.js} +5 -5
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-CsG2EWED.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-CjcLyFV5.js → s3-config-BTO12tCC.js} +2 -4
- package/dist/{s3-config-zz4GWh2T.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +76 -29
- package/dist/{server-discovery-CKjw3-6h.js → server-discovery-Bey8IqXH.js} +3 -3
- package/dist/sharing/index.d.ts +70 -484
- package/dist/sharing/index.js +1 -1
- package/dist/{src-WPaQn75m.js → src-BMPuxWv4.js} +73 -208
- package/dist/{src-BiWNbcF_.js → src-DsM3jO8G.js} +12 -89
- package/dist/{stored-document-migration-dFbewRTG.js → stored-document-migration-CChxtFAy.js} +269 -270
- package/dist/{thread-state-Do5LX10I.js → thread-state-5pYr6_zM.js} +2 -7
- package/dist/{tool-failure-DvI-gys2.js → tool-failure-C9MpbXJJ.js} +3 -10
- package/instructions/authoring.md +2 -3
- package/instructions/scratchpad.md +23 -0
- package/package.json +4 -4
- package/src/ask/agents.ts +1 -13
- package/src/ask/checkout-files.ts +5 -2
- package/src/ask/thread-state.ts +0 -4
- package/src/ask/thread.ts +0 -16
- package/src/ask/threads.ts +3 -24
- package/src/cli-install.ts +2 -13
- package/src/cli-runner.ts +1 -4
- package/src/cli.ts +1 -4
- package/src/diff-selection-migration.ts +34 -48
- package/src/lens-selection.ts +2 -38
- package/src/review-api/README.md +13 -52
- package/src/review-api/activity.ts +4 -1
- package/src/review-api/agent-cli.ts +1 -9
- package/src/review-api/anchor-quotes.ts +7 -14
- package/src/review-api/ask-history.ts +51 -137
- package/src/review-api/authoring-tools.ts +30 -45
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/document-headings.ts +27 -58
- package/src/review-api/document-text.ts +2 -31
- package/src/review-api/document.ts +34 -281
- package/src/review-api/github-provider.ts +13 -51
- package/src/review-api/http.ts +123 -702
- package/src/review-api/instructions.ts +19 -0
- package/src/review-api/local-data.ts +46 -310
- package/src/review-api/mcp.ts +11 -3
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +6 -37
- package/src/review-api/public-tools.ts +2 -24
- package/src/review-api/pull-request.ts +49 -82
- package/src/review-api/request-origin.ts +0 -10
- package/src/review-api/review-progress.ts +27 -61
- package/src/review-api/store-schema.ts +29 -38
- package/src/review-api/store.ts +478 -2142
- package/src/review-api/worktree-events.ts +5 -3
- package/src/review-api/worktree-source.ts +7 -3
- package/src/review-api/worktree-structural.integration.ts +0 -72
- package/src/review-preferences.ts +88 -0
- package/src/review-stack.ts +23 -15
- package/src/runtime.ts +30 -4
- package/src/server/desktop-host.ts +1 -6
- package/src/server/desktop-server.ts +31 -1
- package/src/server/headless-host.ts +2 -0
- package/src/server/hono-http.ts +1 -2
- package/src/server/legacy-tutorial-cleanup.ts +3 -2
- package/src/server/review-gateway-list.ts +5 -3
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +5 -1
- package/src/server/structural-comparisons.ts +0 -47
- package/src/sharing/export.ts +7 -36
- package/src/sharing/host.ts +14 -34
- package/src/sharing/import.ts +24 -156
- package/src/sharing/repository.ts +20 -128
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +19 -110
- package/src/ui-telemetry-events.ts +11 -33
- package/dist/agent-cli-_YyMIMRY.js +0 -2
- package/dist/lens-selection-CyJapcsl.js +0 -214
- package/dist/profile-DJvkBJNQ.js +0 -2
- package/dist/runtime-Bj2tD33R.js +0 -27
- package/src/heading-anchors.ts +0 -42
- package/src/review-api/board-order.ts +0 -144
- package/src/review-api/checkout-groups.ts +0 -560
- package/src/review-api/foreign-references.ts +0 -328
- package/src/review-api/link-preview.ts +0 -113
- package/src/review-api/scratchpad-plan-upgrade.ts +0 -250
- package/src/review-api/store-migrations.ts +0 -357
|
@@ -9,6 +9,7 @@ import { COMPONENT_REFERENCE } from "./component-reference.js";
|
|
|
9
9
|
export const INSTRUCTION_TOPICS = [
|
|
10
10
|
"authoring",
|
|
11
11
|
"file-lenses",
|
|
12
|
+
"scratchpad",
|
|
12
13
|
"trace-archaeology",
|
|
13
14
|
] as const;
|
|
14
15
|
|
|
@@ -19,9 +20,15 @@ export const instructionsQuerySchema = z.strictObject({
|
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
export interface InstructionContext {
|
|
23
|
+
desktopAvailable: boolean;
|
|
24
|
+
scratchpadEnabled: boolean;
|
|
22
25
|
traceEnabled: boolean;
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
export function scratchpadAvailable(context: InstructionContext): boolean {
|
|
29
|
+
return context.desktopAvailable && context.scratchpadEnabled;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
const cache = new Map<string, Promise<string>>();
|
|
26
33
|
|
|
27
34
|
function read(root: string, name: string): Promise<string> {
|
|
@@ -44,12 +51,24 @@ export async function renderInstructions(
|
|
|
44
51
|
context: InstructionContext,
|
|
45
52
|
root = findReviewPackageRoot(import.meta.url),
|
|
46
53
|
): Promise<string> {
|
|
54
|
+
if (topic === "scratchpad" && !scratchpadAvailable(context)) {
|
|
55
|
+
return "The Whiteboard scratchpad is turned off or Whiteboard Desktop is not running. Answer in chat; the scratchpad can be turned on in Whiteboard Desktop Settings.";
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
if (topic === "trace-archaeology" && !context.traceEnabled)
|
|
48
59
|
return "Trace capture is off on this machine, so no agent traces are available. It can be turned on in Whiteboard Desktop Settings under Experimental Features.";
|
|
49
60
|
|
|
61
|
+
if (topic === "scratchpad")
|
|
62
|
+
return `${await read(root, topic)}\n\n${COMPONENT_REFERENCE}`;
|
|
63
|
+
|
|
50
64
|
if (topic !== "authoring") return read(root, topic);
|
|
51
65
|
|
|
52
66
|
const more = [
|
|
67
|
+
...(scratchpadAvailable(context)
|
|
68
|
+
? [
|
|
69
|
+
'- Explaining code visually outside a review: `session_get_instructions({topic:"scratchpad"})`',
|
|
70
|
+
]
|
|
71
|
+
: []),
|
|
53
72
|
...(context.traceEnabled
|
|
54
73
|
? [
|
|
55
74
|
'- Why code exists / past agent sessions: `session_get_instructions({topic:"trace-archaeology"})`',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { existsSync, statSync } from "node:fs";
|
|
2
3
|
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
type BlobBatchReader,
|
|
@@ -9,7 +11,6 @@ import {
|
|
|
9
11
|
type LocalVcsDiffFileSummary,
|
|
10
12
|
type LocalVcsKind,
|
|
11
13
|
LocalVcsToolsMissingError,
|
|
12
|
-
type VcsArg,
|
|
13
14
|
createBlobBatchReader,
|
|
14
15
|
detectLocalVcs,
|
|
15
16
|
diffFileSummariesTrees,
|
|
@@ -21,11 +22,9 @@ import {
|
|
|
21
22
|
listTrackedFilesAtCommit,
|
|
22
23
|
readFileAtCommit,
|
|
23
24
|
resolveRepoContext,
|
|
24
|
-
runVcs,
|
|
25
25
|
} from "@dev.fast/local-vcs";
|
|
26
|
-
import {
|
|
26
|
+
import { structuralChangeCounts } from "@dev.fast/review-protocol";
|
|
27
27
|
import type {
|
|
28
|
-
PullRequestState,
|
|
29
28
|
ReviewLanguageEnvironment,
|
|
30
29
|
ReviewSourceEntry,
|
|
31
30
|
StructuralDiffEvent,
|
|
@@ -69,7 +68,6 @@ import {
|
|
|
69
68
|
type ReviewTarget,
|
|
70
69
|
type SourcePins,
|
|
71
70
|
anchorPins,
|
|
72
|
-
boardRepositoryId,
|
|
73
71
|
elements,
|
|
74
72
|
fileLineRangeSchema,
|
|
75
73
|
pinnedRepositoryIds,
|
|
@@ -84,7 +82,6 @@ import {
|
|
|
84
82
|
resolvePullRequest,
|
|
85
83
|
} from "./provider.js";
|
|
86
84
|
import {
|
|
87
|
-
GitHubRefusedRead,
|
|
88
85
|
type PullRequestDeps,
|
|
89
86
|
defaultPullRequestDeps,
|
|
90
87
|
fetchPullRequest,
|
|
@@ -168,14 +165,7 @@ interface RepositoryVcs {
|
|
|
168
165
|
vcs?: LocalVcs;
|
|
169
166
|
}
|
|
170
167
|
|
|
171
|
-
/** How long a PR's state is shown before it is read again. */
|
|
172
|
-
const PULL_REQUEST_STATE_TTL_MS = 5 * 60_000;
|
|
173
|
-
|
|
174
|
-
/** How many PR states are read from GitHub at once. */
|
|
175
|
-
const PULL_REQUEST_STATE_READS = 2;
|
|
176
|
-
|
|
177
168
|
/** Local source/resource boundary, including Desktop-only local language context. */
|
|
178
|
-
|
|
179
169
|
export class LocalReviewData {
|
|
180
170
|
private readonly workspaceManager?: ReviewWorkspaces;
|
|
181
171
|
|
|
@@ -311,12 +301,7 @@ export class LocalReviewData {
|
|
|
311
301
|
if (commit && !anchor)
|
|
312
302
|
await this.comparison(await this.documentPins(snapshot), commit);
|
|
313
303
|
|
|
314
|
-
const repositoryId =
|
|
315
|
-
anchor?.repositoryId ??
|
|
316
|
-
snapshot.pins?.repositoryId ??
|
|
317
|
-
(snapshot.target?.kind === "project"
|
|
318
|
-
? snapshot.target.repositoryId
|
|
319
|
-
: undefined);
|
|
304
|
+
const repositoryId = anchor?.repositoryId ?? snapshot.pins?.repositoryId;
|
|
320
305
|
|
|
321
306
|
if (!repositoryId)
|
|
322
307
|
throw new ReviewInputError(
|
|
@@ -434,7 +419,7 @@ export class LocalReviewData {
|
|
|
434
419
|
);
|
|
435
420
|
|
|
436
421
|
if (!live) {
|
|
437
|
-
const { stdout } = await
|
|
422
|
+
const { stdout } = await promisify(execFile)("git", [
|
|
438
423
|
"-C",
|
|
439
424
|
rootPath,
|
|
440
425
|
"status",
|
|
@@ -487,7 +472,7 @@ export class LocalReviewData {
|
|
|
487
472
|
),
|
|
488
473
|
));
|
|
489
474
|
|
|
490
|
-
const title = `${
|
|
475
|
+
const title = `${snapshot.title} — ${live ? "Live source" : side === "base" ? "Base source" : "Source"} — Whiteboard`;
|
|
491
476
|
|
|
492
477
|
const workspace = previous ?? {
|
|
493
478
|
folders: [],
|
|
@@ -534,14 +519,9 @@ export class LocalReviewData {
|
|
|
534
519
|
return { workspacePath, filePath, rootPath };
|
|
535
520
|
}
|
|
536
521
|
|
|
537
|
-
/** A document read that needs default pins
|
|
538
|
-
* repository's live checkout with base at head; 409 when it has neither. */
|
|
522
|
+
/** A document read that needs default pins; 409 when the document has none. */
|
|
539
523
|
async documentPins(snapshot: Snapshot): Promise<Pins> {
|
|
540
|
-
const pins =
|
|
541
|
-
(await this.sourcePins(snapshot)) ??
|
|
542
|
-
(snapshot.target?.kind === "project"
|
|
543
|
-
? await this.livePins(snapshot.target.repositoryId)
|
|
544
|
-
: undefined);
|
|
524
|
+
const pins = await this.sourcePins(snapshot);
|
|
545
525
|
|
|
546
526
|
if (!pins)
|
|
547
527
|
throw new ReviewInputError(
|
|
@@ -552,21 +532,6 @@ export class LocalReviewData {
|
|
|
552
532
|
return pins;
|
|
553
533
|
}
|
|
554
534
|
|
|
555
|
-
/** A checkout's current state, base at head: what a plan reads. */
|
|
556
|
-
private async livePins(repositoryId: string): Promise<Pins> {
|
|
557
|
-
const vcs = await this.vcs(repositoryId);
|
|
558
|
-
|
|
559
|
-
if (!vcs) throw unavailableCheckout();
|
|
560
|
-
const { revision, commit } = await this.worktreeState(repositoryId, vcs);
|
|
561
|
-
|
|
562
|
-
return {
|
|
563
|
-
repositoryId,
|
|
564
|
-
base: commit,
|
|
565
|
-
head: commit,
|
|
566
|
-
worktreeRevision: revision,
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
|
|
570
535
|
/** Pins a reference names itself, checked against the registered checkout. */
|
|
571
536
|
async anchorSourcePins(anchor: SourcePins): Promise<Pins> {
|
|
572
537
|
if (!existsSync(this.store.repositoryPath(anchor.repositoryId)))
|
|
@@ -580,14 +545,8 @@ export class LocalReviewData {
|
|
|
580
545
|
async unavailableAnchors(snapshot: Snapshot): Promise<string[]> {
|
|
581
546
|
const missing = new Set<string>();
|
|
582
547
|
|
|
583
|
-
// Only Markdown needs parsing, so check the repositories first.
|
|
584
|
-
|
|
585
|
-
const repositoryIds = pinnedRepositoryIds(snapshot.document);
|
|
586
|
-
const boardRepository = boardRepositoryId(snapshot);
|
|
587
|
-
|
|
588
|
-
if (boardRepository) repositoryIds.add(boardRepository);
|
|
589
|
-
|
|
590
|
-
for (const repositoryId of repositoryIds)
|
|
548
|
+
// Only Markdown needs parsing, so check the repositories first.
|
|
549
|
+
for (const repositoryId of pinnedRepositoryIds(snapshot.document))
|
|
591
550
|
try {
|
|
592
551
|
if (!existsSync(this.store.repositoryPath(repositoryId)))
|
|
593
552
|
missing.add(repositoryId);
|
|
@@ -608,10 +567,7 @@ export class LocalReviewData {
|
|
|
608
567
|
|
|
609
568
|
private readonly storedReferences = new VersionCache(
|
|
610
569
|
(snapshot: Snapshot) =>
|
|
611
|
-
sourceReferences(snapshot.document, {
|
|
612
|
-
tolerant: true,
|
|
613
|
-
repositoryId: boardRepositoryId(snapshot),
|
|
614
|
-
}),
|
|
570
|
+
sourceReferences(snapshot.document, { tolerant: true }),
|
|
615
571
|
{ capacity: 512 },
|
|
616
572
|
);
|
|
617
573
|
|
|
@@ -636,8 +592,6 @@ export class LocalReviewData {
|
|
|
636
592
|
subscribe?: typeof subscribe;
|
|
637
593
|
/** gh, git and GitHub API access for pull request targets. */
|
|
638
594
|
pullRequests?: PullRequestDeps;
|
|
639
|
-
/** Read PR states from GitHub for the catalog. */
|
|
640
|
-
pullRequestStates?: boolean;
|
|
641
595
|
} = {},
|
|
642
596
|
) {
|
|
643
597
|
if (options.manageWorkspaces !== false)
|
|
@@ -1019,7 +973,6 @@ export class LocalReviewData {
|
|
|
1019
973
|
// that no longer exist; the author decides how to update changed source.
|
|
1020
974
|
for (const reference of sourceReferences(snapshot.document, {
|
|
1021
975
|
tolerant: true,
|
|
1022
|
-
repositoryId: boardRepositoryId(snapshot),
|
|
1023
976
|
})) {
|
|
1024
977
|
try {
|
|
1025
978
|
await this.quote(anchorPins(reference.source, pins), reference.source);
|
|
@@ -1037,13 +990,6 @@ export class LocalReviewData {
|
|
|
1037
990
|
if (snapshot.target?.kind === "worktree")
|
|
1038
991
|
return (await this.resolveTarget(snapshot.target, snapshot.pins)).pins;
|
|
1039
992
|
|
|
1040
|
-
// A plan has no pins of its own, but its checkout must exist.
|
|
1041
|
-
if (
|
|
1042
|
-
snapshot.target?.kind === "project" &&
|
|
1043
|
-
!existsSync(this.store.repositoryPath(snapshot.target.repositoryId))
|
|
1044
|
-
)
|
|
1045
|
-
throw unavailableCheckout();
|
|
1046
|
-
|
|
1047
993
|
if (!snapshot.pins) return undefined;
|
|
1048
994
|
|
|
1049
995
|
if (!existsSync(this.store.repositoryPath(snapshot.pins.repositoryId)))
|
|
@@ -1058,15 +1004,18 @@ export class LocalReviewData {
|
|
|
1058
1004
|
if (!vcs || vcs.kind !== "git") return undefined;
|
|
1059
1005
|
|
|
1060
1006
|
try {
|
|
1061
|
-
const
|
|
1007
|
+
const run = promisify(execFile);
|
|
1008
|
+
|
|
1009
|
+
const { stdout } = await run("git", [
|
|
1062
1010
|
"-C",
|
|
1063
1011
|
vcs.rootPath,
|
|
1064
1012
|
"rev-parse",
|
|
1065
1013
|
"--symbolic-full-name",
|
|
1066
1014
|
"--verify",
|
|
1015
|
+
"--end-of-options",
|
|
1067
1016
|
!headRef || /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(headRef)
|
|
1068
1017
|
? "HEAD"
|
|
1069
|
-
:
|
|
1018
|
+
: headRef,
|
|
1070
1019
|
]);
|
|
1071
1020
|
|
|
1072
1021
|
const ref = stdout.trim();
|
|
@@ -1091,9 +1040,6 @@ export class LocalReviewData {
|
|
|
1091
1040
|
target: ReviewTarget,
|
|
1092
1041
|
pinned?: Pins,
|
|
1093
1042
|
): Promise<{ target: ReviewTarget; pins: Pins }> {
|
|
1094
|
-
if (target.kind === "project")
|
|
1095
|
-
throw new ReviewInputError("A project target has no commits.");
|
|
1096
|
-
|
|
1097
1043
|
const vcs = await this.vcs(target.repositoryId);
|
|
1098
1044
|
|
|
1099
1045
|
if (!vcs) throw unavailableCheckout();
|
|
@@ -1205,147 +1151,10 @@ export class LocalReviewData {
|
|
|
1205
1151
|
|
|
1206
1152
|
return { ref: branch, commit: fork.commit };
|
|
1207
1153
|
}
|
|
1208
|
-
/** Each PR's state, by URL, as last read. */
|
|
1209
|
-
private readonly pullRequestStates = new Map<
|
|
1210
|
-
string,
|
|
1211
|
-
{
|
|
1212
|
-
state?: PullRequestState;
|
|
1213
|
-
/** The branch it merges into, and its own. */
|
|
1214
|
-
baseBranch?: string;
|
|
1215
|
-
headBranch?: string;
|
|
1216
|
-
readAt: number;
|
|
1217
|
-
/** When a board last showed it; a deleted board's PR is forgotten. */
|
|
1218
|
-
seenAt: number;
|
|
1219
|
-
reading: boolean;
|
|
1220
|
-
}
|
|
1221
|
-
>();
|
|
1222
|
-
|
|
1223
|
-
private readonly pullRequestStateListeners = new Set<() => void>();
|
|
1224
|
-
private pullRequestStateTimer: ReturnType<typeof setInterval> | undefined;
|
|
1225
|
-
private readonly pullRequestStateQueue: string[] = [];
|
|
1226
|
-
private pullRequestStateReads = 0;
|
|
1227
|
-
|
|
1228
|
-
/** While GitHub rate-limits, no state is read. */
|
|
1229
|
-
private pullRequestStatesPausedUntil = 0;
|
|
1230
|
-
|
|
1231
|
-
/** Calls back when a PR's state changes. While anyone listens, every PR
|
|
1232
|
-
* still shown is read again once its state is a few minutes old. */
|
|
1233
|
-
subscribePullRequestStates(listener: () => void): () => void {
|
|
1234
|
-
this.pullRequestStateListeners.add(listener);
|
|
1235
|
-
this.pullRequestStateTimer ??= setInterval(() => {
|
|
1236
|
-
for (const [url, entry] of this.pullRequestStates)
|
|
1237
|
-
if (Date.now() - entry.seenAt > 2 * PULL_REQUEST_STATE_TTL_MS)
|
|
1238
|
-
this.pullRequestStates.delete(url);
|
|
1239
|
-
else this.pullRequestState(url, { seen: false });
|
|
1240
|
-
this.readPullRequestStates();
|
|
1241
|
-
}, PULL_REQUEST_STATE_TTL_MS / 5);
|
|
1242
|
-
this.pullRequestStateTimer.unref?.();
|
|
1243
|
-
|
|
1244
|
-
return () => {
|
|
1245
|
-
this.pullRequestStateListeners.delete(listener);
|
|
1246
|
-
|
|
1247
|
-
if (!this.pullRequestStateListeners.size) {
|
|
1248
|
-
clearInterval(this.pullRequestStateTimer);
|
|
1249
|
-
this.pullRequestStateTimer = undefined;
|
|
1250
|
-
}
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
/** A PR's state as last read from GitHub; a read older than a few minutes
|
|
1255
|
-
* queues another in the background. Undefined until the first read lands,
|
|
1256
|
-
* and a failed read keeps the last state. A merged or closed PR is not
|
|
1257
|
-
* read again. */
|
|
1258
|
-
pullRequestState(
|
|
1259
|
-
url: string | undefined,
|
|
1260
|
-
{ seen = true }: { seen?: boolean } = {},
|
|
1261
|
-
): PullRequestState | undefined {
|
|
1262
|
-
if (!url || !this.options.pullRequestStates) return undefined;
|
|
1263
|
-
|
|
1264
|
-
const entry = this.pullRequestStates.get(url) ?? {
|
|
1265
|
-
readAt: 0,
|
|
1266
|
-
seenAt: 0,
|
|
1267
|
-
reading: false,
|
|
1268
|
-
};
|
|
1269
|
-
|
|
1270
|
-
this.pullRequestStates.set(url, entry);
|
|
1271
|
-
|
|
1272
|
-
if (seen) entry.seenAt = Date.now();
|
|
1273
|
-
|
|
1274
|
-
if (
|
|
1275
|
-
!entry.reading &&
|
|
1276
|
-
entry.state !== "merged" &&
|
|
1277
|
-
entry.state !== "closed" &&
|
|
1278
|
-
Date.now() - entry.readAt > PULL_REQUEST_STATE_TTL_MS
|
|
1279
|
-
) {
|
|
1280
|
-
entry.reading = true;
|
|
1281
|
-
this.pullRequestStateQueue.push(url);
|
|
1282
|
-
this.readPullRequestStates();
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
return entry.state;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
/** Reads queued PR states a few at a time. */
|
|
1289
|
-
private readPullRequestStates() {
|
|
1290
|
-
while (
|
|
1291
|
-
this.pullRequestStateReads < PULL_REQUEST_STATE_READS &&
|
|
1292
|
-
this.pullRequestStateQueue.length &&
|
|
1293
|
-
Date.now() >= this.pullRequestStatesPausedUntil
|
|
1294
|
-
) {
|
|
1295
|
-
const url = this.pullRequestStateQueue.shift()!;
|
|
1296
|
-
const entry = this.pullRequestStates.get(url);
|
|
1297
|
-
|
|
1298
|
-
if (!entry) continue;
|
|
1299
|
-
this.pullRequestStateReads++;
|
|
1300
|
-
void Promise.resolve()
|
|
1301
|
-
.then(() => {
|
|
1302
|
-
const { provider, address } = resolvePullRequest(url);
|
|
1303
|
-
|
|
1304
|
-
return provider.readPullRequest(
|
|
1305
|
-
address,
|
|
1306
|
-
this.options.pullRequests ?? defaultPullRequestDeps,
|
|
1307
|
-
);
|
|
1308
|
-
})
|
|
1309
|
-
.then((pullRequest) => {
|
|
1310
|
-
if (
|
|
1311
|
-
(!pullRequest.state || pullRequest.state === entry.state) &&
|
|
1312
|
-
pullRequest.baseRefName === entry.baseBranch &&
|
|
1313
|
-
pullRequest.headBranch === entry.headBranch
|
|
1314
|
-
)
|
|
1315
|
-
return;
|
|
1316
|
-
entry.state = pullRequest.state ?? entry.state;
|
|
1317
|
-
entry.baseBranch = pullRequest.baseRefName;
|
|
1318
|
-
entry.headBranch = pullRequest.headBranch;
|
|
1319
|
-
|
|
1320
|
-
for (const listener of this.pullRequestStateListeners) listener();
|
|
1321
|
-
})
|
|
1322
|
-
.catch((error) => {
|
|
1323
|
-
if (error instanceof GitHubRefusedRead)
|
|
1324
|
-
this.pullRequestStatesPausedUntil = error.retryAt;
|
|
1325
|
-
})
|
|
1326
|
-
.finally(() => {
|
|
1327
|
-
entry.readAt = Date.now();
|
|
1328
|
-
entry.reading = false;
|
|
1329
|
-
this.pullRequestStateReads--;
|
|
1330
|
-
this.readPullRequestStates();
|
|
1331
|
-
});
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
/** The branch a PR merges into, as last read with its state. */
|
|
1336
|
-
pullRequestBaseBranch(url: string): string | undefined {
|
|
1337
|
-
return this.pullRequestStates.get(url)?.baseBranch;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
/** A PR's own branch, as last read with its state. */
|
|
1341
|
-
pullRequestHeadBranch(url: string): string | undefined {
|
|
1342
|
-
return this.pullRequestStates.get(url)?.headBranch;
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
1154
|
/** The PR's current comparison, fetched into a registered checkout of its repository. */
|
|
1346
1155
|
async resolvePullRequest(
|
|
1347
1156
|
url: string,
|
|
1348
|
-
repository: { id
|
|
1157
|
+
repository: { id?: string; preferred?: string },
|
|
1349
1158
|
): Promise<ResolvedPullRequest> {
|
|
1350
1159
|
const deps = this.options.pullRequests ?? defaultPullRequestDeps;
|
|
1351
1160
|
const { provider, address } = resolvePullRequest(url);
|
|
@@ -1393,14 +1202,19 @@ export class LocalReviewData {
|
|
|
1393
1202
|
private async pullRequestCheckout(
|
|
1394
1203
|
provider: PullRequestProvider,
|
|
1395
1204
|
address: PullRequestAddress,
|
|
1396
|
-
repository: { id
|
|
1205
|
+
repository: { id?: string; preferred?: string },
|
|
1397
1206
|
deps: PullRequestDeps,
|
|
1398
1207
|
) {
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1208
|
+
const registered = this.store.repositories();
|
|
1209
|
+
|
|
1210
|
+
const candidates = repository.id
|
|
1211
|
+
? registered.filter((entry) => entry.id === repository.id)
|
|
1212
|
+
: [
|
|
1213
|
+
...registered.filter((entry) => entry.id === repository.preferred),
|
|
1214
|
+
...registered.filter((entry) => entry.id !== repository.preferred),
|
|
1215
|
+
];
|
|
1402
1216
|
|
|
1403
|
-
if (candidates.length === 0)
|
|
1217
|
+
if (repository.id && candidates.length === 0)
|
|
1404
1218
|
throw new ReviewInputError("Repository is not registered.", 404);
|
|
1405
1219
|
|
|
1406
1220
|
for (const { id } of candidates) {
|
|
@@ -1428,7 +1242,9 @@ export class LocalReviewData {
|
|
|
1428
1242
|
const name = host === "github.com" ? slug : `${host}/${slug}`;
|
|
1429
1243
|
|
|
1430
1244
|
throw new ReviewInputError(
|
|
1431
|
-
|
|
1245
|
+
repository.id
|
|
1246
|
+
? `That checkout has no remote for ${name}. Add one, or omit repositoryId.`
|
|
1247
|
+
: `No registered checkout has a remote for ${name}. Register a checkout of ${name} with review_register_repository first, or pass a target.`,
|
|
1432
1248
|
404,
|
|
1433
1249
|
);
|
|
1434
1250
|
}
|
|
@@ -1485,74 +1301,6 @@ export class LocalReviewData {
|
|
|
1485
1301
|
|
|
1486
1302
|
return { base, head };
|
|
1487
1303
|
}
|
|
1488
|
-
/** What a commit is, for a quote read at it: its subject, author and
|
|
1489
|
-
* date, and how far behind the default branch's tip it is when both are
|
|
1490
|
-
* known. */
|
|
1491
|
-
async commitInfo(
|
|
1492
|
-
repositoryId: string,
|
|
1493
|
-
commit: string,
|
|
1494
|
-
): Promise<{
|
|
1495
|
-
commit: string;
|
|
1496
|
-
subject: string;
|
|
1497
|
-
author: string;
|
|
1498
|
-
authoredAt: string;
|
|
1499
|
-
behind?: { count: number; branch: string };
|
|
1500
|
-
}> {
|
|
1501
|
-
const vcs = await this.vcs(repositoryId);
|
|
1502
|
-
|
|
1503
|
-
if (!vcs) throw unavailableCheckout();
|
|
1504
|
-
|
|
1505
|
-
const git = async (...args: VcsArg[]) =>
|
|
1506
|
-
(await runVcs("git", ["-C", vcs.rootPath, ...args])).stdout.trim();
|
|
1507
|
-
|
|
1508
|
-
let line: string;
|
|
1509
|
-
|
|
1510
|
-
try {
|
|
1511
|
-
line = await git("log", "-1", "--format=%H%x00%s%x00%an%x00%aI", {
|
|
1512
|
-
rev: commit,
|
|
1513
|
-
});
|
|
1514
|
-
} catch {
|
|
1515
|
-
throw new ReviewInputError("That commit is not in the repository.", 404);
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
const [full = commit, subject = "", author = "", authoredAt = ""] =
|
|
1519
|
-
line.split("\0");
|
|
1520
|
-
|
|
1521
|
-
const branch = await vcs.defaultBranch().catch(() => null);
|
|
1522
|
-
|
|
1523
|
-
const behind = branch
|
|
1524
|
-
? await git("rev-list", "--count", { rev: `${full}..${branch.commit}` })
|
|
1525
|
-
.then((count) => ({
|
|
1526
|
-
count: Number(count),
|
|
1527
|
-
branch: branch.ref.replace(/^origin\//, ""),
|
|
1528
|
-
}))
|
|
1529
|
-
.catch(() => undefined)
|
|
1530
|
-
: undefined;
|
|
1531
|
-
|
|
1532
|
-
return {
|
|
1533
|
-
commit: full,
|
|
1534
|
-
subject,
|
|
1535
|
-
author,
|
|
1536
|
-
authoredAt,
|
|
1537
|
-
...(behind && { behind }),
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
/** The full commit a commit ID or its prefix names in the repository,
|
|
1541
|
-
* or undefined when it names none. */
|
|
1542
|
-
async resolveCommit(
|
|
1543
|
-
repositoryId: string,
|
|
1544
|
-
commit: string,
|
|
1545
|
-
): Promise<string | undefined> {
|
|
1546
|
-
const vcs = await this.vcs(repositoryId);
|
|
1547
|
-
|
|
1548
|
-
if (!vcs) throw unavailableCheckout();
|
|
1549
|
-
const resolved = await vcs.resolveRevision(commit);
|
|
1550
|
-
|
|
1551
|
-
// A branch or tag spelled in hex is not the commit it seems.
|
|
1552
|
-
return resolved?.commit.startsWith(commit.toLowerCase())
|
|
1553
|
-
? resolved.commit
|
|
1554
|
-
: undefined;
|
|
1555
|
-
}
|
|
1556
1304
|
async validatePins(pins: Pins) {
|
|
1557
1305
|
if (pins.worktreeRevision) {
|
|
1558
1306
|
if (!(await this.vcs(pins.repositoryId))) throw unavailableCheckout();
|
|
@@ -2178,41 +1926,29 @@ export function openLocalReviewStore(
|
|
|
2178
1926
|
manageWorkspaces?: boolean;
|
|
2179
1927
|
subscribe?: typeof subscribe;
|
|
2180
1928
|
pullRequests?: PullRequestDeps;
|
|
2181
|
-
pullRequestStates?: boolean;
|
|
2182
|
-
/** From `groupCheckoutsAhead`. */
|
|
2183
|
-
checkoutOwners?: ReadonlyMap<string, string>;
|
|
2184
1929
|
} = {},
|
|
2185
1930
|
) {
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
data.resolveCommit(repositoryId, commit),
|
|
2202
|
-
filesChangedBetween: (from, to) => data.filesChangedBetween(from, to),
|
|
2203
|
-
validateSource: (pins, source, options) =>
|
|
2204
|
-
data.validateSource(pins, source, options),
|
|
2205
|
-
validateResource: (pins, block) => data.validateResource(pins, block),
|
|
2206
|
-
pullRequestBaseBranch: (url) => data.pullRequestBaseBranch(url),
|
|
2207
|
-
},
|
|
2208
|
-
checkoutOwners,
|
|
2209
|
-
);
|
|
1931
|
+
const store: ReviewStore = new ReviewStore(databasePath, {
|
|
1932
|
+
projectSource: (snapshot, pins) => data.projectSource(snapshot, pins),
|
|
1933
|
+
resolveTarget: (target, pinned) => data.resolveTarget(target, pinned),
|
|
1934
|
+
resolvePullRequest: (url, repository) =>
|
|
1935
|
+
data.resolvePullRequest(url, repository),
|
|
1936
|
+
pullRequestTitle: (url) => data.pullRequestTitle(url),
|
|
1937
|
+
headBranch: (pins, headRef) => data.headBranch(pins, headRef),
|
|
1938
|
+
sourcePins: (snapshot) => data.sourcePins(snapshot),
|
|
1939
|
+
unavailableAnchors: (snapshot) => data.unavailableAnchors(snapshot),
|
|
1940
|
+
validatePins: (pins) => data.validatePins(pins),
|
|
1941
|
+
filesChangedBetween: (from, to) => data.filesChangedBetween(from, to),
|
|
1942
|
+
validateSource: (pins, source, options) =>
|
|
1943
|
+
data.validateSource(pins, source, options),
|
|
1944
|
+
validateResource: (pins, block) => data.validateResource(pins, block),
|
|
1945
|
+
});
|
|
2210
1946
|
|
|
2211
1947
|
let data: LocalReviewData;
|
|
2212
1948
|
|
|
2213
1949
|
try {
|
|
2214
1950
|
data = new LocalReviewData(store, {
|
|
2215
|
-
...
|
|
1951
|
+
...options,
|
|
2216
1952
|
workspaceDatabase: `${databasePath}.workspaces`,
|
|
2217
1953
|
});
|
|
2218
1954
|
} catch (error) {
|
package/src/review-api/mcp.ts
CHANGED
|
@@ -29,9 +29,17 @@ import { type ToolFailure, toolFailure } from "./tool-failure.js";
|
|
|
29
29
|
const RELOAD_TOOLS =
|
|
30
30
|
"Whiteboard is running now, but this session listed Whiteboard's tools before it started, so you may see only session_get_instructions. Before authoring, reload the `whiteboard` MCP server's tools (reconnect it in your agent, or start a new agent session). If you can already see tools such as session_create, carry on.";
|
|
31
31
|
|
|
32
|
-
function mcpAuthoringGuidance(context: {
|
|
32
|
+
function mcpAuthoringGuidance(context: {
|
|
33
|
+
scratchpadAvailable: boolean;
|
|
34
|
+
traceEnabled: boolean;
|
|
35
|
+
}): string {
|
|
33
36
|
return [
|
|
34
37
|
"Whiteboard explains code in documents the user reads in Whiteboard Desktop. Call session_get_instructions before creating or editing a Whiteboard and follow it. Read session_capabilities before authoring; session_create opens the new review in Desktop when it is available, so call session_open only for an existing review, and generate software maps only when softwareMapEnabled is true. When the user asks for a Whiteboard or to use Whiteboard (for example to review a branch, a change or a pull request, or to explain a system in Whiteboard), author a Whiteboard with the default topic.",
|
|
38
|
+
...(context.scratchpadAvailable
|
|
39
|
+
? [
|
|
40
|
+
'When the user asks in conversation to be shown how code works or wants a diagram, without asking for a Whiteboard, call session_capabilities; if it reports scratchpadEnabled and desktopAvailable, draw on the Whiteboard scratchpad rather than answering only in chat, starting with session_get_instructions({topic:"scratchpad"}).',
|
|
41
|
+
]
|
|
42
|
+
: []),
|
|
35
43
|
...(context.traceEnabled
|
|
36
44
|
? [
|
|
37
45
|
'For why code exists, what an agent was thinking, or whether an agent solved something before, call session_get_instructions({topic:"trace-archaeology"}).',
|
|
@@ -58,7 +66,7 @@ export async function serveReviewMcp(
|
|
|
58
66
|
environmentAgent?: ReviewSessionAgent,
|
|
59
67
|
) {
|
|
60
68
|
const instructionsTool = {
|
|
61
|
-
...authoringTools(traceEnabled).find(
|
|
69
|
+
...authoringTools(false, traceEnabled).find(
|
|
62
70
|
(tool) => tool.name === "review_get_instructions",
|
|
63
71
|
)!,
|
|
64
72
|
name: "session_get_instructions",
|
|
@@ -127,7 +135,7 @@ export async function serveReviewMcp(
|
|
|
127
135
|
// guidance on the discovery tool instead, including before Desktop starts.
|
|
128
136
|
description:
|
|
129
137
|
name === instructionsTool.name
|
|
130
|
-
? `${mcpAuthoringGuidance({ traceEnabled })}\n\n${description}`
|
|
138
|
+
? `${mcpAuthoringGuidance({ scratchpadAvailable: true, traceEnabled })}\n\n${description}`
|
|
131
139
|
: description,
|
|
132
140
|
inputSchema,
|
|
133
141
|
}),
|