@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { M as ReviewSelectedDiffSchema, T as ReviewApiSelectionSourceSchema } from "./src-BiWNbcF_.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
/** A semantic selection, independent of comment/thread creation. */
|
|
4
4
|
const AgentSelectionSchema = z.strictObject({
|
|
@@ -374,7 +374,12 @@ const askHistoryEntrySchema = z.object({
|
|
|
374
374
|
/** The commit the agent read. */
|
|
375
375
|
head: z.string(),
|
|
376
376
|
createdAt: z.string(),
|
|
377
|
-
updatedAt: z.string()
|
|
377
|
+
updatedAt: z.string(),
|
|
378
|
+
/** The board the conversation started on, and the block if it was about one. */
|
|
379
|
+
anchor: z.object({
|
|
380
|
+
board: z.string(),
|
|
381
|
+
block: z.string().optional()
|
|
382
|
+
}).optional()
|
|
378
383
|
});
|
|
379
384
|
//#endregion
|
|
380
385
|
export { askHistoryEntrySchema as a, askPicksSchema as c, selectionMarkdown as d, askEntrySchema as i, askQuestionSchema as l, askAgentIds as n, askOfferSchema as o, askChoiceKinds as r, askPermissionsSchema as s, applyAskChange as t, AgentSelectionSchema as u };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
|
|
2
|
-
import {
|
|
2
|
+
import { l as boardTitle } from "./src-BiWNbcF_.js";
|
|
3
|
+
import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-G4KxB8zB.js";
|
|
3
4
|
import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
|
|
4
5
|
//#region src/review-api/public-tools.ts
|
|
5
6
|
function publicDescription(text) {
|
|
@@ -38,7 +39,7 @@ function publicResult(value) {
|
|
|
38
39
|
if (Array.isArray(value)) return value.map(publicResult);
|
|
39
40
|
if (!isJsonObject(value)) return value;
|
|
40
41
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
41
|
-
return [key === "reviewId" ? "sessionId" : key === "review" ? "session" : key === "reviews" ? "sessions" : key, key === "review" || key === "reviews" ? publicResult(item) : item];
|
|
42
|
+
return [key === "reviewId" ? "sessionId" : key === "review" ? "session" : key === "reviews" ? "sessions" : key, key === "review" || key === "reviews" ? publicResult(item) : key === "title" && "reviewId" in value && isStringValue(item) ? boardTitle(item) : item];
|
|
42
43
|
}));
|
|
43
44
|
}
|
|
44
45
|
async function callPublicTool(client, tool, input, signal) {
|
|
@@ -47,7 +48,13 @@ async function callPublicTool(client, tool, input, signal) {
|
|
|
47
48
|
const fields = { ...rest };
|
|
48
49
|
if (sessionId !== void 0) fields.reviewId = sessionId;
|
|
49
50
|
const result = await callAuthoringTool(client, tool, fields, signal);
|
|
50
|
-
return result instanceof ToolText ? result : publicResult(result);
|
|
51
|
+
return result instanceof ToolText ? result : withoutRepositoryIds(publicResult(result));
|
|
52
|
+
}
|
|
53
|
+
/** Agents never see repository ids. */
|
|
54
|
+
function withoutRepositoryIds(value) {
|
|
55
|
+
if (Array.isArray(value)) return value.map(withoutRepositoryIds);
|
|
56
|
+
if (!isJsonObject(value)) return value;
|
|
57
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => key !== "repositoryId").map(([key, item]) => [key, withoutRepositoryIds(item)]));
|
|
51
58
|
}
|
|
52
59
|
//#endregion
|
|
53
60
|
//#region src/review-api/recovery.ts
|
|
@@ -3,7 +3,7 @@ you are writing an interactive rfc-style whiteboard, for consumption by a staff
|
|
|
3
3
|
**flow**
|
|
4
4
|
follow these first five steps in order, with no tool calls beyond what they need: loading the tools they use (`session_create`, `session_activity_begin`, `session_edit`, `session_get`, and later `session_activity_update` and `session_activity_end`) and, for a PR, reading its description (`gh pr view`).
|
|
5
5
|
|
|
6
|
-
- create the whiteboard
|
|
6
|
+
- create the whiteboard with a `target` naming the checkout by its `repositoryPath`: `{kind:"commits", head, base?}` for commits, `{kind:"pullRequest", pullRequest}` for a PR by URL or number, or `{kind:"project"}` for a plan with no change yet. if they name none, use a `worktree` target with no `base`: it reviews the checkout, uncommitted work included, against the default branch. for uncommitted work only, pass `base: "HEAD"`. untracked files are left out: run `git add -N <file>` on new files you want reviewed
|
|
7
7
|
- trigger a subagent with this exact instruction: "Call `session_get_instructions({topic:\"file-lenses\"})` and follow it for whiteboard <sessionId>, whose change is `<base>..<head>` in `<repositoryPath>`."
|
|
8
8
|
- call `session_activity_begin` and pass its `activityId` on every `session_edit`
|
|
9
9
|
- read the diff with git (or jj) in `repositoryPath`, at the commits `session_create` returned: `git diff <base> <head>`, or for a worktree target `git diff <base>` plus `git status` for untracked files. if it shows no changes, the target is wrong: fix it with `session_set_target` before writing
|
|
@@ -37,4 +37,5 @@ follow these first five steps in order, with no tool calls beyond what they need
|
|
|
37
37
|
- use `session_activity_update` to provide regular status updates on your area of focus.
|
|
38
38
|
- keep whiteboards short and sweet when possible (esp. for small changes.) feel free to omit sections.
|
|
39
39
|
- when something (a phrase in the prose, diagram node, etc.) describes actual code in the codebase, always default to attaching/hyperlink code.
|
|
40
|
-
- Link repository code as `[label](
|
|
40
|
+
- Link repository code as `[label](whiteboard:source/head/src/file.ts#L10-L24)`; use `base` for old code. To quote any other commit of the board's repository, as a plan does, put the commit in place of `head`: `[label](whiteboard:source/<commit>/src/file.ts#L10-L24)`. Use repository-relative paths and verified line numbers.
|
|
41
|
+
- Link another whiteboard inline as `[label](whiteboard:board/<sessionId>)`, or one of its blocks as `[label](whiteboard:board/<sessionId>#<blockId>)`, wherever the prose refers to it. The board and block must exist.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev.fast/whiteboard",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4-preview.20261019.125",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"@dev.fast/json": "0.0.0",
|
|
78
78
|
"@dev.fast/review-protocol": "0.0.0",
|
|
79
79
|
"@dev.fast/review-share-protocol": "0.1.2",
|
|
80
|
-
"@dev.fast/trace-
|
|
81
|
-
"@dev.fast/trace-
|
|
80
|
+
"@dev.fast/trace-core": "0.0.0",
|
|
81
|
+
"@dev.fast/trace-protocol": "0.5.0"
|
|
82
82
|
},
|
|
83
83
|
"engines": {
|
|
84
84
|
"node": ">=24 <25"
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"repo": "devdotfast/review",
|
|
89
89
|
"docs": "packages/review/instructions/authoring.md"
|
|
90
90
|
},
|
|
91
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "fc51298c9ea617b6624dcbf7416793fa22451afc",
|
|
92
92
|
"scripts": {
|
|
93
93
|
"prebuild": "pnpm run build:workspace-deps",
|
|
94
94
|
"build": "node --max-old-space-size=8192 node_modules/tsdown/dist/run.mjs",
|
package/src/agent-connect.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ReviewConnectResult,
|
|
10
10
|
} from "@dev.fast/review-protocol";
|
|
11
11
|
|
|
12
|
-
import { findExecutable } from "./ask/agents.js";
|
|
12
|
+
import { executableCommand, findExecutable } from "./ask/agents.js";
|
|
13
13
|
import {
|
|
14
14
|
type McpLaunch,
|
|
15
15
|
WHITEBOARD_PLUGIN as PLUGIN,
|
|
@@ -72,12 +72,18 @@ export type ConnectRun = (
|
|
|
72
72
|
args: string[],
|
|
73
73
|
) => Promise<{ code: number; output: string }>;
|
|
74
74
|
|
|
75
|
-
const runProcess: ConnectRun = (
|
|
75
|
+
const runProcess: ConnectRun = (executable, executableArgs) =>
|
|
76
76
|
new Promise((resolve) => {
|
|
77
|
+
const { file, args, windowsVerbatimArguments } = executableCommand(
|
|
78
|
+
executable,
|
|
79
|
+
executableArgs,
|
|
80
|
+
);
|
|
81
|
+
|
|
77
82
|
execFile(
|
|
78
83
|
file,
|
|
79
84
|
args,
|
|
80
85
|
{
|
|
86
|
+
windowsVerbatimArguments,
|
|
81
87
|
timeout: STEP_TIMEOUT_MS,
|
|
82
88
|
maxBuffer: 4 * 1024 * 1024,
|
|
83
89
|
// A plugin install must never wait on a prompt nobody can see.
|
package/src/ask/agents.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type AskPermissions,
|
|
20
20
|
askAgentIds,
|
|
21
21
|
} from "@review/ask/thread-state.js";
|
|
22
|
+
import { ASK_THREAD_ENV } from "@review/review-api/request-origin.js";
|
|
22
23
|
|
|
23
24
|
/** How Whiteboard runs an agent that speaks ACP on stdio. */
|
|
24
25
|
type AskAgentLaunch =
|
|
@@ -113,6 +114,7 @@ export const askAgents: Record<AskAgentId, AskAgentSpec> = {
|
|
|
113
114
|
claude: {
|
|
114
115
|
name: "Claude Code",
|
|
115
116
|
commands: ["claude"],
|
|
117
|
+
installDirs: [".claude/local", ".local/bin"],
|
|
116
118
|
launch: {
|
|
117
119
|
adapter: "@agentclientprotocol/claude-agent-acp/dist/index.js",
|
|
118
120
|
executableEnv: "CLAUDE_CODE_EXECUTABLE",
|
|
@@ -246,34 +248,62 @@ export type AskAgentLauncher = (
|
|
|
246
248
|
/** The MCP servers its sessions are given. */
|
|
247
249
|
mcpServers?: McpServer[];
|
|
248
250
|
env?: NodeJS.ProcessEnv;
|
|
251
|
+
/** The Ask thread this agent serves. */
|
|
252
|
+
threadId?: string;
|
|
249
253
|
},
|
|
250
254
|
) => Promise<AskAgentProcess>;
|
|
251
255
|
|
|
256
|
+
/** Windows can't run the extensionless sh shims npm puts beside `.cmd`s. */
|
|
257
|
+
const WINDOWS_EXTENSIONS = [".exe", ".cmd"];
|
|
258
|
+
|
|
252
259
|
/** Desktop inherits the login shell's PATH, so this sees what a terminal sees.
|
|
253
260
|
* Package-manager bin directories are skipped: the adapters' own dependencies
|
|
254
261
|
* put `codex` there, and that is not the user's install. */
|
|
255
262
|
export async function findExecutable(
|
|
256
263
|
command: string,
|
|
257
264
|
env: NodeJS.ProcessEnv = process.env,
|
|
265
|
+
platform: NodeJS.Platform = process.platform,
|
|
258
266
|
): Promise<string | undefined> {
|
|
267
|
+
const names =
|
|
268
|
+
platform === "win32"
|
|
269
|
+
? WINDOWS_EXTENSIONS.map((extension) => command + extension)
|
|
270
|
+
: [command];
|
|
271
|
+
|
|
259
272
|
for (const directory of (env.PATH ?? "").split(path.delimiter)) {
|
|
260
273
|
if (!directory || directory.split(path.sep).includes("node_modules"))
|
|
261
274
|
continue;
|
|
262
|
-
const candidate = path.join(directory, command);
|
|
263
275
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
276
|
+
for (const name of names) {
|
|
277
|
+
const candidate = path.join(directory, name);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
if (!(await stat(candidate)).isFile()) continue;
|
|
281
|
+
await access(candidate, constants.X_OK);
|
|
267
282
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
283
|
+
return candidate;
|
|
284
|
+
} catch {
|
|
285
|
+
/* Not in this directory. */
|
|
286
|
+
}
|
|
271
287
|
}
|
|
272
288
|
}
|
|
273
289
|
|
|
274
290
|
return undefined;
|
|
275
291
|
}
|
|
276
292
|
|
|
293
|
+
/** Windows runs a `.cmd` only through cmd.exe. */
|
|
294
|
+
export function executableCommand(executable: string, args: string[]) {
|
|
295
|
+
if (process.platform !== "win32" || !/\.cmd$/i.test(executable))
|
|
296
|
+
return { file: executable, args, windowsVerbatimArguments: false };
|
|
297
|
+
|
|
298
|
+
const line = [executable, ...args].map((arg) => `"${arg}"`).join(" ");
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
file: "cmd.exe",
|
|
302
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
303
|
+
windowsVerbatimArguments: true,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
277
307
|
/** The user's install of an agent's CLI: on PATH, else where its installer
|
|
278
308
|
* puts it. */
|
|
279
309
|
async function findAgent(
|
|
@@ -330,7 +360,12 @@ const STDERR_LIMIT = 8_000;
|
|
|
330
360
|
export const launchAskAgent: AskAgentLauncher = async (
|
|
331
361
|
agent,
|
|
332
362
|
cwd,
|
|
333
|
-
{
|
|
363
|
+
{
|
|
364
|
+
permissions = "read-only",
|
|
365
|
+
mcpServers = [],
|
|
366
|
+
env: base = process.env,
|
|
367
|
+
threadId,
|
|
368
|
+
} = {},
|
|
334
369
|
) => {
|
|
335
370
|
const spec = askAgents[agent];
|
|
336
371
|
const executable = await findAgent(spec, base);
|
|
@@ -342,6 +377,10 @@ export const launchAskAgent: AskAgentLauncher = async (
|
|
|
342
377
|
...askAgentSettings(agent, permissions).env,
|
|
343
378
|
};
|
|
344
379
|
|
|
380
|
+
// Lets `whiteboard api` calls from the agent's shell name the thread.
|
|
381
|
+
// The MCP server gets it in its own env, since MCP clients may not pass theirs on.
|
|
382
|
+
if (threadId) env[ASK_THREAD_ENV] = threadId;
|
|
383
|
+
|
|
345
384
|
// A server started from inside a Claude Code session must not look nested.
|
|
346
385
|
delete env.CLAUDECODE;
|
|
347
386
|
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
@@ -364,10 +403,13 @@ export const launchAskAgent: AskAgentLauncher = async (
|
|
|
364
403
|
args = [createRequire(import.meta.url).resolve(launch.adapter)];
|
|
365
404
|
} else args = launch.args;
|
|
366
405
|
|
|
367
|
-
const
|
|
406
|
+
const start = executableCommand(command, args);
|
|
407
|
+
|
|
408
|
+
const child = spawn(start.file, start.args, {
|
|
368
409
|
cwd,
|
|
369
410
|
env,
|
|
370
411
|
stdio: ["pipe", "pipe", "pipe"],
|
|
412
|
+
windowsVerbatimArguments: start.windowsVerbatimArguments,
|
|
371
413
|
});
|
|
372
414
|
|
|
373
415
|
let stderr = "";
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
|
|
4
|
-
const execFileAsync = promisify(execFile);
|
|
1
|
+
import { runVcs } from "@dev.fast/local-vcs";
|
|
5
2
|
|
|
6
3
|
/** The checkout's files relative to its root: tracked ones, and new ones git
|
|
7
4
|
* does not ignore. None where git cannot list them. */
|
|
8
5
|
export async function checkoutFiles(root: string): Promise<string[]> {
|
|
9
6
|
try {
|
|
10
|
-
const { stdout } = await
|
|
7
|
+
const { stdout } = await runVcs(
|
|
11
8
|
"git",
|
|
12
9
|
[
|
|
13
10
|
"-C",
|
package/src/ask/thread-state.ts
CHANGED
|
@@ -375,6 +375,10 @@ export const askHistoryEntrySchema = z.object({
|
|
|
375
375
|
head: z.string(),
|
|
376
376
|
createdAt: z.string(),
|
|
377
377
|
updatedAt: z.string(),
|
|
378
|
+
/** The board the conversation started on, and the block if it was about one. */
|
|
379
|
+
anchor: z
|
|
380
|
+
.object({ board: z.string(), block: z.string().optional() })
|
|
381
|
+
.optional(),
|
|
378
382
|
});
|
|
379
383
|
|
|
380
384
|
export type AskHistoryEntry = z.infer<typeof askHistoryEntrySchema>;
|
package/src/ask/thread.ts
CHANGED
|
@@ -176,6 +176,7 @@ export class AskThread {
|
|
|
176
176
|
/** The number of changes so far; a snapshot carries the one it includes. */
|
|
177
177
|
private seq = 0;
|
|
178
178
|
private readonly listeners = new Set<(update: AskUpdate) => void>();
|
|
179
|
+
private readonly statusListeners = new Set<() => void>();
|
|
179
180
|
private readonly closers = new Set<() => void>();
|
|
180
181
|
private readonly decisions = new Map<
|
|
181
182
|
string,
|
|
@@ -288,6 +289,10 @@ export class AskThread {
|
|
|
288
289
|
return { seq: this.seq, snapshot: this.state };
|
|
289
290
|
}
|
|
290
291
|
|
|
292
|
+
get status() {
|
|
293
|
+
return this.state.status;
|
|
294
|
+
}
|
|
295
|
+
|
|
291
296
|
/** Every change after the current `seq`, in order. */
|
|
292
297
|
subscribe(listener: (update: AskUpdate) => void) {
|
|
293
298
|
this.listeners.add(listener);
|
|
@@ -295,6 +300,13 @@ export class AskThread {
|
|
|
295
300
|
return () => this.listeners.delete(listener);
|
|
296
301
|
}
|
|
297
302
|
|
|
303
|
+
/** Calls back when the status changes, without following the thread. */
|
|
304
|
+
onStatusChange(listener: () => void) {
|
|
305
|
+
this.statusListeners.add(listener);
|
|
306
|
+
|
|
307
|
+
return () => this.statusListeners.delete(listener);
|
|
308
|
+
}
|
|
309
|
+
|
|
298
310
|
/** Whether anything follows the thread, as an open panel does. */
|
|
299
311
|
watched() {
|
|
300
312
|
return this.listeners.size > 0;
|
|
@@ -630,6 +642,7 @@ export class AskThread {
|
|
|
630
642
|
const launched = await this.launch(this.start.agent, this.start.cwd, {
|
|
631
643
|
permissions: this.state.permissions,
|
|
632
644
|
mcpServers: this.mcpServers,
|
|
645
|
+
threadId: this.id,
|
|
633
646
|
});
|
|
634
647
|
|
|
635
648
|
// Closed, stopped or given up on while the process started.
|
|
@@ -1533,5 +1546,8 @@ export class AskThread {
|
|
|
1533
1546
|
const update = { seq: this.seq, change };
|
|
1534
1547
|
|
|
1535
1548
|
for (const listener of this.listeners) listener(update);
|
|
1549
|
+
|
|
1550
|
+
if (change.type === "set" && change.status)
|
|
1551
|
+
for (const listener of this.statusListeners) listener();
|
|
1536
1552
|
}
|
|
1537
1553
|
}
|
package/src/ask/threads.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type AskThreadStart,
|
|
19
19
|
askThreadLimits,
|
|
20
20
|
} from "@review/ask/thread.js";
|
|
21
|
+
import { ASK_THREAD_ENV } from "@review/review-api/request-origin.js";
|
|
21
22
|
|
|
22
23
|
/** How long an agent may take to say what it offers. */
|
|
23
24
|
const OFFER_TIMEOUT_MS = 30_000;
|
|
@@ -163,7 +164,7 @@ export function cliAskTools(
|
|
|
163
164
|
];
|
|
164
165
|
|
|
165
166
|
return {
|
|
166
|
-
mcpServers: () => {
|
|
167
|
+
mcpServers: (threadId) => {
|
|
167
168
|
const cli = cliPath();
|
|
168
169
|
|
|
169
170
|
return cli
|
|
@@ -172,7 +173,9 @@ export function cliAskTools(
|
|
|
172
173
|
name: "whiteboard",
|
|
173
174
|
command: process.execPath,
|
|
174
175
|
args: [cli, "mcp"],
|
|
175
|
-
env
|
|
176
|
+
// MCP clients may not pass their own env on, so the server
|
|
177
|
+
// gets the thread it serves in its own.
|
|
178
|
+
env: [...env(), { name: ASK_THREAD_ENV, value: threadId }],
|
|
176
179
|
},
|
|
177
180
|
]
|
|
178
181
|
: [];
|
|
@@ -218,6 +221,8 @@ export class AskThreads {
|
|
|
218
221
|
|
|
219
222
|
private readonly mcpServers: AskMcpServers;
|
|
220
223
|
|
|
224
|
+
private readonly statusListeners = new Set<() => void>();
|
|
225
|
+
|
|
221
226
|
constructor(
|
|
222
227
|
private readonly launch: AskAgentLauncher,
|
|
223
228
|
private readonly tools: AskTools = {},
|
|
@@ -249,6 +254,8 @@ export class AskThreads {
|
|
|
249
254
|
);
|
|
250
255
|
|
|
251
256
|
this.threads.set(thread.id, thread);
|
|
257
|
+
thread.onStatusChange(() => this.statusChanged());
|
|
258
|
+
this.statusChanged();
|
|
252
259
|
void thread.open();
|
|
253
260
|
this.sweeper ??= setInterval(() => this.sweep(), SWEEP_MS);
|
|
254
261
|
this.sweeper.unref?.();
|
|
@@ -291,6 +298,19 @@ export class AskThreads {
|
|
|
291
298
|
}
|
|
292
299
|
}
|
|
293
300
|
|
|
301
|
+
/** Calls back when a thread opens, closes or changes its status. */
|
|
302
|
+
subscribeStatus(listener: () => void) {
|
|
303
|
+
this.statusListeners.add(listener);
|
|
304
|
+
|
|
305
|
+
return () => {
|
|
306
|
+
this.statusListeners.delete(listener);
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private statusChanged() {
|
|
311
|
+
for (const listener of this.statusListeners) listener();
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
get(id: string) {
|
|
295
315
|
return this.threads.get(id);
|
|
296
316
|
}
|
|
@@ -313,9 +333,10 @@ export class AskThreads {
|
|
|
313
333
|
|
|
314
334
|
close(id: string, save = true) {
|
|
315
335
|
const thread = this.threads.get(id);
|
|
316
|
-
this.threads.delete(id);
|
|
317
336
|
this.unwatchedSince.delete(id);
|
|
318
337
|
thread?.close(save);
|
|
338
|
+
|
|
339
|
+
if (this.threads.delete(id)) this.statusChanged();
|
|
319
340
|
}
|
|
320
341
|
|
|
321
342
|
closeDeletedReviews(isDeleted: (reviewId: string) => boolean) {
|
package/src/cli-runner.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
5
5
|
import { Writable } from "node:stream";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
+
import { boardTitle } from "@dev.fast/review-protocol";
|
|
8
9
|
import {
|
|
9
10
|
REVIEW_CLI_INSTALL_TARGET_LABELS,
|
|
10
11
|
REVIEW_REMOTE_AGENT_IDS,
|
|
@@ -836,7 +837,9 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
836
837
|
: "Whiteboard Desktop is ready in the background. Pass --focus to bring it forward.\n",
|
|
837
838
|
);
|
|
838
839
|
} else {
|
|
839
|
-
input.stdout.write(
|
|
840
|
+
input.stdout.write(
|
|
841
|
+
`Whiteboard Desktop is showing "${boardTitle(event.title)}".\n`,
|
|
842
|
+
);
|
|
840
843
|
}
|
|
841
844
|
};
|
|
842
845
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type JsonObject,
|
|
2
3
|
type JsonValue,
|
|
3
4
|
isJsonObject,
|
|
4
5
|
isNumberValue,
|
|
@@ -14,50 +15,63 @@ export function migrateDiffSelections(input: unknown): JsonValue {
|
|
|
14
15
|
return migrateNode(jsonValueSchema.parse(input));
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/** Keys whose values held retired attachments. */
|
|
19
|
+
export const ATTACHMENT_KEYS = [
|
|
20
|
+
"source",
|
|
21
|
+
"peek",
|
|
22
|
+
"sources",
|
|
23
|
+
"contextSources",
|
|
24
|
+
"callSite",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** A retired attachment as a selection, or undefined for anything else. */
|
|
28
|
+
export function retiredSelection(value: JsonObject): JsonObject | undefined {
|
|
29
|
+
if (!isStringValue(value.file)) return undefined;
|
|
30
|
+
|
|
31
|
+
if (isNumberValue(value.fromLine) && isNumberValue(value.toLine)) {
|
|
32
|
+
const side = value.side ?? value.graph ?? "head";
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
file: value.file,
|
|
36
|
+
start: { side, line: value.fromLine },
|
|
37
|
+
end: { side, line: value.toLine },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
isJsonObject(value.start) &&
|
|
43
|
+
isJsonObject(value.end) &&
|
|
44
|
+
"baseLine" in value.start
|
|
45
|
+
) {
|
|
46
|
+
const endpoint = (row: JsonObject) =>
|
|
47
|
+
isNumberValue(row.headLine)
|
|
48
|
+
? { side: "head", line: row.headLine }
|
|
49
|
+
: { side: "base", line: row.baseLine ?? null };
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
file: value.file,
|
|
53
|
+
start: endpoint(value.start),
|
|
54
|
+
end: endpoint(value.end),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
17
61
|
function migrateNode(value: JsonValue, attachment = false): JsonValue {
|
|
18
62
|
if (Array.isArray(value))
|
|
19
63
|
return value.map((child) => migrateNode(child, attachment));
|
|
20
64
|
|
|
21
65
|
if (!isJsonObject(value)) return value;
|
|
22
66
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
file: value.file,
|
|
29
|
-
start: { side, line: value.fromLine },
|
|
30
|
-
end: { side, line: value.toLine },
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
isJsonObject(value.start) &&
|
|
36
|
-
isJsonObject(value.end) &&
|
|
37
|
-
"baseLine" in value.start
|
|
38
|
-
) {
|
|
39
|
-
const endpoint = (row: typeof value.start) =>
|
|
40
|
-
isNumberValue(row.headLine)
|
|
41
|
-
? { side: "head", line: row.headLine }
|
|
42
|
-
: { side: "base", line: row.baseLine };
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
file: value.file,
|
|
46
|
-
start: endpoint(value.start),
|
|
47
|
-
end: endpoint(value.end),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
67
|
+
const selection = attachment ? retiredSelection(value) : undefined;
|
|
68
|
+
|
|
69
|
+
if (selection) return selection;
|
|
51
70
|
|
|
52
71
|
return Object.fromEntries(
|
|
53
72
|
Object.entries(value).map(([key, child]) => [
|
|
54
73
|
key,
|
|
55
|
-
migrateNode(
|
|
56
|
-
child,
|
|
57
|
-
["source", "peek", "sources", "contextSources", "callSite"].includes(
|
|
58
|
-
key,
|
|
59
|
-
),
|
|
60
|
-
),
|
|
74
|
+
migrateNode(child, ATTACHMENT_KEYS.includes(key)),
|
|
61
75
|
]),
|
|
62
76
|
);
|
|
63
77
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { slugify, uniqueId } from "./slug.js";
|
|
2
|
+
|
|
3
|
+
// A board's headings, as the server publishes them and the editor shows
|
|
4
|
+
// them: one rule for both, so a heading's anchor is the same in each.
|
|
5
|
+
|
|
6
|
+
/** Whether a Markdown heading with this many `#`s is one of the board's
|
|
7
|
+
* headings, with an anchor and a Contents row: `##` and `###`. */
|
|
8
|
+
export function isBoardHeading(hashes: number | undefined): boolean {
|
|
9
|
+
return hashes === 2 || hashes === 3;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Hands out heading anchors in document order: `slugify(text)`, made
|
|
13
|
+
* unique, so imported `#fragment` links still land. */
|
|
14
|
+
export function headingAnchors(): (text: string) => string {
|
|
15
|
+
const used = new Set<string>();
|
|
16
|
+
|
|
17
|
+
return (text) => {
|
|
18
|
+
const id = uniqueId(slugify(text) || "section", used);
|
|
19
|
+
|
|
20
|
+
used.add(id);
|
|
21
|
+
|
|
22
|
+
return id;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A Markdown heading's depth in Contents: one step under its section,
|
|
28
|
+
* whose blocks sit at `base`, and under the block's bigger headings before
|
|
29
|
+
* it. `open` holds those headings by `#` count, and takes this one.
|
|
30
|
+
*/
|
|
31
|
+
export function markdownHeadingDepth(
|
|
32
|
+
open: number[],
|
|
33
|
+
hashes: number,
|
|
34
|
+
base: number,
|
|
35
|
+
): number {
|
|
36
|
+
while (open.length && open.at(-1)! >= hashes) open.pop();
|
|
37
|
+
const depth = base + open.length;
|
|
38
|
+
|
|
39
|
+
open.push(hashes);
|
|
40
|
+
|
|
41
|
+
return depth;
|
|
42
|
+
}
|
package/src/lens-selection.ts
CHANGED
|
@@ -24,8 +24,23 @@ const sameSideAnchor = /^(head|base)\/(.+)#L(\d+)(?:-L(\d+))?$/;
|
|
|
24
24
|
|
|
25
25
|
const crossSideAnchor = /^diff\/(.+)#([LR])(\d+)-([LR])(\d+)$/;
|
|
26
26
|
|
|
27
|
+
const commitLinkAnchor = /^([0-9a-f]{7,40})\/(.+#L\d+(?:-L\d+)?)$/i;
|
|
28
|
+
|
|
29
|
+
/** A source link's commit form, "<sha>/path#L10-L24" (without its
|
|
30
|
+
* `whiteboard:source/` prefix): lines of that commit of the board's own
|
|
31
|
+
* repository, read as their head side. Undefined for any other form. */
|
|
32
|
+
export function commitLinkTarget(
|
|
33
|
+
target: string,
|
|
34
|
+
): { commit: string; anchor: Anchor } | undefined {
|
|
35
|
+
const match = commitLinkAnchor.exec(target);
|
|
36
|
+
|
|
37
|
+
return match
|
|
38
|
+
? { commit: match[1]!.toLowerCase(), anchor: `head/${match[2]!}` }
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
/** An anchor's selection, as markdown source links write it too (without
|
|
28
|
-
* their `
|
|
43
|
+
* their `whiteboard:source/` prefix). Undefined when it isn't one. */
|
|
29
44
|
export function parseAnchor(text: string): DiffSelection | undefined {
|
|
30
45
|
const same = sameSideAnchor.exec(text);
|
|
31
46
|
|
|
@@ -195,6 +210,26 @@ export function sourceAnchor(source: LensSource): FileLineRange {
|
|
|
195
210
|
return sourceAnchors(source)[0]!;
|
|
196
211
|
}
|
|
197
212
|
|
|
213
|
+
/** Why a selection cannot be resolved at its pins, as telemetry reports it. */
|
|
214
|
+
export const SELECTION_UNAVAILABLE_REASONS = [
|
|
215
|
+
"missing_file",
|
|
216
|
+
"no_alignment",
|
|
217
|
+
"outside_lines",
|
|
218
|
+
"other",
|
|
219
|
+
] as const;
|
|
220
|
+
|
|
221
|
+
export type SelectionUnavailableReason =
|
|
222
|
+
(typeof SELECTION_UNAVAILABLE_REASONS)[number];
|
|
223
|
+
|
|
224
|
+
export class SelectionUnavailableError extends Error {
|
|
225
|
+
constructor(
|
|
226
|
+
readonly reason: SelectionUnavailableReason,
|
|
227
|
+
message: string,
|
|
228
|
+
) {
|
|
229
|
+
super(message);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
198
233
|
/** Resolve once against the renderer's rows. No matching or context expansion. */
|
|
199
234
|
export function resolveDiffSelection(
|
|
200
235
|
source: LensSource,
|
|
@@ -210,7 +245,8 @@ export function resolveDiffSelection(
|
|
|
210
245
|
const end = locate(source.end);
|
|
211
246
|
|
|
212
247
|
if (start < 0 || end < start)
|
|
213
|
-
throw new
|
|
248
|
+
throw new SelectionUnavailableError(
|
|
249
|
+
"outside_lines",
|
|
214
250
|
"Lens endpoints do not identify an ordered interval in this diff.",
|
|
215
251
|
);
|
|
216
252
|
const selected = rows.slice(start, end + 1);
|