@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
package/src/review-api/mcp.ts
CHANGED
|
@@ -29,17 +29,9 @@ import { type ToolFailure, toolFailure } from "./tool-failure.js";
|
|
|
29
29
|
const RELOAD_TOOLS =
|
|
30
30
|
"Whiteboard is running now, but this session listed Whiteboard's tools before it started, so you may see only session_get_instructions. Before authoring, reload the `whiteboard` MCP server's tools (reconnect it in your agent, or start a new agent session). If you can already see tools such as session_create, carry on.";
|
|
31
31
|
|
|
32
|
-
function mcpAuthoringGuidance(context: {
|
|
33
|
-
scratchpadAvailable: boolean;
|
|
34
|
-
traceEnabled: boolean;
|
|
35
|
-
}): string {
|
|
32
|
+
function mcpAuthoringGuidance(context: { traceEnabled: boolean }): string {
|
|
36
33
|
return [
|
|
37
34
|
"Whiteboard explains code in documents the user reads in Whiteboard Desktop. Call session_get_instructions before creating or editing a Whiteboard and follow it. Read session_capabilities before authoring; session_create opens the new review in Desktop when it is available, so call session_open only for an existing review, and generate software maps only when softwareMapEnabled is true. When the user asks for a Whiteboard or to use Whiteboard (for example to review a branch, a change or a pull request, or to explain a system in Whiteboard), author a Whiteboard with the default topic.",
|
|
38
|
-
...(context.scratchpadAvailable
|
|
39
|
-
? [
|
|
40
|
-
'When the user asks in conversation to be shown how code works or wants a diagram, without asking for a Whiteboard, call session_capabilities; if it reports scratchpadEnabled and desktopAvailable, draw on the Whiteboard scratchpad rather than answering only in chat, starting with session_get_instructions({topic:"scratchpad"}).',
|
|
41
|
-
]
|
|
42
|
-
: []),
|
|
43
35
|
...(context.traceEnabled
|
|
44
36
|
? [
|
|
45
37
|
'For why code exists, what an agent was thinking, or whether an agent solved something before, call session_get_instructions({topic:"trace-archaeology"}).',
|
|
@@ -66,7 +58,7 @@ export async function serveReviewMcp(
|
|
|
66
58
|
environmentAgent?: ReviewSessionAgent,
|
|
67
59
|
) {
|
|
68
60
|
const instructionsTool = {
|
|
69
|
-
...authoringTools(
|
|
61
|
+
...authoringTools(traceEnabled).find(
|
|
70
62
|
(tool) => tool.name === "review_get_instructions",
|
|
71
63
|
)!,
|
|
72
64
|
name: "session_get_instructions",
|
|
@@ -135,7 +127,7 @@ export async function serveReviewMcp(
|
|
|
135
127
|
// guidance on the discovery tool instead, including before Desktop starts.
|
|
136
128
|
description:
|
|
137
129
|
name === instructionsTool.name
|
|
138
|
-
? `${mcpAuthoringGuidance({
|
|
130
|
+
? `${mcpAuthoringGuidance({ traceEnabled })}\n\n${description}`
|
|
139
131
|
: description,
|
|
140
132
|
inputSchema,
|
|
141
133
|
}),
|
|
@@ -4,8 +4,11 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
4
4
|
|
|
5
5
|
import { withFileLock } from "@dev.fast/trace-core";
|
|
6
6
|
|
|
7
|
+
import { CheckoutGroups } from "./checkout-groups.js";
|
|
7
8
|
import { openLocalReviewStore } from "./local-data.js";
|
|
9
|
+
import { groupCheckoutsAhead } from "./store-migrations.js";
|
|
8
10
|
import { initializeReviewStoreSchema } from "./store-schema.js";
|
|
11
|
+
import { projectOfCheckout } from "./store.js";
|
|
9
12
|
|
|
10
13
|
const lockOptions = {
|
|
11
14
|
retryMs: 50,
|
|
@@ -16,7 +19,7 @@ const lockOptions = {
|
|
|
16
19
|
|
|
17
20
|
export async function openReviewProfile(
|
|
18
21
|
home: string,
|
|
19
|
-
options: { manageWorkspaces: boolean },
|
|
22
|
+
options: { manageWorkspaces: boolean; pullRequestStates?: boolean },
|
|
20
23
|
) {
|
|
21
24
|
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
22
25
|
|
|
@@ -30,7 +33,12 @@ export async function openReviewProfile(
|
|
|
30
33
|
])
|
|
31
34
|
await importHeadlessStore(home, source);
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
const database = path.join(home, "review-api.db");
|
|
37
|
+
|
|
38
|
+
return openLocalReviewStore(database, {
|
|
39
|
+
...options,
|
|
40
|
+
checkoutOwners: await groupCheckoutsAhead(database),
|
|
41
|
+
});
|
|
34
42
|
},
|
|
35
43
|
);
|
|
36
44
|
|
|
@@ -89,7 +97,7 @@ async function importHeadlessStore(home: string, source: string) {
|
|
|
89
97
|
`Cannot merge ${source}: the shared store already contains one of its review IDs. Both databases are unchanged.`,
|
|
90
98
|
);
|
|
91
99
|
database.exec(`
|
|
92
|
-
INSERT INTO repositories SELECT s
|
|
100
|
+
INSERT INTO repositories(id,path,name) SELECT s.id,s.path,s.name FROM headless.repositories s WHERE NOT EXISTS (SELECT 1 FROM repositories t WHERE t.path=s.path);
|
|
93
101
|
CREATE TEMP TABLE repository_ids AS SELECT s.id old_id,t.id new_id FROM headless.repositories s JOIN repositories t ON s.path=t.path;
|
|
94
102
|
`);
|
|
95
103
|
|
|
@@ -114,13 +122,36 @@ async function importHeadlessStore(home: string, source: string) {
|
|
|
114
122
|
`Cannot merge ${source}: a resource ID has different content. Both databases are unchanged.`,
|
|
115
123
|
);
|
|
116
124
|
|
|
125
|
+
if (
|
|
126
|
+
database
|
|
127
|
+
.prepare(
|
|
128
|
+
"SELECT 1 FROM pragma_table_info('reviews') WHERE name='project_id'",
|
|
129
|
+
)
|
|
130
|
+
.get()
|
|
131
|
+
) {
|
|
132
|
+
// A store with projects: file each board under its repository's,
|
|
133
|
+
// grouping the checkouts once for all of them.
|
|
134
|
+
const groups = new CheckoutGroups(database);
|
|
135
|
+
|
|
136
|
+
for (const { new_id: repositoryId } of database
|
|
137
|
+
.prepare("SELECT new_id FROM repository_ids")
|
|
138
|
+
.all())
|
|
139
|
+
projectOfCheckout(database, String(repositoryId), groups);
|
|
140
|
+
database.exec(`INSERT INTO reviews(id,version,next_id,project_id) SELECT s.id,s.version,s.next_id,p.project_id
|
|
141
|
+
FROM headless.reviews s JOIN headless.versions v ON v.review_id=s.id AND v.version=s.version
|
|
142
|
+
JOIN repository_ids r ON json_extract(v.snapshot,'$.pins.repositoryId')=r.old_id
|
|
143
|
+
JOIN repositories p ON p.id=r.new_id;`);
|
|
144
|
+
} else
|
|
145
|
+
database.exec(
|
|
146
|
+
"INSERT INTO reviews(id,version,next_id) SELECT id,version,next_id FROM headless.reviews;",
|
|
147
|
+
);
|
|
117
148
|
database.exec(`
|
|
118
149
|
INSERT OR IGNORE INTO resources SELECT s.id,r.new_id,s.kind,s.mime_type,s.data FROM headless.resources s JOIN repository_ids r ON s.repository_id=r.old_id;
|
|
119
|
-
INSERT INTO
|
|
120
|
-
INSERT INTO versions SELECT s.review_id,s.version,
|
|
150
|
+
INSERT INTO versions(review_id,version,snapshot,thread_id,author_kind) SELECT s.review_id,s.version,
|
|
121
151
|
CASE WHEN json_type(s.snapshot,'$.target')='object'
|
|
122
152
|
THEN json_set(s.snapshot,'$.pins.repositoryId',r.new_id,'$.target.repositoryId',r.new_id)
|
|
123
|
-
ELSE json_set(s.snapshot,'$.pins.repositoryId',r.new_id) END
|
|
153
|
+
ELSE json_set(s.snapshot,'$.pins.repositoryId',r.new_id) END,
|
|
154
|
+
NULL, NULL
|
|
124
155
|
FROM headless.versions s JOIN repository_ids r ON json_extract(s.snapshot,'$.pins.repositoryId')=r.old_id;
|
|
125
156
|
INSERT INTO review_attention SELECT * FROM headless.review_attention;
|
|
126
157
|
`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type JsonValue, isJsonObject, isStringValue } from "@dev.fast/json";
|
|
2
|
+
import { boardTitle } from "@dev.fast/review-protocol";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
type AuthoringTool,
|
|
@@ -78,7 +79,13 @@ export function publicResult(value: JsonValue): JsonValue {
|
|
|
78
79
|
|
|
79
80
|
return [
|
|
80
81
|
name,
|
|
81
|
-
key === "review" || key === "reviews"
|
|
82
|
+
key === "review" || key === "reviews"
|
|
83
|
+
? publicResult(item)
|
|
84
|
+
: // A board's title as every surface shows it: an empty one is
|
|
85
|
+
// "Untitled".
|
|
86
|
+
key === "title" && "reviewId" in value && isStringValue(item)
|
|
87
|
+
? boardTitle(item)
|
|
88
|
+
: item,
|
|
82
89
|
];
|
|
83
90
|
}),
|
|
84
91
|
);
|
|
@@ -98,5 +105,20 @@ export async function callPublicTool(
|
|
|
98
105
|
if (sessionId !== undefined) fields.reviewId = sessionId;
|
|
99
106
|
const result = await callAuthoringTool(client, tool, fields, signal);
|
|
100
107
|
|
|
101
|
-
return result instanceof ToolText
|
|
108
|
+
return result instanceof ToolText
|
|
109
|
+
? result
|
|
110
|
+
: withoutRepositoryIds(publicResult(result));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Agents never see repository ids. */
|
|
114
|
+
function withoutRepositoryIds(value: JsonValue): JsonValue {
|
|
115
|
+
if (Array.isArray(value)) return value.map(withoutRepositoryIds);
|
|
116
|
+
|
|
117
|
+
if (!isJsonObject(value)) return value;
|
|
118
|
+
|
|
119
|
+
return Object.fromEntries(
|
|
120
|
+
Object.entries(value)
|
|
121
|
+
.filter(([key]) => key !== "repositoryId")
|
|
122
|
+
.map(([key, item]) => [key, withoutRepositoryIds(item)]),
|
|
123
|
+
);
|
|
102
124
|
}
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type LocalVcsKind,
|
|
7
|
+
type VcsArg,
|
|
8
|
+
renderVcsArgs,
|
|
9
|
+
runVcs,
|
|
10
|
+
} from "@dev.fast/local-vcs";
|
|
11
|
+
import type { PullRequestState } from "@dev.fast/review-protocol";
|
|
5
12
|
import { errorMessage } from "@dev.fast/trace-core";
|
|
6
13
|
|
|
7
14
|
import { ReviewInputError } from "./document.js";
|
|
8
15
|
|
|
9
|
-
/** Runs one subprocess and resolves its stdout; rejects on failure or
|
|
16
|
+
/** Runs one subprocess and resolves its stdout; rejects on failure or
|
|
17
|
+
* timeout. git, jj and gh go through the VCS runner; Azure DevOps's az (by
|
|
18
|
+
* way of cmd.exe on Windows) gets its operands checked the same way. */
|
|
10
19
|
export type RunCommand = (
|
|
11
|
-
file:
|
|
12
|
-
args:
|
|
20
|
+
file: "git" | "jj" | "gh" | "az" | "cmd.exe",
|
|
21
|
+
args: VcsArg[],
|
|
13
22
|
options: { cwd?: string; timeoutMs: number },
|
|
14
23
|
) => Promise<string>;
|
|
15
24
|
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
|
|
16
27
|
export interface PullRequestDeps {
|
|
17
28
|
run: RunCommand;
|
|
18
29
|
fetch: typeof fetch;
|
|
@@ -31,6 +42,7 @@ export interface PullRequestRecord {
|
|
|
31
42
|
headBranch?: string;
|
|
32
43
|
/** GitHub's base commit, frozen at the PR's last update. */
|
|
33
44
|
baseRefOid?: string;
|
|
45
|
+
state?: PullRequestState;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
const FETCH_TIMEOUT_MS = 120_000;
|
|
@@ -38,35 +50,38 @@ const FETCH_TIMEOUT_MS = 120_000;
|
|
|
38
50
|
const LOCAL_TIMEOUT_MS = 30_000;
|
|
39
51
|
|
|
40
52
|
export const defaultPullRequestDeps: PullRequestDeps = {
|
|
41
|
-
run: (file, args, options) =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
run: async (file, args, options) => {
|
|
54
|
+
const spawnOptions = {
|
|
55
|
+
cwd: options.cwd,
|
|
56
|
+
timeout: options.timeoutMs,
|
|
57
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
58
|
+
// Never wait on ssh asking for a passphrase nobody can see.
|
|
59
|
+
env: {
|
|
60
|
+
...process.env,
|
|
61
|
+
GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return (
|
|
67
|
+
file === "az" || file === "cmd.exe"
|
|
68
|
+
? await execFileAsync(file, renderVcsArgs(args), {
|
|
69
|
+
...spawnOptions,
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
})
|
|
72
|
+
: await runVcs(file, args, spawnOptions)
|
|
73
|
+
).stdout;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// SAFETY: execFile rejects with an ExecFileException carrying stderr.
|
|
76
|
+
const failure = error as { killed?: boolean; stderr?: string };
|
|
77
|
+
|
|
78
|
+
const reason = failure.killed
|
|
79
|
+
? `timed out after ${options.timeoutMs / 1000}s`
|
|
80
|
+
: String(failure.stderr ?? "").trim() || errorMessage(error);
|
|
81
|
+
|
|
82
|
+
throw new Error(`${file} ${String(args[0])}: ${reason}`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
70
85
|
fetch: (input, init) => fetch(input, init),
|
|
71
86
|
};
|
|
72
87
|
|
|
@@ -135,55 +150,62 @@ export async function fetchPullRequest(
|
|
|
135
150
|
const { pullRequest: pr } = input;
|
|
136
151
|
const refs = pullRequestRefs(input.provider, pr);
|
|
137
152
|
|
|
138
|
-
const git = (args:
|
|
153
|
+
const git = (args: VcsArg[], timeoutMs = LOCAL_TIMEOUT_MS) =>
|
|
139
154
|
deps
|
|
140
155
|
.run("git", ["--git-dir", input.gitDir, ...args], { timeoutMs })
|
|
141
156
|
.then((stdout) => stdout.trim());
|
|
142
157
|
|
|
143
|
-
const fetchRefs = (...refspecs:
|
|
158
|
+
const fetchRefs = (...refspecs: VcsArg[]) =>
|
|
144
159
|
git(
|
|
145
160
|
[
|
|
146
161
|
"fetch",
|
|
147
162
|
"--no-tags",
|
|
148
163
|
"--no-write-fetch-head",
|
|
149
164
|
"--quiet",
|
|
150
|
-
input.remote,
|
|
165
|
+
{ remote: input.remote },
|
|
151
166
|
...refspecs,
|
|
152
167
|
],
|
|
153
168
|
FETCH_TIMEOUT_MS,
|
|
154
169
|
);
|
|
155
170
|
|
|
156
171
|
const commit = (ref: string) =>
|
|
157
|
-
git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).catch(
|
|
172
|
+
git(["rev-parse", "--verify", "--quiet", { rev: `${ref}^{commit}` }]).catch(
|
|
158
173
|
() => undefined,
|
|
159
174
|
);
|
|
160
175
|
|
|
161
176
|
const mergeBase = (left: string, right: string) =>
|
|
162
|
-
git(["merge-base", left, right]).catch(() => undefined);
|
|
177
|
+
git(["merge-base", { rev: left }, { rev: right }]).catch(() => undefined);
|
|
163
178
|
|
|
164
179
|
const frozenBase = async () => {
|
|
165
180
|
if (!pr.baseRefOid) return undefined;
|
|
166
181
|
|
|
167
182
|
if (!(await commit(pr.baseRefOid)))
|
|
168
|
-
await fetchRefs(
|
|
169
|
-
|
|
170
|
-
);
|
|
183
|
+
await fetchRefs({
|
|
184
|
+
refspec: { from: pr.baseRefOid, to: refs.frozenBase, force: true },
|
|
185
|
+
}).catch(() => undefined);
|
|
171
186
|
|
|
172
187
|
return commit(pr.baseRefOid);
|
|
173
188
|
};
|
|
174
189
|
|
|
175
190
|
let baseTip: string | undefined;
|
|
176
191
|
|
|
192
|
+
const pullHead: VcsArg = {
|
|
193
|
+
refspec: { from: pr.headRef, to: refs.head, force: true },
|
|
194
|
+
};
|
|
195
|
+
|
|
177
196
|
try {
|
|
178
|
-
await fetchRefs(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
await fetchRefs(pullHead, {
|
|
198
|
+
refspec: {
|
|
199
|
+
from: `refs/heads/${pr.baseRefName}`,
|
|
200
|
+
to: refs.base,
|
|
201
|
+
force: true,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
182
204
|
baseTip = await commit(refs.base);
|
|
183
205
|
} catch (error) {
|
|
184
206
|
// The base branch may be gone; the head and the frozen base suffice.
|
|
185
207
|
try {
|
|
186
|
-
await fetchRefs(
|
|
208
|
+
await fetchRefs(pullHead);
|
|
187
209
|
} catch {
|
|
188
210
|
throw new ReviewInputError(
|
|
189
211
|
`Could not fetch PR #${pr.number} from remote "${input.remote}" (${pr.slug}). Check \`git fetch ${input.remote}\` works with your Git credentials, then retry. Git said: ${firstLine(errorMessage(error))}`,
|
|
@@ -232,7 +254,7 @@ export async function fetchPullRequest(
|
|
|
232
254
|
async function indexForJj(
|
|
233
255
|
rootPath: string,
|
|
234
256
|
commits: string[],
|
|
235
|
-
git: (args:
|
|
257
|
+
git: (args: VcsArg[]) => Promise<string>,
|
|
236
258
|
deps: PullRequestDeps,
|
|
237
259
|
) {
|
|
238
260
|
const tags = commits.map(() => `refs/tags/review-index-${randomUUID()}`);
|
|
@@ -246,7 +268,7 @@ async function indexForJj(
|
|
|
246
268
|
|
|
247
269
|
try {
|
|
248
270
|
for (const [index, tag] of tags.entries())
|
|
249
|
-
await git(["update-ref", tag, commits[index]!]);
|
|
271
|
+
await git(["update-ref", { ref: tag }, { commit: commits[index]! }]);
|
|
250
272
|
await jjImport();
|
|
251
273
|
} finally {
|
|
252
274
|
for (const tag of tags)
|
|
@@ -255,6 +277,17 @@ async function indexForJj(
|
|
|
255
277
|
}
|
|
256
278
|
}
|
|
257
279
|
|
|
280
|
+
/** GitHub refused a read, as it does past a rate limit; it takes reads
|
|
281
|
+
* again at `retryAt`. */
|
|
282
|
+
export class GitHubRefusedRead extends ReviewInputError {
|
|
283
|
+
constructor(
|
|
284
|
+
message: string,
|
|
285
|
+
readonly retryAt: number,
|
|
286
|
+
) {
|
|
287
|
+
super(message, 409);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
258
291
|
export function firstLine(text: string) {
|
|
259
292
|
return text.split("\n")[0]!.slice(0, 300);
|
|
260
293
|
}
|
|
@@ -13,6 +13,16 @@ export const REVIEW_VIA_HEADER = "x-review-via";
|
|
|
13
13
|
|
|
14
14
|
export const REVIEW_AGENT_HEADER = "x-review-agent";
|
|
15
15
|
|
|
16
|
+
/** The Ask thread a request comes from. The CLI reads it from ASK_THREAD_ENV,
|
|
17
|
+
* which Desktop sets for the thread's agent. */
|
|
18
|
+
export const REVIEW_THREAD_HEADER = "x-review-thread";
|
|
19
|
+
|
|
20
|
+
export const ASK_THREAD_ENV = "DEV_REVIEW_ASK_THREAD";
|
|
21
|
+
|
|
22
|
+
/** Set by Desktop and the canvas on a command a person gave, such as a new
|
|
23
|
+
* board from the sidebar; its version is credited to them. */
|
|
24
|
+
export const REVIEW_PERSON_HEADER = "x-review-person";
|
|
25
|
+
|
|
16
26
|
export type ReviewRequestVia = "api" | "mcp" | "other";
|
|
17
27
|
|
|
18
28
|
const viaSchema = z.enum(["api", "mcp", "other"]).catch("other");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type LensSource,
|
|
3
|
+
SelectionUnavailableError,
|
|
4
|
+
type SelectionUnavailableReason,
|
|
3
5
|
comparisonKey,
|
|
4
6
|
resolveDiffSelection,
|
|
5
7
|
selectionKey,
|
|
@@ -15,8 +17,15 @@ import {
|
|
|
15
17
|
UNCATEGORIZED_LENS_ID,
|
|
16
18
|
lensSelections,
|
|
17
19
|
} from "./diff-lenses.js";
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type Pins,
|
|
22
|
+
anchorPins,
|
|
23
|
+
boardRepositoryId,
|
|
24
|
+
selectionReferences,
|
|
25
|
+
} from "./document.js";
|
|
19
26
|
import { resolveFileLens, uncategorizedSources } from "./file-lenses.js";
|
|
27
|
+
import { ReviewInputError } from "./input-error.js";
|
|
28
|
+
import { textualRows } from "./lens-alignment.js";
|
|
20
29
|
import type { LocalReviewData } from "./local-data.js";
|
|
21
30
|
import type { ReviewStore, Snapshot } from "./store.js";
|
|
22
31
|
|
|
@@ -40,7 +49,7 @@ export const progressUpdateSchema = z.strictObject({
|
|
|
40
49
|
|
|
41
50
|
export interface ReviewProgress {
|
|
42
51
|
complete?: boolean;
|
|
43
|
-
unavailableSelections?: Record<string,
|
|
52
|
+
unavailableSelections?: Record<string, SelectionUnavailableReason>;
|
|
44
53
|
files: CoverageFile[];
|
|
45
54
|
/** Changed files of the comparisons that references with their own pins
|
|
46
55
|
* named, by `comparisonKey` and then path. They are not the document's
|
|
@@ -130,7 +139,7 @@ export async function reviewProgress(
|
|
|
130
139
|
const alignments = new Map(comparison.alignments);
|
|
131
140
|
const resolvedSelections: Record<string, FileLineRange[]> = {};
|
|
132
141
|
|
|
133
|
-
const unavailableSelections: Record<string,
|
|
142
|
+
const unavailableSelections: Record<string, SelectionUnavailableReason> = {};
|
|
134
143
|
const pendingSources = new Set<string>();
|
|
135
144
|
|
|
136
145
|
const resolve = async (source: LensSource): Promise<FileLineRange[]> => {
|
|
@@ -169,7 +178,8 @@ export async function reviewProgress(
|
|
|
169
178
|
let rows = file && ownAlignments.get(file.path);
|
|
170
179
|
|
|
171
180
|
// Context-only files are absent from the changed-file stream. Read both pins
|
|
172
|
-
// and require equality rather than inventing correspondence for missing
|
|
181
|
+
// and require equality rather than inventing correspondence for missing
|
|
182
|
+
// diffs; a file on one side only, such as an untracked one, is that side.
|
|
173
183
|
if (!file && counting && !own) {
|
|
174
184
|
pendingSources.add(key);
|
|
175
185
|
|
|
@@ -177,20 +187,43 @@ export async function reviewProgress(
|
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
if (!file) {
|
|
180
|
-
const [base, head] = await Promise.all(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
const [base, head] = await Promise.all(
|
|
191
|
+
(["base", "head"] as const).map((side) =>
|
|
192
|
+
data.file(sourcePins, side, source.file).then(
|
|
193
|
+
(value) => value.text,
|
|
194
|
+
(error) => {
|
|
195
|
+
if (error instanceof ReviewInputError && error.status === 404)
|
|
196
|
+
return null;
|
|
197
|
+
throw error;
|
|
198
|
+
},
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (base === null && head === null)
|
|
204
|
+
throw new SelectionUnavailableError(
|
|
205
|
+
"missing_file",
|
|
206
|
+
"File is unavailable at these pins.",
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (base !== null && head !== null && base !== head)
|
|
210
|
+
throw new SelectionUnavailableError(
|
|
211
|
+
"no_alignment",
|
|
212
|
+
"Changed context file has no alignment.",
|
|
213
|
+
);
|
|
214
|
+
rows = textualRows(
|
|
215
|
+
source.file,
|
|
216
|
+
"",
|
|
217
|
+
base?.split("\n").length ?? 0,
|
|
218
|
+
head?.split("\n").length ?? 0,
|
|
190
219
|
);
|
|
191
220
|
}
|
|
192
221
|
|
|
193
|
-
if (!rows)
|
|
222
|
+
if (!rows)
|
|
223
|
+
throw new SelectionUnavailableError(
|
|
224
|
+
"no_alignment",
|
|
225
|
+
"File has no text alignment.",
|
|
226
|
+
);
|
|
194
227
|
|
|
195
228
|
const result = resolveDiffSelection(
|
|
196
229
|
source,
|
|
@@ -206,9 +239,9 @@ export async function reviewProgress(
|
|
|
206
239
|
const resolveAvailable = async (source: LensSource) => {
|
|
207
240
|
try {
|
|
208
241
|
return await resolve(source);
|
|
209
|
-
} catch {
|
|
242
|
+
} catch (error) {
|
|
210
243
|
unavailableSelections[selectionKey(source)] =
|
|
211
|
-
|
|
244
|
+
error instanceof SelectionUnavailableError ? error.reason : "other";
|
|
212
245
|
|
|
213
246
|
return [];
|
|
214
247
|
}
|
|
@@ -254,15 +287,16 @@ export async function reviewProgress(
|
|
|
254
287
|
|
|
255
288
|
// Peeks share alignment resolution without claiming lens coverage.
|
|
256
289
|
await Promise.all(
|
|
257
|
-
selectionReferences(snapshot.document, {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
290
|
+
selectionReferences(snapshot.document, {
|
|
291
|
+
tolerant: true,
|
|
292
|
+
repositoryId: boardRepositoryId(snapshot),
|
|
293
|
+
}).map(async ({ source }) => {
|
|
294
|
+
try {
|
|
295
|
+
await resolveAvailable(source);
|
|
296
|
+
} catch {
|
|
297
|
+
/* An unavailable peek does not hide other content. */
|
|
298
|
+
}
|
|
299
|
+
}),
|
|
266
300
|
);
|
|
267
301
|
|
|
268
302
|
const uncategorized = counting
|
|
@@ -299,7 +333,7 @@ export async function reviewProgress(
|
|
|
299
333
|
});
|
|
300
334
|
|
|
301
335
|
const progress: ReviewProgress = {
|
|
302
|
-
complete: !counting,
|
|
336
|
+
complete: !counting && !pendingSources.size,
|
|
303
337
|
files,
|
|
304
338
|
referenceFiles,
|
|
305
339
|
lenses,
|