@dev.fast/whiteboard 0.3.3 → 0.3.4-preview.20261019.125
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-xOdmMnDH.js → account-alias-ClRtWF_-.js} +8 -6
- package/dist/{agent-cli-DT7MdaqL.js → agent-cli-C6AM9dml.js} +15 -10
- package/dist/agent-cli-DKpWo-4F.js +2 -0
- package/dist/{agent-client-BRBzv3AV.js → agent-client-G4KxB8zB.js} +13 -3
- package/dist/{authoring-tools-DGSEag8y.js → authoring-tools-Dfo0uOSj.js} +34 -27
- package/dist/{background-server-WKzI_a98.js → background-server-BF_lLFxY.js} +5 -5
- package/dist/{background-server-C_REWFkG.js → background-server-DJfYWRoN.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-BBSDi_dX.js → cli-CqOiiTW_.js} +1 -1
- package/dist/{cli-runner-DshTBske.js → cli-runner-D8CsXff4.js} +26 -26
- package/dist/cli.js +2 -2
- package/dist/{client-BN_FDxFr.js → client-lKVF_Elo.js} +253 -47
- package/dist/{fuzzy-match-bFX6wm8j.js → fuzzy-match-DH9lZivo.js} +42 -14
- package/dist/{headless-host-CS4Gv0c5.js → headless-host-C4bhVPZx.js} +513 -140
- package/dist/{headless-host-CzvYpKOX.js → headless-host-CeNnXxiL.js} +1 -1
- package/dist/lens-selection-CyJapcsl.js +214 -0
- package/dist/{local-data-DtQTIzHX.js → local-data-BdqqCYUD.js} +2434 -328
- package/dist/{mcp-DpKnnmHH.js → mcp-B2mvDEhC.js} +5 -9
- package/dist/{package-paths-jMg6JxDP.js → package-paths-Cm1tR8c4.js} +1 -1
- package/dist/{profile-DaqmODhc.js → profile-DcigfSms.js} +20 -8
- package/dist/profile-DthRnZ7x.js +2 -0
- package/dist/{remote-attach-DCngg8cl.js → remote-attach-DGwVRmY5.js} +4 -4
- package/dist/{remote-extensions-BCAbQZTv.js → remote-extensions-CferWwM9.js} +3 -3
- package/dist/{remote-extensions-Cf3L-ZGG.js → remote-extensions-D8W2IgKH.js} +1 -1
- package/dist/{remote-language-server-UWQgpoh-.js → remote-language-server-By-rEosC.js} +5 -5
- package/dist/{remote-uninstall-CcDOcGyi.js → remote-uninstall-DgHnDAFH.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-DDmfnytX.js} +38 -10
- package/dist/{review-app-launcher-tqSQPlbu.js → review-app-launcher-CG3TFZEx.js} +2 -2
- package/dist/{review-home-paths-Lc02WFuX.js → review-home-paths-D1JVitqN.js} +19 -17
- package/dist/{review-telemetry-BHWJsnoD.js → review-telemetry-Myzm7o6-.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-77-qkEhs.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-DDWWMh5x.js → s3-config-BTO12tCC.js} +1 -1
- package/dist/{s3-config-0xuCh-F_.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +29 -76
- package/dist/{server-discovery-BVqrE1_-.js → server-discovery-Bwa_6Tsj.js} +3 -3
- package/dist/sharing/index.d.ts +486 -70
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-BiWNbcF_.js} +89 -12
- package/dist/{src-BMPuxWv4.js → src-WPaQn75m.js} +208 -73
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-dFbewRTG.js} +270 -269
- package/dist/{thread-state-5pYr6_zM.js → thread-state-Do5LX10I.js} +7 -2
- package/dist/{tool-failure-BFt2xj9i.js → tool-failure-C9J3r7KD.js} +10 -3
- package/instructions/authoring.md +3 -2
- package/package.json +4 -4
- package/src/agent-connect.ts +8 -2
- package/src/ask/agents.ts +51 -9
- 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-runner.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 +3 -11
- 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/workspaces.ts +52 -13
- 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 +25 -9
- package/dist/agent-cli-DeCrUC2s.js +0 -2
- package/dist/profile-Cz7L781D.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
|
@@ -5,7 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
6
|
|
|
7
7
|
import { git, gitCommonDir } from "@dev.fast/local-vcs";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
errorMessage,
|
|
10
|
+
pidReused,
|
|
11
|
+
processIsAlive,
|
|
12
|
+
processStartIdentity,
|
|
13
|
+
} from "@dev.fast/trace-core";
|
|
9
14
|
import { reviewManagedCheckoutRoot } from "@review/review-checkout-paths.js";
|
|
10
15
|
import {
|
|
11
16
|
ensureReviewPinnedCheckout,
|
|
@@ -48,9 +53,6 @@ interface Environment extends WorkspaceStatus {
|
|
|
48
53
|
role: "base" | "head";
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
const OWNED_ELSEWHERE =
|
|
52
|
-
"Another Whiteboard server is managing this review's language workspaces. Retry once it stops.";
|
|
53
|
-
|
|
54
56
|
/** Local lifecycle only: source and authored history never depend on preparation.
|
|
55
57
|
* Status/queue/process lifecycle follows #334, retaining the legacy prepare config
|
|
56
58
|
* and per-review managed checkout layout instead of a new settings system.
|
|
@@ -75,11 +77,21 @@ export class ReviewWorkspaces {
|
|
|
75
77
|
this.db.exec(
|
|
76
78
|
"PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS pinned_environments(id TEXT PRIMARY KEY, value TEXT NOT NULL)",
|
|
77
79
|
);
|
|
80
|
+
|
|
78
81
|
// Desktops sharing a profile each own the reviews they prepare; the
|
|
79
82
|
// lease stops a second process from preparing or collecting them. The
|
|
80
83
|
// profile-wide lock it replaces only ever shipped in preview builds.
|
|
84
|
+
// Older leases have no start identity and may name a reused pid.
|
|
85
|
+
if (
|
|
86
|
+
!this.db
|
|
87
|
+
.prepare(
|
|
88
|
+
"SELECT 1 FROM pragma_table_info('workspace_leases') WHERE name='started'",
|
|
89
|
+
)
|
|
90
|
+
.get()
|
|
91
|
+
)
|
|
92
|
+
this.db.exec("DROP TABLE IF EXISTS workspace_leases");
|
|
81
93
|
this.db.exec(
|
|
82
|
-
"DROP TABLE IF EXISTS workspace_owner; CREATE TABLE IF NOT EXISTS workspace_leases(review_id TEXT PRIMARY KEY,owner TEXT NOT NULL,pid INTEGER NOT NULL)",
|
|
94
|
+
"DROP TABLE IF EXISTS workspace_owner; CREATE TABLE IF NOT EXISTS workspace_leases(review_id TEXT PRIMARY KEY,owner TEXT NOT NULL,pid INTEGER NOT NULL,started TEXT)",
|
|
83
95
|
);
|
|
84
96
|
this.db.exec("BEGIN IMMEDIATE");
|
|
85
97
|
|
|
@@ -284,7 +296,8 @@ export class ReviewWorkspaces {
|
|
|
284
296
|
}
|
|
285
297
|
|
|
286
298
|
async remove(reviewId: string) {
|
|
287
|
-
if (!this.claim(reviewId))
|
|
299
|
+
if (!this.claim(reviewId))
|
|
300
|
+
throw new ReviewInputError(this.ownedElsewhere(reviewId), 409);
|
|
288
301
|
await Promise.all(this.requests.values());
|
|
289
302
|
this.collect(undefined, reviewId);
|
|
290
303
|
await this.cleanup;
|
|
@@ -297,9 +310,19 @@ export class ReviewWorkspaces {
|
|
|
297
310
|
}
|
|
298
311
|
|
|
299
312
|
private lease(reviewId: string) {
|
|
300
|
-
|
|
301
|
-
.prepare(
|
|
313
|
+
const row = this.db
|
|
314
|
+
.prepare(
|
|
315
|
+
"SELECT owner,pid,started FROM workspace_leases WHERE review_id=?",
|
|
316
|
+
)
|
|
302
317
|
.get(reviewId);
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
row && {
|
|
321
|
+
owner: String(row.owner),
|
|
322
|
+
pid: Number(row.pid),
|
|
323
|
+
started: String(row.started ?? ""),
|
|
324
|
+
}
|
|
325
|
+
);
|
|
303
326
|
}
|
|
304
327
|
|
|
305
328
|
private leasedElsewhere(reviewId: string): boolean {
|
|
@@ -308,10 +331,20 @@ export class ReviewWorkspaces {
|
|
|
308
331
|
return Boolean(
|
|
309
332
|
lease &&
|
|
310
333
|
lease.owner !== this.ownerId &&
|
|
311
|
-
processIsAlive(
|
|
334
|
+
processIsAlive(lease.pid) &&
|
|
335
|
+
!pidReused(lease.pid, lease.started),
|
|
312
336
|
);
|
|
313
337
|
}
|
|
314
338
|
|
|
339
|
+
/** The holder may release its lease after `claim` fails. */
|
|
340
|
+
private ownedElsewhere(reviewId: string) {
|
|
341
|
+
const pid = this.lease(reviewId)?.pid;
|
|
342
|
+
|
|
343
|
+
return pid
|
|
344
|
+
? `Whiteboard process ${pid} is managing this review's language workspaces. Quit that Whiteboard Desktop or run \`whiteboard server stop\`, then retry.`
|
|
345
|
+
: "Another Whiteboard process was managing this review's language workspaces. Retry.";
|
|
346
|
+
}
|
|
347
|
+
|
|
315
348
|
/** Takes the review's lease unless another live Desktop holds it. */
|
|
316
349
|
private claim(reviewId: string): boolean {
|
|
317
350
|
// Status reads claim on every poll; holding the lease needs no write lock.
|
|
@@ -323,8 +356,13 @@ export class ReviewWorkspaces {
|
|
|
323
356
|
|
|
324
357
|
if (free)
|
|
325
358
|
this.db
|
|
326
|
-
.prepare("INSERT OR REPLACE INTO workspace_leases VALUES(
|
|
327
|
-
.run(
|
|
359
|
+
.prepare("INSERT OR REPLACE INTO workspace_leases VALUES(?,?,?,?)")
|
|
360
|
+
.run(
|
|
361
|
+
reviewId,
|
|
362
|
+
this.ownerId,
|
|
363
|
+
process.pid,
|
|
364
|
+
processStartIdentity(process.pid),
|
|
365
|
+
);
|
|
328
366
|
this.db.exec("COMMIT");
|
|
329
367
|
|
|
330
368
|
return free;
|
|
@@ -437,7 +475,7 @@ export class ReviewWorkspaces {
|
|
|
437
475
|
rootPath: null,
|
|
438
476
|
generation: "",
|
|
439
477
|
state: "pending",
|
|
440
|
-
log:
|
|
478
|
+
log: this.ownedElsewhere(reviewId),
|
|
441
479
|
},
|
|
442
480
|
);
|
|
443
481
|
}
|
|
@@ -607,7 +645,8 @@ export class ReviewWorkspaces {
|
|
|
607
645
|
|
|
608
646
|
if (this.jobs.has(id)) return this.status(environment);
|
|
609
647
|
|
|
610
|
-
if (!this.claim(reviewId))
|
|
648
|
+
if (!this.claim(reviewId))
|
|
649
|
+
throw new ReviewInputError(this.ownedElsewhere(reviewId), 409);
|
|
611
650
|
|
|
612
651
|
if (environment.state === "cleanup-failed") {
|
|
613
652
|
this.collect();
|
|
@@ -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
|
|
|
@@ -212,8 +212,6 @@ async function serve(input: HeadlessServerInput) {
|
|
|
212
212
|
token: discovery.token,
|
|
213
213
|
instanceId: discovery.instanceId,
|
|
214
214
|
softwareMapEnabled: input.softwareMapEnabled,
|
|
215
|
-
// The scratchpad is the laptop's alone, even with a Desktop attached.
|
|
216
|
-
scratchpad: () => false,
|
|
217
215
|
status: () => ({ key: "headless", home: input.stateDir }),
|
|
218
216
|
ask: {
|
|
219
217
|
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
|
|