@dev.fast/whiteboard 0.4.0 → 0.4.1-preview.20261026.138
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-2yTAxoBx.js → account-alias-BPlPnzQM.js} +7 -7
- package/dist/{agent-cli-CHNDd4Ga.js → agent-cli-BbCf9uid.js} +11 -11
- package/dist/agent-cli-ipN_JmzG.js +2 -0
- package/dist/{agent-client-Dc0vHFJt.js → agent-client-DfpM8F5N.js} +3 -3
- package/dist/{authoring-tools-D8EkIDjm.js → authoring-tools-bm39DRoV.js} +23 -10
- package/dist/{background-server-DyWnPJyZ.js → background-server--0EeCCeV.js} +1 -1
- package/dist/{background-server-DHWHzpo5.js → background-server-B--5p72o.js} +6 -6
- package/dist/build-info.json +1 -1
- package/dist/{cli-qZomwJmK.js → cli-DBTQIroM.js} +2 -2
- package/dist/{cli-runner-DFBaVGij.js → cli-runner-CRFCdb7G.js} +25 -25
- package/dist/cli.js +3 -3
- package/dist/{fuzzy-match-CfSxqzBF.js → fuzzy-match-l3mJZ1Ge.js} +5 -5
- package/dist/{headless-host-dwlqhqMY.js → headless-host-5ei3nHS8.js} +1 -1
- package/dist/{headless-host-DJhcab6r.js → headless-host-BkJ9OUQY.js} +131 -299
- package/dist/{client-p9WB8Sj0.js → import-Bwvf9Wos.js} +505 -390
- package/dist/{lens-selection-9byzEgvU.js → lens-selection-EuZdag4Q.js} +1 -1
- package/dist/{local-data-DHedfhxw.js → local-data-BCSeTyic.js} +98 -154
- package/dist/{mcp-Br-r_wwR.js → mcp-_Xg1gQuA.js} +17 -8
- package/dist/{package-paths-BeYHYjeR.js → package-paths-Bg731FHS.js} +2 -2
- package/dist/profile-C8zL4u3L.js +2 -0
- package/dist/{profile-D9wdlPgw.js → profile-xzrUFgbT.js} +4 -4
- package/dist/{remote-attach-DSGpTOJ4.js → remote-attach-DF25Epoa.js} +4 -4
- package/dist/{remote-extensions-WtJO7pkb.js → remote-extensions-BJamgsCe.js} +1 -1
- package/dist/{remote-extensions-CsI_BJ_f.js → remote-extensions-CeywZL-Z.js} +4 -4
- package/dist/{remote-language-server-Dx13Mg2m.js → remote-language-server-CH8sG_CU.js} +6 -6
- package/dist/{remote-uninstall-Dp2o3qz0.js → remote-uninstall-Cq-lkBY7.js} +7 -7
- package/dist/{request-origin-CNSLVn7x.js → request-origin-CargJFvH.js} +14 -3
- package/dist/{review-app-launcher-BZAnR8Vp.js → review-app-launcher-D8ApNDUV.js} +3 -3
- package/dist/{review-home-paths-CG-sb9Cw.js → review-home-paths-DcB4xLD-.js} +2 -2
- package/dist/{review-telemetry-BM-_KdLi.js → review-telemetry-BW7gJrSo.js} +7 -7
- package/dist/{runtime-C6_1K-3Z.js → runtime-DY6Ctyog.js} +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-CsG2EWED.js → s3-BIWv4h1J.js} +2 -2
- package/dist/{s3-config-zz4GWh2T.js → s3-config-Du3RBIvv.js} +1 -1
- package/dist/server/desktop-host.js +51 -17
- package/dist/{server-discovery-DKLIOgg3.js → server-discovery-aHK_1r3U.js} +5 -5
- package/dist/sharing/index.d.ts +209 -98
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DYHVzX3b.js → src-DEOc8bvC.js} +63 -2
- package/dist/{src-Z1x4-Qmb.js → src-DjRaCctk.js} +6 -6
- package/dist/{stored-document-migration-DEt8iwem.js → stored-document-migration-CwmTwTb8.js} +139 -13
- package/dist/{thread-state-CO97FUqK.js → thread-state-CbLB0jUA.js} +1 -1
- package/dist/{tool-failure-C1L1vJyz.js → tool-failure-DZaQzi4f.js} +2 -2
- package/instructions/animation.md +345 -0
- package/instructions/authoring.md +2 -2
- package/package.json +6 -6
- package/src/fixtures/blocks/animation.json +19 -0
- package/src/review-api/authoring-tools.ts +17 -6
- package/src/review-api/blocks/animation.ts +98 -0
- package/src/review-api/blocks/index.ts +3 -0
- package/src/review-api/blocks/sequence.ts +3 -1
- package/src/review-api/comparison-coverage.ts +120 -89
- package/src/review-api/component-reference.ts +16 -0
- package/src/review-api/document-text.ts +11 -0
- package/src/review-api/document.ts +26 -5
- package/src/review-api/http.ts +31 -2
- package/src/review-api/instructions.ts +11 -2
- package/src/review-api/local-data.ts +21 -4
- package/src/review-api/mcp.ts +22 -14
- package/src/review-api/onboarding.ts +100 -26
- package/src/review-preferences.ts +43 -0
- package/src/server/desktop-server.ts +24 -0
- package/src/server/json-review-reporting.ts +1 -1
- package/src/server/review-server-core.ts +2 -0
- package/src/server/structural-comparisons.ts +5 -11
- package/src/server/structural-diff.ts +8 -2
- package/src/sharing/cli.ts +1 -1
- package/src/sharing/client.ts +54 -222
- package/src/sharing/export.ts +172 -114
- package/src/sharing/host.ts +32 -36
- package/src/sharing/import.ts +93 -147
- package/src/sharing/index.ts +1 -6
- package/src/sharing/routes.ts +18 -17
- package/src/ui-telemetry-events.ts +11 -0
- package/dist/agent-cli-Cn4-16XA.js +0 -2
- package/dist/profile-BopGMZUh.js +0 -2
- package/dist/{s3-config-CjcLyFV5.js → s3-config-D7Yt0VhS.js} +1 -1
- package/dist/{src-X9ZebeOD.js → src-DXnn7RrI.js} +1 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type StructuralDiff,
|
|
5
5
|
type StructuralRegion,
|
|
6
6
|
type StructuralSource,
|
|
7
|
+
applyStructuralUpdate,
|
|
7
8
|
structuralRows,
|
|
8
9
|
} from "@dev.fast/review-protocol";
|
|
9
10
|
import type { AlignmentRow } from "@review/lens-selection.js";
|
|
@@ -158,101 +159,131 @@ export async function comparisonCoverage(
|
|
|
158
159
|
coverage.files.map((file, index) => [file.path, index]),
|
|
159
160
|
);
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
for await (const event of data.structuralChanges({
|
|
164
|
-
reviewId,
|
|
165
|
-
pins,
|
|
166
|
-
signal,
|
|
167
|
-
})) {
|
|
168
|
-
if (event.type === "start") {
|
|
169
|
-
for (const entry of event.files)
|
|
170
|
-
remaining.add((entry.file.rhs ?? entry.file.lhs)!.path);
|
|
171
|
-
|
|
172
|
-
if (!remaining.size) break;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (event.type === "complete" && (event.failed || event.aborted))
|
|
177
|
-
throw new Error(
|
|
178
|
-
event.aborted?.message ?? "Structural coverage is incomplete.",
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
if (event.type !== "file") continue;
|
|
182
|
-
|
|
183
|
-
const path = (event.file.rhs ?? event.file.lhs)!.path;
|
|
184
|
-
|
|
185
|
-
if (!remaining.delete(path))
|
|
186
|
-
throw new Error(`Unexpected structural result: ${path}`);
|
|
187
|
-
|
|
188
|
-
const baseIndex = event.file.lhs && baseFiles.get(event.file.lhs.path);
|
|
189
|
-
const headIndex = event.file.rhs && headFiles.get(event.file.rhs.path);
|
|
190
|
-
|
|
191
|
-
if (event.error) {
|
|
192
|
-
if (
|
|
193
|
-
event.error.code !== "unsupported_file_type" &&
|
|
194
|
-
event.error.code !== "not_utf8"
|
|
195
|
-
)
|
|
196
|
-
throw new Error(`Cannot count ${path}: ${event.error.message}`);
|
|
197
|
-
|
|
198
|
-
for (const index of [baseIndex, headIndex])
|
|
199
|
-
if (index !== undefined) unsupported.add(coverage.files[index].path);
|
|
200
|
-
} else if (event.diff.type === "text") {
|
|
201
|
-
const diff = event.diff;
|
|
202
|
-
const folded = foldedChanges(diff);
|
|
203
|
-
|
|
204
|
-
// Match each side separately: diffr can split a Git rename into two files.
|
|
205
|
-
for (const side of ["base", "head"] as const) {
|
|
206
|
-
const source = side === "base" ? event.file.lhs : event.file.rhs;
|
|
207
|
-
|
|
208
|
-
const index =
|
|
209
|
-
source && (side === "base" ? baseFiles : headFiles).get(source.path);
|
|
210
|
-
|
|
211
|
-
if (index === undefined) continue;
|
|
212
|
-
const file = coverage.files[index];
|
|
213
|
-
|
|
214
|
-
const visible = subtractIntervals(
|
|
215
|
-
diff.structural_changes[side],
|
|
216
|
-
folded[side],
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
coverage.files[index] = {
|
|
220
|
-
...file,
|
|
221
|
-
folded: {
|
|
222
|
-
...(file.folded ?? emptyCoverage()),
|
|
223
|
-
[side]: subtractIntervals(file.changed[side], visible),
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const index = headIndex ?? baseIndex;
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
let resolved = false;
|
|
229
164
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
165
|
+
const counted = () => {
|
|
166
|
+
resolved = true;
|
|
167
|
+
resolve(current());
|
|
168
|
+
};
|
|
233
169
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
170
|
+
// Deferred updates keep refining `folded` after the counts resolve.
|
|
171
|
+
void (async () => {
|
|
172
|
+
const remaining = new Set<string>();
|
|
173
|
+
|
|
174
|
+
const diffs = new Map<
|
|
175
|
+
string,
|
|
176
|
+
Extract<StructuralDiff, { type: "text" }>
|
|
177
|
+
>();
|
|
178
|
+
|
|
179
|
+
for await (const event of data.structuralChanges({
|
|
180
|
+
reviewId,
|
|
181
|
+
pins,
|
|
182
|
+
signal,
|
|
183
|
+
})) {
|
|
184
|
+
if (event.type === "start") {
|
|
185
|
+
for (const entry of event.files)
|
|
186
|
+
remaining.add((entry.file.rhs ?? entry.file.lhs)!.path);
|
|
187
|
+
|
|
188
|
+
if (!remaining.size) break;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (event.type === "complete" && event.aborted)
|
|
193
|
+
throw new Error(event.aborted.message);
|
|
194
|
+
|
|
195
|
+
if (event.type !== "file" && event.type !== "update") continue;
|
|
196
|
+
|
|
197
|
+
const path = (event.file.rhs ?? event.file.lhs)!.path;
|
|
198
|
+
const baseIndex = event.file.lhs && baseFiles.get(event.file.lhs.path);
|
|
199
|
+
const headIndex = event.file.rhs && headFiles.get(event.file.rhs.path);
|
|
200
|
+
let diff: StructuralDiff | undefined;
|
|
201
|
+
|
|
202
|
+
if (event.type === "update") {
|
|
203
|
+
const previous = diffs.get(path);
|
|
204
|
+
|
|
205
|
+
if (!previous)
|
|
206
|
+
throw new Error(`Unexpected structural result: ${path}`);
|
|
207
|
+
|
|
208
|
+
if (event.error) continue;
|
|
209
|
+
diff = applyStructuralUpdate(previous, event);
|
|
210
|
+
} else if (!remaining.delete(path))
|
|
211
|
+
throw new Error(`Unexpected structural result: ${path}`);
|
|
212
|
+
else if (event.error) {
|
|
213
|
+
if (
|
|
214
|
+
event.error.code !== "unsupported_file_type" &&
|
|
215
|
+
event.error.code !== "not_utf8"
|
|
239
216
|
)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
217
|
+
throw new Error(`Cannot count ${path}: ${event.error.message}`);
|
|
218
|
+
|
|
219
|
+
for (const index of [baseIndex, headIndex])
|
|
220
|
+
if (index !== undefined)
|
|
221
|
+
unsupported.add(coverage.files[index].path);
|
|
222
|
+
} else diff = event.diff;
|
|
223
|
+
|
|
224
|
+
if (diff?.type === "text") {
|
|
225
|
+
diffs.set(path, diff);
|
|
226
|
+
const folded = foldedChanges(diff);
|
|
227
|
+
|
|
228
|
+
// Match each side separately: diffr can split a Git rename into two files.
|
|
229
|
+
for (const side of ["base", "head"] as const) {
|
|
230
|
+
const source = side === "base" ? event.file.lhs : event.file.rhs;
|
|
231
|
+
|
|
232
|
+
const index =
|
|
233
|
+
source &&
|
|
234
|
+
(side === "base" ? baseFiles : headFiles).get(source.path);
|
|
235
|
+
|
|
236
|
+
if (index === undefined) continue;
|
|
237
|
+
const file = coverage.files[index];
|
|
238
|
+
|
|
239
|
+
const visible = subtractIntervals(
|
|
240
|
+
diff.structural_changes[side],
|
|
241
|
+
folded[side],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
coverage.files[index] = {
|
|
245
|
+
...file,
|
|
246
|
+
folded: {
|
|
247
|
+
...(file.folded ?? emptyCoverage()),
|
|
248
|
+
[side]: subtractIntervals(file.changed[side], visible),
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const index = headIndex ?? baseIndex;
|
|
254
|
+
|
|
255
|
+
if (event.type === "file" && index !== undefined) {
|
|
256
|
+
const sources =
|
|
257
|
+
coverage.fileSources.get(coverage.files[index].path) ?? [];
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
sources.every(
|
|
261
|
+
(source) =>
|
|
262
|
+
source.file ===
|
|
263
|
+
(source.side === "base" ? event.file.lhs : event.file.rhs)
|
|
264
|
+
?.path,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
coverage.alignments = new Map(coverage.alignments).set(
|
|
268
|
+
coverage.files[index].path,
|
|
269
|
+
structuralRows(diff),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
publish?.(current());
|
|
275
|
+
|
|
276
|
+
if (!remaining.size && !resolved) counted();
|
|
245
277
|
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
publish?.(current());
|
|
249
278
|
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (remaining.size) throw new Error("Structural coverage is incomplete.");
|
|
279
|
+
if (remaining.size) throw new Error("Structural coverage is incomplete.");
|
|
254
280
|
|
|
255
|
-
|
|
281
|
+
if (!resolved) counted();
|
|
282
|
+
})().catch((error) => {
|
|
283
|
+
if (resolved) console.warn("Could not refine folded coverage:", error);
|
|
284
|
+
else reject(error);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
256
287
|
}
|
|
257
288
|
|
|
258
289
|
/**
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** The animation component, shown only while Animations are on. */
|
|
2
|
+
const ANIMATION = [
|
|
3
|
+
'- animation {title, html, frames: {key, title, label?, caption?, duration? (ms)}[] (1 to 60), code?: {name: anchor}, height?: "auto" (default) | number (120 to 1200 CSS px), pins?}; declare every possible frame',
|
|
4
|
+
'Use an animation for change over time that a static diagram cannot show: state before and after, a cache filling, data changing as a request crosses layers. It can sit in design beside the diagram, or replace it when the motion is the point. If nothing changes between frames, use a sequence or flow_diagram instead: their steps and nodes link to code. Read session_get_instructions({topic:"animation"}) before writing one.',
|
|
5
|
+
];
|
|
6
|
+
|
|
1
7
|
/** Every component's fields, for session_get_instructions: the edit tool's
|
|
2
8
|
* description must stay short enough that clients don't cut it off. */
|
|
3
9
|
export const COMPONENT_REFERENCE = [
|
|
@@ -26,6 +32,16 @@ export const COMPONENT_REFERENCE = [
|
|
|
26
32
|
"- store {label, storage: relational|document, collections: {key: {label, fields: {key: field}, source?: anchor of its schema declaration}}, dataStoreKind?: database|objectStore|bucket|artifactStore|fileStore}",
|
|
27
33
|
"- field {label, dataType, nullable?, primaryKey?, references?: {store, collection, field}, fields?: {key: field}}",
|
|
28
34
|
"- useCase {label, summary?, operations: operation[] (1 or more)}; operation {kind: read|write, actor, store, collection, label, source: anchor, field?, detail?, pins?}, naming declared keys",
|
|
35
|
+
...ANIMATION,
|
|
29
36
|
"",
|
|
30
37
|
'Example (replace its path and lines with verified ones): {"type":"sequence","title":"Request","actors":{"a":"Client","b":"Server"},"steps":[{"from":"a","to":"b","label":"Send","source":"head/src/app.ts#L10-L20"}]}',
|
|
31
38
|
].join("\n");
|
|
39
|
+
|
|
40
|
+
/** Keep disabled experimental components out of agent guidance. */
|
|
41
|
+
export function componentReference(animationsEnabled: boolean): string {
|
|
42
|
+
return animationsEnabled
|
|
43
|
+
? COMPONENT_REFERENCE
|
|
44
|
+
: COMPONENT_REFERENCE.split("\n")
|
|
45
|
+
.filter((line) => !ANIMATION.includes(line))
|
|
46
|
+
.join("\n");
|
|
47
|
+
}
|
|
@@ -196,6 +196,17 @@ export function documentText(
|
|
|
196
196
|
`[${edge.id}] ${edge.from} → ${edge.to}${edge.label ? `: ${edge.label}` : ""}`,
|
|
197
197
|
);
|
|
198
198
|
break;
|
|
199
|
+
case "animation":
|
|
200
|
+
for (const [index, frame] of element.frames.entries())
|
|
201
|
+
detail(
|
|
202
|
+
`Frame ${index + 1} [${frame.key}]: ${frame.title}${frame.caption ? ` — ${frame.caption}` : ""}`,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
for (const [name, anchor] of Object.entries(element.code))
|
|
206
|
+
detail(`Code ${name}: ${anchor}`);
|
|
207
|
+
|
|
208
|
+
if (detailed) code(depth + 1, { language: "html", text: element.html });
|
|
209
|
+
break;
|
|
199
210
|
case "software_map":
|
|
200
211
|
detail(
|
|
201
212
|
`Map: ${element.mapVersionId}${element.focusElementId ? `, focus: ${element.focusElementId}` : ""}`,
|
|
@@ -198,6 +198,11 @@ const unitTypes = new Set(["step", "flow_node", "flow_edge"]);
|
|
|
198
198
|
export const isUnit = (element: Element): element is Unit =>
|
|
199
199
|
unitTypes.has(element.type);
|
|
200
200
|
|
|
201
|
+
/** The reference id of a code peek an animation names: the canvas finds the
|
|
202
|
+
* peek's resolved anchor by it. */
|
|
203
|
+
export const animationCodeId = (blockId: string, name: string) =>
|
|
204
|
+
`${blockId}:code:${name}`;
|
|
205
|
+
|
|
201
206
|
/** Rewrites old review-source: and review-trace: links to their whiteboard: form. */
|
|
202
207
|
export function canonicalLink(href: string): string {
|
|
203
208
|
return href.replace(
|
|
@@ -523,6 +528,16 @@ function documentReferences(
|
|
|
523
528
|
),
|
|
524
529
|
);
|
|
525
530
|
|
|
531
|
+
if (element.type === "animation")
|
|
532
|
+
return Object.entries(element.code).flatMap(([name, anchor]) =>
|
|
533
|
+
select(anchor, element.pins).map((source) => ({
|
|
534
|
+
id: animationCodeId(element.id!, name),
|
|
535
|
+
source,
|
|
536
|
+
label: name,
|
|
537
|
+
peek: true,
|
|
538
|
+
})),
|
|
539
|
+
);
|
|
540
|
+
|
|
526
541
|
if (element.type === "call_stack_diff")
|
|
527
542
|
return [...element.base, ...element.head].flatMap((frame) => {
|
|
528
543
|
const pins = frame.pins ?? element.pins;
|
|
@@ -774,11 +789,17 @@ const namedByType = (types: string[]) =>
|
|
|
774
789
|
|
|
775
790
|
/** What agents are shown: content names only its type. The host parses
|
|
776
791
|
* editSchema, reporting the named kind's issues. */
|
|
777
|
-
export
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
792
|
+
export function publishedEdits(animationsEnabled = true) {
|
|
793
|
+
const types = writableBlockTypes.filter(
|
|
794
|
+
(type) => animationsEnabled || type !== "animation",
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
return edits(
|
|
798
|
+
namedByType([...types, "step", "flow_node", "flow_edge"]),
|
|
799
|
+
namedByType(types),
|
|
800
|
+
z.unknown(),
|
|
801
|
+
);
|
|
802
|
+
}
|
|
782
803
|
|
|
783
804
|
/**
|
|
784
805
|
* What one saved version did, for a canvas drawing the document as the
|
package/src/review-api/http.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
type ReviewTarget,
|
|
71
71
|
boardLinks,
|
|
72
72
|
boardRepositoryId,
|
|
73
|
+
elements,
|
|
73
74
|
} from "./document.js";
|
|
74
75
|
import { instructionsQuerySchema, renderInstructions } from "./instructions.js";
|
|
75
76
|
import type { LocalReviewData } from "./local-data.js";
|
|
@@ -206,6 +207,7 @@ export function createReviewApi(
|
|
|
206
207
|
hooks: ReviewApiHooks = {},
|
|
207
208
|
/** Desktop's Ask: local agents answering questions about a selection. */
|
|
208
209
|
ask?: AskHost,
|
|
210
|
+
animationsEnabled: () => boolean = () => false,
|
|
209
211
|
) {
|
|
210
212
|
const app = new Hono();
|
|
211
213
|
app.onError((error, context) => {
|
|
@@ -372,12 +374,15 @@ export function createReviewApi(
|
|
|
372
374
|
// renderer block tool listing and the first instructions call.
|
|
373
375
|
const instructionContext = async () => ({
|
|
374
376
|
traceEnabled: await traceEnabled(),
|
|
377
|
+
animationsEnabled: animationsEnabled(),
|
|
375
378
|
});
|
|
376
379
|
|
|
377
380
|
app.get("/authoring", async (context) => {
|
|
378
381
|
const instructions = await instructionContext();
|
|
379
382
|
|
|
380
|
-
return context.json(
|
|
383
|
+
return context.json(
|
|
384
|
+
authoringTools(instructions.traceEnabled, instructions.animationsEnabled),
|
|
385
|
+
);
|
|
381
386
|
});
|
|
382
387
|
app.get("/instructions", async (context) => {
|
|
383
388
|
const { topic } = instructionsQuerySchema.parse(context.req.query());
|
|
@@ -508,7 +513,10 @@ export function createReviewApi(
|
|
|
508
513
|
);
|
|
509
514
|
|
|
510
515
|
app.get("/capabilities", async (context) =>
|
|
511
|
-
context.json(
|
|
516
|
+
context.json({
|
|
517
|
+
...(await capabilities()),
|
|
518
|
+
animationsEnabled: animationsEnabled(),
|
|
519
|
+
}),
|
|
512
520
|
);
|
|
513
521
|
|
|
514
522
|
app.get("/:id/activity", (context) => {
|
|
@@ -2259,6 +2267,27 @@ export function createReviewApi(
|
|
|
2259
2267
|
|
|
2260
2268
|
const input = commandSchema.parse(request);
|
|
2261
2269
|
|
|
2270
|
+
if (!animationsEnabled() && input.operation.type === "edit") {
|
|
2271
|
+
const { edit, reviewId } = input.operation;
|
|
2272
|
+
const incoming = "content" in edit ? elements([edit.content]) : [];
|
|
2273
|
+
|
|
2274
|
+
const target =
|
|
2275
|
+
"targetId" in edit
|
|
2276
|
+
? elements(readReview(reviewId).document).find(
|
|
2277
|
+
(element) => element.id === edit.targetId,
|
|
2278
|
+
)
|
|
2279
|
+
: undefined;
|
|
2280
|
+
|
|
2281
|
+
if (
|
|
2282
|
+
incoming.some((element) => element.type === "animation") ||
|
|
2283
|
+
target?.type === "animation"
|
|
2284
|
+
)
|
|
2285
|
+
throw new ReviewInputError(
|
|
2286
|
+
"Animation authoring is disabled. Enable Animations in Settings under Experimental Features to edit animations. Existing animations can still be viewed.",
|
|
2287
|
+
409,
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2262
2291
|
const command = sharedCommandSchema.safeParse(input);
|
|
2263
2292
|
|
|
2264
2293
|
if (
|
|
@@ -4,10 +4,12 @@ import path from "node:path";
|
|
|
4
4
|
import { findReviewPackageRoot } from "@review/package-paths.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { componentReference } from "./component-reference.js";
|
|
8
|
+
import { ReviewInputError } from "./input-error.js";
|
|
8
9
|
|
|
9
10
|
export const INSTRUCTION_TOPICS = [
|
|
10
11
|
"authoring",
|
|
12
|
+
"animation",
|
|
11
13
|
"file-lenses",
|
|
12
14
|
"trace-archaeology",
|
|
13
15
|
] as const;
|
|
@@ -20,6 +22,7 @@ export const instructionsQuerySchema = z.strictObject({
|
|
|
20
22
|
|
|
21
23
|
export interface InstructionContext {
|
|
22
24
|
traceEnabled: boolean;
|
|
25
|
+
animationsEnabled?: boolean;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
const cache = new Map<string, Promise<string>>();
|
|
@@ -44,6 +47,12 @@ export async function renderInstructions(
|
|
|
44
47
|
context: InstructionContext,
|
|
45
48
|
root = findReviewPackageRoot(import.meta.url),
|
|
46
49
|
): Promise<string> {
|
|
50
|
+
if (topic === "animation" && !context.animationsEnabled)
|
|
51
|
+
throw new ReviewInputError(
|
|
52
|
+
"Animation authoring is disabled. Enable Animations in Settings under Experimental Features.",
|
|
53
|
+
409,
|
|
54
|
+
);
|
|
55
|
+
|
|
47
56
|
if (topic === "trace-archaeology" && !context.traceEnabled)
|
|
48
57
|
return "Trace capture is off on this machine, so no agent traces are available. It can be turned on in Whiteboard Desktop Settings under Experimental Features.";
|
|
49
58
|
|
|
@@ -59,7 +68,7 @@ export async function renderInstructions(
|
|
|
59
68
|
|
|
60
69
|
return [
|
|
61
70
|
await read(root, "authoring"),
|
|
62
|
-
|
|
71
|
+
componentReference(context.animationsEnabled === true),
|
|
63
72
|
...(context.traceEnabled
|
|
64
73
|
? [
|
|
65
74
|
'## Traces\n\nAt the end, run `whiteboard trace list --session <sessionId> --json`, which covers every commit in the review. If it lists no sessions, there are no traces to quote: skip this step. Otherwise read `session_get_instructions({topic:"trace-archaeology"})` and rewrite as much as possible of the what/why, design, and requirements sections in terms of literal trace quotes from the user.',
|
|
@@ -1817,6 +1817,8 @@ export class LocalReviewData {
|
|
|
1817
1817
|
state: "pending" | "ready" | "error";
|
|
1818
1818
|
error?: unknown;
|
|
1819
1819
|
partial?: ComparisonCoverage;
|
|
1820
|
+
abort: AbortController;
|
|
1821
|
+
worktree?: string;
|
|
1820
1822
|
}
|
|
1821
1823
|
>();
|
|
1822
1824
|
private readonly coverageAbort = new AbortController();
|
|
@@ -1877,11 +1879,24 @@ export class LocalReviewData {
|
|
|
1877
1879
|
let entry = this.coverageCache.get(key);
|
|
1878
1880
|
|
|
1879
1881
|
if (!entry || entry.state === "error") {
|
|
1882
|
+
const worktree = pins.worktreeRevision
|
|
1883
|
+
? JSON.stringify({ ...pins, worktreeRevision: undefined })
|
|
1884
|
+
: undefined;
|
|
1885
|
+
|
|
1886
|
+
// A newer worktree revision cancels older ones.
|
|
1887
|
+
for (const [cachedKey, cached] of this.coverageCache)
|
|
1888
|
+
if (worktree && cached.worktree === worktree) {
|
|
1889
|
+
cached.abort.abort();
|
|
1890
|
+
this.coverageCache.delete(cachedKey);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const abort = new AbortController();
|
|
1894
|
+
|
|
1880
1895
|
const promise = comparisonCoverage(
|
|
1881
1896
|
this,
|
|
1882
1897
|
reviewId,
|
|
1883
1898
|
pins,
|
|
1884
|
-
this.coverageAbort.signal,
|
|
1899
|
+
AbortSignal.any([this.coverageAbort.signal, abort.signal]),
|
|
1885
1900
|
(partial) => {
|
|
1886
1901
|
const current = this.coverageCache.get(key);
|
|
1887
1902
|
|
|
@@ -1890,7 +1905,7 @@ export class LocalReviewData {
|
|
|
1890
1905
|
},
|
|
1891
1906
|
);
|
|
1892
1907
|
|
|
1893
|
-
entry = { promise, state: "pending" };
|
|
1908
|
+
entry = { promise, state: "pending", abort, worktree };
|
|
1894
1909
|
this.coverageCache.set(key, entry);
|
|
1895
1910
|
const current = entry;
|
|
1896
1911
|
|
|
@@ -1910,7 +1925,7 @@ export class LocalReviewData {
|
|
|
1910
1925
|
|
|
1911
1926
|
void promise.then(
|
|
1912
1927
|
(value) => {
|
|
1913
|
-
current.partial
|
|
1928
|
+
current.partial ??= value;
|
|
1914
1929
|
|
|
1915
1930
|
if (!this.closed)
|
|
1916
1931
|
this.store.setDiffStats(pins, {
|
|
@@ -1931,7 +1946,9 @@ export class LocalReviewData {
|
|
|
1931
1946
|
);
|
|
1932
1947
|
}
|
|
1933
1948
|
|
|
1934
|
-
return entry.
|
|
1949
|
+
return entry.state === "ready" && entry.partial
|
|
1950
|
+
? Promise.resolve(entry.partial)
|
|
1951
|
+
: entry.promise;
|
|
1935
1952
|
}
|
|
1936
1953
|
|
|
1937
1954
|
changes(pins: Pins): Promise<LocalVcsDiffFileSummary[]>;
|
package/src/review-api/mcp.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Readable, Writable } from "node:stream";
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { type JsonValue, isJsonObject } from "@dev.fast/json";
|
|
4
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import {
|
|
7
7
|
CallToolRequestSchema,
|
|
8
|
+
type CallToolResult,
|
|
8
9
|
ListToolsRequestSchema,
|
|
9
10
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
11
|
import type { ReviewToolCall } from "@review/review-telemetry.js";
|
|
@@ -13,6 +14,7 @@ import type { ReviewSessionAgent } from "@review/ui-telemetry-events.js";
|
|
|
13
14
|
import {
|
|
14
15
|
type AuthoringTool,
|
|
15
16
|
type ConnectedReview,
|
|
17
|
+
ToolText,
|
|
16
18
|
toolResultText,
|
|
17
19
|
} from "./agent-client.js";
|
|
18
20
|
import { authoringTools } from "./authoring-tools.js";
|
|
@@ -37,6 +39,7 @@ function mcpAuthoringGuidance(context: { traceEnabled: boolean }): string {
|
|
|
37
39
|
'For why code exists, what an agent was thinking, or whether an agent solved something before, call session_get_instructions({topic:"trace-archaeology"}).',
|
|
38
40
|
]
|
|
39
41
|
: []),
|
|
42
|
+
"For programmatic MCP calls, use structuredContent when present; otherwise parse JSON from the text content when the result is JSON. An insert through session_edit uses edit.content for the actual document component, never the MCP transport wrapper containing content or structuredContent.",
|
|
40
43
|
"Never read or write Whiteboard files or SQL.",
|
|
41
44
|
].join(" ");
|
|
42
45
|
}
|
|
@@ -168,20 +171,20 @@ export async function serveReviewMcp(
|
|
|
168
171
|
)
|
|
169
172
|
return { content: [{ type: "text" as const, text: RECOVERY }] };
|
|
170
173
|
|
|
171
|
-
if (request.params.name === REVIEW_STATUS_TOOL.name && offlineStatus)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
error instanceof Error ? error.message : String(error),
|
|
179
|
-
),
|
|
180
|
-
),
|
|
181
|
-
},
|
|
182
|
-
],
|
|
174
|
+
if (request.params.name === REVIEW_STATUS_TOOL.name && offlineStatus) {
|
|
175
|
+
const result = await offlineStatus(
|
|
176
|
+
error instanceof Error ? error.message : String(error),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const response: CallToolResult = {
|
|
180
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
183
181
|
};
|
|
184
182
|
|
|
183
|
+
if (isJsonObject(result)) response.structuredContent = result;
|
|
184
|
+
|
|
185
|
+
return response;
|
|
186
|
+
}
|
|
187
|
+
|
|
185
188
|
throw error;
|
|
186
189
|
}
|
|
187
190
|
|
|
@@ -203,7 +206,7 @@ export async function serveReviewMcp(
|
|
|
203
206
|
const text = toolResultText(tool, result);
|
|
204
207
|
report();
|
|
205
208
|
|
|
206
|
-
|
|
209
|
+
const response: CallToolResult = {
|
|
207
210
|
content: [
|
|
208
211
|
{
|
|
209
212
|
type: "text",
|
|
@@ -214,6 +217,11 @@ export async function serveReviewMcp(
|
|
|
214
217
|
},
|
|
215
218
|
],
|
|
216
219
|
};
|
|
220
|
+
|
|
221
|
+
if (!(result instanceof ToolText) && isJsonObject(result))
|
|
222
|
+
response.structuredContent = result;
|
|
223
|
+
|
|
224
|
+
return response;
|
|
217
225
|
} catch (error) {
|
|
218
226
|
report(toolFailure(error, phase));
|
|
219
227
|
|