@dev.fast/whiteboard 0.3.4 → 0.3.5-preview.20261020.130
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-CprZojXd.js → account-alias-CMyXCuuq.js} +13 -8
- package/dist/{agent-cli-CThN7Q59.js → agent-cli-CahKfDT0.js} +15 -10
- package/dist/agent-cli-CvYV2R6c.js +2 -0
- package/dist/{agent-client-BOOi7EUR.js → agent-client-D2RmE9NL.js} +13 -3
- package/dist/{authoring-tools-BNwuMnz6.js → authoring-tools-BLhYKrNN.js} +34 -27
- package/dist/{background-server-DLSDogkI.js → background-server-CFUU4VtN.js} +1 -1
- package/dist/{background-server-CkvxA-6O.js → background-server-k4K_Hvh1.js} +5 -5
- package/dist/build-info.json +1 -1
- package/dist/{cli-R4yfa5DT.js → cli-BUVSMGud.js} +1 -1
- package/dist/{cli-runner-Cuc9FSzu.js → cli-runner-C7W6VA94.js} +26 -26
- package/dist/cli.js +4 -3
- package/dist/{client-BN_FDxFr.js → client-B0f4EJ7v.js} +253 -47
- package/dist/{fuzzy-match-BO9bK37N.js → fuzzy-match-BbaX-dGh.js} +6 -4
- package/dist/{headless-host-Cog6SpS9.js → headless-host-C7xYMe74.js} +513 -140
- package/dist/{headless-host-CVBOQyRS.js → headless-host-cYdlRYPB.js} +1 -1
- package/dist/lens-selection-CAtsGaym.js +214 -0
- package/dist/{local-data-D_9tvdsp.js → local-data-DrNSaiS5.js} +2416 -320
- package/dist/{mcp-D1hziANa.js → mcp-yXnjUkOV.js} +6 -10
- package/dist/{package-paths-C--__jEv.js → package-paths-BWxO-UEL.js} +1 -1
- package/dist/{profile-DokFw782.js → profile-BlqZlCPZ.js} +20 -8
- package/dist/profile-CGxNfeYQ.js +2 -0
- package/dist/{remote-attach-CEJ-kvDY.js → remote-attach-CZTrtWIk.js} +4 -4
- package/dist/{remote-extensions-Blcjr9Ze.js → remote-extensions-CNXwqK0D.js} +3 -3
- package/dist/{remote-extensions-GN8Gf_qG.js → remote-extensions-VP-kfq0C.js} +1 -1
- package/dist/{remote-language-server-BTKBQ7x4.js → remote-language-server-BiPzol99.js} +5 -5
- package/dist/{remote-uninstall-DW7Gdryh.js → remote-uninstall-CFTkoioA.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin--8lLCclv.js} +55 -14
- package/dist/{review-app-launcher-CkwLclzR.js → review-app-launcher-KA2_mOeo.js} +2 -2
- package/dist/{review-home-paths-j2BRAaNv.js → review-home-paths-D8AoCdd2.js} +19 -17
- package/dist/{review-telemetry-BLNwYnLV.js → review-telemetry-B2JniTIs.js} +5 -5
- package/dist/runtime-Bj2tD33R.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-DtJFWC0G.js → s3-CsG2EWED.js} +1 -1
- package/dist/{s3-config-BTO12tCC.js → s3-config-CjcLyFV5.js} +4 -2
- package/dist/{s3-config-CSy3fx8H.js → s3-config-zz4GWh2T.js} +1 -1
- package/dist/server/desktop-host.js +29 -76
- package/dist/{server-discovery-Bey8IqXH.js → server-discovery-CRXG48Uz.js} +3 -3
- package/dist/sharing/index.d.ts +484 -70
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-CyYCvVHx.js} +90 -17
- package/dist/{src-BMPuxWv4.js → src-WPaQn75m.js} +208 -73
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-B_BijGHF.js} +270 -269
- package/dist/{thread-state-5pYr6_zM.js → thread-state-D6MlnGv7.js} +7 -2
- package/dist/{tool-failure-C9MpbXJJ.js → tool-failure-_makh_Sr.js} +10 -3
- package/instructions/authoring.md +18 -5
- package/package.json +6 -6
- package/src/ask/agents.ts +13 -1
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +16 -0
- package/src/ask/threads.ts +24 -3
- package/src/cli-install.ts +13 -2
- package/src/cli-runner.ts +4 -1
- package/src/cli.ts +4 -1
- package/src/diff-selection-migration.ts +48 -34
- package/src/heading-anchors.ts +42 -0
- package/src/lens-selection.ts +38 -2
- package/src/review-api/README.md +52 -13
- package/src/review-api/activity.ts +1 -4
- package/src/review-api/agent-cli.ts +9 -1
- package/src/review-api/anchor-quotes.ts +14 -7
- package/src/review-api/ask-history.ts +137 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/board-order.ts +144 -0
- package/src/review-api/checkout-groups.ts +560 -0
- package/src/review-api/document-headings.ts +58 -27
- package/src/review-api/document-text.ts +31 -2
- package/src/review-api/document.ts +281 -34
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +702 -123
- package/src/review-api/instructions.ts +0 -19
- package/src/review-api/link-preview.ts +113 -0
- package/src/review-api/local-data.ts +310 -46
- package/src/review-api/mcp.ts +4 -12
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +37 -6
- package/src/review-api/public-tools.ts +24 -2
- package/src/review-api/pull-request.ts +82 -49
- package/src/review-api/request-origin.ts +10 -0
- package/src/review-api/review-progress.ts +61 -27
- package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
- package/src/review-api/store-migrations.ts +357 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2144 -480
- package/src/review-api/worktree-events.ts +3 -5
- package/src/review-api/worktree-source.ts +3 -7
- package/src/review-api/worktree-structural.integration.ts +72 -0
- package/src/review-stack.ts +15 -23
- package/src/runtime.ts +4 -30
- package/src/server/desktop-host.ts +6 -1
- package/src/server/desktop-server.ts +1 -31
- package/src/server/headless-host.ts +0 -2
- package/src/server/hono-http.ts +2 -1
- package/src/server/legacy-tutorial-cleanup.ts +2 -3
- package/src/server/review-gateway-list.ts +3 -5
- 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 +1 -5
- package/src/server/structural-comparisons.ts +47 -0
- package/src/sharing/export.ts +36 -7
- package/src/sharing/host.ts +34 -14
- package/src/sharing/import.ts +156 -24
- package/src/sharing/repository.ts +128 -20
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +44 -14
- package/dist/agent-cli-C6SPSII0.js +0 -2
- package/dist/profile-CYPmvBoA.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { isAbsolute, relative } from "node:path";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
import { runVcs } from "@dev.fast/local-vcs";
|
|
6
4
|
|
|
7
5
|
const normalize = (name: string) => name.replaceAll("\\", "/").normalize("NFC");
|
|
8
6
|
|
|
@@ -53,7 +51,7 @@ export async function ignoredDirectories(
|
|
|
53
51
|
gitDirectory?: string,
|
|
54
52
|
): Promise<string[]> {
|
|
55
53
|
try {
|
|
56
|
-
const { stdout } = await
|
|
54
|
+
const { stdout } = await runVcs(
|
|
57
55
|
"git",
|
|
58
56
|
[
|
|
59
57
|
"-C",
|
|
@@ -105,7 +103,7 @@ export async function gitDirectoryPrefix(
|
|
|
105
103
|
common: string,
|
|
106
104
|
): Promise<string | null> {
|
|
107
105
|
try {
|
|
108
|
-
const { stdout } = await
|
|
106
|
+
const { stdout } = await runVcs("git", [
|
|
109
107
|
"-C",
|
|
110
108
|
rootPath,
|
|
111
109
|
"rev-parse",
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
2
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
3
|
|
|
6
|
-
import { type LocalVcs, gitCommonDir } from "@dev.fast/local-vcs";
|
|
4
|
+
import { type LocalVcs, gitCommonDir, runVcs } from "@dev.fast/local-vcs";
|
|
7
5
|
import { isMissingFileError } from "@review/fs-utils.js";
|
|
8
6
|
|
|
9
7
|
import { checkoutFs } from "./checkout-fs.js";
|
|
@@ -11,8 +9,6 @@ import { ReviewInputError } from "./document.js";
|
|
|
11
9
|
|
|
12
10
|
const { lstat, readFile, readlink, realpath } = checkoutFs;
|
|
13
11
|
|
|
14
|
-
const exec = promisify(execFile);
|
|
15
|
-
|
|
16
12
|
export const EMPTY_SOURCE = "empty-worktree-baseline";
|
|
17
13
|
|
|
18
14
|
/** Never follow a working-copy link outside the selected checkout. */
|
|
@@ -49,7 +45,7 @@ export async function workingFiles(vcs: LocalVcs): Promise<string[]> {
|
|
|
49
45
|
"Working source requires a Git-backed jj repository.",
|
|
50
46
|
);
|
|
51
47
|
|
|
52
|
-
const { stdout } = await
|
|
48
|
+
const { stdout } = await runVcs(
|
|
53
49
|
"git",
|
|
54
50
|
[
|
|
55
51
|
"-C",
|
|
@@ -100,7 +96,7 @@ export async function untrackedFileCount(
|
|
|
100
96
|
if (vcs.kind !== "git") return undefined;
|
|
101
97
|
|
|
102
98
|
try {
|
|
103
|
-
const { stdout } = await
|
|
99
|
+
const { stdout } = await runVcs(
|
|
104
100
|
"git",
|
|
105
101
|
[
|
|
106
102
|
"-C",
|
|
@@ -318,3 +318,75 @@ 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
|
+
});
|
package/src/review-stack.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
|
|
4
|
-
import { defaultBranch, git } from "@dev.fast/local-vcs";
|
|
1
|
+
import { defaultBranch, git, runVcs } from "@dev.fast/local-vcs";
|
|
5
2
|
import type {
|
|
6
3
|
ReviewApiSummary,
|
|
7
4
|
ReviewStackLayer,
|
|
8
5
|
} from "@dev.fast/review-protocol";
|
|
9
6
|
import { z } from "zod";
|
|
10
7
|
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
|
-
|
|
13
8
|
const GitHubStacksSchema = z.array(
|
|
14
9
|
z.object({
|
|
15
10
|
pull_requests: z.array(
|
|
@@ -27,7 +22,7 @@ type Subject = Pick<ReviewApiSummary, "origin"> & { reviewId?: string };
|
|
|
27
22
|
|
|
28
23
|
type Candidate = Pick<
|
|
29
24
|
ReviewApiSummary,
|
|
30
|
-
"reviewId" | "title" | "origin" | "pins" | "trashedAt" | "
|
|
25
|
+
"reviewId" | "title" | "origin" | "pins" | "trashedAt" | "project"
|
|
31
26
|
>;
|
|
32
27
|
|
|
33
28
|
interface StackEntry {
|
|
@@ -140,7 +135,7 @@ async function localStack(
|
|
|
140
135
|
repositoryPath: string,
|
|
141
136
|
): Promise<StackEntry[]> {
|
|
142
137
|
const group = reviews.find((review) => review.reviewId === subject.reviewId)
|
|
143
|
-
?.
|
|
138
|
+
?.project?.id;
|
|
144
139
|
|
|
145
140
|
const current = subject.origin?.branch;
|
|
146
141
|
|
|
@@ -152,9 +147,7 @@ async function localStack(
|
|
|
152
147
|
|
|
153
148
|
const name = (ref: string) => ref.replace(/^origin\//, "");
|
|
154
149
|
|
|
155
|
-
const inGroup = reviews.filter(
|
|
156
|
-
(review) => review.repositoryGroup?.key === group,
|
|
157
|
-
);
|
|
150
|
+
const inGroup = reviews.filter((review) => review.project?.id === group);
|
|
158
151
|
|
|
159
152
|
const latest = (field: "branch" | "baseBranch", branch: string) => {
|
|
160
153
|
const matches = inGroup.filter(
|
|
@@ -208,9 +201,13 @@ async function localStack(
|
|
|
208
201
|
async function forkPoint(repositoryPath: string, main: string, branch: string) {
|
|
209
202
|
const ref = `refs/heads/${branch}`;
|
|
210
203
|
|
|
211
|
-
const fork = await git(
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
const fork = await git(
|
|
205
|
+
repositoryPath,
|
|
206
|
+
["merge-base", { rev: main }, { rev: ref }],
|
|
207
|
+
{
|
|
208
|
+
allowFailure: true,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
214
211
|
|
|
215
212
|
if (!fork.ok) return null;
|
|
216
213
|
|
|
@@ -218,7 +215,7 @@ async function forkPoint(repositoryPath: string, main: string, branch: string) {
|
|
|
218
215
|
|
|
219
216
|
const log = await git(
|
|
220
217
|
repositoryPath,
|
|
221
|
-
["log", "--reverse", "--format=%s", `${base}..${ref}`],
|
|
218
|
+
["log", "--reverse", "--format=%s", { rev: `${base}..${ref}` }],
|
|
222
219
|
{ allowFailure: true },
|
|
223
220
|
);
|
|
224
221
|
|
|
@@ -229,15 +226,10 @@ async function defaultRunGitHubApi(
|
|
|
229
226
|
host: string,
|
|
230
227
|
endpoint: string,
|
|
231
228
|
): Promise<string> {
|
|
232
|
-
const { stdout } = await
|
|
229
|
+
const { stdout } = await runVcs(
|
|
233
230
|
"gh",
|
|
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
|
-
},
|
|
231
|
+
["api", "--hostname", { value: host }, { value: endpoint }],
|
|
232
|
+
{ timeout: 10_000, maxBuffer: 2 * 1024 * 1024 },
|
|
241
233
|
);
|
|
242
234
|
|
|
243
235
|
return stdout;
|
package/src/runtime.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
|
|
3
|
+
import { runVcs } from "@dev.fast/local-vcs";
|
|
4
|
+
|
|
4
5
|
export async function resolveReviewRoot(
|
|
5
6
|
cwd: string,
|
|
6
7
|
execFile: typeof execFilePromise = execFilePromise,
|
|
@@ -39,36 +40,9 @@ export async function resolveReviewRoot(
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function execFilePromise(
|
|
42
|
-
file:
|
|
43
|
+
file: "git" | "jj",
|
|
43
44
|
args: string[],
|
|
44
45
|
options: { cwd?: string } = {},
|
|
45
46
|
): Promise<{ stdout: string }> {
|
|
46
|
-
return
|
|
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
|
-
});
|
|
47
|
+
return runVcs(file, args, options);
|
|
74
48
|
}
|
|
@@ -84,6 +84,8 @@ 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,
|
|
87
89
|
});
|
|
88
90
|
|
|
89
91
|
// JSON is the sole user-review store. A failure is surfaced, never replaced
|
|
@@ -197,9 +199,12 @@ function requiredPositiveInteger(
|
|
|
197
199
|
|
|
198
200
|
if (process.env.DEV_FAST_REVIEW_DESKTOP_HOST_AUTOSTART !== "0") {
|
|
199
201
|
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.
|
|
200
205
|
process.stderr.write(
|
|
201
206
|
`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
|
|
207
|
+
() => process.exit(1),
|
|
202
208
|
);
|
|
203
|
-
process.exitCode = 1;
|
|
204
209
|
});
|
|
205
210
|
}
|
|
@@ -47,10 +47,6 @@ 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";
|
|
54
50
|
import {
|
|
55
51
|
ReviewTelemetry,
|
|
56
52
|
type ReviewTelemetryContext,
|
|
@@ -176,10 +172,6 @@ export function createGlobalReviewServer(
|
|
|
176
172
|
let closing = false;
|
|
177
173
|
const cliPath = path.join(input.packageRoot, "dist", "cli.js");
|
|
178
174
|
|
|
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
|
-
|
|
183
175
|
const discovery: ReviewDesktopDiscovery = {
|
|
184
176
|
version: REVIEW_DESKTOP_DISCOVERY_VERSION,
|
|
185
177
|
instanceId,
|
|
@@ -212,7 +204,6 @@ export function createGlobalReviewServer(
|
|
|
212
204
|
relay,
|
|
213
205
|
token,
|
|
214
206
|
instanceId,
|
|
215
|
-
scratchpad: () => scratchpadEnabled,
|
|
216
207
|
status: () => {
|
|
217
208
|
const { key, channel, checkout, appVersion, cliVersion, instanceId } =
|
|
218
209
|
discovery;
|
|
@@ -265,25 +256,6 @@ export function createGlobalReviewServer(
|
|
|
265
256
|
app.all("/reviews-api", (context) => gateway.fetch(context.req.raw));
|
|
266
257
|
app.all("/reviews-api/*", (context) => gateway.fetch(context.req.raw));
|
|
267
258
|
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
|
-
});
|
|
287
259
|
app.post("/app/focus", async () => {
|
|
288
260
|
const result = await relay.dispatch({
|
|
289
261
|
name: "focusWindow",
|
|
@@ -525,7 +497,6 @@ export function createGlobalReviewServer(
|
|
|
525
497
|
store: reviewStore,
|
|
526
498
|
data: input.reviewData,
|
|
527
499
|
});
|
|
528
|
-
scratchpadEnabled = await readScratchpadEnabled();
|
|
529
500
|
boundPort = await listen(httpServer, input.port);
|
|
530
501
|
discovery.url = urlForBoundPort();
|
|
531
502
|
|
|
@@ -583,8 +554,7 @@ export function sessionStartedSource(
|
|
|
583
554
|
try {
|
|
584
555
|
const snapshot = reviewStore.read(reviewUuid);
|
|
585
556
|
|
|
586
|
-
const sourceKind =
|
|
587
|
-
snapshot.kind === "scratchpad" ? snapshot.kind : snapshot.target?.kind;
|
|
557
|
+
const sourceKind = snapshot.target?.kind;
|
|
588
558
|
|
|
589
559
|
const pullRequestUrl = snapshot.origin?.pullRequestUrl;
|
|
590
560
|
|
|
@@ -221,8 +221,6 @@ 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,
|
|
226
224
|
status: () => ({ key: "headless", home: input.stateDir }),
|
|
227
225
|
ask: {
|
|
228
226
|
tools: cliAskTools(() => (existsSync(cliPath) ? cliPath : undefined), {
|
package/src/server/hono-http.ts
CHANGED
|
@@ -7,6 +7,7 @@ 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";
|
|
10
11
|
import { REVIEW_APP_SESSION_ID_HEADER } from "@review/ui-telemetry-events";
|
|
11
12
|
import type { Hono } from "hono";
|
|
12
13
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
@@ -73,7 +74,7 @@ export function applyCorsHeaders(
|
|
|
73
74
|
|
|
74
75
|
response.headers.set(
|
|
75
76
|
"access-control-allow-headers",
|
|
76
|
-
`content-type, x-review-token, ${REVIEW_APP_SESSION_ID_HEADER}`,
|
|
77
|
+
`content-type, x-review-token, ${REVIEW_PERSON_HEADER}, ${REVIEW_APP_SESSION_ID_HEADER}`,
|
|
77
78
|
);
|
|
78
79
|
response.headers.set(
|
|
79
80
|
"access-control-allow-methods",
|
|
@@ -13,14 +13,13 @@ import { devReviewHome } from "@review/review-home-paths";
|
|
|
13
13
|
*/
|
|
14
14
|
export async function removeLegacyTutorial(input: {
|
|
15
15
|
store: ReviewStore;
|
|
16
|
-
data: LocalReviewData
|
|
16
|
+
data: Pick<LocalReviewData, "forgetRepository">;
|
|
17
17
|
root?: string;
|
|
18
18
|
}): Promise<void> {
|
|
19
19
|
const tutorialRoot = input.root ?? path.join(devReviewHome(), "tutorial");
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
|
-
|
|
23
|
-
await input.store.execute({ operation: { type: "delete", reviewId } });
|
|
22
|
+
await input.store.removeLegacyTutorials();
|
|
24
23
|
|
|
25
24
|
for (const repository of input.store.repositories()) {
|
|
26
25
|
const relative = path.relative(tutorialRoot, repository.path);
|
|
@@ -32,11 +32,9 @@ function decorate(
|
|
|
32
32
|
|
|
33
33
|
return {
|
|
34
34
|
...entry,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
label: entry.repositoryGroup.label,
|
|
39
|
-
},
|
|
35
|
+
// A project on another machine is that machine's.
|
|
36
|
+
...(entry.project && {
|
|
37
|
+
project: { id: `${alias}:${entry.project.id}`, name: entry.project.name },
|
|
40
38
|
}),
|
|
41
39
|
host: alias,
|
|
42
40
|
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
|
+
project: z.object({ id: z.string(), name: 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,
|
|
37
|
+
onReviewCreated: ({ reviewId, 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 = {
|
|
42
|
+
const properties: PostHogCaptureProperties = { blocks, via };
|
|
43
43
|
|
|
44
44
|
if (agentKind) properties.agent_kind = agentKind;
|
|
45
45
|
capture("review_review_created", properties, reviewId);
|
|
@@ -132,7 +132,6 @@ export interface WhiteboardCoreInput {
|
|
|
132
132
|
token: string;
|
|
133
133
|
instanceId: string;
|
|
134
134
|
softwareMapEnabled?: boolean;
|
|
135
|
-
scratchpad: () => boolean;
|
|
136
135
|
status: () => JsonObject;
|
|
137
136
|
hooks?: ReviewApiHooks;
|
|
138
137
|
ask?: {
|
|
@@ -192,7 +191,6 @@ export function createWhiteboardCore(input: WhiteboardCoreInput) {
|
|
|
192
191
|
callbacks.open,
|
|
193
192
|
shared,
|
|
194
193
|
callbacks.capabilities,
|
|
195
|
-
input.scratchpad,
|
|
196
194
|
() => traceMachineEnabled(),
|
|
197
195
|
input.status,
|
|
198
196
|
input.hooks,
|
|
@@ -236,9 +234,7 @@ export function relayReviewCallbacks(
|
|
|
236
234
|
|
|
237
235
|
return z.object({ softwareMapEnabled: z.boolean() }).parse(result.result);
|
|
238
236
|
},
|
|
239
|
-
async capabilities(): Promise<
|
|
240
|
-
Omit<AuthoringCapabilities, "scratchpadEnabled">
|
|
241
|
-
> {
|
|
237
|
+
async capabilities(): Promise<AuthoringCapabilities> {
|
|
242
238
|
if (!relay.attached)
|
|
243
239
|
return { desktopAvailable: false, softwareMapEnabled };
|
|
244
240
|
|
|
@@ -1,3 +1,8 @@
|
|
|
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";
|
|
1
6
|
import type { StructuralDiffEvent } from "@dev.fast/review-protocol";
|
|
2
7
|
|
|
3
8
|
import {
|
|
@@ -18,6 +23,7 @@ export class StructuralComparisons {
|
|
|
18
23
|
|
|
19
24
|
const key = JSON.stringify([
|
|
20
25
|
configGeneration,
|
|
26
|
+
await classificationStamp(input.repositoryPath),
|
|
21
27
|
input.repositoryPath,
|
|
22
28
|
input.comparison,
|
|
23
29
|
input.paths,
|
|
@@ -66,6 +72,47 @@ export class StructuralComparisons {
|
|
|
66
72
|
|
|
67
73
|
let configGeneration = 0;
|
|
68
74
|
|
|
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
|
+
|
|
69
116
|
export function invalidateStructuralComparisons(): void {
|
|
70
117
|
configGeneration++;
|
|
71
118
|
}
|
package/src/sharing/export.ts
CHANGED
|
@@ -14,10 +14,11 @@ import { lensSelections } from "@review/review-api/diff-lenses.js";
|
|
|
14
14
|
import { ReviewInputError } from "@review/review-api/document.js";
|
|
15
15
|
import {
|
|
16
16
|
anchorPins,
|
|
17
|
+
boardRepositoryId,
|
|
17
18
|
checkReferences,
|
|
18
19
|
documentSchema,
|
|
19
20
|
elements,
|
|
20
|
-
|
|
21
|
+
readsAtBoardPins,
|
|
21
22
|
resourceReference,
|
|
22
23
|
resourceReferences,
|
|
23
24
|
sourceReferences,
|
|
@@ -46,13 +47,33 @@ export function digestBytes(bytes: Uint8Array): string {
|
|
|
46
47
|
export function validateShareSources(
|
|
47
48
|
snapshot: Pick<Snapshot, "pins" | "document" | "lenses">,
|
|
48
49
|
) {
|
|
49
|
-
if (!snapshot.pins &&
|
|
50
|
+
if (!snapshot.pins && readsAtBoardPins(snapshot))
|
|
50
51
|
throw new ReviewInputError(
|
|
51
52
|
"Reviews with code references need source pins before sharing.",
|
|
52
53
|
409,
|
|
53
54
|
);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/** The commits a board's references read at besides its base and head. A
|
|
58
|
+
* recipient fetches them too. A shared board names no repository of its
|
|
59
|
+
* own, so one stands in for its commit links. */
|
|
60
|
+
export function referenceCommits(
|
|
61
|
+
snapshot: Pick<Snapshot, "pins" | "document" | "target">,
|
|
62
|
+
repositoryId = boardRepositoryId(snapshot) ?? "shared",
|
|
63
|
+
): string[] {
|
|
64
|
+
const own = new Set([snapshot.pins?.base, snapshot.pins?.head]);
|
|
65
|
+
const commits = new Set<string>();
|
|
66
|
+
|
|
67
|
+
for (const { source } of sourceReferences(snapshot.document, {
|
|
68
|
+
tolerant: true,
|
|
69
|
+
repositoryId,
|
|
70
|
+
}))
|
|
71
|
+
for (const commit of [source.pins?.base, source.pins?.head])
|
|
72
|
+
if (commit && !own.has(commit)) commits.add(commit);
|
|
73
|
+
|
|
74
|
+
return [...commits].sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
/** Freeze one stored version before any asynchronous source reads. */
|
|
57
78
|
export async function exportShare(input: {
|
|
58
79
|
store: ShareExportStore;
|
|
@@ -61,16 +82,24 @@ export async function exportShare(input: {
|
|
|
61
82
|
version?: number;
|
|
62
83
|
repository?: ShareManifest["repository"];
|
|
63
84
|
}): Promise<ShareBundle> {
|
|
64
|
-
|
|
85
|
+
const stored = structuredClone(
|
|
86
|
+
input.store.read(input.reviewId, input.version),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Commit links read from the board's repository, which a plan names only
|
|
90
|
+
// in its target.
|
|
91
|
+
const repositoryId = boardRepositoryId(stored);
|
|
92
|
+
|
|
93
|
+
// Local-only fields stay home; the target names a local checkout.
|
|
65
94
|
const {
|
|
66
95
|
target,
|
|
67
|
-
kind: _kind,
|
|
68
96
|
staleSources: _staleSources,
|
|
69
97
|
sourceUnavailable: _sourceUnavailable,
|
|
70
98
|
lastEdit: _lastEdit,
|
|
71
99
|
trashedAt: _trashedAt,
|
|
100
|
+
personEdit: _personEdit,
|
|
72
101
|
...snapshot
|
|
73
|
-
} =
|
|
102
|
+
} = stored;
|
|
74
103
|
|
|
75
104
|
if (target?.kind === "worktree")
|
|
76
105
|
throw new ReviewInputError("Pin this review to commits before sharing it.");
|
|
@@ -102,7 +131,7 @@ export async function exportShare(input: {
|
|
|
102
131
|
|
|
103
132
|
// Lens ranges travel with the snapshot and must resolve at its pins too.
|
|
104
133
|
const sources = [
|
|
105
|
-
...sourceReferences(snapshot.document),
|
|
134
|
+
...sourceReferences(snapshot.document, { repositoryId }),
|
|
106
135
|
...lensSelections(snapshot.lenses ?? []).flatMap((selection) =>
|
|
107
136
|
sourceAnchors(selection.source).map((source) => ({
|
|
108
137
|
...selection,
|
|
@@ -116,7 +145,7 @@ export async function exportShare(input: {
|
|
|
116
145
|
|
|
117
146
|
validateShareSources(snapshot);
|
|
118
147
|
|
|
119
|
-
if (
|
|
148
|
+
if ((pins || referenceCommits(stored).length) && !input.repository)
|
|
120
149
|
throw new ReviewInputError("Shared source pins require a repository.", 409);
|
|
121
150
|
|
|
122
151
|
for (const block of elements(snapshot.document)) {
|