@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,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);
|
package/src/markdown.ts
CHANGED
|
@@ -35,6 +35,48 @@ export function markdownText(node: MarkdownNode): string {
|
|
|
35
35
|
return [...markdownNodes(node)].map((child) => child.value ?? "").join("");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Link destination spans in source order; never inside code. */
|
|
39
|
+
export function linkDestinations(
|
|
40
|
+
nodes: Iterable<MarkdownNode>,
|
|
41
|
+
): { from: number; to: number }[] {
|
|
42
|
+
const spans: { from: number; to: number }[] = [];
|
|
43
|
+
|
|
44
|
+
for (const node of nodes) {
|
|
45
|
+
const from = node.position?.start.offset;
|
|
46
|
+
const to = node.position?.end.offset;
|
|
47
|
+
|
|
48
|
+
if (from === undefined || to === undefined) continue;
|
|
49
|
+
|
|
50
|
+
if (node.type === "link")
|
|
51
|
+
spans.push({
|
|
52
|
+
from: node.children?.at(-1)?.position?.end.offset ?? from + 1,
|
|
53
|
+
to,
|
|
54
|
+
});
|
|
55
|
+
else if (node.type === "image" || node.type === "definition")
|
|
56
|
+
spans.push({ from, to });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// An image inside a link comes after the link's tail in the walk.
|
|
60
|
+
return spans.toSorted((a, b) => a.from - b.from);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function rewriteLinkDestinations(
|
|
64
|
+
markdown: string,
|
|
65
|
+
rewrite: (span: string) => string,
|
|
66
|
+
): string {
|
|
67
|
+
let result = markdown;
|
|
68
|
+
|
|
69
|
+
for (const { from, to } of linkDestinations(
|
|
70
|
+
markdownNodes(parseMarkdown(markdown)),
|
|
71
|
+
).toReversed())
|
|
72
|
+
result =
|
|
73
|
+
result.slice(0, from) +
|
|
74
|
+
rewrite(result.slice(from, to)) +
|
|
75
|
+
result.slice(to);
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
38
80
|
/** Shared syntax for submit-time source checks and safe Markdown rendering. */
|
|
39
81
|
export function parseMarkdown(source: string): MarkdownNode {
|
|
40
82
|
const tree: MarkdownNode = fromMarkdown(source, {
|
|
@@ -37,7 +37,7 @@ export const PROGRESSIVE_REVIEW_POSTHOG_KEY_ENV =
|
|
|
37
37
|
export const PROGRESSIVE_REVIEW_POSTHOG_HOST_ENV =
|
|
38
38
|
"PROGRESSIVE_REVIEW_POSTHOG_HOST";
|
|
39
39
|
|
|
40
|
-
const DEFAULT_POSTHOG_HOST = "https://
|
|
40
|
+
const DEFAULT_POSTHOG_HOST = "https://e.dev.fast";
|
|
41
41
|
|
|
42
42
|
const DEFAULT_CAPTURE_TIMEOUT_MS = 1_000;
|
|
43
43
|
|
package/src/review-api/README.md
CHANGED
|
@@ -17,6 +17,7 @@ catalog. Tests inject the native store and source-data provider.
|
|
|
17
17
|
- `repositories`: server-only local paths; clients receive an ID and display name.
|
|
18
18
|
- `resources`: immutable image, trace and software-map bytes, scoped to a repository.
|
|
19
19
|
- `review_attention`: viewed/trashed timestamps, separate from document history.
|
|
20
|
+
- `board_relations`: where a board sits in its project, outside its versions: a child's `parent` (one per board) and a board's `depends_on` edges.
|
|
20
21
|
|
|
21
22
|
One host-owned store serializes writes, including asynchronous validation.
|
|
22
23
|
Presences show who is working across database connections and never block a
|
|
@@ -51,11 +52,14 @@ All paths below are relative to `/reviews-api`.
|
|
|
51
52
|
| `GET /:id?targetId=step-3` | Full block, sequence step, flow node or flow edge |
|
|
52
53
|
| `GET /:id?full=true` | Full snapshot |
|
|
53
54
|
| `GET /:id?version=2&full=true` | Historical snapshot |
|
|
54
|
-
| `GET /:id/history` | Saved versions with titles and
|
|
55
|
+
| `GET /:id/history` | Saved versions with titles, timestamps and `author`: `"you"` for a person's save, else `"agent"` with the Ask `threadId` when one made it |
|
|
55
56
|
| `GET /:id/inspect` | Agent reading view: nested text outline with IDs; `targetId` reads one component completely, `full=true` includes all content, `version` selects history. `format=json` returns raw data instead. |
|
|
56
57
|
| `POST /:id/open` | Open the review in the attached Desktop; report an error when none is attached |
|
|
57
58
|
| `POST /commands` | Apply one command; return review ID, version, and for an edit the target ID, its `type`, and — after an insert or replace — `children`: its first-level children as `{id,type}` (a container's blocks; a diagram's steps, or nodes then edges), so new components are addressable without a read. A `create` with `pullRequestUrl` returns the newest existing review for that PR instead (owner/repository matched case-insensitively) unless `operation.reuseExisting` is `false`: `created:false`, a `note`, its stored `target`, `headMoved`, and `working`/`otherReviewIds` when they apply; its target is never moved. A new review reports `created:true`. Either way the result carries `review`, the review's `GET /` catalog entry (target, origin, repository name and path). An interactive `create` also opens the new review in an attached Desktop unless `operation.open` is `false`, and reports `opened` with the open result or an `openError`; the review is saved either way |
|
|
58
59
|
| `POST /commands {operation:{type:"lens_edit",reviewId,edit}}` | Write one file lens, credited to `activityId` when given: `insert {title,targets,afterId?}` (host id `lens-N`), `update {targetId,title?,targets?}` or `remove {targetId}`. Returns `{targetId, type:"lens", uncategorized}`, where `uncategorized` lists changed files and ranges no lens covers yet (at most 50 files) |
|
|
60
|
+
| `POST /:id/person-edit {baseVersion,title,document}` | Save a person's whole board as their version; return `{reviewId,version,ids}`. See Person edits |
|
|
61
|
+
| `POST /:id/focus {clientId,blockId}` | Lease the board open on a canvas, and the block a person is in (`blockId`, or null), for 15 s; `open:false` lets go. Returns `{ok:true}` |
|
|
62
|
+
| `POST /:id/discard` | Delete the board only if it has no title (it reads "Untitled") and no blocks and no child boards, no canvas holds it open and no agent is working on it; waits up to 15 s for a closing canvas to let go. Returns `{deleted}` |
|
|
59
63
|
| `GET /:id/lenses` | The current version's file lenses with each one's file count, plus the same `uncategorized` report |
|
|
60
64
|
| `POST /repositories {path}` | Register a local Git/jj repository; return ID/name |
|
|
61
65
|
| `POST /resources` | Upload an image, trace, or map; return resource ID/kind/MIME type |
|
|
@@ -89,10 +93,17 @@ The canvas continues to use the JSON snapshot routes above.
|
|
|
89
93
|
}
|
|
90
94
|
```
|
|
91
95
|
|
|
92
|
-
Commands: `create {title
|
|
93
|
-
`restore {reviewId,version}`. A review's pins
|
|
94
|
-
|
|
95
|
-
PR URLs must be canonical `https://github.com/owner/repository/pull/123` URLs. The PR number is derived from the URL; identity is metadata alongside immutable pins, not a moving source reference. A `
|
|
96
|
+
Commands: `create {title?,target,reuseExisting?,open?,projectId?}` (`projectId` files it in another project; it defaults to its checkout's), `set_target {reviewId,target}`, `edit {reviewId,edit}`, `rename {reviewId,title}`,
|
|
97
|
+
`restore {reviewId,version}`. A target names its checkout by `repositoryPath` (see Review targets). A PR is a `pullRequest` target. A review's pins are its target resolved to immutable commits. Agents write pins as `{head,base?}`, and the host adds the board's repository.
|
|
98
|
+
`/commands` registers the path and passes its id to the store. Agent tools never show repository ids, and `/commands` sets every pins object in an edit to the board's own repository. Retargeting preserves content and component IDs. Restore restores title, target, PR identity, and content. Live targets still read the current checkout.
|
|
99
|
+
PR URLs must be canonical `https://github.com/owner/repository/pull/123` URLs. The PR number is derived from the URL; identity is metadata alongside immutable pins, not a moving source reference. A `pullRequest` target names the PR by URL or by number (a number is looked up on the checkout's GitHub remote) and resolves it when accepted: it reads the PR with `gh pr view` (falling back to the public GitHub API) and fetches `refs/pull/N/head` and the base branch from the checkout named by `repositoryPath` into `refs/review/github/owner/repository/pull/N/*`, never moving branches or bookmarks (a jj repository indexes the commits through a transient tag). It pins that head and its merge base with the base branch; for a PR absorbed by a merge commit it uses the merge base with GitHub's frozen base commit instead. The title defaults to the PR title. A repeat resolves the PR again, so `headMoved` compares the existing review with the PR's current head. `set_target` preserves the document and component IDs, including when source commits change. Its response reports retained source ranges to verify, those in files the new commits changed, and resources that no longer match the pins; agents repair these with `edit`. Existing versions keep their original pins and content. It preserves omitted PR identity within one repository, clears it when switching repositories, and accepts an explicit URL or null.
|
|
100
|
+
|
|
101
|
+
`set_parent {reviewId,parentId}` nests a board one level under a top-level
|
|
102
|
+
board of its project; `null` makes it top-level. `set_dependencies
|
|
103
|
+
{reviewId,dependsOn}` replaces what it builds on, and refuses a cycle.
|
|
104
|
+
`create` takes both. Neither saves a version. Summaries list `parentId`,
|
|
105
|
+
`children` (in sibling order) and `dependsOn`. Deleting a parent moves its
|
|
106
|
+
children to top level.
|
|
96
107
|
|
|
97
108
|
`attention {reviewId,action:"view"}` records viewing without changing lifecycle or document history.
|
|
98
109
|
`lifecycle {reviewId,state:"active"|"trash"}` moves a whiteboard to Trash or restores it.
|
|
@@ -110,8 +121,7 @@ the same version and side through the API, without a client-side checkout path.
|
|
|
110
121
|
|
|
111
122
|
Edits: `insert {content,parentId?,afterId?}`, `update {targetId,changes}`,
|
|
112
123
|
`move {targetId,parentId?,afterId?}`, `remove {targetId}`,
|
|
113
|
-
`replace {targetId,content}`. Omitted placement appends to the root
|
|
114
|
-
scratchpad it prepends instead, so the pad reads newest first. Diagram
|
|
124
|
+
`replace {targetId,content}`. Omitted placement appends to the root. Diagram
|
|
115
125
|
units (a `step` in a sequence, a `flow_node` or `flow_edge` in a flow diagram)
|
|
116
126
|
require their diagram as the parent and can move within it, but not between
|
|
117
127
|
diagrams; removing a flow node removes the edges that touched it. A new
|
|
@@ -138,10 +148,35 @@ There is no expected-version parameter. Later same-field edits win. A command
|
|
|
138
148
|
whose response was lost may or may not have applied; read the review before
|
|
139
149
|
retrying.
|
|
140
150
|
|
|
151
|
+
## Person edits
|
|
152
|
+
|
|
153
|
+
The canvas saves a person's edits with `POST /:id/person-edit`, never through
|
|
154
|
+
an agent tool. The body is the whole board: `title`, and `document` with each
|
|
155
|
+
block as stored. A new block has no `id` and may carry a `clientKey`; the host
|
|
156
|
+
gives it a fresh id and returns `ids: {clientKey: id}`. Ids must be unique; an id
|
|
157
|
+
from an earlier version may come back. The save is checked like an agent edit.
|
|
158
|
+
If `baseVersion` is not the current version, nothing is saved and the request
|
|
159
|
+
fails with 409: "The board changed since version N; it is now at version M.
|
|
160
|
+
Merge the latest version and save again." A save that changes nothing returns
|
|
161
|
+
the current version and saves none. The version carries `personEdit: {changed}`,
|
|
162
|
+
the ids of blocks added, removed or changed, and no `lastEdit`, so no courier
|
|
163
|
+
draws it.
|
|
164
|
+
|
|
165
|
+
While a person edits a block, the canvas posts `POST /:id/focus` about every
|
|
166
|
+
5 s. Each lease expires 15 s after its last post. An agent `edit` that adds,
|
|
167
|
+
removes or changes a leased block fails with 409: "The user is editing this
|
|
168
|
+
block (ID). Edit another block, or try again after they move on." Nothing is
|
|
169
|
+
saved. A section or callout changes only when its own fields do, and a block
|
|
170
|
+
moved unchanged does not change. A lease never refuses a person's edit or
|
|
171
|
+
restore, only an agent's. A board in Trash refuses every change, a person's
|
|
172
|
+
included, until it is restored.
|
|
173
|
+
|
|
141
174
|
## Validation and remaining work
|
|
142
175
|
|
|
143
|
-
Markdown source links use `[label](
|
|
144
|
-
(or `base`, or `#L10` for one line)
|
|
176
|
+
Markdown source links use `[label](whiteboard:source/head/src/save.ts#L10-L24)`
|
|
177
|
+
(or `base`, or `#L10` for one line), or `whiteboard:source/<commit>/src/save.ts#L10-L24`
|
|
178
|
+
to quote any commit of the board's own repository; a short commit is saved in full,
|
|
179
|
+
and one the repository lacks rejects the edit. Paths are repository-relative and URL-encoded
|
|
145
180
|
where needed. Inline and reference-style links open the existing native side peek.
|
|
146
181
|
The same Markdown parser feeds the host's source checks and the renderer, so code
|
|
147
182
|
examples and unused definitions do not become source requests. Invalid paths or
|
|
@@ -163,9 +198,10 @@ working-copy files. It checks code ranges and resource ownership before saving.
|
|
|
163
198
|
Images are fully decoded to PNG; traces retain supplied text with an explicit
|
|
164
199
|
client-supplied provenance label. Map uploads use the existing nested map format,
|
|
165
200
|
with a JSON shape check followed by the existing relationship/coverage validator
|
|
166
|
-
and pinned source-range checks. Uploads take `{id,
|
|
201
|
+
and pinned source-range checks. Uploads take `{id,repositoryPath,kind,...}` with
|
|
167
202
|
`base64` for images, `trace:{label,events:[{id,role,text}]}` for traces, or
|
|
168
|
-
`pins,side,model` for maps
|
|
203
|
+
`pins,side,model` for maps, whose pins still name `{repositoryId,head,base}`
|
|
204
|
+
(no agent tool uploads maps). Reusing an upload ID requires identical content.
|
|
169
205
|
|
|
170
206
|
The canvas preserves React identities during updates. The stream coalesces
|
|
171
207
|
updates when a reader falls behind; reconnecting starts with the current saved
|
|
@@ -277,8 +313,11 @@ commands are local and are never authored into review documents.
|
|
|
277
313
|
|
|
278
314
|
## Review targets
|
|
279
315
|
|
|
280
|
-
`target` is
|
|
281
|
-
`{kind:"
|
|
316
|
+
`target` names its checkout by path and is one of `{kind:"project",repositoryPath}`
|
|
317
|
+
(a plan in that repository's project, with no change), `{kind:"worktree",repositoryPath,base?}`,
|
|
318
|
+
`{kind:"commits",repositoryPath,head,base?}` or
|
|
319
|
+
`{kind:"pullRequest",repositoryPath,pullRequest}` (stored as commits plus the PR
|
|
320
|
+
origin). Commit revisions resolve on acceptance.
|
|
282
321
|
Omitted commit base means source at head with no diff, exactly as base=head;
|
|
283
322
|
supply its parent to review the changes introduced by a single commit.
|
|
284
323
|
|
|
@@ -68,12 +68,9 @@ export class ReviewActivity {
|
|
|
68
68
|
private readonly db: DatabaseSync,
|
|
69
69
|
private readonly assertReview?: (reviewId: string) => void,
|
|
70
70
|
) {
|
|
71
|
-
// Presences replace the per-scope leases; both last minutes, so the old
|
|
72
|
-
// table is dropped rather than migrated.
|
|
73
71
|
db.exec(`CREATE TABLE IF NOT EXISTS authoring_presences(
|
|
74
72
|
activity_id TEXT PRIMARY KEY, review_id TEXT NOT NULL, slot INTEGER NOT NULL,
|
|
75
|
-
started_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, focus TEXT, surface TEXT);
|
|
76
|
-
DROP TABLE IF EXISTS authoring_sessions;`);
|
|
73
|
+
started_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, focus TEXT, surface TEXT);`);
|
|
77
74
|
|
|
78
75
|
for (const row of db
|
|
79
76
|
.prepare(
|
|
@@ -20,7 +20,12 @@ import { type ReviewApiClient, ReviewApiError } from "./client.js";
|
|
|
20
20
|
import { exitOnInstallUpdate } from "./install-update.js";
|
|
21
21
|
import { callPublicTool, publicTool } from "./public-tools.js";
|
|
22
22
|
import { RECOVERY } from "./recovery.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
ASK_THREAD_ENV,
|
|
25
|
+
REVIEW_AGENT_HEADER,
|
|
26
|
+
REVIEW_THREAD_HEADER,
|
|
27
|
+
REVIEW_VIA_HEADER,
|
|
28
|
+
} from "./request-origin.js";
|
|
24
29
|
import { toolFailure } from "./tool-failure.js";
|
|
25
30
|
|
|
26
31
|
interface AgentCliInput {
|
|
@@ -64,9 +69,12 @@ export async function runReviewAgentCli(input: AgentCliInput): Promise<number> {
|
|
|
64
69
|
if (extra.length || (mode === "mcp" && name))
|
|
65
70
|
throw new Error("Unexpected arguments. Use whiteboard api --help.");
|
|
66
71
|
|
|
72
|
+
const thread = env[ASK_THREAD_ENV]?.trim();
|
|
73
|
+
|
|
67
74
|
const headers = Object.fromEntries([
|
|
68
75
|
[REVIEW_VIA_HEADER, mode === "mcp" ? "mcp" : "api"],
|
|
69
76
|
[REVIEW_AGENT_HEADER, reviewSessionAgent(env)],
|
|
77
|
+
[REVIEW_THREAD_HEADER, thread ?? ""],
|
|
70
78
|
]);
|
|
71
79
|
|
|
72
80
|
if (mode === "mcp") {
|
|
@@ -4,7 +4,12 @@ import {
|
|
|
4
4
|
selectionKey,
|
|
5
5
|
} from "@review/lens-selection.js";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type Pins,
|
|
9
|
+
anchorPins,
|
|
10
|
+
boardRepositoryId,
|
|
11
|
+
selectionReferences,
|
|
12
|
+
} from "./document.js";
|
|
8
13
|
import type { Snapshot } from "./store.js";
|
|
9
14
|
|
|
10
15
|
export interface AnchorQuote {
|
|
@@ -40,15 +45,17 @@ export async function anchorQuotes(
|
|
|
40
45
|
|
|
41
46
|
const known = new Set(
|
|
42
47
|
before
|
|
43
|
-
? selectionReferences(before.document, {
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
? selectionReferences(before.document, {
|
|
49
|
+
tolerant: true,
|
|
50
|
+
repositoryId: boardRepositoryId(before),
|
|
51
|
+
}).map(({ id, source }) => key(id, source))
|
|
46
52
|
: [],
|
|
47
53
|
);
|
|
48
54
|
|
|
49
|
-
const added = selectionReferences(after.document, {
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
const added = selectionReferences(after.document, {
|
|
56
|
+
tolerant: true,
|
|
57
|
+
repositoryId: boardRepositoryId(after),
|
|
58
|
+
}).filter(({ id, source }) => !known.has(key(id, source)));
|
|
52
59
|
|
|
53
60
|
const quotes = await Promise.all(
|
|
54
61
|
added
|