@dev.fast/whiteboard 0.2.5-preview.20261013.113 → 0.2.5-preview.20261015.115
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/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
- package/dist/{agent-cli-04xbDj23.js → agent-cli-CR2PnA9G.js} +9 -12
- package/dist/agent-cli-CRPPmS3l.js +2 -0
- package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
- package/dist/{authoring-tools-fdA0RPIT.js → authoring-tools-BXKEwhfi.js} +5 -5
- package/dist/background-server-Cw7CcfaZ.js +156 -0
- package/dist/background-server-D2Rrr_e2.js +2 -0
- package/dist/build-info.json +1 -1
- package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
- package/dist/{cli-runner-C3men22A.js → cli-runner-PlGqBeMw.js} +368 -252
- package/dist/cli.js +3 -2
- package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
- package/dist/{process-error-telemetry-BCBYp2t4.js → headless-host-CVXTSIlx.js} +3285 -2585
- package/dist/headless-host-Cp37C-v0.js +2 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/{local-data-Dy1pNtLF.js → local-data-yfLp5PiJ.js} +113 -62
- package/dist/{mcp-B8kuRSXM.js → mcp-DBbDwG4y.js} +4 -4
- package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
- package/dist/profile-BSOkA6A-.js +2 -0
- package/dist/{profile-tbGWuWHu.js → profile-pAzg7fxC.js} +3 -3
- package/dist/remote-attach-BAt2eZx3.js +137 -0
- package/dist/remote-extensions-2Cxf3hMo.js +2 -0
- package/dist/remote-extensions-BiNx6NQK.js +323 -0
- package/dist/remote-language-server-DQgWUNeV.js +252 -0
- package/dist/remote-uninstall-DLtDG0o7.js +153 -0
- package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
- package/dist/review-app-launcher-D7I4DGoH.js +222 -0
- package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
- package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
- package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
- package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
- package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
- package/dist/server/desktop-host.js +1663 -266
- package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
- package/dist/sharing/index.d.ts +88 -32
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
- package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
- package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
- package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
- package/instructions/authoring.md +16 -12
- package/instructions/file-lenses.md +4 -2
- package/package.json +9 -14
- package/src/agent-connect.ts +507 -0
- package/src/agent-selection.ts +4 -0
- package/src/ask/agents.ts +59 -13
- package/src/ask/pi-mcp.ts +7 -3
- package/src/ask/thread-state.ts +9 -4
- package/src/ask/thread.ts +88 -50
- package/src/ask/threads.ts +53 -7
- package/src/cli-install.ts +3 -1
- package/src/cli-runner.ts +472 -81
- package/src/cli.ts +1 -1
- package/src/connect-agents.ts +285 -0
- package/src/connect-prompts.ts +60 -6
- package/src/desktop-discovery.ts +24 -0
- package/src/pinned-download.ts +118 -0
- package/src/remote-attach.ts +130 -0
- package/src/remote-extensions.ts +359 -0
- package/src/remote-language-server.ts +428 -0
- package/src/remote-toolchains.ts +107 -0
- package/src/remote-uninstall.ts +280 -0
- package/src/review-api/README.md +1 -3
- package/src/review-api/agent-cli.ts +4 -4
- package/src/review-api/agent-client.ts +23 -0
- package/src/review-api/ask-history.ts +31 -16
- package/src/review-api/authoring-tools.ts +1 -1
- package/src/review-api/blocks/index.ts +2 -4
- package/src/review-api/document.ts +1 -4
- package/src/review-api/http.ts +265 -237
- package/src/review-api/local-data.ts +18 -3
- package/src/review-api/onboarding.ts +478 -0
- package/src/review-api/origin.ts +7 -0
- package/src/review-api/recent-folders.ts +173 -0
- package/src/review-api/store.ts +56 -10
- package/src/review-api/workspaces.ts +56 -11
- package/src/review-app-launcher.ts +61 -3
- package/src/review-stack.ts +162 -36
- package/src/server/background-server.ts +250 -0
- package/src/server/desktop-host-shutdown.ts +24 -0
- package/src/server/desktop-host.ts +39 -24
- package/src/server/desktop-server.ts +68 -118
- package/src/server/global-verb-relay.ts +16 -0
- package/src/server/headless-host.ts +128 -3
- package/src/server/hono-http.ts +6 -1
- package/src/server/legacy-tutorial-cleanup.ts +38 -0
- package/src/server/review-gateway-hosts.ts +379 -0
- package/src/server/review-gateway-list.ts +109 -0
- package/src/server/review-gateway-memory.ts +130 -0
- package/src/server/review-gateway-pushes.ts +142 -0
- package/src/server/review-gateway-streams.ts +415 -0
- package/src/server/review-gateway-transport.ts +204 -0
- package/src/server/review-gateway.ts +433 -0
- package/src/server/review-server-core.ts +36 -6
- package/src/server-discovery.ts +11 -0
- package/src/sharing/import.ts +1 -0
- package/src/ui-telemetry-events.ts +5 -1
- package/src/vsix.ts +122 -0
- package/dist/agent-cli-BR_Jw5BL.js +0 -2
- package/dist/headless-host-BLLJ7Dyu.js +0 -95
- package/dist/profile-aMSw1oCM.js +0 -2
- package/dist/server-discovery-ZVi8Ay8J.js +0 -66
- package/src/fixtures/blocks/tutorial.json +0 -14
- package/src/review-api/blocks/tutorial.ts +0 -23
- package/src/server/tutorial-service.ts +0 -217
- package/tutorial/document.json +0 -216
- package/tutorial/lenses.json +0 -10
- package/tutorial/runtime-manifest.json +0 -11
- package/tutorial/sample-service/package.json +0 -9
- package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
- package/tutorial/sample-service/src/app.ts +0 -26
- package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
- package/tutorial/sample-service/src/database/schema.ts +0 -9
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
- package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
- package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
- package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
- package/tutorial/sample-service/src/orders/order-service.ts +0 -35
- package/tutorial/sample-service/src/orders/order.ts +0 -43
- package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
- package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
- package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
- package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
- package/tutorial/sample-service/tsconfig.json +0 -12
|
@@ -949,8 +949,8 @@ export class LocalReviewData {
|
|
|
949
949
|
await realpath(vcs.rootPath),
|
|
950
950
|
);
|
|
951
951
|
|
|
952
|
-
// Registration may follow replacement of a
|
|
953
|
-
//
|
|
952
|
+
// Registration may follow replacement of a repository at the same path.
|
|
953
|
+
// Reopen its Git reader too.
|
|
954
954
|
await this.closeReader(repository.id);
|
|
955
955
|
this.repositories.delete(repository.id);
|
|
956
956
|
|
|
@@ -1170,9 +1170,23 @@ export class LocalReviewData {
|
|
|
1170
1170
|
return {
|
|
1171
1171
|
target: { kind: "commits", repositoryId, head, base },
|
|
1172
1172
|
pins: { repositoryId, base, head },
|
|
1173
|
-
title: pullRequest.title.trim()
|
|
1173
|
+
title: pullRequest.title.trim(),
|
|
1174
1174
|
};
|
|
1175
1175
|
}
|
|
1176
|
+
async pullRequestTitle(url: string) {
|
|
1177
|
+
const { provider, address } = resolvePullRequest(url);
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
const { title } = await provider.readPullRequest(
|
|
1181
|
+
address,
|
|
1182
|
+
this.options.pullRequests ?? defaultPullRequestDeps,
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
return title.trim() || undefined;
|
|
1186
|
+
} catch {
|
|
1187
|
+
return undefined;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1176
1190
|
/** A registered checkout with a remote for host/owner/repo, and that remote. */
|
|
1177
1191
|
private async pullRequestCheckout(
|
|
1178
1192
|
provider: PullRequestProvider,
|
|
@@ -1914,6 +1928,7 @@ export function openLocalReviewStore(
|
|
|
1914
1928
|
resolveTarget: (target, pinned) => data.resolveTarget(target, pinned),
|
|
1915
1929
|
resolvePullRequest: (url, repository) =>
|
|
1916
1930
|
data.resolvePullRequest(url, repository),
|
|
1931
|
+
pullRequestTitle: (url) => data.pullRequestTitle(url),
|
|
1917
1932
|
headBranch: (pins, headRef) => data.headBranch(pins, headRef),
|
|
1918
1933
|
sourcePins: (snapshot) => data.sourcePins(snapshot),
|
|
1919
1934
|
unavailableAnchors: (snapshot) => data.unavailableAnchors(snapshot),
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type ReviewOnboardingProject,
|
|
9
|
+
ReviewOnboardingRunRequestSchema,
|
|
10
|
+
type ReviewOnboardingTarget,
|
|
11
|
+
type ReviewOnboardingTargetsResponse,
|
|
12
|
+
} from "@dev.fast/review-protocol";
|
|
13
|
+
import { askAgentIds } from "@review/ask/thread-state.js";
|
|
14
|
+
import type { AskThreadHooks } from "@review/ask/thread.js";
|
|
15
|
+
import { readBoundedRequestJson } from "@review/server/hono-http.js";
|
|
16
|
+
import type { Hono } from "hono";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
import type { AskRecord } from "./ask-history.js";
|
|
20
|
+
import { ReviewInputError } from "./document.js";
|
|
21
|
+
import type { AskHost } from "./http.js";
|
|
22
|
+
import { recentFolders } from "./recent-folders.js";
|
|
23
|
+
import type { ReviewStore } from "./store.js";
|
|
24
|
+
|
|
25
|
+
const COMMAND_TIMEOUT_MS = 15_000;
|
|
26
|
+
|
|
27
|
+
const PULL_REQUEST_LIMIT = 10;
|
|
28
|
+
|
|
29
|
+
/** Runs one subprocess and resolves its stdout; rejects on failure. */
|
|
30
|
+
export type OnboardingRun = (
|
|
31
|
+
file: string,
|
|
32
|
+
args: string[],
|
|
33
|
+
cwd: string,
|
|
34
|
+
) => Promise<string>;
|
|
35
|
+
|
|
36
|
+
const runCommand: OnboardingRun = (file, args, cwd) =>
|
|
37
|
+
new Promise((resolve, reject) => {
|
|
38
|
+
execFile(
|
|
39
|
+
file,
|
|
40
|
+
args,
|
|
41
|
+
{
|
|
42
|
+
cwd,
|
|
43
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
44
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
45
|
+
// Never wait on a credential prompt nobody can see.
|
|
46
|
+
env: {
|
|
47
|
+
...process.env,
|
|
48
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
49
|
+
GH_PROMPT_DISABLED: "1",
|
|
50
|
+
},
|
|
51
|
+
windowsHide: true,
|
|
52
|
+
},
|
|
53
|
+
(error, stdout) => (error ? reject(error) : resolve(stdout)),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** Scratch repositories, such as test fixtures, are nobody's project. */
|
|
58
|
+
const TEMPORARY_ROOTS = [os.tmpdir(), "/tmp", "/private/tmp", "/var/folders"];
|
|
59
|
+
|
|
60
|
+
const isInside = (roots: string[], folder: string) =>
|
|
61
|
+
roots.some((root) => {
|
|
62
|
+
const relative = path.relative(root, folder);
|
|
63
|
+
|
|
64
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
async function modified(file: string): Promise<number> {
|
|
68
|
+
try {
|
|
69
|
+
return (await stat(file)).mtimeMs;
|
|
70
|
+
} catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The repository a folder belongs to, and when it last saw Git activity.
|
|
76
|
+
* Worktrees resolve to their main checkout, which lists them as targets. */
|
|
77
|
+
async function repositoryOf(folder: string, run: OnboardingRun) {
|
|
78
|
+
try {
|
|
79
|
+
if (!(await stat(folder)).isDirectory()) return undefined;
|
|
80
|
+
|
|
81
|
+
const common = path.resolve(
|
|
82
|
+
folder,
|
|
83
|
+
(
|
|
84
|
+
await run(
|
|
85
|
+
"git",
|
|
86
|
+
["rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
87
|
+
folder,
|
|
88
|
+
)
|
|
89
|
+
).trim(),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// A bare repository has no checkout to draw from.
|
|
93
|
+
if (path.basename(common) !== ".git") return undefined;
|
|
94
|
+
|
|
95
|
+
const activity = await Promise.all(
|
|
96
|
+
["index", "FETCH_HEAD", path.join("logs", "HEAD")].map((file) =>
|
|
97
|
+
modified(path.join(common, file)),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return { root: path.dirname(common), active: Math.max(...activity) };
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const RESOLVE_CONCURRENCY = 8;
|
|
108
|
+
|
|
109
|
+
const PROJECT_LIMIT = 30;
|
|
110
|
+
|
|
111
|
+
/** The repositories behind folders Whiteboard, the user's agents, and their
|
|
112
|
+
* editors have worked in, most recently active first. */
|
|
113
|
+
export async function onboardingProjects(
|
|
114
|
+
folders: string[],
|
|
115
|
+
{
|
|
116
|
+
run = runCommand,
|
|
117
|
+
temporaryRoots = TEMPORARY_ROOTS,
|
|
118
|
+
}: { run?: OnboardingRun; temporaryRoots?: string[] } = {},
|
|
119
|
+
): Promise<ReviewOnboardingProject[]> {
|
|
120
|
+
const isTemporary = (folder: string) => isInside(temporaryRoots, folder);
|
|
121
|
+
const queue = [...new Set(folders)].filter((folder) => !isTemporary(folder));
|
|
122
|
+
const roots = new Map<string, number>();
|
|
123
|
+
|
|
124
|
+
await Promise.all(
|
|
125
|
+
Array.from({ length: RESOLVE_CONCURRENCY }, async () => {
|
|
126
|
+
for (let folder = queue.shift(); folder; folder = queue.shift()) {
|
|
127
|
+
const repository = await repositoryOf(folder, run);
|
|
128
|
+
|
|
129
|
+
if (repository && !isTemporary(repository.root))
|
|
130
|
+
roots.set(repository.root, repository.active);
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return [...roots]
|
|
136
|
+
.sort((a, b) => b[1] - a[1])
|
|
137
|
+
.slice(0, PROJECT_LIMIT)
|
|
138
|
+
.map(([root]) => ({ path: root, name: path.basename(root) }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface Worktree {
|
|
142
|
+
path: string;
|
|
143
|
+
branch?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `git worktree list --porcelain`: blocks of `worktree`, `HEAD`, `branch`. */
|
|
147
|
+
export function parseWorktrees(output: string): Worktree[] {
|
|
148
|
+
const worktrees: Worktree[] = [];
|
|
149
|
+
let current: Worktree | undefined;
|
|
150
|
+
|
|
151
|
+
for (const line of output.split("\n")) {
|
|
152
|
+
if (line.startsWith("worktree ")) {
|
|
153
|
+
current = { path: line.slice("worktree ".length) };
|
|
154
|
+
worktrees.push(current);
|
|
155
|
+
} else if (current && line.startsWith("branch "))
|
|
156
|
+
current.branch = line
|
|
157
|
+
.slice("branch ".length)
|
|
158
|
+
.replace(/^refs\/heads\//, "");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return worktrees;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const pullRequestsSchema = z.array(
|
|
165
|
+
z.object({
|
|
166
|
+
number: z.number().int().positive(),
|
|
167
|
+
title: z.string().min(1),
|
|
168
|
+
url: z.string().min(1),
|
|
169
|
+
author: z.object({ login: z.string() }).nullish(),
|
|
170
|
+
headRefName: z.string().optional(),
|
|
171
|
+
additions: z.number().int().nonnegative().optional(),
|
|
172
|
+
deletions: z.number().int().nonnegative().optional(),
|
|
173
|
+
updatedAt: z.string().optional(),
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
/** A target, and when it last moved, in milliseconds. */
|
|
178
|
+
interface Dated {
|
|
179
|
+
target: ReviewOnboardingTarget;
|
|
180
|
+
at: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** When each local branch last got a commit, in milliseconds. */
|
|
184
|
+
async function branchTimes(
|
|
185
|
+
root: string,
|
|
186
|
+
run: OnboardingRun,
|
|
187
|
+
): Promise<Map<string, number>> {
|
|
188
|
+
const output = await run(
|
|
189
|
+
"git",
|
|
190
|
+
[
|
|
191
|
+
"for-each-ref",
|
|
192
|
+
"--format=%(refname:short)%09%(committerdate:unix)",
|
|
193
|
+
"refs/heads",
|
|
194
|
+
],
|
|
195
|
+
root,
|
|
196
|
+
).catch(() => "");
|
|
197
|
+
|
|
198
|
+
return new Map(
|
|
199
|
+
output
|
|
200
|
+
.split("\n")
|
|
201
|
+
.map((line) => line.split("\t"))
|
|
202
|
+
.filter(([branch, seconds]) => branch && seconds)
|
|
203
|
+
.map(([branch, seconds]) => [branch, Number(seconds) * 1000]),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function defaultBase(root: string, run: OnboardingRun): Promise<string> {
|
|
208
|
+
try {
|
|
209
|
+
return (
|
|
210
|
+
await run(
|
|
211
|
+
"git",
|
|
212
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
213
|
+
root,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
.trim()
|
|
217
|
+
.replace(/^origin\//, "");
|
|
218
|
+
} catch {
|
|
219
|
+
return "main";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function pullRequests(
|
|
224
|
+
root: string,
|
|
225
|
+
run: OnboardingRun,
|
|
226
|
+
): Promise<Dated[]> {
|
|
227
|
+
try {
|
|
228
|
+
const listed = pullRequestsSchema.parse(
|
|
229
|
+
JSON.parse(
|
|
230
|
+
await run(
|
|
231
|
+
"gh",
|
|
232
|
+
[
|
|
233
|
+
"pr",
|
|
234
|
+
"list",
|
|
235
|
+
"--limit",
|
|
236
|
+
String(PULL_REQUEST_LIMIT),
|
|
237
|
+
// The most recently touched, not the most recently opened.
|
|
238
|
+
"--search",
|
|
239
|
+
"sort:updated-desc",
|
|
240
|
+
"--json",
|
|
241
|
+
"number,title,url,author,headRefName,additions,deletions,updatedAt",
|
|
242
|
+
],
|
|
243
|
+
root,
|
|
244
|
+
),
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return listed.map((pr) => ({
|
|
249
|
+
target: {
|
|
250
|
+
kind: "pr",
|
|
251
|
+
number: pr.number,
|
|
252
|
+
title: pr.title,
|
|
253
|
+
url: pr.url,
|
|
254
|
+
author: pr.author?.login ?? "",
|
|
255
|
+
branch: pr.headRefName ?? "",
|
|
256
|
+
additions: pr.additions ?? 0,
|
|
257
|
+
deletions: pr.deletions ?? 0,
|
|
258
|
+
},
|
|
259
|
+
at: pr.updatedAt ? Date.parse(pr.updatedAt) || 0 : 0,
|
|
260
|
+
}));
|
|
261
|
+
} catch {
|
|
262
|
+
// No gh, not signed in, or no GitHub remote: branches still work.
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** What a repository offers to draw: its checkout's branch, its other
|
|
268
|
+
* worktrees, and its open pull requests, most recently touched first. */
|
|
269
|
+
export async function onboardingTargets(
|
|
270
|
+
directory: string,
|
|
271
|
+
run: OnboardingRun = runCommand,
|
|
272
|
+
): Promise<ReviewOnboardingTargetsResponse> {
|
|
273
|
+
let root: string;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
root = (
|
|
277
|
+
await run("git", ["rev-parse", "--show-toplevel"], directory)
|
|
278
|
+
).trim();
|
|
279
|
+
} catch {
|
|
280
|
+
throw new ReviewInputError("Choose a folder inside a Git repository.", 400);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const [branch, worktreeList, base, prs, times] = await Promise.all([
|
|
284
|
+
run("git", ["branch", "--show-current"], root).then(
|
|
285
|
+
(output) => output.trim(),
|
|
286
|
+
() => "",
|
|
287
|
+
),
|
|
288
|
+
run("git", ["worktree", "list", "--porcelain"], root).catch(() => ""),
|
|
289
|
+
defaultBase(root, run),
|
|
290
|
+
pullRequests(root, run),
|
|
291
|
+
branchTimes(root, run),
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
const local = (target: ReviewOnboardingTarget & { branch: string }) => ({
|
|
295
|
+
target,
|
|
296
|
+
at: times.get(target.branch) ?? 0,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const worktrees = parseWorktrees(worktreeList)
|
|
300
|
+
.filter(
|
|
301
|
+
(worktree) =>
|
|
302
|
+
worktree.branch &&
|
|
303
|
+
path.resolve(worktree.path) !== path.resolve(root) &&
|
|
304
|
+
// Whiteboard's own checkouts live under the git dir.
|
|
305
|
+
!worktree.path.split(/[\\/]/).includes("dev-fast") &&
|
|
306
|
+
// Scratch checkouts, such as an agent's throwaway worktree.
|
|
307
|
+
!isInside(TEMPORARY_ROOTS, worktree.path),
|
|
308
|
+
)
|
|
309
|
+
.map((worktree) =>
|
|
310
|
+
local({
|
|
311
|
+
kind: "worktree",
|
|
312
|
+
path: worktree.path,
|
|
313
|
+
branch: worktree.branch ?? "",
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
project: { path: root, name: path.basename(root) },
|
|
319
|
+
base,
|
|
320
|
+
// A stable sort: what git and gh leave undated keeps their order.
|
|
321
|
+
targets: [
|
|
322
|
+
...(branch ? [local({ kind: "branch", path: root, branch })] : []),
|
|
323
|
+
...worktrees,
|
|
324
|
+
...prs,
|
|
325
|
+
]
|
|
326
|
+
.sort((a, b) => b.at - a.at)
|
|
327
|
+
.map(({ target }) => target),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** What the agent reads before the user's prompt: the whiteboard is made,
|
|
332
|
+
* and only needs drawing. */
|
|
333
|
+
async function runContext(
|
|
334
|
+
ask: AskHost,
|
|
335
|
+
agent: (typeof askAgentIds)[number],
|
|
336
|
+
threadId: string,
|
|
337
|
+
reviewId: string,
|
|
338
|
+
) {
|
|
339
|
+
const reach = await ask.threads.reach(agent, threadId);
|
|
340
|
+
|
|
341
|
+
return [
|
|
342
|
+
`The user asked for a whiteboard from inside the Whiteboard app. The app already made it, empty, and shows it beside this conversation: its sessionId is "${reviewId}". Draw into it; do not create another.`,
|
|
343
|
+
...(reach?.kind === "mcp"
|
|
344
|
+
? [
|
|
345
|
+
`Use the whiteboard MCP tools: read session_get_instructions({}) for the guidelines and each component's fields, then fill this whiteboard with session_edit({"sessionId":"${reviewId}", ...}).`,
|
|
346
|
+
]
|
|
347
|
+
: reach?.kind === "cli"
|
|
348
|
+
? [
|
|
349
|
+
`Whiteboard's CLI changes whiteboards from your shell: read \`${reach.command} api session_get_instructions '{}'\` for the guidelines and each component's fields, then fill this whiteboard with \`${reach.command} api session_edit '{"sessionId":"${reviewId}", ...}'\`. \`${reach.command} api tools\` lists each tool's input.`,
|
|
350
|
+
]
|
|
351
|
+
: []),
|
|
352
|
+
].join("\n");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** What starting a run borrows from the review API it is served by. */
|
|
356
|
+
export interface OnboardingRunHost {
|
|
357
|
+
/** Registers a checkout, for a whiteboard to target. */
|
|
358
|
+
register(root: string): Promise<{ id: string }>;
|
|
359
|
+
/** Where a thread in a review saves itself. */
|
|
360
|
+
historyHooks(record: Omit<AskRecord, "sessionId">): AskThreadHooks;
|
|
361
|
+
/** Queues a thread for its review's Ask panel to open. */
|
|
362
|
+
handOff(reviewId: string, threadId: string): void;
|
|
363
|
+
/** Shows a review in Desktop; reports, rather than throws, a failure. */
|
|
364
|
+
open(reviewId: string): Promise<{ opened: boolean }>;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** First run's Run step, and Home's New whiteboard: make the whiteboard, open
|
|
368
|
+
* it, and start an agent drawing it in its Ask panel. */
|
|
369
|
+
export function registerOnboardingRoutes(
|
|
370
|
+
app: Hono,
|
|
371
|
+
store: ReviewStore,
|
|
372
|
+
ask: AskHost,
|
|
373
|
+
host: OnboardingRunHost,
|
|
374
|
+
run: OnboardingRun = runCommand,
|
|
375
|
+
): void {
|
|
376
|
+
app.get("/onboarding/agents", async (context) =>
|
|
377
|
+
context.json({ agents: await ask.agents() }),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
app.get("/onboarding/projects", async (context) =>
|
|
381
|
+
context.json({
|
|
382
|
+
projects: await onboardingProjects(
|
|
383
|
+
[
|
|
384
|
+
...store.repositories().map((repository) => repository.path),
|
|
385
|
+
...(await recentFolders()),
|
|
386
|
+
],
|
|
387
|
+
{ run },
|
|
388
|
+
),
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
app.get("/onboarding/targets", async (context) => {
|
|
393
|
+
const directory = context.req.query("path");
|
|
394
|
+
|
|
395
|
+
if (!directory) throw new ReviewInputError("A folder is required.", 400);
|
|
396
|
+
|
|
397
|
+
return context.json(await onboardingTargets(directory, run));
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
app.post("/onboarding/runs", async (context) => {
|
|
401
|
+
const parsed = ReviewOnboardingRunRequestSchema.extend({
|
|
402
|
+
agent: z.enum(askAgentIds),
|
|
403
|
+
}).safeParse(await readBoundedRequestJson(context.req.raw));
|
|
404
|
+
|
|
405
|
+
if (!parsed.success) throw new ReviewInputError("Invalid run.", 400);
|
|
406
|
+
const input = parsed.data;
|
|
407
|
+
const { target } = input;
|
|
408
|
+
|
|
409
|
+
// A pull request is drawn from the project; a branch from its checkout.
|
|
410
|
+
const cwd = target.kind === "pr" ? input.projectPath : target.path;
|
|
411
|
+
let head: string;
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
head = (await run("git", ["rev-parse", "HEAD"], cwd)).trim();
|
|
415
|
+
} catch {
|
|
416
|
+
throw new ReviewInputError(
|
|
417
|
+
"Choose a folder inside a Git repository.",
|
|
418
|
+
400,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const repository = await host.register(cwd);
|
|
423
|
+
|
|
424
|
+
// An existing whiteboard for the same pull request comes back instead.
|
|
425
|
+
const created = await store.execute({
|
|
426
|
+
operation:
|
|
427
|
+
target.kind === "pr"
|
|
428
|
+
? {
|
|
429
|
+
type: "create",
|
|
430
|
+
pullRequestUrl: target.url,
|
|
431
|
+
repositoryId: repository.id,
|
|
432
|
+
}
|
|
433
|
+
: {
|
|
434
|
+
type: "create",
|
|
435
|
+
title: target.branch,
|
|
436
|
+
target: {
|
|
437
|
+
kind: "worktree",
|
|
438
|
+
repositoryId: repository.id,
|
|
439
|
+
...(input.base && { base: input.base }),
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const { reviewId } = created;
|
|
445
|
+
const title = store.read(reviewId).title;
|
|
446
|
+
const id = crypto.randomUUID();
|
|
447
|
+
const now = new Date().toISOString();
|
|
448
|
+
|
|
449
|
+
ask.threads.open({
|
|
450
|
+
id,
|
|
451
|
+
reviewId,
|
|
452
|
+
agent: input.agent,
|
|
453
|
+
...host.historyHooks({
|
|
454
|
+
id,
|
|
455
|
+
reviewId,
|
|
456
|
+
agent: input.agent,
|
|
457
|
+
version: created.version,
|
|
458
|
+
head,
|
|
459
|
+
cwd,
|
|
460
|
+
selection: { target: { kind: "review" }, title },
|
|
461
|
+
title,
|
|
462
|
+
createdAt: now,
|
|
463
|
+
updatedAt: now,
|
|
464
|
+
}),
|
|
465
|
+
cwd,
|
|
466
|
+
head,
|
|
467
|
+
selection: { title },
|
|
468
|
+
context: await runContext(ask, input.agent, id, reviewId),
|
|
469
|
+
question: { text: input.prompt },
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Before it opens, so its canvas finds the conversation waiting.
|
|
473
|
+
host.handOff(reviewId, id);
|
|
474
|
+
await host.open(reviewId);
|
|
475
|
+
|
|
476
|
+
return context.json({ reviewId });
|
|
477
|
+
});
|
|
478
|
+
}
|
package/src/review-api/origin.ts
CHANGED
|
@@ -20,6 +20,7 @@ export function pullRequestKey(url: string): string {
|
|
|
20
20
|
export function setPullRequest(
|
|
21
21
|
snapshot: Pick<Snapshot, "origin">,
|
|
22
22
|
url: string | null | undefined,
|
|
23
|
+
title?: string,
|
|
23
24
|
) {
|
|
24
25
|
if (url === undefined) return;
|
|
25
26
|
|
|
@@ -27,6 +28,7 @@ export function setPullRequest(
|
|
|
27
28
|
if (snapshot.origin) {
|
|
28
29
|
delete snapshot.origin.pullRequestUrl;
|
|
29
30
|
delete snapshot.origin.pullRequestNumber;
|
|
31
|
+
delete snapshot.origin.pullRequestTitle;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
return;
|
|
@@ -36,5 +38,10 @@ export function setPullRequest(
|
|
|
36
38
|
...snapshot.origin,
|
|
37
39
|
pullRequestUrl: url,
|
|
38
40
|
pullRequestNumber: Number(url.split("/").at(-1)),
|
|
41
|
+
pullRequestTitle:
|
|
42
|
+
title ??
|
|
43
|
+
(snapshot.origin?.pullRequestUrl === url
|
|
44
|
+
? snapshot.origin.pullRequestTitle
|
|
45
|
+
: undefined),
|
|
39
46
|
};
|
|
40
47
|
}
|