@dev.fast/whiteboard 0.3.3 → 0.3.4-preview.20261019.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-alias-xOdmMnDH.js → account-alias-ClRtWF_-.js} +8 -6
- package/dist/{agent-cli-DT7MdaqL.js → agent-cli-C6AM9dml.js} +15 -10
- package/dist/agent-cli-DKpWo-4F.js +2 -0
- package/dist/{agent-client-BRBzv3AV.js → agent-client-G4KxB8zB.js} +13 -3
- package/dist/{authoring-tools-DGSEag8y.js → authoring-tools-Dfo0uOSj.js} +34 -27
- package/dist/{background-server-WKzI_a98.js → background-server-BF_lLFxY.js} +5 -5
- package/dist/{background-server-C_REWFkG.js → background-server-DJfYWRoN.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-BBSDi_dX.js → cli-CqOiiTW_.js} +1 -1
- package/dist/{cli-runner-DshTBske.js → cli-runner-D8CsXff4.js} +26 -26
- package/dist/cli.js +2 -2
- package/dist/{client-BN_FDxFr.js → client-lKVF_Elo.js} +253 -47
- package/dist/{fuzzy-match-bFX6wm8j.js → fuzzy-match-DH9lZivo.js} +42 -14
- package/dist/{headless-host-CS4Gv0c5.js → headless-host-C4bhVPZx.js} +513 -140
- package/dist/{headless-host-CzvYpKOX.js → headless-host-CeNnXxiL.js} +1 -1
- package/dist/lens-selection-CyJapcsl.js +214 -0
- package/dist/{local-data-DtQTIzHX.js → local-data-BdqqCYUD.js} +2434 -328
- package/dist/{mcp-DpKnnmHH.js → mcp-B2mvDEhC.js} +5 -9
- package/dist/{package-paths-jMg6JxDP.js → package-paths-Cm1tR8c4.js} +1 -1
- package/dist/{profile-DaqmODhc.js → profile-DcigfSms.js} +20 -8
- package/dist/profile-DthRnZ7x.js +2 -0
- package/dist/{remote-attach-DCngg8cl.js → remote-attach-DGwVRmY5.js} +4 -4
- package/dist/{remote-extensions-BCAbQZTv.js → remote-extensions-CferWwM9.js} +3 -3
- package/dist/{remote-extensions-Cf3L-ZGG.js → remote-extensions-D8W2IgKH.js} +1 -1
- package/dist/{remote-language-server-UWQgpoh-.js → remote-language-server-By-rEosC.js} +5 -5
- package/dist/{remote-uninstall-CcDOcGyi.js → remote-uninstall-DgHnDAFH.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-DDmfnytX.js} +38 -10
- package/dist/{review-app-launcher-tqSQPlbu.js → review-app-launcher-CG3TFZEx.js} +2 -2
- package/dist/{review-home-paths-Lc02WFuX.js → review-home-paths-D1JVitqN.js} +19 -17
- package/dist/{review-telemetry-BHWJsnoD.js → review-telemetry-Myzm7o6-.js} +5 -5
- package/dist/runtime-Bj2tD33R.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-77-qkEhs.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-DDWWMh5x.js → s3-config-BTO12tCC.js} +1 -1
- package/dist/{s3-config-0xuCh-F_.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +29 -76
- package/dist/{server-discovery-BVqrE1_-.js → server-discovery-Bwa_6Tsj.js} +3 -3
- package/dist/sharing/index.d.ts +486 -70
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-BiWNbcF_.js} +89 -12
- package/dist/{src-BMPuxWv4.js → src-WPaQn75m.js} +208 -73
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-dFbewRTG.js} +270 -269
- package/dist/{thread-state-5pYr6_zM.js → thread-state-Do5LX10I.js} +7 -2
- package/dist/{tool-failure-BFt2xj9i.js → tool-failure-C9J3r7KD.js} +10 -3
- package/instructions/authoring.md +3 -2
- package/package.json +4 -4
- package/src/agent-connect.ts +8 -2
- package/src/ask/agents.ts +51 -9
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +16 -0
- package/src/ask/threads.ts +24 -3
- package/src/cli-runner.ts +4 -1
- package/src/diff-selection-migration.ts +48 -34
- package/src/heading-anchors.ts +42 -0
- package/src/lens-selection.ts +38 -2
- package/src/review-api/README.md +52 -13
- package/src/review-api/activity.ts +1 -4
- package/src/review-api/agent-cli.ts +9 -1
- package/src/review-api/anchor-quotes.ts +14 -7
- package/src/review-api/ask-history.ts +137 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/board-order.ts +144 -0
- package/src/review-api/checkout-groups.ts +560 -0
- package/src/review-api/document-headings.ts +58 -27
- package/src/review-api/document-text.ts +31 -2
- package/src/review-api/document.ts +281 -34
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +702 -123
- package/src/review-api/instructions.ts +0 -19
- package/src/review-api/link-preview.ts +113 -0
- package/src/review-api/local-data.ts +310 -46
- package/src/review-api/mcp.ts +3 -11
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +37 -6
- package/src/review-api/public-tools.ts +24 -2
- package/src/review-api/pull-request.ts +82 -49
- package/src/review-api/request-origin.ts +10 -0
- package/src/review-api/review-progress.ts +61 -27
- package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
- package/src/review-api/store-migrations.ts +357 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2144 -480
- package/src/review-api/workspaces.ts +52 -13
- package/src/review-api/worktree-events.ts +3 -5
- package/src/review-api/worktree-source.ts +3 -7
- package/src/review-api/worktree-structural.integration.ts +72 -0
- package/src/review-stack.ts +15 -23
- package/src/runtime.ts +4 -30
- package/src/server/desktop-host.ts +6 -1
- package/src/server/desktop-server.ts +1 -31
- package/src/server/headless-host.ts +0 -2
- package/src/server/hono-http.ts +2 -1
- package/src/server/legacy-tutorial-cleanup.ts +2 -3
- package/src/server/review-gateway-list.ts +3 -5
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +1 -5
- package/src/server/structural-comparisons.ts +47 -0
- package/src/sharing/export.ts +36 -7
- package/src/sharing/host.ts +34 -14
- package/src/sharing/import.ts +156 -24
- package/src/sharing/repository.ts +128 -20
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +25 -9
- package/dist/agent-cli-DeCrUC2s.js +0 -2
- package/dist/profile-Cz7L781D.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
|
@@ -2,12 +2,25 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { gitAt } from "@dev.fast/local-vcs";
|
|
5
|
+
import { type VcsArg, gitAt } from "@dev.fast/local-vcs";
|
|
6
6
|
import { normalizeGitHubRemote } from "@dev.fast/review-share-protocol";
|
|
7
7
|
import { type Pins, ReviewInputError } from "@review/review-api/document.js";
|
|
8
8
|
|
|
9
|
+
/** A full commit id, SHA-1 or SHA-256. */
|
|
10
|
+
export const COMMIT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
11
|
+
|
|
12
|
+
/** Refuses anything but full commit ids, before any of them reaches git:
|
|
13
|
+
* a shared board's commits come from someone else, and one spelled as an
|
|
14
|
+
* option or a refspec would make git do something else. */
|
|
15
|
+
export function assertCommitIds(commits: readonly string[]) {
|
|
16
|
+
if (!commits.every((commit) => COMMIT_ID.test(commit)))
|
|
17
|
+
throw new ReviewInputError(
|
|
18
|
+
"The shared whiteboard names a commit that isn't a full commit id.",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
/** Keep credential helpers but prevent the calling shell from redirecting Git's object store. */
|
|
10
|
-
export function sharedGit(cwd: string, args:
|
|
23
|
+
export function sharedGit(cwd: string, args: VcsArg[]) {
|
|
11
24
|
const env = Object.fromEntries(
|
|
12
25
|
Object.entries(process.env).filter(
|
|
13
26
|
([key]) =>
|
|
@@ -25,7 +38,7 @@ export function sharedGit(cwd: string, args: string[]) {
|
|
|
25
38
|
cwd,
|
|
26
39
|
["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", ...args],
|
|
27
40
|
{
|
|
28
|
-
env
|
|
41
|
+
env,
|
|
29
42
|
timeout: 300_000,
|
|
30
43
|
},
|
|
31
44
|
);
|
|
@@ -46,18 +59,28 @@ function fetchError(stderr: string, publishing: boolean): ReviewInputError {
|
|
|
46
59
|
);
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
/** Recipients need complete history; publication only checks that the commits exist.
|
|
62
|
+
/** Recipients need complete history; publication only checks that the commits exist.
|
|
63
|
+
* `commits` are other commits the board's references read at; a plan has
|
|
64
|
+
* only those. */
|
|
50
65
|
export async function fetchPinnedRepository(
|
|
51
66
|
root: string,
|
|
52
67
|
url: string,
|
|
53
|
-
pins: Pick<Pins, "base" | "head"
|
|
68
|
+
pins: Pick<Pins, "base" | "head"> | undefined,
|
|
54
69
|
publishing = false,
|
|
70
|
+
commits: readonly string[] = [],
|
|
55
71
|
) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
// Each commit and the ref it is fetched to.
|
|
73
|
+
const named = [
|
|
74
|
+
...(pins
|
|
75
|
+
? [
|
|
76
|
+
{ commit: pins.base, ref: "base" },
|
|
77
|
+
{ commit: pins.head, ref: "head" },
|
|
78
|
+
]
|
|
79
|
+
: []),
|
|
80
|
+
...commits.map((commit) => ({ commit, ref: `commits/${commit}` })),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
if (!named.every(({ commit }) => COMMIT_ID.test(commit)))
|
|
61
84
|
throw new ReviewInputError(
|
|
62
85
|
"Sharing requires immutable base and head commits.",
|
|
63
86
|
);
|
|
@@ -67,7 +90,7 @@ export async function fetchPinnedRepository(
|
|
|
67
90
|
if (!publishing) {
|
|
68
91
|
await sharedGit(root, ["config", "core.hooksPath", "/dev/null"]);
|
|
69
92
|
await sharedGit(root, ["config", "core.fsmonitor", "false"]);
|
|
70
|
-
await sharedGit(root, ["remote", "add", "origin", url]);
|
|
93
|
+
await sharedGit(root, ["remote", "add", "origin", { remote: url }]);
|
|
71
94
|
}
|
|
72
95
|
|
|
73
96
|
// Branch refs make Git reject non-commit objects during the fetch itself.
|
|
@@ -79,9 +102,10 @@ export async function fetchPinnedRepository(
|
|
|
79
102
|
"--no-tags",
|
|
80
103
|
"--force",
|
|
81
104
|
...(publishing ? ["--depth=1", "--filter=tree:0"] : []),
|
|
82
|
-
publishing ? url : "origin",
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
{ remote: publishing ? url : "origin" },
|
|
106
|
+
...named.map(({ commit, ref }) => ({
|
|
107
|
+
refspec: { from: commit, to: `${refs}/${ref}` },
|
|
108
|
+
})),
|
|
85
109
|
]);
|
|
86
110
|
} catch (error) {
|
|
87
111
|
throw fetchError(
|
|
@@ -91,21 +115,104 @@ export async function fetchPinnedRepository(
|
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
if (!publishing)
|
|
94
|
-
for (const commit of
|
|
95
|
-
await sharedGit(root, ["cat-file", "-e", `${commit}^{commit}`]);
|
|
118
|
+
for (const { commit } of named)
|
|
119
|
+
await sharedGit(root, ["cat-file", "-e", { rev: `${commit}^{commit}` }]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Fetches the commits a shared board's links read at into a checkout of
|
|
123
|
+
* its repository, from its origin unless it has them, and keeps each under
|
|
124
|
+
* `refs/review/shared/<share>/<commit>`, so `git gc` can't prune one the
|
|
125
|
+
* board still quotes. */
|
|
126
|
+
export async function fetchSharedCommits(
|
|
127
|
+
root: string,
|
|
128
|
+
commits: readonly string[],
|
|
129
|
+
share: string,
|
|
130
|
+
) {
|
|
131
|
+
assertCommitIds(commits);
|
|
132
|
+
|
|
133
|
+
const ref = (commit: string) => `refs/review/shared/${share}/${commit}`;
|
|
134
|
+
const missing: string[] = [];
|
|
135
|
+
|
|
136
|
+
for (const commit of commits)
|
|
137
|
+
try {
|
|
138
|
+
await sharedGit(root, ["cat-file", "-e", { rev: `${commit}^{commit}` }]);
|
|
139
|
+
await sharedGit(root, ["update-ref", { ref: ref(commit) }, { commit }]);
|
|
140
|
+
} catch {
|
|
141
|
+
missing.push(commit);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!missing.length) return;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await sharedGit(root, [
|
|
148
|
+
"fetch",
|
|
149
|
+
"--no-tags",
|
|
150
|
+
"origin",
|
|
151
|
+
...missing.map((commit) => ({
|
|
152
|
+
refspec: { from: commit, to: ref(commit), force: true },
|
|
153
|
+
})),
|
|
154
|
+
]);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw fetchError(
|
|
157
|
+
error instanceof Error && "stderr" in error ? String(error.stderr) : "",
|
|
158
|
+
false,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Drops the refs `fetchSharedCommits` kept for a share, once the share is
|
|
164
|
+
* gone, so `git gc` can prune what only it quoted. */
|
|
165
|
+
export async function forgetSharedCommits(root: string, share: string) {
|
|
166
|
+
const { stdout } = await sharedGit(root, [
|
|
167
|
+
"for-each-ref",
|
|
168
|
+
"--format=%(refname)",
|
|
169
|
+
{ ref: `refs/review/shared/${share}/` },
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
for (const ref of stdout.split("\n").filter(Boolean))
|
|
173
|
+
await sharedGit(root, ["update-ref", "-d", { ref }]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A managed clone of a repository's default branch, for a shared board
|
|
177
|
+
* with no pins whose repository the receiver has no checkout of. */
|
|
178
|
+
export async function cloneSharedRepository(root: string, url: string) {
|
|
179
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
180
|
+
await sharedGit(root, ["init", "--quiet"]);
|
|
181
|
+
await sharedGit(root, ["config", "core.hooksPath", "/dev/null"]);
|
|
182
|
+
await sharedGit(root, ["config", "core.fsmonitor", "false"]);
|
|
183
|
+
await sharedGit(root, ["remote", "add", "origin", { remote: url }]);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await sharedGit(root, ["fetch", "--no-tags", "origin", "HEAD"]);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw fetchError(
|
|
189
|
+
error instanceof Error && "stderr" in error ? String(error.stderr) : "",
|
|
190
|
+
false,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await sharedGit(root, ["checkout", "--detach", "FETCH_HEAD"]);
|
|
96
195
|
}
|
|
97
196
|
|
|
98
197
|
export async function repositoryReady(
|
|
99
198
|
root: string,
|
|
100
199
|
pins: Pick<Pins, "base" | "head">,
|
|
200
|
+
commits: readonly string[] = [],
|
|
101
201
|
): Promise<boolean> {
|
|
202
|
+
if (
|
|
203
|
+
![pins.base, pins.head, ...commits].every((commit) =>
|
|
204
|
+
COMMIT_ID.test(commit),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
return false;
|
|
208
|
+
|
|
102
209
|
try {
|
|
103
210
|
const head = await sharedGit(root, ["rev-parse", "HEAD"]);
|
|
104
211
|
|
|
105
212
|
if (head.stdout.trim() !== pins.head) return false;
|
|
106
213
|
|
|
107
|
-
for (const commit of [pins.base, pins.head])
|
|
108
|
-
await sharedGit(root, ["cat-file", "-e", `${commit}^{commit}`]);
|
|
214
|
+
for (const commit of [pins.base, pins.head, ...commits])
|
|
215
|
+
await sharedGit(root, ["cat-file", "-e", { rev: `${commit}^{commit}` }]);
|
|
109
216
|
|
|
110
217
|
return true;
|
|
111
218
|
} catch {
|
|
@@ -129,14 +236,15 @@ export async function readShareRepository(root: string) {
|
|
|
129
236
|
|
|
130
237
|
export async function verifyShareRepository(
|
|
131
238
|
root: string,
|
|
132
|
-
pins: Pins,
|
|
239
|
+
pins: Pins | undefined,
|
|
133
240
|
repository?: { cloneUrl: string },
|
|
241
|
+
commits: readonly string[] = [],
|
|
134
242
|
) {
|
|
135
243
|
const { cloneUrl } = repository ?? (await readShareRepository(root));
|
|
136
244
|
const temporary = await mkdtemp(path.join(tmpdir(), "review-share-check-"));
|
|
137
245
|
|
|
138
246
|
try {
|
|
139
|
-
await fetchPinnedRepository(temporary, cloneUrl, pins, true);
|
|
247
|
+
await fetchPinnedRepository(temporary, cloneUrl, pins, true, commits);
|
|
140
248
|
} finally {
|
|
141
249
|
await rm(temporary, { recursive: true, force: true });
|
|
142
250
|
}
|
package/src/startup-trace.ts
CHANGED
|
@@ -221,9 +221,9 @@ if (traceEnabled) {
|
|
|
221
221
|
// Every git/jj spawn inside @dev.fast/local-vcs becomes a `$ …` span under
|
|
222
222
|
// whatever span is active in the caller's async context.
|
|
223
223
|
setLocalVcsCommandObserver({
|
|
224
|
-
start({
|
|
225
|
-
const handle = startSpan(commandSpanName(
|
|
226
|
-
detail: commandDetail(
|
|
224
|
+
start({ tool, args, cwd }) {
|
|
225
|
+
const handle = startSpan(commandSpanName(tool, args), {
|
|
226
|
+
detail: commandDetail(tool, args, cwd),
|
|
227
227
|
});
|
|
228
228
|
|
|
229
229
|
return ({ ok }) => (ok ? handle.end() : handle.fail());
|
|
@@ -1,18 +1,124 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type JsonObject,
|
|
3
|
+
type JsonValue,
|
|
4
|
+
isBooleanValue,
|
|
5
|
+
isJsonObject,
|
|
6
|
+
isNumberValue,
|
|
7
|
+
isStringValue,
|
|
8
|
+
} from "@dev.fast/review-protocol";
|
|
2
9
|
import { z } from "zod";
|
|
3
10
|
|
|
4
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ATTACHMENT_KEYS,
|
|
13
|
+
migrateDiffSelections,
|
|
14
|
+
retiredSelection,
|
|
15
|
+
} from "./diff-selection-migration.js";
|
|
5
16
|
import { formatAnchor } from "./lens-selection.js";
|
|
6
17
|
import { label } from "./review-api/blocks/definition.js";
|
|
7
18
|
import { type Lens, lensTargetsSchema } from "./review-api/diff-lenses.js";
|
|
19
|
+
import { respellLinks } from "./review-api/document.js";
|
|
8
20
|
|
|
9
21
|
/** Upgrade a saved or shared Review document to the current block schema at the
|
|
10
22
|
* read/import boundary. New edits use the strict schema and never accept these
|
|
11
|
-
* retired forms.
|
|
23
|
+
* retired forms. One walk makes a fresh copy: retired attachments become
|
|
24
|
+
* selections, anchors become strings, sections lose their status, and
|
|
25
|
+
* Markdown links are spelled the current way. */
|
|
12
26
|
// This is the decoder boundary for stored documents in retired wire formats.
|
|
13
27
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters
|
|
14
28
|
export function migrateStoredDocument(input: unknown): JsonValue {
|
|
15
|
-
return
|
|
29
|
+
return upgradeNode(input, { attachment: false, anchors: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Where a node sits: under a key that held a retired attachment, and
|
|
33
|
+
* whether its anchors still need converting (not inside an anchor). */
|
|
34
|
+
interface NodePlace {
|
|
35
|
+
attachment: boolean;
|
|
36
|
+
anchors: boolean;
|
|
37
|
+
blockPins?: JsonValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Decoder boundary: every value is checked to be JSON as it is copied.
|
|
41
|
+
function upgradeNode(value: unknown, place: NodePlace): JsonValue {
|
|
42
|
+
if (isStringValue(value) || isBooleanValue(value) || value === null)
|
|
43
|
+
return value;
|
|
44
|
+
|
|
45
|
+
if (isNumberValue(value) && Number.isFinite(value)) return value;
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return value.map((child) => upgradeNode(child, place));
|
|
49
|
+
|
|
50
|
+
if (!isJsonObject(value)) throw new TypeError("A document must be JSON.");
|
|
51
|
+
|
|
52
|
+
const selection = place.attachment ? retiredSelection(value) : undefined;
|
|
53
|
+
|
|
54
|
+
if (selection)
|
|
55
|
+
return upgradeNode(selection, { attachment: false, anchors: false });
|
|
56
|
+
|
|
57
|
+
const inherited = place.anchors ? (value.pins ?? place.blockPins) : undefined;
|
|
58
|
+
let pins: JsonValue | undefined;
|
|
59
|
+
|
|
60
|
+
const convert = (anchor: JsonValue): JsonValue => {
|
|
61
|
+
// Most anchors are strings already.
|
|
62
|
+
if (isStringValue(anchor)) return anchor;
|
|
63
|
+
|
|
64
|
+
const parsed = selectionObjectSchema.safeParse(anchor);
|
|
65
|
+
|
|
66
|
+
if (!parsed.success) return anchor;
|
|
67
|
+
|
|
68
|
+
const { pins: own, ...anchorAt } = parsed.data;
|
|
69
|
+
|
|
70
|
+
if (own) pins = own;
|
|
71
|
+
|
|
72
|
+
return formatAnchor(anchorAt);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const entries: [string, JsonValue][] = [];
|
|
76
|
+
|
|
77
|
+
for (const [key, child] of Object.entries(value)) {
|
|
78
|
+
if (key === "status" && value.type === "section") continue;
|
|
79
|
+
|
|
80
|
+
if (key === "markdown" && isStringValue(child)) {
|
|
81
|
+
entries.push([key, respellLinks(child)]);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const attachment = ATTACHMENT_KEYS.includes(key);
|
|
86
|
+
|
|
87
|
+
if (place.anchors && ANCHOR_KEYS.includes(key)) {
|
|
88
|
+
const anchors = upgradeNode(child, { attachment, anchors: false });
|
|
89
|
+
|
|
90
|
+
entries.push([
|
|
91
|
+
key,
|
|
92
|
+
Array.isArray(anchors) ? anchors.map(convert) : convert(anchors),
|
|
93
|
+
]);
|
|
94
|
+
} else
|
|
95
|
+
entries.push([
|
|
96
|
+
key,
|
|
97
|
+
upgradeNode(child, {
|
|
98
|
+
attachment,
|
|
99
|
+
anchors: place.anchors,
|
|
100
|
+
blockPins: inherited,
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const upgraded: JsonObject = Object.fromEntries(entries);
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
pins !== undefined &&
|
|
109
|
+
value.kind !== "ranges" &&
|
|
110
|
+
JSON.stringify(pins) !== JSON.stringify(place.blockPins)
|
|
111
|
+
)
|
|
112
|
+
upgraded.pins = pins;
|
|
113
|
+
|
|
114
|
+
return upgraded;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A stored document as the store reads it: retired forms upgraded, and
|
|
118
|
+
* lens blocks lifted out to the snapshot's own lenses. */
|
|
119
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Decoder boundary for stored documents in retired formats.
|
|
120
|
+
export function readStoredDocument(input: unknown): LiftedLenses {
|
|
121
|
+
return liftFileLenses(migrateStoredDocument(input));
|
|
16
122
|
}
|
|
17
123
|
|
|
18
124
|
/** Lenses saved before ranges were anchor strings. */
|
|
@@ -82,21 +188,6 @@ export function anchorStrings(
|
|
|
82
188
|
return migrated;
|
|
83
189
|
}
|
|
84
190
|
|
|
85
|
-
/** Sections once carried an optional `status` (pending, in_progress or
|
|
86
|
-
* complete). The authoring lease is now the only signal of work in progress,
|
|
87
|
-
* so versions saved before it was retired drop the field when read. */
|
|
88
|
-
function dropSectionStatus(value: JsonValue): JsonValue {
|
|
89
|
-
if (Array.isArray(value)) return value.map(dropSectionStatus);
|
|
90
|
-
|
|
91
|
-
if (!isJsonObject(value)) return value;
|
|
92
|
-
|
|
93
|
-
return Object.fromEntries(
|
|
94
|
-
Object.entries(value)
|
|
95
|
-
.filter(([key]) => !(value.type === "section" && key === "status"))
|
|
96
|
-
.map(([key, child]) => [key, dropSectionStatus(child)]),
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
191
|
/** A saved `file_lens` block, from before lenses left the document. */
|
|
101
192
|
const legacyFileLensSchema = z.object({
|
|
102
193
|
type: z.literal("file_lens"),
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { z } from "zod";
|
|
30
30
|
|
|
31
31
|
import { askAgentIds } from "./ask/thread-state";
|
|
32
|
+
import { SELECTION_UNAVAILABLE_REASONS } from "./lens-selection";
|
|
32
33
|
import { containsFilePath, hasPossibleUserInfo } from "./telemetry-clean-text";
|
|
33
34
|
|
|
34
35
|
/** Property value validators: an enum of allowed strings, or a scalar type. */
|
|
@@ -70,6 +71,13 @@ const PEEK_VIA = [
|
|
|
70
71
|
|
|
71
72
|
export const PEEK_ROOT_KIND = ["range"] as const;
|
|
72
73
|
|
|
74
|
+
/** `unresolved`: progress finished without this selection; `error`: progress failed. */
|
|
75
|
+
export const PEEK_FAILURE_REASON = [
|
|
76
|
+
...SELECTION_UNAVAILABLE_REASONS,
|
|
77
|
+
"error",
|
|
78
|
+
"unresolved",
|
|
79
|
+
] as const;
|
|
80
|
+
|
|
73
81
|
export const LSP_FEATURE = [
|
|
74
82
|
"hover",
|
|
75
83
|
"goto_definition",
|
|
@@ -137,7 +145,6 @@ export const SETTING_NAME = [
|
|
|
137
145
|
"telemetry_enabled",
|
|
138
146
|
"keymap",
|
|
139
147
|
"software_map_enabled",
|
|
140
|
-
"scratchpad_enabled",
|
|
141
148
|
"diffr_config",
|
|
142
149
|
"theme",
|
|
143
150
|
"ctrl_tab",
|
|
@@ -149,12 +156,8 @@ export const SETTING_NAME = [
|
|
|
149
156
|
|
|
150
157
|
export const REVIEW_OPENED_VIA = ["home", "cli", "other"] as const;
|
|
151
158
|
|
|
152
|
-
/** Where a review's source comes from: its target's kind
|
|
153
|
-
export const SESSION_SOURCE_KIND = [
|
|
154
|
-
"worktree",
|
|
155
|
-
"commits",
|
|
156
|
-
"scratchpad",
|
|
157
|
-
] as const;
|
|
159
|
+
/** Where a review's source comes from: its target's kind. */
|
|
160
|
+
export const SESSION_SOURCE_KIND = ["worktree", "commits", "project"] as const;
|
|
158
161
|
|
|
159
162
|
/**
|
|
160
163
|
* The authoring agent a review session is attributed to. This is the one
|
|
@@ -246,6 +249,7 @@ export const INSTALL_LOCATION = [
|
|
|
246
249
|
|
|
247
250
|
export const DIFF_OPENED_VIA = [
|
|
248
251
|
"topbar",
|
|
252
|
+
"sidebar",
|
|
249
253
|
"lens",
|
|
250
254
|
"locate",
|
|
251
255
|
"file_picker",
|
|
@@ -254,6 +258,15 @@ export const DIFF_OPENED_VIA = [
|
|
|
254
258
|
/** Which diff opened: a commit's, or a file diff drawn plain or structural. */
|
|
255
259
|
export const DIFF_OPENED_KIND = ["commit", "structural"] as const;
|
|
256
260
|
|
|
261
|
+
export const DIFF_FAILED_REASON = [
|
|
262
|
+
"missing_binary",
|
|
263
|
+
"process_exit",
|
|
264
|
+
"stream_ended",
|
|
265
|
+
"aborted",
|
|
266
|
+
"file_error",
|
|
267
|
+
"other",
|
|
268
|
+
] as const;
|
|
269
|
+
|
|
257
270
|
/** GitHub Enterprise is any GitHub host but github.com. */
|
|
258
271
|
export const PR_PROVIDER = [
|
|
259
272
|
"github",
|
|
@@ -594,7 +607,10 @@ export const UI_TELEMETRY_EVENTS = {
|
|
|
594
607
|
event: "review_diff_opened",
|
|
595
608
|
properties: { kind: DIFF_OPENED_KIND, via: DIFF_OPENED_VIA },
|
|
596
609
|
},
|
|
597
|
-
|
|
610
|
+
diff_failed: {
|
|
611
|
+
event: "review_diff_failed",
|
|
612
|
+
properties: { reason: DIFF_FAILED_REASON, files: "number" },
|
|
613
|
+
},
|
|
598
614
|
discord_clicked: {
|
|
599
615
|
event: "review_discord_clicked",
|
|
600
616
|
properties: { via: DISCORD_VIA },
|
|
@@ -613,7 +629,7 @@ export const UI_TELEMETRY_EVENTS = {
|
|
|
613
629
|
},
|
|
614
630
|
peek_resolve_failed: {
|
|
615
631
|
event: "review_peek_resolve_failed",
|
|
616
|
-
properties: { root_kind: PEEK_ROOT_KIND },
|
|
632
|
+
properties: { root_kind: PEEK_ROOT_KIND, reason: PEEK_FAILURE_REASON },
|
|
617
633
|
},
|
|
618
634
|
review_deleted: {
|
|
619
635
|
event: "review_review_deleted",
|
package/dist/profile-Cz7L781D.js
DELETED
package/dist/runtime-CKiWhGrf.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
//#region src/runtime.ts
|
|
4
|
-
async function resolveReviewRoot(cwd, execFile = execFilePromise) {
|
|
5
|
-
const initialCwd = path.resolve(cwd);
|
|
6
|
-
try {
|
|
7
|
-
const { stdout } = await execFile("jj", ["root"], { cwd: initialCwd });
|
|
8
|
-
const root = stdout.trim();
|
|
9
|
-
if (root) return path.resolve(root);
|
|
10
|
-
} catch {}
|
|
11
|
-
try {
|
|
12
|
-
const { stdout } = await execFile("git", [
|
|
13
|
-
"-C",
|
|
14
|
-
initialCwd,
|
|
15
|
-
"rev-parse",
|
|
16
|
-
"--show-toplevel"
|
|
17
|
-
]);
|
|
18
|
-
const root = stdout.trim();
|
|
19
|
-
if (root) return path.resolve(root);
|
|
20
|
-
} catch {}
|
|
21
|
-
return initialCwd;
|
|
22
|
-
}
|
|
23
|
-
function execFilePromise(file, args, options = {}) {
|
|
24
|
-
return execFileUntraced(file, args, options);
|
|
25
|
-
}
|
|
26
|
-
function execFileUntraced(file, args, options = {}) {
|
|
27
|
-
return new Promise((resolve, reject) => {
|
|
28
|
-
const child = spawn(file, args, {
|
|
29
|
-
cwd: options.cwd,
|
|
30
|
-
stdio: [
|
|
31
|
-
"ignore",
|
|
32
|
-
"pipe",
|
|
33
|
-
"pipe"
|
|
34
|
-
]
|
|
35
|
-
});
|
|
36
|
-
let stdout = "";
|
|
37
|
-
let stderr = "";
|
|
38
|
-
child.stdout?.on("data", (chunk) => {
|
|
39
|
-
stdout += String(chunk);
|
|
40
|
-
});
|
|
41
|
-
child.stderr?.on("data", (chunk) => {
|
|
42
|
-
stderr += String(chunk);
|
|
43
|
-
});
|
|
44
|
-
child.on("error", reject);
|
|
45
|
-
child.on("exit", (code) => {
|
|
46
|
-
if (code === 0) resolve({ stdout });
|
|
47
|
-
else reject(new Error(stderr.trim() || `${file} exited with ${code}`));
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
//#endregion
|
|
52
|
-
export { resolveReviewRoot as t };
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Scratchpad
|
|
2
|
-
|
|
3
|
-
The scratchpad is one document in Whiteboard that is not a session for any change. It has no target, base, head or lifecycle. You draw on it the way you would sketch an explanation on a whiteboard: a short paragraph, a sequence or flow diagram, a call tree, a code peek, and the smallest view that makes the point. The user reads it in the app and copies blocks back to you.
|
|
4
|
-
|
|
5
|
-
Its id is fixed: `scratchpad`. While it is turned on in Whiteboard Settings it always exists; never create, rename, trash, delete or share it.
|
|
6
|
-
|
|
7
|
-
## When to draw here
|
|
8
|
-
|
|
9
|
-
- The user asks to be shown how something works, wants a picture of a flow, a call path, a data shape or a layout, or is thinking out loud with you before there is a concrete plan.
|
|
10
|
-
- The explanation spans more than one file or more than one repository, or would be an ASCII diagram in chat.
|
|
11
|
-
|
|
12
|
-
Answer in chat when one sentence or one code line does it. Do not use the scratchpad to review a change; author a session instead (`session_get_instructions({})`).
|
|
13
|
-
|
|
14
|
-
## How
|
|
15
|
-
|
|
16
|
-
1. `session_capabilities({})`. Draw only when `desktopAvailable` and `scratchpadEnabled` are true. Otherwise answer in chat; if the pad is off, say once that it can be turned on in Whiteboard Settings.
|
|
17
|
-
2. Every source reference needs pins. For each repository you will quote, `session_register_repository({path})` for its `repositoryId`, and `git rev-parse HEAD` (or the commit the user is looking at) for a commit id. Put `pins: {repositoryId, head}` on each block that quotes code (`code_peek`, `sequence`, `flow_diagram`, `call_stack_diff`, `database_lens` and `markdown` with `review-source:` links); a step, frame, attachment or operation that quotes other commits than its block takes its own `pins`. Add `base` only when an anchor reads the base side. A reference without pins is rejected, since the scratchpad has none to lend.
|
|
18
|
-
3. Read what you cite at those commits, for example `git show <head>:<path>` in the repository, so line numbers match the pins.
|
|
19
|
-
4. Call `session_activity_begin` and pass the `activityId` it returns on each `session_edit` as you append blocks. Omitted placement puts the block at the top of the pad, and the response's `targetId` names it. A thought that spans several blocks reads top-down only if you insert it bottom-up (last block first) or give each following block `afterId` of the block you just inserted. Draw a diagram whole in one insert; fix one you drew earlier by patching its nodes, edges or steps by ID, and `replace` a block only when the whole picture was wrong. Each block's fields are in the Components section below; read `session_get_instructions({topic:"authoring"})` for choosing components. End the activity when finished.
|
|
20
|
-
5. After your first insert in a session, `session_open({sessionId: "scratchpad"})` once so the pad is showing. Do not call it again for later edits; they appear live.
|
|
21
|
-
6. In chat, say in one line what you drew. Do not repeat the diagram there.
|
|
22
|
-
|
|
23
|
-
Keep it a log, not a document: the newest thought sits at the top, above older ones. Do not build sections, outlines or status markers unless the user asks for structure. When the user asks to clear the pad, `replace` or `remove` its blocks; history keeps what was there.
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
type JsonValue,
|
|
6
|
-
isJsonObject,
|
|
7
|
-
parseJsonText,
|
|
8
|
-
} from "@dev.fast/review-protocol";
|
|
9
|
-
import { writePrivateJsonAtomic } from "@dev.fast/trace-core";
|
|
10
|
-
|
|
11
|
-
import { devReviewHome } from "./review-home-paths";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Machine-wide Review preferences the server itself needs. Workbench settings
|
|
15
|
-
* do not work here: the review server never reads the workbench
|
|
16
|
-
* configuration.
|
|
17
|
-
*/
|
|
18
|
-
export interface ReviewPreferences {
|
|
19
|
-
/**
|
|
20
|
-
* Whether the scratchpad exists on this machine. Off, the server neither
|
|
21
|
-
* makes nor lists it and Review's instructions do not offer it to agents.
|
|
22
|
-
*/
|
|
23
|
-
scratchpadEnabled: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const DEFAULT_REVIEW_PREFERENCES: ReviewPreferences = {
|
|
27
|
-
scratchpadEnabled: false,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
function reviewPreferencesPath(devHome = devReviewHome()): string {
|
|
31
|
-
return path.join(devHome, "preferences.json");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** A missing or unreadable file falls back to the defaults; it never throws. */
|
|
35
|
-
export async function readReviewPreferences(
|
|
36
|
-
devHome?: string,
|
|
37
|
-
): Promise<ReviewPreferences> {
|
|
38
|
-
try {
|
|
39
|
-
const raw = parseJsonText(
|
|
40
|
-
await readFile(reviewPreferencesPath(devHome), "utf8"),
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
scratchpadEnabled: parseScratchpadEnabled(raw),
|
|
45
|
-
};
|
|
46
|
-
} catch {
|
|
47
|
-
return { ...DEFAULT_REVIEW_PREFERENCES };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function writeReviewPreferences(
|
|
52
|
-
preferences: ReviewPreferences,
|
|
53
|
-
devHome?: string,
|
|
54
|
-
): Promise<ReviewPreferences> {
|
|
55
|
-
const next: ReviewPreferences = {
|
|
56
|
-
scratchpadEnabled: preferences.scratchpadEnabled === true,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
await writePrivateJsonAtomic(reviewPreferencesPath(devHome), next);
|
|
60
|
-
|
|
61
|
-
return next;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Reads the scratchpad preference; a missing file means off. */
|
|
65
|
-
export async function readScratchpadEnabled(
|
|
66
|
-
devHome?: string,
|
|
67
|
-
): Promise<boolean> {
|
|
68
|
-
return (await readReviewPreferences(devHome)).scratchpadEnabled;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Sets the scratchpad preference, keeping the other preferences as they are. */
|
|
72
|
-
export async function writeScratchpadEnabled(
|
|
73
|
-
enabled: boolean,
|
|
74
|
-
devHome?: string,
|
|
75
|
-
): Promise<boolean> {
|
|
76
|
-
const current = await readReviewPreferences(devHome);
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
await writeReviewPreferences(
|
|
80
|
-
{ ...current, scratchpadEnabled: enabled },
|
|
81
|
-
devHome,
|
|
82
|
-
)
|
|
83
|
-
).scratchpadEnabled;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function parseScratchpadEnabled(raw: JsonValue): boolean {
|
|
87
|
-
return isJsonObject(raw) && raw.scratchpadEnabled === true;
|
|
88
|
-
}
|