@dev.fast/whiteboard 0.3.4-preview.20261019.126 → 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
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { isAbsolute, relative } from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
const exec = promisify(execFile);
|
|
4
6
|
|
|
5
7
|
const normalize = (name: string) => name.replaceAll("\\", "/").normalize("NFC");
|
|
6
8
|
|
|
@@ -51,7 +53,7 @@ export async function ignoredDirectories(
|
|
|
51
53
|
gitDirectory?: string,
|
|
52
54
|
): Promise<string[]> {
|
|
53
55
|
try {
|
|
54
|
-
const { stdout } = await
|
|
56
|
+
const { stdout } = await exec(
|
|
55
57
|
"git",
|
|
56
58
|
[
|
|
57
59
|
"-C",
|
|
@@ -103,7 +105,7 @@ export async function gitDirectoryPrefix(
|
|
|
103
105
|
common: string,
|
|
104
106
|
): Promise<string | null> {
|
|
105
107
|
try {
|
|
106
|
-
const { stdout } = await
|
|
108
|
+
const { stdout } = await exec("git", [
|
|
107
109
|
"-C",
|
|
108
110
|
rootPath,
|
|
109
111
|
"rev-parse",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
3
5
|
|
|
4
|
-
import { type LocalVcs, gitCommonDir
|
|
6
|
+
import { type LocalVcs, gitCommonDir } from "@dev.fast/local-vcs";
|
|
5
7
|
import { isMissingFileError } from "@review/fs-utils.js";
|
|
6
8
|
|
|
7
9
|
import { checkoutFs } from "./checkout-fs.js";
|
|
@@ -9,6 +11,8 @@ import { ReviewInputError } from "./document.js";
|
|
|
9
11
|
|
|
10
12
|
const { lstat, readFile, readlink, realpath } = checkoutFs;
|
|
11
13
|
|
|
14
|
+
const exec = promisify(execFile);
|
|
15
|
+
|
|
12
16
|
export const EMPTY_SOURCE = "empty-worktree-baseline";
|
|
13
17
|
|
|
14
18
|
/** Never follow a working-copy link outside the selected checkout. */
|
|
@@ -45,7 +49,7 @@ export async function workingFiles(vcs: LocalVcs): Promise<string[]> {
|
|
|
45
49
|
"Working source requires a Git-backed jj repository.",
|
|
46
50
|
);
|
|
47
51
|
|
|
48
|
-
const { stdout } = await
|
|
52
|
+
const { stdout } = await exec(
|
|
49
53
|
"git",
|
|
50
54
|
[
|
|
51
55
|
"-C",
|
|
@@ -96,7 +100,7 @@ export async function untrackedFileCount(
|
|
|
96
100
|
if (vcs.kind !== "git") return undefined;
|
|
97
101
|
|
|
98
102
|
try {
|
|
99
|
-
const { stdout } = await
|
|
103
|
+
const { stdout } = await exec(
|
|
100
104
|
"git",
|
|
101
105
|
[
|
|
102
106
|
"-C",
|
|
@@ -318,75 +318,3 @@ it("compares a staged file in an unborn repository with empty source", async ()
|
|
|
318
318
|
rmSync(root, { recursive: true, force: true });
|
|
319
319
|
}
|
|
320
320
|
}, 30_000);
|
|
321
|
-
|
|
322
|
-
it("shows a peek on an untracked file as head lines", async () => {
|
|
323
|
-
const root = mkdtempSync(path.join(tmpdir(), "worktree-untracked-"));
|
|
324
|
-
const repository = path.join(root, "repository");
|
|
325
|
-
mkdirSync(repository);
|
|
326
|
-
|
|
327
|
-
const git = (...args: string[]) =>
|
|
328
|
-
execFileSync("git", ["-C", repository, ...args], {
|
|
329
|
-
encoding: "utf8",
|
|
330
|
-
}).trim();
|
|
331
|
-
|
|
332
|
-
git("init", "-q", "-b", "main");
|
|
333
|
-
writeFileSync(path.join(repository, "value.ts"), "export const value = 1;\n");
|
|
334
|
-
git("add", "value.ts");
|
|
335
|
-
git(
|
|
336
|
-
"-c",
|
|
337
|
-
"user.name=Test",
|
|
338
|
-
"-c",
|
|
339
|
-
"user.email=test@example.invalid",
|
|
340
|
-
"commit",
|
|
341
|
-
"-qm",
|
|
342
|
-
"Base",
|
|
343
|
-
);
|
|
344
|
-
writeFileSync(path.join(repository, "new.ts"), "export const a = 1;\n");
|
|
345
|
-
|
|
346
|
-
const local = openLocalReviewStore(path.join(root, "reviews.db"));
|
|
347
|
-
|
|
348
|
-
const selection = {
|
|
349
|
-
file: "new.ts",
|
|
350
|
-
start: { side: "head" as const, line: 1 },
|
|
351
|
-
end: { side: "head" as const, line: 1 },
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
try {
|
|
355
|
-
const { id } = await local.data.register(repository);
|
|
356
|
-
|
|
357
|
-
const { reviewId } = await local.store.execute({
|
|
358
|
-
operation: {
|
|
359
|
-
type: "create",
|
|
360
|
-
title: "Live",
|
|
361
|
-
target: { kind: "worktree", repositoryId: id },
|
|
362
|
-
},
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
await local.store.execute({
|
|
366
|
-
operation: {
|
|
367
|
-
type: "edit",
|
|
368
|
-
reviewId,
|
|
369
|
-
edit: {
|
|
370
|
-
type: "insert",
|
|
371
|
-
content: { type: "code_peek", source: formatAnchor(selection) },
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const progress = await (
|
|
377
|
-
await createReviewApi(local.store, local.data).request(
|
|
378
|
-
`/${reviewId}/progress`,
|
|
379
|
-
)
|
|
380
|
-
).json();
|
|
381
|
-
|
|
382
|
-
expect(progress.untrackedFiles).toBe(1);
|
|
383
|
-
expect(progress.unavailableSelections).toEqual({});
|
|
384
|
-
expect(progress.resolvedSelections[selectionKey(selection)]).toEqual([
|
|
385
|
-
{ file: "new.ts", side: "head", fromLine: 1, toLine: 1 },
|
|
386
|
-
]);
|
|
387
|
-
} finally {
|
|
388
|
-
await local.data.close();
|
|
389
|
-
await local.store.close();
|
|
390
|
-
rmSync(root, { recursive: true, force: true });
|
|
391
|
-
}
|
|
392
|
-
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type JsonValue,
|
|
6
|
+
isJsonObject,
|
|
7
|
+
parseJsonText,
|
|
8
|
+
} from "@dev.fast/review-protocol";
|
|
9
|
+
import { writePrivateJsonAtomic } from "@dev.fast/trace-core";
|
|
10
|
+
|
|
11
|
+
import { devReviewHome } from "./review-home-paths";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Machine-wide Review preferences the server itself needs. Workbench settings
|
|
15
|
+
* do not work here: the review server never reads the workbench
|
|
16
|
+
* configuration.
|
|
17
|
+
*/
|
|
18
|
+
export interface ReviewPreferences {
|
|
19
|
+
/**
|
|
20
|
+
* Whether the scratchpad exists on this machine. Off, the server neither
|
|
21
|
+
* makes nor lists it and Review's instructions do not offer it to agents.
|
|
22
|
+
*/
|
|
23
|
+
scratchpadEnabled: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_REVIEW_PREFERENCES: ReviewPreferences = {
|
|
27
|
+
scratchpadEnabled: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function reviewPreferencesPath(devHome = devReviewHome()): string {
|
|
31
|
+
return path.join(devHome, "preferences.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A missing or unreadable file falls back to the defaults; it never throws. */
|
|
35
|
+
export async function readReviewPreferences(
|
|
36
|
+
devHome?: string,
|
|
37
|
+
): Promise<ReviewPreferences> {
|
|
38
|
+
try {
|
|
39
|
+
const raw = parseJsonText(
|
|
40
|
+
await readFile(reviewPreferencesPath(devHome), "utf8"),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
scratchpadEnabled: parseScratchpadEnabled(raw),
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return { ...DEFAULT_REVIEW_PREFERENCES };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeReviewPreferences(
|
|
52
|
+
preferences: ReviewPreferences,
|
|
53
|
+
devHome?: string,
|
|
54
|
+
): Promise<ReviewPreferences> {
|
|
55
|
+
const next: ReviewPreferences = {
|
|
56
|
+
scratchpadEnabled: preferences.scratchpadEnabled === true,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
await writePrivateJsonAtomic(reviewPreferencesPath(devHome), next);
|
|
60
|
+
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Reads the scratchpad preference; a missing file means off. */
|
|
65
|
+
export async function readScratchpadEnabled(
|
|
66
|
+
devHome?: string,
|
|
67
|
+
): Promise<boolean> {
|
|
68
|
+
return (await readReviewPreferences(devHome)).scratchpadEnabled;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Sets the scratchpad preference, keeping the other preferences as they are. */
|
|
72
|
+
export async function writeScratchpadEnabled(
|
|
73
|
+
enabled: boolean,
|
|
74
|
+
devHome?: string,
|
|
75
|
+
): Promise<boolean> {
|
|
76
|
+
const current = await readReviewPreferences(devHome);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
await writeReviewPreferences(
|
|
80
|
+
{ ...current, scratchpadEnabled: enabled },
|
|
81
|
+
devHome,
|
|
82
|
+
)
|
|
83
|
+
).scratchpadEnabled;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseScratchpadEnabled(raw: JsonValue): boolean {
|
|
87
|
+
return isJsonObject(raw) && raw.scratchpadEnabled === true;
|
|
88
|
+
}
|
package/src/review-stack.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
import { defaultBranch, git } from "@dev.fast/local-vcs";
|
|
2
5
|
import type {
|
|
3
6
|
ReviewApiSummary,
|
|
4
7
|
ReviewStackLayer,
|
|
5
8
|
} from "@dev.fast/review-protocol";
|
|
6
9
|
import { z } from "zod";
|
|
7
10
|
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
|
|
8
13
|
const GitHubStacksSchema = z.array(
|
|
9
14
|
z.object({
|
|
10
15
|
pull_requests: z.array(
|
|
@@ -22,7 +27,7 @@ type Subject = Pick<ReviewApiSummary, "origin"> & { reviewId?: string };
|
|
|
22
27
|
|
|
23
28
|
type Candidate = Pick<
|
|
24
29
|
ReviewApiSummary,
|
|
25
|
-
"reviewId" | "title" | "origin" | "pins" | "trashedAt" | "
|
|
30
|
+
"reviewId" | "title" | "origin" | "pins" | "trashedAt" | "repositoryGroup"
|
|
26
31
|
>;
|
|
27
32
|
|
|
28
33
|
interface StackEntry {
|
|
@@ -135,7 +140,7 @@ async function localStack(
|
|
|
135
140
|
repositoryPath: string,
|
|
136
141
|
): Promise<StackEntry[]> {
|
|
137
142
|
const group = reviews.find((review) => review.reviewId === subject.reviewId)
|
|
138
|
-
?.
|
|
143
|
+
?.repositoryGroup?.key;
|
|
139
144
|
|
|
140
145
|
const current = subject.origin?.branch;
|
|
141
146
|
|
|
@@ -147,7 +152,9 @@ async function localStack(
|
|
|
147
152
|
|
|
148
153
|
const name = (ref: string) => ref.replace(/^origin\//, "");
|
|
149
154
|
|
|
150
|
-
const inGroup = reviews.filter(
|
|
155
|
+
const inGroup = reviews.filter(
|
|
156
|
+
(review) => review.repositoryGroup?.key === group,
|
|
157
|
+
);
|
|
151
158
|
|
|
152
159
|
const latest = (field: "branch" | "baseBranch", branch: string) => {
|
|
153
160
|
const matches = inGroup.filter(
|
|
@@ -201,13 +208,9 @@ async function localStack(
|
|
|
201
208
|
async function forkPoint(repositoryPath: string, main: string, branch: string) {
|
|
202
209
|
const ref = `refs/heads/${branch}`;
|
|
203
210
|
|
|
204
|
-
const fork = await git(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
{
|
|
208
|
-
allowFailure: true,
|
|
209
|
-
},
|
|
210
|
-
);
|
|
211
|
+
const fork = await git(repositoryPath, ["merge-base", main, ref], {
|
|
212
|
+
allowFailure: true,
|
|
213
|
+
});
|
|
211
214
|
|
|
212
215
|
if (!fork.ok) return null;
|
|
213
216
|
|
|
@@ -215,7 +218,7 @@ async function forkPoint(repositoryPath: string, main: string, branch: string) {
|
|
|
215
218
|
|
|
216
219
|
const log = await git(
|
|
217
220
|
repositoryPath,
|
|
218
|
-
["log", "--reverse", "--format=%s",
|
|
221
|
+
["log", "--reverse", "--format=%s", `${base}..${ref}`],
|
|
219
222
|
{ allowFailure: true },
|
|
220
223
|
);
|
|
221
224
|
|
|
@@ -226,10 +229,15 @@ async function defaultRunGitHubApi(
|
|
|
226
229
|
host: string,
|
|
227
230
|
endpoint: string,
|
|
228
231
|
): Promise<string> {
|
|
229
|
-
const { stdout } = await
|
|
232
|
+
const { stdout } = await execFileAsync(
|
|
230
233
|
"gh",
|
|
231
|
-
["api", "--hostname",
|
|
232
|
-
{
|
|
234
|
+
["api", "--hostname", host, endpoint],
|
|
235
|
+
{
|
|
236
|
+
env: { ...process.env, GH_PROMPT_DISABLED: "1" },
|
|
237
|
+
encoding: "utf8",
|
|
238
|
+
timeout: 10_000,
|
|
239
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
240
|
+
},
|
|
233
241
|
);
|
|
234
242
|
|
|
235
243
|
return stdout;
|
package/src/runtime.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
|
|
3
|
-
import { runVcs } from "@dev.fast/local-vcs";
|
|
4
|
-
|
|
5
4
|
export async function resolveReviewRoot(
|
|
6
5
|
cwd: string,
|
|
7
6
|
execFile: typeof execFilePromise = execFilePromise,
|
|
@@ -40,9 +39,36 @@ export async function resolveReviewRoot(
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
function execFilePromise(
|
|
43
|
-
file:
|
|
42
|
+
file: string,
|
|
44
43
|
args: string[],
|
|
45
44
|
options: { cwd?: string } = {},
|
|
46
45
|
): Promise<{ stdout: string }> {
|
|
47
|
-
return
|
|
46
|
+
return execFileUntraced(file, args, options);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function execFileUntraced(
|
|
50
|
+
file: string,
|
|
51
|
+
args: string[],
|
|
52
|
+
options: { cwd?: string } = {},
|
|
53
|
+
): Promise<{ stdout: string }> {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const child = spawn(file, args, {
|
|
56
|
+
cwd: options.cwd,
|
|
57
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let stdout = "";
|
|
61
|
+
let stderr = "";
|
|
62
|
+
child.stdout?.on("data", (chunk) => {
|
|
63
|
+
stdout += String(chunk);
|
|
64
|
+
});
|
|
65
|
+
child.stderr?.on("data", (chunk) => {
|
|
66
|
+
stderr += String(chunk);
|
|
67
|
+
});
|
|
68
|
+
child.on("error", reject);
|
|
69
|
+
child.on("exit", (code) => {
|
|
70
|
+
if (code === 0) resolve({ stdout });
|
|
71
|
+
else reject(new Error(stderr.trim() || `${file} exited with ${code}`));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
48
74
|
}
|
|
@@ -84,8 +84,6 @@ export async function runDesktopHost(
|
|
|
84
84
|
const { local, shared, server } = await takeOverServerHome(home, async () => {
|
|
85
85
|
const local = await openReviewProfile(home, {
|
|
86
86
|
manageWorkspaces: true,
|
|
87
|
-
// Desktop shows each PR's state beside its board.
|
|
88
|
-
pullRequestStates: true,
|
|
89
87
|
});
|
|
90
88
|
|
|
91
89
|
// JSON is the sole user-review store. A failure is surfaced, never replaced
|
|
@@ -199,12 +197,9 @@ function requiredPositiveInteger(
|
|
|
199
197
|
|
|
200
198
|
if (process.env.DEV_FAST_REVIEW_DESKTOP_HOST_AUTOSTART !== "0") {
|
|
201
199
|
void runDesktopHost().catch((error) => {
|
|
202
|
-
// Exit now: a host that couldn't start (a store that couldn't be
|
|
203
|
-
// upgraded) must not linger with timers alive, or Desktop waits out its
|
|
204
|
-
// ready timeout instead of reporting why.
|
|
205
200
|
process.stderr.write(
|
|
206
201
|
`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
|
|
207
|
-
() => process.exit(1),
|
|
208
202
|
);
|
|
203
|
+
process.exitCode = 1;
|
|
209
204
|
});
|
|
210
205
|
}
|
|
@@ -47,6 +47,10 @@ import {
|
|
|
47
47
|
reviewInstanceDiscoveryPath,
|
|
48
48
|
reviewLegacyDiscoveryPath,
|
|
49
49
|
} from "@review/review-home-paths";
|
|
50
|
+
import {
|
|
51
|
+
readScratchpadEnabled,
|
|
52
|
+
writeScratchpadEnabled,
|
|
53
|
+
} from "@review/review-preferences";
|
|
50
54
|
import {
|
|
51
55
|
ReviewTelemetry,
|
|
52
56
|
type ReviewTelemetryContext,
|
|
@@ -172,6 +176,10 @@ export function createGlobalReviewServer(
|
|
|
172
176
|
let closing = false;
|
|
173
177
|
const cliPath = path.join(input.packageRoot, "dist", "cli.js");
|
|
174
178
|
|
|
179
|
+
// The scratchpad preference, read once at listen and kept current by the
|
|
180
|
+
// settings endpoint below: this server is the only writer while it runs.
|
|
181
|
+
let scratchpadEnabled = false;
|
|
182
|
+
|
|
175
183
|
const discovery: ReviewDesktopDiscovery = {
|
|
176
184
|
version: REVIEW_DESKTOP_DISCOVERY_VERSION,
|
|
177
185
|
instanceId,
|
|
@@ -204,6 +212,7 @@ export function createGlobalReviewServer(
|
|
|
204
212
|
relay,
|
|
205
213
|
token,
|
|
206
214
|
instanceId,
|
|
215
|
+
scratchpad: () => scratchpadEnabled,
|
|
207
216
|
status: () => {
|
|
208
217
|
const { key, channel, checkout, appVersion, cliVersion, instanceId } =
|
|
209
218
|
discovery;
|
|
@@ -256,6 +265,25 @@ export function createGlobalReviewServer(
|
|
|
256
265
|
app.all("/reviews-api", (context) => gateway.fetch(context.req.raw));
|
|
257
266
|
app.all("/reviews-api/*", (context) => gateway.fetch(context.req.raw));
|
|
258
267
|
app.get("/remote-hosts", () => serverJson(200, gateway.hosts()));
|
|
268
|
+
app.get("/preferences/scratchpad", () =>
|
|
269
|
+
serverJson(200, { enabled: scratchpadEnabled }),
|
|
270
|
+
);
|
|
271
|
+
app.put("/preferences/scratchpad", async (context) => {
|
|
272
|
+
const request = z
|
|
273
|
+
.object({ enabled: z.boolean() })
|
|
274
|
+
.safeParse(await readBoundedRequestJson(context.req.raw));
|
|
275
|
+
|
|
276
|
+
if (!request.success)
|
|
277
|
+
throw new ReviewServerError("enabled must be a boolean.", 400);
|
|
278
|
+
|
|
279
|
+
scratchpadEnabled = await writeScratchpadEnabled(request.data.enabled);
|
|
280
|
+
|
|
281
|
+
// Home watches the catalog; the pad appears or goes without a store write.
|
|
282
|
+
if (scratchpadEnabled) await reviewStore.ensureScratchpad();
|
|
283
|
+
reviewStore.invalidateCatalog();
|
|
284
|
+
|
|
285
|
+
return serverJson(200, { enabled: scratchpadEnabled });
|
|
286
|
+
});
|
|
259
287
|
app.post("/app/focus", async () => {
|
|
260
288
|
const result = await relay.dispatch({
|
|
261
289
|
name: "focusWindow",
|
|
@@ -497,6 +525,7 @@ export function createGlobalReviewServer(
|
|
|
497
525
|
store: reviewStore,
|
|
498
526
|
data: input.reviewData,
|
|
499
527
|
});
|
|
528
|
+
scratchpadEnabled = await readScratchpadEnabled();
|
|
500
529
|
boundPort = await listen(httpServer, input.port);
|
|
501
530
|
discovery.url = urlForBoundPort();
|
|
502
531
|
|
|
@@ -554,7 +583,8 @@ export function sessionStartedSource(
|
|
|
554
583
|
try {
|
|
555
584
|
const snapshot = reviewStore.read(reviewUuid);
|
|
556
585
|
|
|
557
|
-
const sourceKind =
|
|
586
|
+
const sourceKind =
|
|
587
|
+
snapshot.kind === "scratchpad" ? snapshot.kind : snapshot.target?.kind;
|
|
558
588
|
|
|
559
589
|
const pullRequestUrl = snapshot.origin?.pullRequestUrl;
|
|
560
590
|
|
|
@@ -221,6 +221,8 @@ async function serve(input: HeadlessServerInput) {
|
|
|
221
221
|
token: discovery.token,
|
|
222
222
|
instanceId: discovery.instanceId,
|
|
223
223
|
softwareMapEnabled: input.softwareMapEnabled,
|
|
224
|
+
// The scratchpad is the laptop's alone, even with a Desktop attached.
|
|
225
|
+
scratchpad: () => false,
|
|
224
226
|
status: () => ({ key: "headless", home: input.stateDir }),
|
|
225
227
|
ask: {
|
|
226
228
|
tools: cliAskTools(() => (existsSync(cliPath) ? cliPath : undefined), {
|
package/src/server/hono-http.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
parseJsonText,
|
|
8
8
|
} from "@dev.fast/review-protocol";
|
|
9
9
|
import { type HttpBindings, getRequestListener } from "@hono/node-server";
|
|
10
|
-
import { REVIEW_PERSON_HEADER } from "@review/review-api/request-origin";
|
|
11
10
|
import { REVIEW_APP_SESSION_ID_HEADER } from "@review/ui-telemetry-events";
|
|
12
11
|
import type { Hono } from "hono";
|
|
13
12
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
@@ -74,7 +73,7 @@ export function applyCorsHeaders(
|
|
|
74
73
|
|
|
75
74
|
response.headers.set(
|
|
76
75
|
"access-control-allow-headers",
|
|
77
|
-
`content-type, x-review-token, ${
|
|
76
|
+
`content-type, x-review-token, ${REVIEW_APP_SESSION_ID_HEADER}`,
|
|
78
77
|
);
|
|
79
78
|
response.headers.set(
|
|
80
79
|
"access-control-allow-methods",
|
|
@@ -13,13 +13,14 @@ import { devReviewHome } from "@review/review-home-paths";
|
|
|
13
13
|
*/
|
|
14
14
|
export async function removeLegacyTutorial(input: {
|
|
15
15
|
store: ReviewStore;
|
|
16
|
-
data:
|
|
16
|
+
data: LocalReviewData;
|
|
17
17
|
root?: string;
|
|
18
18
|
}): Promise<void> {
|
|
19
19
|
const tutorialRoot = input.root ?? path.join(devReviewHome(), "tutorial");
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
|
-
|
|
22
|
+
for (const reviewId of input.store.legacyTutorialIds())
|
|
23
|
+
await input.store.execute({ operation: { type: "delete", reviewId } });
|
|
23
24
|
|
|
24
25
|
for (const repository of input.store.repositories()) {
|
|
25
26
|
const relative = path.relative(tutorialRoot, repository.path);
|
|
@@ -32,9 +32,11 @@ function decorate(
|
|
|
32
32
|
|
|
33
33
|
return {
|
|
34
34
|
...entry,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
...(entry.repositoryGroup && {
|
|
36
|
+
repositoryGroup: {
|
|
37
|
+
key: `${alias}:${entry.repositoryGroup.key}`,
|
|
38
|
+
label: entry.repositoryGroup.label,
|
|
39
|
+
},
|
|
38
40
|
}),
|
|
39
41
|
host: alias,
|
|
40
42
|
hostState,
|
|
@@ -16,7 +16,7 @@ const listEntrySchema = z.looseObject({
|
|
|
16
16
|
repositoryName: z.string(),
|
|
17
17
|
viewedAt: z.string().nullable(),
|
|
18
18
|
trashedAt: z.string().nullable(),
|
|
19
|
-
|
|
19
|
+
repositoryGroup: z.object({ key: z.string(), label: z.string() }).optional(),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
export const listEntriesSchema = z.array(z.unknown()).transform(
|
|
@@ -34,12 +34,12 @@ export function reviewLifecycleTelemetry(
|
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
return {
|
|
37
|
-
onReviewCreated: ({ reviewId, blocks, via, agentKind }) => {
|
|
37
|
+
onReviewCreated: ({ reviewId, kind, blocks, via, agentKind }) => {
|
|
38
38
|
if (reported.has(reviewId)) return;
|
|
39
39
|
reported.add(reviewId);
|
|
40
40
|
|
|
41
41
|
if (via === "api" || via === "mcp") authoring.set(reviewId, agentKind);
|
|
42
|
-
const properties: PostHogCaptureProperties = { blocks, via };
|
|
42
|
+
const properties: PostHogCaptureProperties = { kind, blocks, via };
|
|
43
43
|
|
|
44
44
|
if (agentKind) properties.agent_kind = agentKind;
|
|
45
45
|
capture("review_review_created", properties, reviewId);
|
|
@@ -132,6 +132,7 @@ export interface WhiteboardCoreInput {
|
|
|
132
132
|
token: string;
|
|
133
133
|
instanceId: string;
|
|
134
134
|
softwareMapEnabled?: boolean;
|
|
135
|
+
scratchpad: () => boolean;
|
|
135
136
|
status: () => JsonObject;
|
|
136
137
|
hooks?: ReviewApiHooks;
|
|
137
138
|
ask?: {
|
|
@@ -191,6 +192,7 @@ export function createWhiteboardCore(input: WhiteboardCoreInput) {
|
|
|
191
192
|
callbacks.open,
|
|
192
193
|
shared,
|
|
193
194
|
callbacks.capabilities,
|
|
195
|
+
input.scratchpad,
|
|
194
196
|
() => traceMachineEnabled(),
|
|
195
197
|
input.status,
|
|
196
198
|
input.hooks,
|
|
@@ -234,7 +236,9 @@ export function relayReviewCallbacks(
|
|
|
234
236
|
|
|
235
237
|
return z.object({ softwareMapEnabled: z.boolean() }).parse(result.result);
|
|
236
238
|
},
|
|
237
|
-
async capabilities(): Promise<
|
|
239
|
+
async capabilities(): Promise<
|
|
240
|
+
Omit<AuthoringCapabilities, "scratchpadEnabled">
|
|
241
|
+
> {
|
|
238
242
|
if (!relay.attached)
|
|
239
243
|
return { desktopAvailable: false, softwareMapEnabled };
|
|
240
244
|
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
import { git, gitCommonDir } from "@dev.fast/local-vcs";
|
|
6
1
|
import type { StructuralDiffEvent } from "@dev.fast/review-protocol";
|
|
7
2
|
|
|
8
3
|
import {
|
|
@@ -23,7 +18,6 @@ export class StructuralComparisons {
|
|
|
23
18
|
|
|
24
19
|
const key = JSON.stringify([
|
|
25
20
|
configGeneration,
|
|
26
|
-
await classificationStamp(input.repositoryPath),
|
|
27
21
|
input.repositoryPath,
|
|
28
22
|
input.comparison,
|
|
29
23
|
input.paths,
|
|
@@ -72,47 +66,6 @@ export class StructuralComparisons {
|
|
|
72
66
|
|
|
73
67
|
let configGeneration = 0;
|
|
74
68
|
|
|
75
|
-
const classificationFiles = new Map<string, Promise<string[]>>();
|
|
76
|
-
|
|
77
|
-
/** Git attribute and diffr config files that diffr's classifier reads. */
|
|
78
|
-
function classificationInputs(root: string): Promise<string[]> {
|
|
79
|
-
let files = classificationFiles.get(root);
|
|
80
|
-
|
|
81
|
-
if (!files) {
|
|
82
|
-
const config =
|
|
83
|
-
process.env.XDG_CONFIG_HOME || path.join(homedir(), ".config");
|
|
84
|
-
|
|
85
|
-
const diffrDirectory =
|
|
86
|
-
process.env.DIFFR_CONFIG_DIR || path.join(config, "diffr");
|
|
87
|
-
|
|
88
|
-
files = Promise.all([
|
|
89
|
-
gitCommonDir(root),
|
|
90
|
-
git(root, ["config", "--path", "core.attributesFile"], {
|
|
91
|
-
allowFailure: true,
|
|
92
|
-
}),
|
|
93
|
-
]).then(([common, user]) => [
|
|
94
|
-
...(common ? [path.join(common, "info", "attributes")] : []),
|
|
95
|
-
path.join(root, ".gitattributes"),
|
|
96
|
-
user.stdout.trim() || path.join(config, "git", "attributes"),
|
|
97
|
-
path.join(diffrDirectory, "config.toml"),
|
|
98
|
-
]);
|
|
99
|
-
classificationFiles.set(root, files);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return files;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function classificationStamp(root: string) {
|
|
106
|
-
return Promise.all(
|
|
107
|
-
(await classificationInputs(root)).map((file) =>
|
|
108
|
-
stat(file).then(
|
|
109
|
-
({ mtimeMs, size }) => [mtimeMs, size],
|
|
110
|
-
() => null,
|
|
111
|
-
),
|
|
112
|
-
),
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
69
|
export function invalidateStructuralComparisons(): void {
|
|
117
70
|
configGeneration++;
|
|
118
71
|
}
|