@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
|
@@ -17,7 +17,10 @@ import { ReviewInputError } from "./document.js";
|
|
|
17
17
|
|
|
18
18
|
/** A saved Ask conversation: its history entry, and how to reopen it. */
|
|
19
19
|
export const askRecordSchema = askHistoryEntrySchema.extend({
|
|
20
|
-
|
|
20
|
+
/** The board the conversation started on, and the block if any. */
|
|
21
|
+
anchor: z.object({ board: z.string(), block: z.string().optional() }),
|
|
22
|
+
/** Absent for a shared board. */
|
|
23
|
+
projectId: z.string().optional(),
|
|
21
24
|
/** The ACP session the agent keeps the transcript under. */
|
|
22
25
|
sessionId: z.string(),
|
|
23
26
|
/** The review version asked about; its pins recreate the checkout. */
|
|
@@ -34,7 +37,9 @@ export type AskRecord = z.infer<typeof askRecordSchema>;
|
|
|
34
37
|
const rowSchema = z
|
|
35
38
|
.object({
|
|
36
39
|
id: z.string(),
|
|
37
|
-
|
|
40
|
+
anchor_board: z.string(),
|
|
41
|
+
anchor_block: z.string().nullable(),
|
|
42
|
+
project_id: z.string().nullable(),
|
|
38
43
|
agent: z.string(),
|
|
39
44
|
session_id: z.string(),
|
|
40
45
|
version: z.number(),
|
|
@@ -51,7 +56,11 @@ const rowSchema = z
|
|
|
51
56
|
.transform((row) =>
|
|
52
57
|
askRecordSchema.parse({
|
|
53
58
|
id: row.id,
|
|
54
|
-
|
|
59
|
+
anchor:
|
|
60
|
+
row.anchor_block === null
|
|
61
|
+
? { board: row.anchor_board }
|
|
62
|
+
: { board: row.anchor_board, block: row.anchor_block },
|
|
63
|
+
projectId: row.project_id ?? undefined,
|
|
55
64
|
agent: row.agent,
|
|
56
65
|
sessionId: row.session_id,
|
|
57
66
|
version: row.version,
|
|
@@ -68,17 +77,33 @@ const rowSchema = z
|
|
|
68
77
|
}),
|
|
69
78
|
);
|
|
70
79
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
80
|
+
/** A thread's project is always its anchor board's. */
|
|
81
|
+
const THREAD_PROJECT_SQL =
|
|
82
|
+
"(SELECT project_id FROM reviews WHERE reviews.id=ask_conversations.anchor_board)";
|
|
83
|
+
|
|
84
|
+
/** Every column of a thread, with its project. */
|
|
85
|
+
const THREAD_COLUMNS = `ask_conversations.*, ${THREAD_PROJECT_SQL} AS project_id`;
|
|
86
|
+
|
|
87
|
+
/** Saved Ask conversations by project, each anchored on the board it started
|
|
88
|
+
* on. No foreign key on the anchor: shared reviews are not in `reviews`.
|
|
89
|
+
* So deleting a board removes its threads by hand (removeReview). */
|
|
73
90
|
export class AskHistory {
|
|
91
|
+
private readonly listeners = new Set<() => void>();
|
|
92
|
+
|
|
74
93
|
private externalWriteGuard?: (reviewId: string) => void;
|
|
75
94
|
setExternalWriteGuard(guard: (reviewId: string) => void) {
|
|
76
95
|
this.externalWriteGuard = guard;
|
|
77
96
|
}
|
|
78
|
-
constructor(
|
|
97
|
+
constructor(
|
|
98
|
+
private readonly db: DatabaseSync,
|
|
99
|
+
/** The store's rule for writing about a board: one in Trash is
|
|
100
|
+
* read-only. */
|
|
101
|
+
private readonly writable: (reviewId: string) => void = () => {},
|
|
102
|
+
) {
|
|
79
103
|
db.exec(`CREATE TABLE IF NOT EXISTS ask_conversations(
|
|
80
104
|
id TEXT PRIMARY KEY,
|
|
81
|
-
|
|
105
|
+
anchor_board TEXT NOT NULL,
|
|
106
|
+
anchor_block TEXT,
|
|
82
107
|
agent TEXT NOT NULL,
|
|
83
108
|
session_id TEXT NOT NULL,
|
|
84
109
|
version INTEGER NOT NULL,
|
|
@@ -91,25 +116,12 @@ export class AskHistory {
|
|
|
91
116
|
entries TEXT,
|
|
92
117
|
bypass INTEGER NOT NULL DEFAULT 0,
|
|
93
118
|
permissions TEXT,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Conversations saved before bypassing permissions, or before auto mode,
|
|
98
|
-
// lack its column.
|
|
99
|
-
const columns = new Set(
|
|
100
|
-
db
|
|
101
|
-
.prepare("PRAGMA table_info(ask_conversations)")
|
|
102
|
-
.all()
|
|
103
|
-
.map((column) => String(column.name)),
|
|
104
|
-
);
|
|
119
|
+
viewed_at TEXT,
|
|
120
|
+
UNIQUE(agent, session_id));`);
|
|
105
121
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
if (!columns.has("permissions"))
|
|
112
|
-
db.exec("ALTER TABLE ask_conversations ADD COLUMN permissions TEXT");
|
|
122
|
+
db.exec(
|
|
123
|
+
"CREATE INDEX IF NOT EXISTS ask_conversations_review ON ask_conversations(anchor_board, updated_at)",
|
|
124
|
+
);
|
|
113
125
|
|
|
114
126
|
// What each agent offered last, so a new question can pick a model and
|
|
115
127
|
// effort, and a command, before its agent starts.
|
|
@@ -122,34 +134,46 @@ export class AskHistory {
|
|
|
122
134
|
);
|
|
123
135
|
}
|
|
124
136
|
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
/** Calls back when a conversation is saved, renamed, used or deleted. */
|
|
138
|
+
subscribe(listener: () => void) {
|
|
139
|
+
this.listeners.add(listener);
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
.
|
|
130
|
-
|
|
141
|
+
return () => {
|
|
142
|
+
this.listeners.delete(listener);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
private changed() {
|
|
147
|
+
for (const listener of this.listeners) listener();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private assertWritable(reviewId: string) {
|
|
151
|
+
if (reviewId.startsWith("shared-")) this.externalWriteGuard?.(reviewId);
|
|
152
|
+
else this.writable(reviewId);
|
|
137
153
|
}
|
|
154
|
+
|
|
138
155
|
private assertThreadWritable(id: string) {
|
|
139
156
|
const record = this.get(id);
|
|
140
157
|
|
|
141
|
-
if (record) this.assertWritable(record.
|
|
158
|
+
if (record) this.assertWritable(record.anchor.board);
|
|
142
159
|
}
|
|
143
160
|
save(record: AskRecord) {
|
|
144
|
-
this.assertWritable(record.
|
|
161
|
+
this.assertWritable(record.anchor.board);
|
|
145
162
|
this.db
|
|
146
163
|
.prepare(
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
// Its columns, not the row: when it was last seen, and what the
|
|
165
|
+
// panel showed, stay.
|
|
166
|
+
`INSERT INTO ask_conversations(id,anchor_board,anchor_block,agent,session_id,version,head,cwd,selection,title,created_at,updated_at,bypass,permissions)
|
|
167
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
168
|
+
ON CONFLICT(id) DO UPDATE SET anchor_board=excluded.anchor_board, anchor_block=excluded.anchor_block,
|
|
169
|
+
agent=excluded.agent, session_id=excluded.session_id, version=excluded.version, head=excluded.head,
|
|
170
|
+
cwd=excluded.cwd, selection=excluded.selection, title=excluded.title, created_at=excluded.created_at,
|
|
171
|
+
updated_at=excluded.updated_at, bypass=excluded.bypass, permissions=excluded.permissions`,
|
|
149
172
|
)
|
|
150
173
|
.run(
|
|
151
174
|
record.id,
|
|
152
|
-
record.
|
|
175
|
+
record.anchor.board,
|
|
176
|
+
record.anchor.block ?? null,
|
|
153
177
|
record.agent,
|
|
154
178
|
record.sessionId,
|
|
155
179
|
record.version,
|
|
@@ -162,6 +186,7 @@ export class AskHistory {
|
|
|
162
186
|
record.permissions === "bypass" ? 1 : 0,
|
|
163
187
|
record.permissions ?? null,
|
|
164
188
|
);
|
|
189
|
+
this.changed();
|
|
165
190
|
}
|
|
166
191
|
|
|
167
192
|
/** What the agent may do without asking when the conversation reopens. */
|
|
@@ -189,6 +214,7 @@ export class AskHistory {
|
|
|
189
214
|
this.db
|
|
190
215
|
.prepare("UPDATE ask_conversations SET entries=? WHERE id=?")
|
|
191
216
|
.run(JSON.stringify(entries), id);
|
|
217
|
+
this.changed();
|
|
192
218
|
}
|
|
193
219
|
|
|
194
220
|
/** The agent's name for the conversation, which the list shows. */
|
|
@@ -197,6 +223,7 @@ export class AskHistory {
|
|
|
197
223
|
this.db
|
|
198
224
|
.prepare("UPDATE ask_conversations SET title=? WHERE id=?")
|
|
199
225
|
.run(title.slice(0, 200), id);
|
|
226
|
+
this.changed();
|
|
200
227
|
}
|
|
201
228
|
|
|
202
229
|
saveOffer(agent: AskAgentId, offer: AskOffer) {
|
|
@@ -245,50 +272,90 @@ export class AskHistory {
|
|
|
245
272
|
this.db
|
|
246
273
|
.prepare("UPDATE ask_conversations SET updated_at=? WHERE id=?")
|
|
247
274
|
.run(at, id);
|
|
275
|
+
this.changed();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Conversations in a board's project, or started on the board when it
|
|
279
|
+
* has none. Newest first. */
|
|
280
|
+
list(board: string, projectId?: string) {
|
|
281
|
+
// The project's boards by their index on project_id, then their
|
|
282
|
+
// threads by ask_conversations_review.
|
|
283
|
+
return this.entries(
|
|
284
|
+
"anchor_board=? OR anchor_board IN (SELECT id FROM reviews WHERE project_id=?)",
|
|
285
|
+
board,
|
|
286
|
+
projectId ?? null,
|
|
287
|
+
);
|
|
248
288
|
}
|
|
249
289
|
|
|
250
|
-
/**
|
|
251
|
-
|
|
290
|
+
/** Every project's conversations, newest first. */
|
|
291
|
+
listAll() {
|
|
292
|
+
return this.entries("1");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private entries(
|
|
296
|
+
where: string,
|
|
297
|
+
...params: (string | null)[]
|
|
298
|
+
): (AskHistoryEntry & { projectId?: string; viewedAt?: string })[] {
|
|
299
|
+
// A transcript can be long: SQLite finds its first question, and the
|
|
300
|
+
// rest of it is left unread.
|
|
252
301
|
return this.db
|
|
253
302
|
.prepare(
|
|
254
|
-
|
|
303
|
+
`SELECT id, anchor_board, anchor_block, ${THREAD_PROJECT_SQL} AS project_id, agent, session_id, version, head, cwd, selection, title,
|
|
304
|
+
created_at, updated_at, NULL AS entries, bypass, permissions, viewed_at,
|
|
305
|
+
(SELECT json_extract(value,'$.text') FROM json_each(entries)
|
|
306
|
+
WHERE json_extract(value,'$.kind')='user' LIMIT 1) AS question
|
|
307
|
+
FROM ask_conversations WHERE ${where} ORDER BY updated_at DESC`,
|
|
255
308
|
)
|
|
256
|
-
.all(
|
|
309
|
+
.all(...params)
|
|
257
310
|
.map((row) => {
|
|
258
311
|
const {
|
|
259
|
-
|
|
312
|
+
anchor,
|
|
260
313
|
sessionId: _sessionId,
|
|
261
314
|
version: _version,
|
|
262
315
|
cwd: _cwd,
|
|
263
|
-
entries,
|
|
316
|
+
entries: _entries,
|
|
264
317
|
permissions: _permissions,
|
|
265
318
|
...entry
|
|
266
319
|
} = rowSchema.parse(row);
|
|
267
320
|
|
|
268
|
-
const question =
|
|
321
|
+
const question = z.string().nullable().parse(row.question) ?? "";
|
|
269
322
|
|
|
270
|
-
if (question
|
|
323
|
+
if (question.trim()) entry.question = question;
|
|
271
324
|
|
|
272
|
-
|
|
325
|
+
const viewedAt = z.string().nullable().parse(row.viewed_at);
|
|
326
|
+
|
|
327
|
+
return { ...entry, anchor, ...(viewedAt !== null && { viewedAt }) };
|
|
273
328
|
});
|
|
274
329
|
}
|
|
275
330
|
|
|
276
331
|
get(id: string): AskRecord | undefined {
|
|
277
332
|
const row = this.db
|
|
278
|
-
.prepare(
|
|
333
|
+
.prepare(`SELECT ${THREAD_COLUMNS} FROM ask_conversations WHERE id=?`)
|
|
279
334
|
.get(id);
|
|
280
335
|
|
|
281
336
|
return row ? rowSchema.parse(row) : undefined;
|
|
282
337
|
}
|
|
283
338
|
|
|
339
|
+
/** The reviewer saw the thread, on screen with the app focused. Only
|
|
340
|
+
* when it was seen changes: not its order, nor when it was used. */
|
|
341
|
+
markViewed(id: string, at = new Date().toISOString()) {
|
|
342
|
+
this.db
|
|
343
|
+
.prepare("UPDATE ask_conversations SET viewed_at=? WHERE id=?")
|
|
344
|
+
.run(at, id);
|
|
345
|
+
this.changed();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Forgets the threads that started on a board gone for good. */
|
|
284
349
|
removeReview(reviewId: string) {
|
|
285
350
|
this.db
|
|
286
|
-
.prepare("DELETE FROM ask_conversations WHERE
|
|
351
|
+
.prepare("DELETE FROM ask_conversations WHERE anchor_board=?")
|
|
287
352
|
.run(reviewId);
|
|
353
|
+
this.changed();
|
|
288
354
|
}
|
|
289
355
|
|
|
290
356
|
delete(id: string) {
|
|
291
357
|
this.assertThreadWritable(id);
|
|
292
358
|
this.db.prepare("DELETE FROM ask_conversations WHERE id=?").run(id);
|
|
359
|
+
this.changed();
|
|
293
360
|
}
|
|
294
361
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type JsonValue, isJsonObject } from "@dev.fast/json";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -15,10 +16,7 @@ import { commandSchema } from "./store.js";
|
|
|
15
16
|
/** The host publishes its input schemas, except where agents write less than
|
|
16
17
|
* the host accepts (publishedEditSchema, publishedLensEditSchema) or name a
|
|
17
18
|
* checkout by path (pathTargetSchema); adapters validate nothing. */
|
|
18
|
-
export function authoringTools(
|
|
19
|
-
scratchpadAvailable = false,
|
|
20
|
-
traceEnabled = false,
|
|
21
|
-
) {
|
|
19
|
+
export function authoringTools(traceEnabled = false) {
|
|
22
20
|
const id = z.string().min(1);
|
|
23
21
|
const review = { reviewId: id };
|
|
24
22
|
|
|
@@ -31,19 +29,21 @@ export function authoringTools(
|
|
|
31
29
|
...trace.shape,
|
|
32
30
|
kind: z.enum(["image", "trace"]),
|
|
33
31
|
})
|
|
32
|
+
.omit({ repositoryId: true })
|
|
33
|
+
.extend({ repositoryPath: id })
|
|
34
34
|
.partial()
|
|
35
|
-
.required({ id: true,
|
|
35
|
+
.required({ id: true, repositoryPath: true, kind: true });
|
|
36
36
|
|
|
37
37
|
const descriptions = {
|
|
38
38
|
create:
|
|
39
|
-
'Create a
|
|
39
|
+
'Create a whiteboard. target names its repository by the checkout\'s path, and what it shows: {kind:"project"} for a plan in that repository\'s project with no change; {kind:"worktree", base?} for the saved files in the checkout, uncommitted ones included and untracked ones left out (git add -N a new file to include it), against base, by default the repository\'s default branch, from the merge base of base and HEAD, which follows rebases; {kind:"commits", head, base?} for immutable commits (omitted base means source at head with no diff; supply the parent to review introduced changes); {kind:"pullRequest", pullRequest} for a GitHub PR by URL or number, fetched into that checkout and pinned at the PR head and GitHub diff base. For a branch stacked on another, pass the branch below it as base. Group plan implementations or a PR stack under one project-target overview with parentId, and order its children with dependsOn; Git bases do not set these relationships. Revisions are resolved on acceptance. With a PR and no title the PR\'s title is used. When the existing review for that PR is in Trash, creation reports its ID; restore it with review_lifecycle state:"active" before continuing. When an active review for that PR exists, it is returned instead, reporting whether its head moved and whether an agent is working on it; update it in place, move its target with review_set_target, or create a separate review with reuseExisting:false. The result carries review, as review_list shows it, with its target and repositoryPath, so no follow-up read is needed before diffing. When Desktop is available the review opens there and the result reports opened, softwareMapEnabled and environmentIssues, as review_open does; set open:false to author in the background without taking over Desktop. parentId and dependsOn set its place among the project\'s reviews, as review_set_parent and review_set_dependencies do; projectId files it in another project, by project.id from review_list.',
|
|
40
40
|
set_target:
|
|
41
|
-
"Move the review to a new target, such as the commits after a rebase or amend,
|
|
41
|
+
"Move the review to a new target (the same targets review_create takes), such as the commits after a rebase or amend, or commits or a PR for a board that had none. The document and component IDs are kept. Revisions are resolved on acceptance. Returns warnings for retained source ranges in files the new commits changed, to verify, and resources that no longer match; fix them with review_edit. Earlier versions keep their source. A PR target names the PR; other targets keep it within the same repository and clear it across repositories.",
|
|
42
42
|
edit: [
|
|
43
|
-
"Edit one document component. content is one component, named by its type; each type's fields are in the Components section of session_get_instructions
|
|
44
|
-
"Anchors are head/path#L10-L20 or base/path#L7, or diff/path#L84-R90 for a range across sides, with repository-relative paths and 1-based inclusive lines. pins {
|
|
45
|
-
"insert adds content: parentId nests it, afterId places it, and omitted placement appends
|
|
46
|
-
"The host assigns short durable IDs. The result names the edited component and, for an insert or replace, its first-level children, and quotes the first and last line of each anchor the edit added or changed (the first 20; unquotedAnchors counts the rest): check they are the lines you meant. Accepted edits save at once.",
|
|
43
|
+
"Edit one document component. content is one component, named by its type; each type's fields are in the Components section of session_get_instructions.",
|
|
44
|
+
"Anchors are head/path#L10-L20 or base/path#L7, or diff/path#L84-R90 for a range across sides, with repository-relative paths and 1-based inclusive lines. pins {head, base?} say which commits of the whiteboard's repository anchors quote: a step, frame, attachment or operation without pins uses its block's, and a block without them the review's.",
|
|
45
|
+
"insert adds content: parentId nests it, afterId places it, and omitted placement appends. update patches a component's own fields; null removes an optional one. A section's children and a diagram's units change through their own IDs; a call stack's frames change by replacing the call stack. move and remove take a targetId; removing a flow_node removes its edges.",
|
|
46
|
+
"The host assigns short durable IDs. The result names the edited component and, for an insert or replace, its first-level children, and quotes the first and last line of each anchor the edit added or changed (the first 20; unquotedAnchors counts the rest): check they are the lines you meant. Accepted edits save at once. An edit to a block the user is editing is refused with an error: edit another block, or retry later.",
|
|
47
47
|
"While a reader may be watching, write small and often: one paragraph per edit. Insert a new diagram whole. Change one already drawn a unit at a time (a flow_node, flow_edge or step, with parentId naming the diagram), and link each added flow_node to a node already drawn.",
|
|
48
48
|
].join("\n"),
|
|
49
49
|
lens_edit:
|
|
@@ -56,7 +56,11 @@ export function authoringTools(
|
|
|
56
56
|
lifecycle:
|
|
57
57
|
"Move a review between active and trash. Trash retains the review for 30 days before automatic deletion. Moving it back to active restores it; moving it to trash again does not restart that period.",
|
|
58
58
|
delete:
|
|
59
|
-
"Permanently delete a review and its history. The review must be in Trash. This cannot be undone.",
|
|
59
|
+
"Permanently delete a review and its history. The review must be in Trash. This cannot be undone. Its children move up to top level, and dependencies on it are dropped.",
|
|
60
|
+
set_parent:
|
|
61
|
+
"Nest the review under another review of the same project, or make it top-level with parentId:null. Nesting is one level deep: the parent must be top-level, and a review with children can't become a child. The sidebar shows children under their parent. Saves no review version. Returns the review's relations: parentId, children in sibling order, and dependsOn.",
|
|
62
|
+
set_dependencies:
|
|
63
|
+
"Replace the list of reviews this one builds on: any reviews of the same project. Siblings list after what they depend on, directly or through other reviews; a PR stack is a chain. A list that would close a cycle is refused, naming the cycle. Saves no review version. Returns the review's relations.",
|
|
60
64
|
};
|
|
61
65
|
|
|
62
66
|
const tool = (
|
|
@@ -70,7 +74,7 @@ export function authoringTools(
|
|
|
70
74
|
name: `review_${name === "restore" ? "restore_version" : name}`,
|
|
71
75
|
description,
|
|
72
76
|
inputSchema: {
|
|
73
|
-
...z.toJSONSchema(schema, { io: "input" }),
|
|
77
|
+
...withoutRepositoryIds(z.toJSONSchema(schema, { io: "input" })),
|
|
74
78
|
type: "object" as const,
|
|
75
79
|
},
|
|
76
80
|
method,
|
|
@@ -92,9 +96,6 @@ export function authoringTools(
|
|
|
92
96
|
'Read Whiteboard\'s guidance before creating or editing a review. The default topic gives the authoring workflow; "file-lenses" covers Diff-view file lenses.' +
|
|
93
97
|
(traceEnabled
|
|
94
98
|
? ' Call review_get_instructions({topic:"trace-archaeology"}) for why code exists, what an agent was thinking, or whether an agent solved this before.'
|
|
95
|
-
: "") +
|
|
96
|
-
(scratchpadAvailable
|
|
97
|
-
? ' When the user asks in conversation to be shown how code works or wants a diagram, without asking for a review, draw it on the scratchpad rather than answering only in chat: call review_get_instructions({topic:"scratchpad"}) first. A request for a review or to use Whiteboard means authoring a review with the default topic.'
|
|
98
99
|
: ""),
|
|
99
100
|
instructionsQuerySchema.partial(),
|
|
100
101
|
"GET",
|
|
@@ -124,10 +125,12 @@ export function authoringTools(
|
|
|
124
125
|
...commandSchema.shape.operation.options.map((operation) => {
|
|
125
126
|
const type = operation.shape.type.value;
|
|
126
127
|
|
|
127
|
-
//
|
|
128
|
+
// Hidden from agents: they name checkouts by path inside the target
|
|
129
|
+
// and give PRs as a target.
|
|
128
130
|
const {
|
|
129
131
|
type: _type,
|
|
130
132
|
repositoryId: _repositoryId,
|
|
133
|
+
pullRequestUrl: _pullRequestUrl,
|
|
131
134
|
...fields
|
|
132
135
|
}: Record<string, z.ZodType> = operation.shape;
|
|
133
136
|
|
|
@@ -137,12 +140,7 @@ export function authoringTools(
|
|
|
137
140
|
z.strictObject({
|
|
138
141
|
...fields,
|
|
139
142
|
...(type === "create" && {
|
|
140
|
-
target: pathTargetSchema
|
|
141
|
-
repositoryPath: id
|
|
142
|
-
.optional()
|
|
143
|
-
.describe(
|
|
144
|
-
"Only with pullRequestUrl and no target: the local checkout to fetch the PR into. Default: the existing review's, else the first registered checkout with a remote for the PR's repository.",
|
|
145
|
-
),
|
|
143
|
+
target: pathTargetSchema,
|
|
146
144
|
open: z.boolean().optional(),
|
|
147
145
|
}),
|
|
148
146
|
...(type === "set_target" && { target: pathTargetSchema }),
|
|
@@ -196,13 +194,6 @@ export function authoringTools(
|
|
|
196
194
|
"POST",
|
|
197
195
|
"/workspace-cleanup",
|
|
198
196
|
),
|
|
199
|
-
tool(
|
|
200
|
-
"register_repository",
|
|
201
|
-
"Register a local Git or jj repository and return its id, for pins on scratchpad blocks. session_create and session_set_target take the checkout's path instead.",
|
|
202
|
-
z.strictObject({ path: id }),
|
|
203
|
-
"POST",
|
|
204
|
-
"/repositories",
|
|
205
|
-
),
|
|
206
197
|
tool(
|
|
207
198
|
"upload",
|
|
208
199
|
'Retain an image or trace for use in a review. kind:"image" takes base64; kind:"trace" takes trace. Reusing an upload ID requires identical content; rejected uploads are not saved.',
|
|
@@ -212,3 +203,27 @@ export function authoringTools(
|
|
|
212
203
|
),
|
|
213
204
|
];
|
|
214
205
|
}
|
|
206
|
+
|
|
207
|
+
/** Drops repositoryId from pins: the host fills in the board's own. */
|
|
208
|
+
function withoutRepositoryIds<T>(schema: T): T {
|
|
209
|
+
// SAFETY: a JSON round trip of a JSON Schema, dropping one property.
|
|
210
|
+
return JSON.parse(JSON.stringify(schema), (_key, value: JsonValue) => {
|
|
211
|
+
if (
|
|
212
|
+
!isJsonObject(value) ||
|
|
213
|
+
!isJsonObject(value.properties) ||
|
|
214
|
+
!("repositoryId" in value.properties) ||
|
|
215
|
+
!("head" in value.properties)
|
|
216
|
+
)
|
|
217
|
+
return value;
|
|
218
|
+
|
|
219
|
+
const { repositoryId: _repositoryId, ...properties } = value.properties;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
...value,
|
|
223
|
+
properties,
|
|
224
|
+
...(Array.isArray(value.required) && {
|
|
225
|
+
required: value.required.filter((name) => name !== "repositoryId"),
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
}) as T;
|
|
229
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { errorMessage } from "@dev.fast/trace-core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
|
+
import { azureRepositoryOf, slugOf } from "./azure-remote.js";
|
|
4
5
|
import { ReviewInputError } from "./document.js";
|
|
5
6
|
import type { PullRequestProvider } from "./provider.js";
|
|
6
7
|
import { firstLine } from "./pull-request.js";
|
|
@@ -12,13 +13,6 @@ const PULL_REQUEST_URLS = [
|
|
|
12
13
|
/^https:\/\/([A-Za-z0-9-]+)\.visualstudio\.com\/(?:DefaultCollection\/)?([^/]+)\/_git\/([^/]+)\/pullrequest\/([1-9]\d*)$/,
|
|
13
14
|
];
|
|
14
15
|
|
|
15
|
-
const REMOTE_URLS = [
|
|
16
|
-
/^https:\/\/(?:[^@]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?\/?$/,
|
|
17
|
-
/^https:\/\/([^.]+)\.visualstudio\.com\/(?:DefaultCollection\/)?([^/]+)\/_git\/([^/]+?)(?:\.git)?\/?$/,
|
|
18
|
-
/^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
|
|
19
|
-
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
|
|
20
|
-
];
|
|
21
|
-
|
|
22
16
|
const metadataSchema = z.object({
|
|
23
17
|
title: z.string(),
|
|
24
18
|
sourceRefName: z.string().min(1),
|
|
@@ -31,17 +25,6 @@ const metadataSchema = z.object({
|
|
|
31
25
|
}),
|
|
32
26
|
});
|
|
33
27
|
|
|
34
|
-
/** org/project/repository, decoded; undefined for malformed escapes. */
|
|
35
|
-
function slugOf(patterns: RegExp[], url: string) {
|
|
36
|
-
const match = patterns.map((pattern) => pattern.exec(url)).find(Boolean);
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
return match && match.slice(1, 4).map(decodeURIComponent).join("/");
|
|
40
|
-
} catch {
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
28
|
function azFailure(url: string, failure: string) {
|
|
46
29
|
const unreadable = (hint: string) =>
|
|
47
30
|
new ReviewInputError(
|
|
@@ -162,8 +145,7 @@ export const azureDevOpsProvider: PullRequestProvider = {
|
|
|
162
145
|
|
|
163
146
|
matchesRemote(remoteUrl, address) {
|
|
164
147
|
return (
|
|
165
|
-
|
|
166
|
-
address.slug.toLowerCase()
|
|
148
|
+
azureRepositoryOf(remoteUrl)?.toLowerCase() === address.slug.toLowerCase()
|
|
167
149
|
);
|
|
168
150
|
},
|
|
169
151
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const REMOTE_URLS = [
|
|
2
|
+
/^https:\/\/(?:[^@]+@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?\/?$/,
|
|
3
|
+
/^https:\/\/([^.]+)\.visualstudio\.com\/(?:DefaultCollection\/)?([^/]+)\/_git\/([^/]+?)(?:\.git)?\/?$/,
|
|
4
|
+
/^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
|
|
5
|
+
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
/** org/project/repository, decoded; undefined for malformed escapes. */
|
|
9
|
+
export function slugOf(patterns: RegExp[], url: string) {
|
|
10
|
+
const match = patterns.map((pattern) => pattern.exec(url)).find(Boolean);
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
return match && match.slice(1, 4).map(decodeURIComponent).join("/");
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function azureRepositoryOf(remoteUrl: string) {
|
|
20
|
+
return slugOf(REMOTE_URLS, remoteUrl.trim());
|
|
21
|
+
}
|
|
@@ -13,7 +13,7 @@ export const identity = { id: text.optional() };
|
|
|
13
13
|
* them reads its document's. */
|
|
14
14
|
export const elementPins = sourcePinsSchema
|
|
15
15
|
.optional()
|
|
16
|
-
.describe("Pins for the anchors here
|
|
16
|
+
.describe("Pins for the anchors here.");
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* One block kind: the strict schema the store parses with, and the rules a
|
|
@@ -6,12 +6,12 @@ export const markdown = {
|
|
|
6
6
|
type: "markdown",
|
|
7
7
|
schema: defineBlock("markdown", {
|
|
8
8
|
markdown: text.describe(
|
|
9
|
-
"Safe Markdown. Repository file links must use [label](
|
|
9
|
+
"Safe Markdown. Repository file links must use [label](whiteboard:source/head/path#L10-L24) or whiteboard:source/base/path#L10-L24, or whiteboard:source/<commit>/path#L10-L24 at any commit of the board's repository, with a repository-relative path and verified line numbers. Link another board as [label](whiteboard:board/<sessionId>) or one of its blocks as whiteboard:board/<sessionId>#<blockId>; both must exist. Relative paths, absolute filesystem paths, and file/editor URLs are rejected. External links use https://, http://, or mailto:; document anchors use #heading.",
|
|
10
10
|
),
|
|
11
11
|
pins: sourcePinsSchema
|
|
12
12
|
.optional()
|
|
13
13
|
.describe(
|
|
14
|
-
"Repository and commits this block's
|
|
14
|
+
"Repository and commits this block's whiteboard:source/ links resolve against, instead of the document's pins.",
|
|
15
15
|
),
|
|
16
16
|
}),
|
|
17
17
|
} as const;
|