@dev.fast/whiteboard 0.3.4 → 0.3.5-preview.20261021.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-alias-CprZojXd.js → account-alias-CisiYUZT.js} +13 -8
- package/dist/agent-cli-BglNRDZN.js +2 -0
- package/dist/{agent-cli-CThN7Q59.js → agent-cli-TJ7_5_B3.js} +15 -10
- package/dist/{agent-client-BOOi7EUR.js → agent-client-H53ajvrm.js} +13 -3
- package/dist/{authoring-tools-BNwuMnz6.js → authoring-tools-BCquUsB3.js} +34 -27
- package/dist/{background-server-DLSDogkI.js → background-server-BSYXD9au.js} +1 -1
- package/dist/{background-server-CkvxA-6O.js → background-server-Xom8cA_u.js} +5 -5
- package/dist/build-info.json +1 -1
- package/dist/{cli-R4yfa5DT.js → cli-BOnV4cCh.js} +1 -1
- package/dist/{cli-runner-Cuc9FSzu.js → cli-runner-cRY5NOno.js} +70 -34
- package/dist/cli.js +4 -3
- package/dist/{client-BN_FDxFr.js → client-UH1bEG7Z.js} +260 -49
- package/dist/{fuzzy-match-BO9bK37N.js → fuzzy-match-D_x3wumC.js} +6 -4
- package/dist/{headless-host-CVBOQyRS.js → headless-host-BEe-aFtB.js} +1 -1
- package/dist/{headless-host-Cog6SpS9.js → headless-host-CEb5d-A0.js} +563 -175
- package/dist/lens-selection-BxPk53c8.js +214 -0
- package/dist/{local-data-D_9tvdsp.js → local-data-mbCb1HDH.js} +2448 -347
- package/dist/{mcp-D1hziANa.js → mcp-cBtzXsmN.js} +9 -12
- package/dist/{package-paths-C--__jEv.js → package-paths-BkeQsdaw.js} +1 -1
- package/dist/profile-Dbyidrcr.js +2 -0
- package/dist/{profile-DokFw782.js → profile-duqp5uPa.js} +20 -8
- package/dist/{remote-attach-CEJ-kvDY.js → remote-attach-BeKuMm_x.js} +4 -4
- package/dist/{remote-extensions-GN8Gf_qG.js → remote-extensions-CX2fAec1.js} +1 -1
- package/dist/{remote-extensions-Blcjr9Ze.js → remote-extensions-CYWNTVSf.js} +3 -3
- package/dist/{remote-language-server-BTKBQ7x4.js → remote-language-server-DLWluAeR.js} +5 -5
- package/dist/{remote-uninstall-DW7Gdryh.js → remote-uninstall-Y3S0YmJs.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-icXPv6jh.js} +55 -18
- package/dist/{review-app-launcher-CkwLclzR.js → review-app-launcher-DZvG5ZAh.js} +2 -2
- package/dist/{review-home-paths-j2BRAaNv.js → review-home-paths-BwH08ihj.js} +19 -17
- package/dist/{review-telemetry-BLNwYnLV.js → review-telemetry-AZ0imsmJ.js} +6 -6
- package/dist/runtime-CF4JXdb8.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-DtJFWC0G.js → s3-CsG2EWED.js} +1 -1
- package/dist/{s3-config-BTO12tCC.js → s3-config-CjcLyFV5.js} +4 -2
- package/dist/{s3-config-CSy3fx8H.js → s3-config-zz4GWh2T.js} +1 -1
- package/dist/server/desktop-host.js +28 -104
- package/dist/{server-discovery-Bey8IqXH.js → server-discovery-DeVTdcqK.js} +3 -3
- package/dist/sharing/index.d.ts +484 -74
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-DYZLCUP_.js} +98 -18
- package/dist/{src-BMPuxWv4.js → src-DtfSndqS.js} +269 -227
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-B29lve9-.js} +318 -271
- package/dist/{thread-state-5pYr6_zM.js → thread-state-BPpvKFwQ.js} +12 -2
- package/dist/{tool-failure-C9MpbXJJ.js → tool-failure-CQhyLht3.js} +10 -3
- package/instructions/authoring.md +20 -5
- package/package.json +4 -4
- package/src/agent-selection.ts +6 -0
- package/src/ask/agents.ts +13 -1
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +61 -31
- package/src/ask/threads.ts +24 -3
- package/src/cli-install.ts +13 -2
- package/src/cli-runner.ts +4 -1
- package/src/cli.ts +4 -1
- package/src/diff-selection-migration.ts +48 -34
- package/src/heading-anchors.ts +42 -0
- package/src/lens-selection.ts +38 -2
- package/src/markdown.ts +42 -0
- package/src/posthog-capture-client.ts +1 -1
- 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 +118 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/azure-devops-provider.ts +2 -20
- package/src/review-api/azure-remote.ts +21 -0
- 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 +586 -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 +362 -67
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +734 -135
- 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 +7 -13
- 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 +355 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2176 -495
- 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 -36
- package/src/server/headless-host.ts +0 -2
- package/src/server/hono-http.ts +2 -1
- 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 +143 -24
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +44 -18
- package/dist/agent-cli-C6SPSII0.js +0 -2
- package/dist/profile-CYPmvBoA.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
- package/src/server/legacy-tutorial-cleanup.ts +0 -38
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { M as ReviewSelectedDiffSchema, T as ReviewApiSelectionSourceSchema } from "./src-DYZLCUP_.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
/** A semantic selection, independent of comment/thread creation. */
|
|
4
4
|
const AgentSelectionSchema = z.strictObject({
|
|
@@ -24,6 +24,10 @@ const AgentSelectionSchema = z.strictObject({
|
|
|
24
24
|
startLine: z.number().int().positive(),
|
|
25
25
|
endLine: z.number().int().positive()
|
|
26
26
|
}),
|
|
27
|
+
z.strictObject({
|
|
28
|
+
kind: z.literal("block"),
|
|
29
|
+
blockId: z.string().min(1).max(200)
|
|
30
|
+
}),
|
|
27
31
|
z.strictObject({ kind: z.literal("review") })
|
|
28
32
|
]),
|
|
29
33
|
title: z.string().max(500),
|
|
@@ -36,6 +40,7 @@ function selectionMarkdown(selection, sourceExcerpt = "", diffPaths) {
|
|
|
36
40
|
const target = selection.target;
|
|
37
41
|
if (selection.selectedDiff) return `Selected diff:\n${selectedDiffMarkdown(selection.selectedDiff, diffPaths)}`;
|
|
38
42
|
if (target.kind === "review") return "";
|
|
43
|
+
if (target.kind === "block") return `Selected block:\n${sourceExcerpt}`;
|
|
39
44
|
if (target.kind === "text") return `Selected text:\n${target.quote.split(/\r?\n/).map((line) => `> ${line}`).join("\n")}`;
|
|
40
45
|
return `Selected code:\n${[
|
|
41
46
|
`## ${selection.title}`,
|
|
@@ -374,7 +379,12 @@ const askHistoryEntrySchema = z.object({
|
|
|
374
379
|
/** The commit the agent read. */
|
|
375
380
|
head: z.string(),
|
|
376
381
|
createdAt: z.string(),
|
|
377
|
-
updatedAt: z.string()
|
|
382
|
+
updatedAt: z.string(),
|
|
383
|
+
/** The board the conversation started on, and the block if it was about one. */
|
|
384
|
+
anchor: z.object({
|
|
385
|
+
board: z.string(),
|
|
386
|
+
block: z.string().optional()
|
|
387
|
+
}).optional()
|
|
378
388
|
});
|
|
379
389
|
//#endregion
|
|
380
390
|
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-DYZLCUP_.js";
|
|
3
|
+
import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-H53ajvrm.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
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
you are writing an interactive rfc-style whiteboard, for consumption by a staff engineer.
|
|
2
2
|
|
|
3
|
+
**plans and stacks**
|
|
4
|
+
|
|
5
|
+
Keep plans and their implementation reviews together, and group reviews of an actual PR stack. Create these relationships automatically when supported by the work's history; the user need not request nesting. Choose the board structure before the authoring flow below. Use `session_list` to find the plan governing this work or the overview of this PR stack before creating one.
|
|
6
|
+
|
|
7
|
+
- Before assigning a parent, identify the plan requirement this change implements or the actual PR stack it belongs to. A plan may be authored by the user or developed with the agent during this work. A PR stack must have established branch or dependency relationships. If the change neither implements part of an established plan nor belongs to an actual PR stack, leave its review top-level.
|
|
8
|
+
- Shared subject matter, repository, conversation, or resemblance to an earlier change is insufficient reason to nest boards. Related follow-ups remain top-level; link to earlier boards for context.
|
|
9
|
+
- For a plan with no changes yet, create a board with `target:{kind:"project", repositoryPath}`. Do not use a worktree or commit target for a plan: those attach code and show the code-board icon.
|
|
10
|
+
- When implementing a plan in several changes, keep the plan as the top-level board. Create one child board per PR or change with `parentId` set to the plan's session ID. Keep the plan's project target; do not replace it with one child's diff.
|
|
11
|
+
- When reviewing an existing PR stack without a plan, create one project-target board for the overview, then one child per PR. Explain the whole change on the overview and each layer's own diff on its child.
|
|
12
|
+
- Set each child's `dependsOn` only to the session IDs it actually builds on, not to impose a convenient reading order. For a linear PR stack, each child after the first depends on the previous child; independent pieces of a plan need no dependencies. `parentId` groups boards; `dependsOn` records dependencies and orders them. Git bases alone do not create these relationships.
|
|
13
|
+
- For commit targets, use the branch below each layer as its `base`. For PR targets, use the PR itself; its GitHub base selects that layer's diff.
|
|
14
|
+
- Nest only one level: plan or overview → change boards. Update existing boards after rebases instead of creating duplicates. If `session_create` returns an existing PR board, preserve its relationships unless the current task establishes a reason to change them. Use `session_set_parent` and `session_set_dependencies` only for the supported relationships above.
|
|
15
|
+
- Link the children from the overview and link the parent from each child with `[label](whiteboard:board/<sessionId>)`. Read the overview and all children back to check their targets, parent, dependency order, and links.
|
|
16
|
+
|
|
3
17
|
**flow**
|
|
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`).
|
|
18
|
+
For each board, 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
19
|
|
|
6
|
-
- create the whiteboard
|
|
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>`."
|
|
20
|
+
- 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
|
|
21
|
+
- for a change board only, 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
22
|
- call `session_activity_begin` and pass its `activityId` on every `session_edit`
|
|
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
|
|
23
|
+
- for a project-target plan or overview, read the relevant source or the stack's combined diff; no diff is expected on the project target itself. For a change board, 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
|
|
10
24
|
- immediately after reading the diff (and the PR description) - put down a first pass at the what/why section.
|
|
11
25
|
|
|
12
26
|
- whiteboard structure - each of these should be written as a top-level `section`, in this order:
|
|
@@ -37,4 +51,5 @@ follow these first five steps in order, with no tool calls beyond what they need
|
|
|
37
51
|
- use `session_activity_update` to provide regular status updates on your area of focus.
|
|
38
52
|
- keep whiteboards short and sweet when possible (esp. for small changes.) feel free to omit sections.
|
|
39
53
|
- 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](
|
|
54
|
+
- 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.
|
|
55
|
+
- 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.5-preview.20261021.131",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"@agentclientprotocol/sdk": "1.5.1",
|
|
45
45
|
"@hono/node-server": "2.0.12",
|
|
46
46
|
"@img/sharp-wasm32": "0.35.5",
|
|
47
|
-
"@modelcontextprotocol/sdk": "1.
|
|
47
|
+
"@modelcontextprotocol/sdk": "1.32.0",
|
|
48
48
|
"@parcel/watcher": "2.6.0",
|
|
49
49
|
"commander": "14.0.3",
|
|
50
50
|
"fuzzysort": "4.0.2",
|
|
51
|
-
"hono": "4.13.
|
|
51
|
+
"hono": "4.13.11",
|
|
52
52
|
"mdast-util-from-markdown": "2.0.3",
|
|
53
53
|
"mdast-util-gfm": "3.1.0",
|
|
54
54
|
"mdast-util-math": "3.0.0",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"repo": "devdotfast/review",
|
|
89
89
|
"docs": "packages/review/instructions/authoring.md"
|
|
90
90
|
},
|
|
91
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "6bf7b672e0c4d566b582d555044a624620c6311f",
|
|
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-selection.ts
CHANGED
|
@@ -35,6 +35,10 @@ export const AgentSelectionSchema = z.strictObject({
|
|
|
35
35
|
startLine: z.number().int().positive(),
|
|
36
36
|
endLine: z.number().int().positive(),
|
|
37
37
|
}),
|
|
38
|
+
z.strictObject({
|
|
39
|
+
kind: z.literal("block"),
|
|
40
|
+
blockId: z.string().min(1).max(200),
|
|
41
|
+
}),
|
|
38
42
|
/** Nothing in particular: a question about the whole review. */
|
|
39
43
|
z.strictObject({ kind: z.literal("review") }),
|
|
40
44
|
]),
|
|
@@ -61,6 +65,8 @@ export function selectionMarkdown(
|
|
|
61
65
|
|
|
62
66
|
if (target.kind === "review") return "";
|
|
63
67
|
|
|
68
|
+
if (target.kind === "block") return `Selected block:\n${sourceExcerpt}`;
|
|
69
|
+
|
|
64
70
|
if (target.kind === "text")
|
|
65
71
|
return `Selected text:\n${target.quote
|
|
66
72
|
.split(/\r?\n/)
|
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 =
|
|
@@ -247,6 +248,8 @@ export type AskAgentLauncher = (
|
|
|
247
248
|
/** The MCP servers its sessions are given. */
|
|
248
249
|
mcpServers?: McpServer[];
|
|
249
250
|
env?: NodeJS.ProcessEnv;
|
|
251
|
+
/** The Ask thread this agent serves. */
|
|
252
|
+
threadId?: string;
|
|
250
253
|
},
|
|
251
254
|
) => Promise<AskAgentProcess>;
|
|
252
255
|
|
|
@@ -357,7 +360,12 @@ const STDERR_LIMIT = 8_000;
|
|
|
357
360
|
export const launchAskAgent: AskAgentLauncher = async (
|
|
358
361
|
agent,
|
|
359
362
|
cwd,
|
|
360
|
-
{
|
|
363
|
+
{
|
|
364
|
+
permissions = "read-only",
|
|
365
|
+
mcpServers = [],
|
|
366
|
+
env: base = process.env,
|
|
367
|
+
threadId,
|
|
368
|
+
} = {},
|
|
361
369
|
) => {
|
|
362
370
|
const spec = askAgents[agent];
|
|
363
371
|
const executable = await findAgent(spec, base);
|
|
@@ -369,6 +377,10 @@ export const launchAskAgent: AskAgentLauncher = async (
|
|
|
369
377
|
...askAgentSettings(agent, permissions).env,
|
|
370
378
|
};
|
|
371
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
|
+
|
|
372
384
|
// A server started from inside a Claude Code session must not look nested.
|
|
373
385
|
delete env.CLAUDECODE;
|
|
374
386
|
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
@@ -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
|
@@ -88,6 +88,15 @@ const steeringSchema = z.object({
|
|
|
88
88
|
|
|
89
89
|
const steerOutcomeSchema = z.object({ outcome: z.string() });
|
|
90
90
|
|
|
91
|
+
/** A review Codex couldn't run; one it ran has a risk level. */
|
|
92
|
+
const failedReviewSchema = z.object({
|
|
93
|
+
review: z.object({
|
|
94
|
+
status: z.literal("denied"),
|
|
95
|
+
riskLevel: z.null(),
|
|
96
|
+
rationale: z.string(),
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
91
100
|
/** MCP servers the reviewer's own agent may bring whose tools only read,
|
|
92
101
|
* so their calls run without asking, like the agent reading files. */
|
|
93
102
|
const READ_ONLY_MCP_SERVERS = new Set([
|
|
@@ -100,8 +109,6 @@ const READ_ONLY_MCP_SERVERS = new Set([
|
|
|
100
109
|
* the reviewer. */
|
|
101
110
|
export type AskMcpServers = (threadId: string) => McpServer[];
|
|
102
111
|
|
|
103
|
-
/** Where a thread saves what happens to it, which moves with the thread
|
|
104
|
-
* when another review adopts it. */
|
|
105
112
|
export interface AskThreadHooks {
|
|
106
113
|
/** The agent created a session: the id a later reopen loads. A new one
|
|
107
114
|
* replaces a session the agent could not reopen. */
|
|
@@ -176,6 +183,7 @@ export class AskThread {
|
|
|
176
183
|
/** The number of changes so far; a snapshot carries the one it includes. */
|
|
177
184
|
private seq = 0;
|
|
178
185
|
private readonly listeners = new Set<(update: AskUpdate) => void>();
|
|
186
|
+
private readonly statusListeners = new Set<() => void>();
|
|
179
187
|
private readonly closers = new Set<() => void>();
|
|
180
188
|
private readonly decisions = new Map<
|
|
181
189
|
string,
|
|
@@ -214,6 +222,8 @@ export class AskThread {
|
|
|
214
222
|
private needsContext: boolean;
|
|
215
223
|
/** The reviewer changed what the agent may do since it was last told. */
|
|
216
224
|
private permitted = false;
|
|
225
|
+
/** Why the agent's automatic approval review failed this turn. */
|
|
226
|
+
private reviewFailure?: string;
|
|
217
227
|
/** The agent could not reopen the session, for a reason signing in would
|
|
218
228
|
* not fix, so trying again starts a new one. */
|
|
219
229
|
private unloadable = false;
|
|
@@ -227,9 +237,6 @@ export class AskThread {
|
|
|
227
237
|
private steerable = false;
|
|
228
238
|
/** Follow-ups waiting for the turn under way to end, oldest first. */
|
|
229
239
|
private queue: { id: string; question: AskQuestion }[] = [];
|
|
230
|
-
/** The review the thread answers in, and where it saves; adopting moves
|
|
231
|
-
* both. */
|
|
232
|
-
private home: AskThreadHooks & { reviewId: string };
|
|
233
240
|
|
|
234
241
|
constructor(
|
|
235
242
|
private readonly launch: AskAgentLauncher,
|
|
@@ -240,15 +247,6 @@ export class AskThread {
|
|
|
240
247
|
this.id = start.id ?? randomUUID();
|
|
241
248
|
this.mcpServers = mcpServers(this.id);
|
|
242
249
|
this.picks = { ...start.picks };
|
|
243
|
-
this.home = {
|
|
244
|
-
reviewId: start.reviewId,
|
|
245
|
-
onSession: start.onSession,
|
|
246
|
-
onTurn: start.onTurn,
|
|
247
|
-
onSave: start.onSave,
|
|
248
|
-
onPermissions: start.onPermissions,
|
|
249
|
-
onOffer: start.onOffer,
|
|
250
|
-
onTitle: start.onTitle,
|
|
251
|
-
};
|
|
252
250
|
this.state = {
|
|
253
251
|
id: this.id,
|
|
254
252
|
agent: start.agent,
|
|
@@ -268,16 +266,7 @@ export class AskThread {
|
|
|
268
266
|
}
|
|
269
267
|
|
|
270
268
|
get reviewId() {
|
|
271
|
-
return this.
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/** Moves the thread to another review, as when its agent creates the
|
|
275
|
-
* review it was asked for, and saves it there as it stands. */
|
|
276
|
-
adopt(reviewId: string, hooks: AskThreadHooks) {
|
|
277
|
-
this.home = { reviewId, ...hooks };
|
|
278
|
-
|
|
279
|
-
if (this.sessionId) hooks.onSession?.(this.sessionId);
|
|
280
|
-
hooks.onSave?.(this.state.entries);
|
|
269
|
+
return this.start.reviewId;
|
|
281
270
|
}
|
|
282
271
|
|
|
283
272
|
read(): AskThreadState {
|
|
@@ -288,6 +277,10 @@ export class AskThread {
|
|
|
288
277
|
return { seq: this.seq, snapshot: this.state };
|
|
289
278
|
}
|
|
290
279
|
|
|
280
|
+
get status() {
|
|
281
|
+
return this.state.status;
|
|
282
|
+
}
|
|
283
|
+
|
|
291
284
|
/** Every change after the current `seq`, in order. */
|
|
292
285
|
subscribe(listener: (update: AskUpdate) => void) {
|
|
293
286
|
this.listeners.add(listener);
|
|
@@ -295,6 +288,13 @@ export class AskThread {
|
|
|
295
288
|
return () => this.listeners.delete(listener);
|
|
296
289
|
}
|
|
297
290
|
|
|
291
|
+
/** Calls back when the status changes, without following the thread. */
|
|
292
|
+
onStatusChange(listener: () => void) {
|
|
293
|
+
this.statusListeners.add(listener);
|
|
294
|
+
|
|
295
|
+
return () => this.statusListeners.delete(listener);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
298
|
/** Whether anything follows the thread, as an open panel does. */
|
|
299
299
|
watched() {
|
|
300
300
|
return this.listeners.size > 0;
|
|
@@ -506,7 +506,7 @@ export class AskThread {
|
|
|
506
506
|
this.state.status !== "starting" &&
|
|
507
507
|
this.state.status !== "failed"
|
|
508
508
|
)
|
|
509
|
-
this.
|
|
509
|
+
this.start.onSave?.(settled(this.state.entries));
|
|
510
510
|
} finally {
|
|
511
511
|
for (const resolve of this.decisions.values())
|
|
512
512
|
resolve({ outcome: { outcome: "cancelled" } });
|
|
@@ -630,6 +630,7 @@ export class AskThread {
|
|
|
630
630
|
const launched = await this.launch(this.start.agent, this.start.cwd, {
|
|
631
631
|
permissions: this.state.permissions,
|
|
632
632
|
mcpServers: this.mcpServers,
|
|
633
|
+
threadId: this.id,
|
|
633
634
|
});
|
|
634
635
|
|
|
635
636
|
// Closed, stopped or given up on while the process started.
|
|
@@ -851,7 +852,7 @@ export class AskThread {
|
|
|
851
852
|
if (commands) offer.commands = commands;
|
|
852
853
|
|
|
853
854
|
if (accepts) offer.accepts = accepts;
|
|
854
|
-
this.
|
|
855
|
+
this.start.onOffer?.(offer);
|
|
855
856
|
}
|
|
856
857
|
|
|
857
858
|
/** Changes what the agent may do from the next answer: it starts again
|
|
@@ -874,7 +875,7 @@ export class AskThread {
|
|
|
874
875
|
|
|
875
876
|
this.emit({ type: "set", permissions });
|
|
876
877
|
this.permitted = true;
|
|
877
|
-
this.
|
|
878
|
+
this.start.onPermissions?.(permissions);
|
|
878
879
|
this.disconnect();
|
|
879
880
|
// Its mode is Whiteboard's change, not one to announce as the agent's.
|
|
880
881
|
this.settings.clear();
|
|
@@ -923,7 +924,7 @@ export class AskThread {
|
|
|
923
924
|
if (this.closed) throw new Error("This conversation has ended.");
|
|
924
925
|
|
|
925
926
|
this.sessionId = response.sessionId;
|
|
926
|
-
this.
|
|
927
|
+
this.start.onSession?.(response.sessionId);
|
|
927
928
|
|
|
928
929
|
this.greeting = piGreetingSchema.safeParse(
|
|
929
930
|
response._meta,
|
|
@@ -1055,6 +1056,20 @@ export class AskThread {
|
|
|
1055
1056
|
title: `${this.state.agentName} finished without replying.`,
|
|
1056
1057
|
});
|
|
1057
1058
|
|
|
1059
|
+
// Every action would fail review again; read-only asks Whiteboard instead.
|
|
1060
|
+
const reviewFailed =
|
|
1061
|
+
this.reviewFailure !== undefined && this.state.permissions === "auto";
|
|
1062
|
+
|
|
1063
|
+
if (reviewFailed)
|
|
1064
|
+
this.push({
|
|
1065
|
+
kind: "notice",
|
|
1066
|
+
id: randomUUID(),
|
|
1067
|
+
severity: "warning",
|
|
1068
|
+
title: `${this.state.agentName}'s automatic approval review failed, so this conversation is now read-only. Ask again to continue.`,
|
|
1069
|
+
description: this.reviewFailure,
|
|
1070
|
+
});
|
|
1071
|
+
this.reviewFailure = undefined;
|
|
1072
|
+
|
|
1058
1073
|
this.emit({
|
|
1059
1074
|
type: "set",
|
|
1060
1075
|
status: "idle",
|
|
@@ -1065,8 +1080,10 @@ export class AskThread {
|
|
|
1065
1080
|
? `${this.state.agentName} stopped before finishing.`
|
|
1066
1081
|
: null,
|
|
1067
1082
|
});
|
|
1068
|
-
this.
|
|
1069
|
-
this.
|
|
1083
|
+
this.start.onTurn?.();
|
|
1084
|
+
this.start.onSave?.(this.state.entries);
|
|
1085
|
+
|
|
1086
|
+
if (reviewFailed) await this.permit("read-only");
|
|
1070
1087
|
this.askQueued();
|
|
1071
1088
|
}
|
|
1072
1089
|
|
|
@@ -1245,6 +1262,7 @@ export class AskThread {
|
|
|
1245
1262
|
|
|
1246
1263
|
case "tool_call":
|
|
1247
1264
|
this.noteMcpServer(update);
|
|
1265
|
+
this.noteReview(update);
|
|
1248
1266
|
this.push({
|
|
1249
1267
|
kind: "tool",
|
|
1250
1268
|
id: update.toolCallId,
|
|
@@ -1258,6 +1276,7 @@ export class AskThread {
|
|
|
1258
1276
|
return;
|
|
1259
1277
|
case "tool_call_update":
|
|
1260
1278
|
this.noteMcpServer(update);
|
|
1279
|
+
this.noteReview(update);
|
|
1261
1280
|
|
|
1262
1281
|
// An agent may report a tool call's first state as an update.
|
|
1263
1282
|
if (
|
|
@@ -1340,7 +1359,7 @@ export class AskThread {
|
|
|
1340
1359
|
|
|
1341
1360
|
if (title && title !== this.state.title) {
|
|
1342
1361
|
this.emit({ type: "set", title });
|
|
1343
|
-
this.
|
|
1362
|
+
this.start.onTitle?.(title);
|
|
1344
1363
|
}
|
|
1345
1364
|
|
|
1346
1365
|
return true;
|
|
@@ -1357,6 +1376,14 @@ export class AskThread {
|
|
|
1357
1376
|
if (server) this.mcpCalls.set(update.toolCallId, server);
|
|
1358
1377
|
}
|
|
1359
1378
|
|
|
1379
|
+
private noteReview(update: ToolCallUpdate) {
|
|
1380
|
+
const failed = failedReviewSchema.safeParse(
|
|
1381
|
+
update.rawOutput ?? update.rawInput,
|
|
1382
|
+
).data;
|
|
1383
|
+
|
|
1384
|
+
if (failed && !this.replaying) this.reviewFailure = failed.review.rationale;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1360
1387
|
/**
|
|
1361
1388
|
* In its read-only mode Claude answers by writing a plan and asking to
|
|
1362
1389
|
* leave the mode with it. That request is refused, so the plan is shown
|
|
@@ -1533,5 +1560,8 @@ export class AskThread {
|
|
|
1533
1560
|
const update = { seq: this.seq, change };
|
|
1534
1561
|
|
|
1535
1562
|
for (const listener of this.listeners) listener(update);
|
|
1563
|
+
|
|
1564
|
+
if (change.type === "set" && change.status)
|
|
1565
|
+
for (const listener of this.statusListeners) listener();
|
|
1536
1566
|
}
|
|
1537
1567
|
}
|
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-install.ts
CHANGED
|
@@ -699,11 +699,16 @@ export async function writePathShim(
|
|
|
699
699
|
cliPath: string,
|
|
700
700
|
runtimePath: string | undefined,
|
|
701
701
|
devHome: string,
|
|
702
|
+
previewHome = devHome,
|
|
702
703
|
): Promise<void> {
|
|
703
704
|
if (process.platform === "win32") {
|
|
704
705
|
await writeFileAtomicAsync(
|
|
705
706
|
shimPath,
|
|
706
|
-
windowsCliShim(
|
|
707
|
+
windowsCliShim(
|
|
708
|
+
cliPath,
|
|
709
|
+
runtimePath ?? process.execPath,
|
|
710
|
+
devHome === previewHome ? devHome : undefined,
|
|
711
|
+
),
|
|
707
712
|
{ replaceSymlink: true },
|
|
708
713
|
);
|
|
709
714
|
|
|
@@ -716,6 +721,8 @@ export async function writePathShim(
|
|
|
716
721
|
FALLBACK_CLI=${shSingleQuote(cliPath)}
|
|
717
722
|
FALLBACK_RUNTIME=${shSingleQuote(runtimePath ?? "")}
|
|
718
723
|
DEFAULT_HOME=${shSingleQuote(devHome)}
|
|
724
|
+
channel="\${DEV_FAST_REVIEW_CHANNEL:-\${DEV_REVIEW_INSTANCE:-}}"
|
|
725
|
+
[ "$channel" = preview ] && DEFAULT_HOME=${shSingleQuote(previewHome)}
|
|
719
726
|
export DEV_REVIEW_HOME="\${DEV_REVIEW_HOME:-$DEFAULT_HOME}"
|
|
720
727
|
DESKTOP="$DEV_REVIEW_HOME/review-desktop"
|
|
721
728
|
|
|
@@ -836,7 +843,11 @@ export async function installReviewCommand(input: {
|
|
|
836
843
|
shimPath,
|
|
837
844
|
input.cliPath,
|
|
838
845
|
input.cliRuntimePath,
|
|
839
|
-
devReviewHome(env, homeDir),
|
|
846
|
+
devReviewHome({ DEV_REVIEW_HOME: env.DEV_REVIEW_HOME }, homeDir),
|
|
847
|
+
devReviewHome(
|
|
848
|
+
{ DEV_REVIEW_HOME: env.DEV_REVIEW_HOME, DEV_REVIEW_INSTANCE: "preview" },
|
|
849
|
+
homeDir,
|
|
850
|
+
),
|
|
840
851
|
);
|
|
841
852
|
|
|
842
853
|
if (await traceMachineEnabled({ homeDir, env })) {
|
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
|
|
package/src/cli.ts
CHANGED
|
@@ -84,9 +84,12 @@ async function maybeDelegateToDesktopCli(
|
|
|
84
84
|
// This bootstrap runs before the Node floor check, so it cannot import
|
|
85
85
|
// devReviewHome() from @dev.fast/trace-core: that module graph needs a modern
|
|
86
86
|
// Node. Keep this copy in step with devReviewHome().
|
|
87
|
+
const channel =
|
|
88
|
+
env.DEV_FAST_REVIEW_CHANNEL?.trim() || env.DEV_REVIEW_INSTANCE?.trim();
|
|
89
|
+
|
|
87
90
|
const devHome = env.DEV_REVIEW_HOME?.trim()
|
|
88
91
|
? path.resolve(env.DEV_REVIEW_HOME.trim())
|
|
89
|
-
: path.join(os.homedir(), ".dev");
|
|
92
|
+
: path.join(os.homedir(), channel === "preview" ? ".dev-preview" : ".dev");
|
|
90
93
|
|
|
91
94
|
let cliPath: string;
|
|
92
95
|
let runtimePath: string | undefined;
|