@dev.fast/whiteboard 0.2.5-preview.20261013.113 → 0.2.5-preview.20261015.115
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/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
- package/dist/{agent-cli-04xbDj23.js → agent-cli-CR2PnA9G.js} +9 -12
- package/dist/agent-cli-CRPPmS3l.js +2 -0
- package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
- package/dist/{authoring-tools-fdA0RPIT.js → authoring-tools-BXKEwhfi.js} +5 -5
- package/dist/background-server-Cw7CcfaZ.js +156 -0
- package/dist/background-server-D2Rrr_e2.js +2 -0
- package/dist/build-info.json +1 -1
- package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
- package/dist/{cli-runner-C3men22A.js → cli-runner-PlGqBeMw.js} +368 -252
- package/dist/cli.js +3 -2
- package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
- package/dist/{process-error-telemetry-BCBYp2t4.js → headless-host-CVXTSIlx.js} +3285 -2585
- package/dist/headless-host-Cp37C-v0.js +2 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/{local-data-Dy1pNtLF.js → local-data-yfLp5PiJ.js} +113 -62
- package/dist/{mcp-B8kuRSXM.js → mcp-DBbDwG4y.js} +4 -4
- package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
- package/dist/profile-BSOkA6A-.js +2 -0
- package/dist/{profile-tbGWuWHu.js → profile-pAzg7fxC.js} +3 -3
- package/dist/remote-attach-BAt2eZx3.js +137 -0
- package/dist/remote-extensions-2Cxf3hMo.js +2 -0
- package/dist/remote-extensions-BiNx6NQK.js +323 -0
- package/dist/remote-language-server-DQgWUNeV.js +252 -0
- package/dist/remote-uninstall-DLtDG0o7.js +153 -0
- package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
- package/dist/review-app-launcher-D7I4DGoH.js +222 -0
- package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
- package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
- package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
- package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
- package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
- package/dist/server/desktop-host.js +1663 -266
- package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
- package/dist/sharing/index.d.ts +88 -32
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
- package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
- package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
- package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
- package/instructions/authoring.md +16 -12
- package/instructions/file-lenses.md +4 -2
- package/package.json +9 -14
- package/src/agent-connect.ts +507 -0
- package/src/agent-selection.ts +4 -0
- package/src/ask/agents.ts +59 -13
- package/src/ask/pi-mcp.ts +7 -3
- package/src/ask/thread-state.ts +9 -4
- package/src/ask/thread.ts +88 -50
- package/src/ask/threads.ts +53 -7
- package/src/cli-install.ts +3 -1
- package/src/cli-runner.ts +472 -81
- package/src/cli.ts +1 -1
- package/src/connect-agents.ts +285 -0
- package/src/connect-prompts.ts +60 -6
- package/src/desktop-discovery.ts +24 -0
- package/src/pinned-download.ts +118 -0
- package/src/remote-attach.ts +130 -0
- package/src/remote-extensions.ts +359 -0
- package/src/remote-language-server.ts +428 -0
- package/src/remote-toolchains.ts +107 -0
- package/src/remote-uninstall.ts +280 -0
- package/src/review-api/README.md +1 -3
- package/src/review-api/agent-cli.ts +4 -4
- package/src/review-api/agent-client.ts +23 -0
- package/src/review-api/ask-history.ts +31 -16
- package/src/review-api/authoring-tools.ts +1 -1
- package/src/review-api/blocks/index.ts +2 -4
- package/src/review-api/document.ts +1 -4
- package/src/review-api/http.ts +265 -237
- package/src/review-api/local-data.ts +18 -3
- package/src/review-api/onboarding.ts +478 -0
- package/src/review-api/origin.ts +7 -0
- package/src/review-api/recent-folders.ts +173 -0
- package/src/review-api/store.ts +56 -10
- package/src/review-api/workspaces.ts +56 -11
- package/src/review-app-launcher.ts +61 -3
- package/src/review-stack.ts +162 -36
- package/src/server/background-server.ts +250 -0
- package/src/server/desktop-host-shutdown.ts +24 -0
- package/src/server/desktop-host.ts +39 -24
- package/src/server/desktop-server.ts +68 -118
- package/src/server/global-verb-relay.ts +16 -0
- package/src/server/headless-host.ts +128 -3
- package/src/server/hono-http.ts +6 -1
- package/src/server/legacy-tutorial-cleanup.ts +38 -0
- package/src/server/review-gateway-hosts.ts +379 -0
- package/src/server/review-gateway-list.ts +109 -0
- package/src/server/review-gateway-memory.ts +130 -0
- package/src/server/review-gateway-pushes.ts +142 -0
- package/src/server/review-gateway-streams.ts +415 -0
- package/src/server/review-gateway-transport.ts +204 -0
- package/src/server/review-gateway.ts +433 -0
- package/src/server/review-server-core.ts +36 -6
- package/src/server-discovery.ts +11 -0
- package/src/sharing/import.ts +1 -0
- package/src/ui-telemetry-events.ts +5 -1
- package/src/vsix.ts +122 -0
- package/dist/agent-cli-BR_Jw5BL.js +0 -2
- package/dist/headless-host-BLLJ7Dyu.js +0 -95
- package/dist/profile-aMSw1oCM.js +0 -2
- package/dist/server-discovery-ZVi8Ay8J.js +0 -66
- package/src/fixtures/blocks/tutorial.json +0 -14
- package/src/review-api/blocks/tutorial.ts +0 -23
- package/src/server/tutorial-service.ts +0 -217
- package/tutorial/document.json +0 -216
- package/tutorial/lenses.json +0 -10
- package/tutorial/runtime-manifest.json +0 -11
- package/tutorial/sample-service/package.json +0 -9
- package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
- package/tutorial/sample-service/src/app.ts +0 -26
- package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
- package/tutorial/sample-service/src/database/schema.ts +0 -9
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
- package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
- package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
- package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
- package/tutorial/sample-service/src/orders/order-service.ts +0 -35
- package/tutorial/sample-service/src/orders/order.ts +0 -43
- package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
- package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
- package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
- package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
- package/tutorial/sample-service/tsconfig.json +0 -12
package/src/ask/agents.ts
CHANGED
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
ndJsonStream,
|
|
15
15
|
} from "@agentclientprotocol/sdk";
|
|
16
16
|
import { piTakesMcp, piWithMcp } from "@review/ask/pi-mcp.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
type AskAgentId,
|
|
19
|
+
type AskPermissions,
|
|
20
|
+
askAgentIds,
|
|
21
|
+
} from "@review/ask/thread-state.js";
|
|
18
22
|
|
|
19
23
|
/** How Whiteboard runs an agent that speaks ACP on stdio. */
|
|
20
24
|
type AskAgentLaunch =
|
|
@@ -48,10 +52,13 @@ interface AskAgentSpec {
|
|
|
48
52
|
/** How its model gets the MCP servers a session is given, where its
|
|
49
53
|
* adapter keeps them; without it, the adapter passes them on. */
|
|
50
54
|
mcp?: AskAgentMcp;
|
|
55
|
+
/** How it decides for itself what is safe to run, in place of its
|
|
56
|
+
* read-only mode and settings: the default where it has one. */
|
|
57
|
+
auto?: AskAgentModeSettings;
|
|
51
58
|
/** How it edits and runs commands without asking, in place of its
|
|
52
59
|
* read-only mode and settings, when the reviewer bypasses permissions.
|
|
53
60
|
* Whiteboard also allows whatever it still asks. */
|
|
54
|
-
bypass?:
|
|
61
|
+
bypass?: AskAgentModeSettings;
|
|
55
62
|
/** A follow-up asked while it answers joins that turn, when it says it
|
|
56
63
|
* takes one. Only for an adapter that leaves a follow-up arriving as the
|
|
57
64
|
* turn ends to Whiteboard: Codex's starts a turn of its own instead,
|
|
@@ -61,7 +68,7 @@ interface AskAgentSpec {
|
|
|
61
68
|
|
|
62
69
|
interface AskAgentMcp {
|
|
63
70
|
/** Whether this install can take them. */
|
|
64
|
-
supported(executable: string): Promise<boolean>;
|
|
71
|
+
supported(executable: string, env: NodeJS.ProcessEnv): Promise<boolean>;
|
|
65
72
|
/** What the adapter runs in place of the user's CLI to give them, with
|
|
66
73
|
* what it needs in `env`. */
|
|
67
74
|
launch(
|
|
@@ -71,12 +78,37 @@ interface AskAgentMcp {
|
|
|
71
78
|
): Promise<string>;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
export interface
|
|
81
|
+
export interface AskAgentModeSettings {
|
|
75
82
|
mode?: string;
|
|
76
83
|
sessionMeta?: NewSessionRequest["_meta"];
|
|
77
84
|
env?: Record<string, string>;
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
/** The mode, session settings and process settings an agent runs with. */
|
|
88
|
+
export function askAgentSettings(
|
|
89
|
+
agent: AskAgentId,
|
|
90
|
+
permissions: AskPermissions,
|
|
91
|
+
): AskAgentModeSettings {
|
|
92
|
+
const spec = askAgents[agent];
|
|
93
|
+
|
|
94
|
+
const own =
|
|
95
|
+
permissions === "read-only"
|
|
96
|
+
? { mode: spec.readOnlyMode }
|
|
97
|
+
: spec[permissions];
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
mode: own?.mode,
|
|
101
|
+
sessionMeta: own?.sessionMeta ?? spec.sessionMeta,
|
|
102
|
+
env: own?.env ?? spec.env,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** What a new conversation may do: decide for itself where the agent can,
|
|
107
|
+
* else read only. */
|
|
108
|
+
export function defaultAskPermissions(agent: AskAgentId): AskPermissions {
|
|
109
|
+
return askAgents[agent].auto ? "auto" : "read-only";
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
export const askAgents: Record<AskAgentId, AskAgentSpec> = {
|
|
81
113
|
claude: {
|
|
82
114
|
name: "Claude Code",
|
|
@@ -107,6 +139,9 @@ export const askAgents: Record<AskAgentId, AskAgentSpec> = {
|
|
|
107
139
|
},
|
|
108
140
|
},
|
|
109
141
|
},
|
|
142
|
+
// Claude's classifier allows what it judges safe and asks about the rest;
|
|
143
|
+
// its file tools stay off, as read-only's are.
|
|
144
|
+
auto: { mode: "auto" },
|
|
110
145
|
// Its file tools back, and its own bypass, where its settings allow it.
|
|
111
146
|
bypass: {
|
|
112
147
|
mode: "bypassPermissions",
|
|
@@ -126,6 +161,8 @@ export const askAgents: Record<AskAgentId, AskAgentSpec> = {
|
|
|
126
161
|
},
|
|
127
162
|
signIn: "codex login",
|
|
128
163
|
readOnlyMode: "read-only",
|
|
164
|
+
// Codex reviews each action and asks only about ones it finds unsafe.
|
|
165
|
+
auto: { mode: "agent" },
|
|
129
166
|
bypass: { mode: "agent-full-access" },
|
|
130
167
|
},
|
|
131
168
|
cursor: {
|
|
@@ -187,6 +224,8 @@ export interface AskAgentStatus {
|
|
|
187
224
|
available: boolean;
|
|
188
225
|
/** Whether it has a mode that keeps the checkout as it is. */
|
|
189
226
|
readOnly: boolean;
|
|
227
|
+
/** Whether it can decide for itself what is safe to run. */
|
|
228
|
+
auto: boolean;
|
|
190
229
|
/** Whether Whiteboard can have it edit and run commands without asking. */
|
|
191
230
|
bypass: boolean;
|
|
192
231
|
}
|
|
@@ -203,9 +242,10 @@ export type AskAgentLauncher = (
|
|
|
203
242
|
agent: AskAgentId,
|
|
204
243
|
cwd: string,
|
|
205
244
|
options?: {
|
|
206
|
-
|
|
245
|
+
permissions?: AskPermissions;
|
|
207
246
|
/** The MCP servers its sessions are given. */
|
|
208
247
|
mcpServers?: McpServer[];
|
|
248
|
+
env?: NodeJS.ProcessEnv;
|
|
209
249
|
},
|
|
210
250
|
) => Promise<AskAgentProcess>;
|
|
211
251
|
|
|
@@ -256,13 +296,16 @@ async function findAgent(
|
|
|
256
296
|
}
|
|
257
297
|
|
|
258
298
|
/** Whether the agent's model gets the MCP servers its sessions are given. */
|
|
259
|
-
export async function askAgentTakesMcp(
|
|
299
|
+
export async function askAgentTakesMcp(
|
|
300
|
+
agent: AskAgentId,
|
|
301
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
302
|
+
): Promise<boolean> {
|
|
260
303
|
const spec = askAgents[agent];
|
|
261
304
|
|
|
262
305
|
if (!spec.mcp) return true;
|
|
263
|
-
const executable = await findAgent(spec);
|
|
306
|
+
const executable = await findAgent(spec, env);
|
|
264
307
|
|
|
265
|
-
return executable !== undefined && spec.mcp.supported(executable);
|
|
308
|
+
return executable !== undefined && spec.mcp.supported(executable, env);
|
|
266
309
|
}
|
|
267
310
|
|
|
268
311
|
export async function detectAskAgents(
|
|
@@ -274,6 +317,7 @@ export async function detectAskAgents(
|
|
|
274
317
|
name: askAgents[id].name,
|
|
275
318
|
available: (await findAgent(askAgents[id], env)) !== undefined,
|
|
276
319
|
readOnly: askAgents[id].readOnlyMode !== undefined,
|
|
320
|
+
auto: askAgents[id].auto !== undefined,
|
|
277
321
|
bypass: askAgents[id].bypass !== undefined,
|
|
278
322
|
})),
|
|
279
323
|
);
|
|
@@ -286,16 +330,16 @@ const STDERR_LIMIT = 8_000;
|
|
|
286
330
|
export const launchAskAgent: AskAgentLauncher = async (
|
|
287
331
|
agent,
|
|
288
332
|
cwd,
|
|
289
|
-
{
|
|
333
|
+
{ permissions = "read-only", mcpServers = [], env: base = process.env } = {},
|
|
290
334
|
) => {
|
|
291
335
|
const spec = askAgents[agent];
|
|
292
|
-
const executable = await findAgent(spec);
|
|
336
|
+
const executable = await findAgent(spec, base);
|
|
293
337
|
|
|
294
338
|
if (!executable) throw new Error(`${spec.name} is not installed.`);
|
|
295
339
|
|
|
296
340
|
const env: NodeJS.ProcessEnv = {
|
|
297
|
-
...
|
|
298
|
-
...(
|
|
341
|
+
...base,
|
|
342
|
+
...askAgentSettings(agent, permissions).env,
|
|
299
343
|
};
|
|
300
344
|
|
|
301
345
|
// A server started from inside a Claude Code session must not look nested.
|
|
@@ -309,7 +353,9 @@ export const launchAskAgent: AskAgentLauncher = async (
|
|
|
309
353
|
|
|
310
354
|
if ("adapter" in launch) {
|
|
311
355
|
env[launch.executableEnv] =
|
|
312
|
-
spec.mcp &&
|
|
356
|
+
spec.mcp &&
|
|
357
|
+
mcpServers.length &&
|
|
358
|
+
(await spec.mcp.supported(executable, env))
|
|
313
359
|
? await spec.mcp.launch(executable, mcpServers, env)
|
|
314
360
|
: executable;
|
|
315
361
|
|
package/src/ask/pi-mcp.ts
CHANGED
|
@@ -49,13 +49,14 @@ const versions = new Map<
|
|
|
49
49
|
{ read: number; version: Promise<number[] | undefined> }
|
|
50
50
|
>();
|
|
51
51
|
|
|
52
|
-
function piVersion(executable: string) {
|
|
52
|
+
function piVersion(executable: string, env: NodeJS.ProcessEnv) {
|
|
53
53
|
const cached = versions.get(executable);
|
|
54
54
|
|
|
55
55
|
if (cached && Date.now() - cached.read < VERSION_TTL_MS)
|
|
56
56
|
return cached.version;
|
|
57
57
|
|
|
58
58
|
const version = promisify(execFile)(executable, ["--version"], {
|
|
59
|
+
env,
|
|
59
60
|
timeout: 5_000,
|
|
60
61
|
})
|
|
61
62
|
.then(({ stdout }) =>
|
|
@@ -69,9 +70,12 @@ function piVersion(executable: string) {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/** Whether this Pi can be given MCP servers. */
|
|
72
|
-
export async function piTakesMcp(
|
|
73
|
+
export async function piTakesMcp(
|
|
74
|
+
executable: string,
|
|
75
|
+
env: NodeJS.ProcessEnv,
|
|
76
|
+
): Promise<boolean> {
|
|
73
77
|
if (process.platform === "win32") return false;
|
|
74
|
-
const version = await piVersion(executable);
|
|
78
|
+
const version = await piVersion(executable, env);
|
|
75
79
|
|
|
76
80
|
if (!version) return false;
|
|
77
81
|
|
package/src/ask/thread-state.ts
CHANGED
|
@@ -185,6 +185,12 @@ const statusSchema = z.enum([
|
|
|
185
185
|
"failed",
|
|
186
186
|
]);
|
|
187
187
|
|
|
188
|
+
/** What an agent may do without asking: nothing that changes the checkout,
|
|
189
|
+
* what its own judgment calls safe, or everything. */
|
|
190
|
+
export const askPermissionsSchema = z.enum(["read-only", "auto", "bypass"]);
|
|
191
|
+
|
|
192
|
+
export type AskPermissions = z.infer<typeof askPermissionsSchema>;
|
|
193
|
+
|
|
188
194
|
/** Everything the Ask panel renders. A watcher gets it whole once, then
|
|
189
195
|
* follows it through `AskChange`s. */
|
|
190
196
|
export const askThreadStateSchema = z.object({
|
|
@@ -197,8 +203,7 @@ export const askThreadStateSchema = z.object({
|
|
|
197
203
|
signIn: z.string().optional(),
|
|
198
204
|
/** The mode that keeps the agent from changing files was accepted. */
|
|
199
205
|
readOnly: z.boolean(),
|
|
200
|
-
|
|
201
|
-
bypass: z.boolean(),
|
|
206
|
+
permissions: askPermissionsSchema,
|
|
202
207
|
head: z.string(),
|
|
203
208
|
/** The checkout the agent works in. */
|
|
204
209
|
cwd: z.string(),
|
|
@@ -228,7 +233,7 @@ export const askChangeSchema = z.discriminatedUnion("type", [
|
|
|
228
233
|
type: z.literal("set"),
|
|
229
234
|
status: statusSchema.optional(),
|
|
230
235
|
readOnly: z.boolean().optional(),
|
|
231
|
-
|
|
236
|
+
permissions: askPermissionsSchema.optional(),
|
|
232
237
|
choices: askChoicesSchema.optional(),
|
|
233
238
|
commands: z.array(askCommandSchema).optional(),
|
|
234
239
|
accepts: askAcceptsSchema.optional(),
|
|
@@ -290,7 +295,7 @@ export function applyAskChange(
|
|
|
290
295
|
...rest,
|
|
291
296
|
status: change.status ?? state.status,
|
|
292
297
|
readOnly: change.readOnly ?? state.readOnly,
|
|
293
|
-
|
|
298
|
+
permissions: change.permissions ?? state.permissions,
|
|
294
299
|
};
|
|
295
300
|
|
|
296
301
|
const choices = change.choices ?? state.choices;
|
package/src/ask/thread.ts
CHANGED
|
@@ -22,7 +22,9 @@ import { errorMessage } from "@dev.fast/trace-core";
|
|
|
22
22
|
import {
|
|
23
23
|
type AskAgentLauncher,
|
|
24
24
|
type AskAgentProcess,
|
|
25
|
+
askAgentSettings,
|
|
25
26
|
askAgents,
|
|
27
|
+
defaultAskPermissions,
|
|
26
28
|
} from "@review/ask/agents.js";
|
|
27
29
|
import {
|
|
28
30
|
attachmentsOf,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
type AskChoices,
|
|
45
47
|
type AskEntry,
|
|
46
48
|
type AskOffer,
|
|
49
|
+
type AskPermissions,
|
|
47
50
|
type AskPicks,
|
|
48
51
|
type AskQuestion,
|
|
49
52
|
type AskThreadState,
|
|
@@ -92,21 +95,14 @@ const READ_ONLY_MCP_SERVERS = new Set([
|
|
|
92
95
|
"fff",
|
|
93
96
|
]);
|
|
94
97
|
|
|
95
|
-
/** MCP servers Whiteboard gives every Ask session
|
|
96
|
-
* Whiteboard's own, so they run without asking
|
|
97
|
-
|
|
98
|
+
/** MCP servers Whiteboard gives every Ask session, for the thread they
|
|
99
|
+
* serve. Their tool calls are Whiteboard's own, so they run without asking
|
|
100
|
+
* the reviewer. */
|
|
101
|
+
export type AskMcpServers = (threadId: string) => McpServer[];
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
reviewId: string;
|
|
103
|
-
agent: AskAgentId;
|
|
104
|
-
cwd: string;
|
|
105
|
-
head: string;
|
|
106
|
-
selection: { title: string; quote?: string };
|
|
107
|
-
/** What the agent reads before a session's first question: the selection
|
|
108
|
-
* and how to find the review. */
|
|
109
|
-
context: string;
|
|
103
|
+
/** Where a thread saves what happens to it, which moves with the thread
|
|
104
|
+
* when another review adopts it. */
|
|
105
|
+
export interface AskThreadHooks {
|
|
110
106
|
/** The agent created a session: the id a later reopen loads. A new one
|
|
111
107
|
* replaces a session the agent could not reopen. */
|
|
112
108
|
onSession?: (sessionId: string) => void;
|
|
@@ -115,12 +111,8 @@ interface AskThreadBase {
|
|
|
115
111
|
/** What the panel shows, after each turn and on close, so a reopen can
|
|
116
112
|
* show it without waiting for the agent to replay it. */
|
|
117
113
|
onSave?: (entries: AskEntry[]) => void;
|
|
118
|
-
/** The
|
|
119
|
-
|
|
120
|
-
/** Edit and run commands without asking. */
|
|
121
|
-
bypass?: boolean;
|
|
122
|
-
/** The reviewer started or stopped bypassing permissions. */
|
|
123
|
-
onBypass?: (bypass: boolean) => void;
|
|
114
|
+
/** The reviewer changed what the agent may do. */
|
|
115
|
+
onPermissions?: (permissions: AskPermissions) => void;
|
|
124
116
|
/** The agent said what it offers: choices, commands, or what a question
|
|
125
117
|
* may carry. */
|
|
126
118
|
onOffer?: (offer: AskOffer) => void;
|
|
@@ -128,6 +120,23 @@ interface AskThreadBase {
|
|
|
128
120
|
onTitle?: (title: string) => void;
|
|
129
121
|
}
|
|
130
122
|
|
|
123
|
+
interface AskThreadBase extends AskThreadHooks {
|
|
124
|
+
/** Stable across reopening; a new conversation gets a fresh one. */
|
|
125
|
+
id?: string;
|
|
126
|
+
reviewId: string;
|
|
127
|
+
agent: AskAgentId;
|
|
128
|
+
cwd: string;
|
|
129
|
+
head: string;
|
|
130
|
+
selection: { title: string; quote?: string };
|
|
131
|
+
/** What the agent reads before a session's first question: the selection
|
|
132
|
+
* and how to find the review. */
|
|
133
|
+
context: string;
|
|
134
|
+
/** The model and effort to answer with, when the agent offers them. */
|
|
135
|
+
picks?: AskPicks;
|
|
136
|
+
/** What the agent may do without asking; the agent's default without. */
|
|
137
|
+
permissions?: AskPermissions;
|
|
138
|
+
}
|
|
139
|
+
|
|
131
140
|
/** A new conversation, or an earlier one to load from the agent. */
|
|
132
141
|
export type AskThreadStart = AskThreadBase &
|
|
133
142
|
(
|
|
@@ -218,6 +227,9 @@ export class AskThread {
|
|
|
218
227
|
private steerable = false;
|
|
219
228
|
/** Follow-ups waiting for the turn under way to end, oldest first. */
|
|
220
229
|
private queue: { id: string; question: AskQuestion }[] = [];
|
|
230
|
+
/** The review the thread answers in, and where it saves; adopting moves
|
|
231
|
+
* both. */
|
|
232
|
+
private home: AskThreadHooks & { reviewId: string };
|
|
221
233
|
|
|
222
234
|
constructor(
|
|
223
235
|
private readonly launch: AskAgentLauncher,
|
|
@@ -226,15 +238,24 @@ export class AskThread {
|
|
|
226
238
|
private readonly limits: AskThreadLimits = askThreadLimits,
|
|
227
239
|
) {
|
|
228
240
|
this.id = start.id ?? randomUUID();
|
|
229
|
-
this.mcpServers = mcpServers();
|
|
241
|
+
this.mcpServers = mcpServers(this.id);
|
|
230
242
|
this.picks = { ...start.picks };
|
|
243
|
+
this.home = {
|
|
244
|
+
reviewId: start.reviewId,
|
|
245
|
+
onSession: start.onSession,
|
|
246
|
+
onTurn: start.onTurn,
|
|
247
|
+
onSave: start.onSave,
|
|
248
|
+
onPermissions: start.onPermissions,
|
|
249
|
+
onOffer: start.onOffer,
|
|
250
|
+
onTitle: start.onTitle,
|
|
251
|
+
};
|
|
231
252
|
this.state = {
|
|
232
253
|
id: this.id,
|
|
233
254
|
agent: start.agent,
|
|
234
255
|
agentName: askAgents[start.agent].name,
|
|
235
256
|
status: "starting",
|
|
236
257
|
readOnly: false,
|
|
237
|
-
|
|
258
|
+
permissions: start.permissions ?? defaultAskPermissions(start.agent),
|
|
238
259
|
head: start.head,
|
|
239
260
|
cwd: start.cwd,
|
|
240
261
|
selection: start.selection,
|
|
@@ -247,7 +268,16 @@ export class AskThread {
|
|
|
247
268
|
}
|
|
248
269
|
|
|
249
270
|
get reviewId() {
|
|
250
|
-
return this.
|
|
271
|
+
return this.home.reviewId;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Moves the thread to another review, as when its agent creates the
|
|
275
|
+
* review it was asked for, and saves it there as it stands. */
|
|
276
|
+
adopt(reviewId: string, hooks: AskThreadHooks) {
|
|
277
|
+
this.home = { reviewId, ...hooks };
|
|
278
|
+
|
|
279
|
+
if (this.sessionId) hooks.onSession?.(this.sessionId);
|
|
280
|
+
hooks.onSave?.(this.state.entries);
|
|
251
281
|
}
|
|
252
282
|
|
|
253
283
|
read(): AskThreadState {
|
|
@@ -468,14 +498,15 @@ export class AskThread {
|
|
|
468
498
|
this.closed = true;
|
|
469
499
|
|
|
470
500
|
try {
|
|
471
|
-
//
|
|
501
|
+
// A conversation still starting may be half replayed, and one that
|
|
502
|
+
// failed has nothing new; neither replaces what was saved.
|
|
472
503
|
if (
|
|
473
504
|
save &&
|
|
474
505
|
this.sessionId &&
|
|
475
506
|
this.state.status !== "starting" &&
|
|
476
507
|
this.state.status !== "failed"
|
|
477
508
|
)
|
|
478
|
-
this.
|
|
509
|
+
this.home.onSave?.(settled(this.state.entries));
|
|
479
510
|
} finally {
|
|
480
511
|
for (const resolve of this.decisions.values())
|
|
481
512
|
resolve({ outcome: { outcome: "cancelled" } });
|
|
@@ -597,7 +628,7 @@ export class AskThread {
|
|
|
597
628
|
const generation = ++this.generation;
|
|
598
629
|
|
|
599
630
|
const launched = await this.launch(this.start.agent, this.start.cwd, {
|
|
600
|
-
|
|
631
|
+
permissions: this.state.permissions,
|
|
601
632
|
mcpServers: this.mcpServers,
|
|
602
633
|
});
|
|
603
634
|
|
|
@@ -659,9 +690,8 @@ export class AskThread {
|
|
|
659
690
|
initialized.agentCapabilities?.loadSession === true,
|
|
660
691
|
);
|
|
661
692
|
|
|
662
|
-
const {
|
|
663
|
-
const
|
|
664
|
-
const mode = bypass ? spec.bypass?.mode : spec.readOnlyMode;
|
|
693
|
+
const { permissions } = this.state;
|
|
694
|
+
const { mode } = askAgentSettings(this.start.agent, permissions);
|
|
665
695
|
|
|
666
696
|
// An agent without a read-only mode still answers, as it is.
|
|
667
697
|
const configurable =
|
|
@@ -703,7 +733,8 @@ export class AskThread {
|
|
|
703
733
|
|
|
704
734
|
this.emit({
|
|
705
735
|
type: "set",
|
|
706
|
-
readOnly:
|
|
736
|
+
readOnly:
|
|
737
|
+
permissions === "read-only" && Boolean(configurable || selectable),
|
|
707
738
|
});
|
|
708
739
|
this.useConfig(config);
|
|
709
740
|
|
|
@@ -820,23 +851,30 @@ export class AskThread {
|
|
|
820
851
|
if (commands) offer.commands = commands;
|
|
821
852
|
|
|
822
853
|
if (accepts) offer.accepts = accepts;
|
|
823
|
-
this.
|
|
854
|
+
this.home.onOffer?.(offer);
|
|
824
855
|
}
|
|
825
856
|
|
|
826
|
-
/**
|
|
827
|
-
*
|
|
828
|
-
async permit(
|
|
857
|
+
/** Changes what the agent may do from the next answer: it starts again
|
|
858
|
+
* in that mode and reopens the conversation. */
|
|
859
|
+
async permit(permissions: AskPermissions) {
|
|
829
860
|
if (this.state.status !== "idle")
|
|
830
861
|
throw new Error("Settings can change between answers.");
|
|
831
862
|
|
|
832
|
-
if (
|
|
863
|
+
if (permissions === this.state.permissions) return;
|
|
833
864
|
|
|
834
|
-
if (
|
|
835
|
-
|
|
865
|
+
if (
|
|
866
|
+
permissions !== "read-only" &&
|
|
867
|
+
!askAgents[this.start.agent][permissions]
|
|
868
|
+
)
|
|
869
|
+
throw new Error(
|
|
870
|
+
permissions === "auto"
|
|
871
|
+
? `${this.state.agentName} has no auto mode.`
|
|
872
|
+
: `${this.state.agentName} cannot bypass permissions.`,
|
|
873
|
+
);
|
|
836
874
|
|
|
837
|
-
this.emit({ type: "set",
|
|
875
|
+
this.emit({ type: "set", permissions });
|
|
838
876
|
this.permitted = true;
|
|
839
|
-
this.
|
|
877
|
+
this.home.onPermissions?.(permissions);
|
|
840
878
|
this.disconnect();
|
|
841
879
|
// Its mode is Whiteboard's change, not one to announce as the agent's.
|
|
842
880
|
this.settings.clear();
|
|
@@ -847,18 +885,18 @@ export class AskThread {
|
|
|
847
885
|
});
|
|
848
886
|
}
|
|
849
887
|
|
|
850
|
-
/** What the session starts with
|
|
888
|
+
/** What the session starts with, for what the agent may do. */
|
|
851
889
|
private sessionMeta() {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return (this.state.bypass && spec.bypass?.sessionMeta) || spec.sessionMeta;
|
|
890
|
+
return askAgentSettings(this.start.agent, this.state.permissions)
|
|
891
|
+
.sessionMeta;
|
|
855
892
|
}
|
|
856
893
|
|
|
857
894
|
/** The agent says which mode it is in now; only its read-only one keeps
|
|
858
895
|
* the checkout as it is. */
|
|
859
896
|
private useMode(mode: string) {
|
|
860
897
|
const readOnly =
|
|
861
|
-
|
|
898
|
+
this.state.permissions === "read-only" &&
|
|
899
|
+
mode === askAgents[this.start.agent].readOnlyMode;
|
|
862
900
|
|
|
863
901
|
if (readOnly !== this.state.readOnly) this.emit({ type: "set", readOnly });
|
|
864
902
|
}
|
|
@@ -885,7 +923,7 @@ export class AskThread {
|
|
|
885
923
|
if (this.closed) throw new Error("This conversation has ended.");
|
|
886
924
|
|
|
887
925
|
this.sessionId = response.sessionId;
|
|
888
|
-
|
|
926
|
+
this.home.onSession?.(response.sessionId);
|
|
889
927
|
|
|
890
928
|
this.greeting = piGreetingSchema.safeParse(
|
|
891
929
|
response._meta,
|
|
@@ -954,7 +992,7 @@ export class AskThread {
|
|
|
954
992
|
...(withSelection ? [this.start.context] : []),
|
|
955
993
|
...(withPermission
|
|
956
994
|
? [
|
|
957
|
-
this.state.bypass
|
|
995
|
+
this.state.permissions === "bypass"
|
|
958
996
|
? "The reviewer lets you change files in the checkout and run commands without asking."
|
|
959
997
|
: "Do not change files in the checkout.",
|
|
960
998
|
]
|
|
@@ -1027,8 +1065,8 @@ export class AskThread {
|
|
|
1027
1065
|
? `${this.state.agentName} stopped before finishing.`
|
|
1028
1066
|
: null,
|
|
1029
1067
|
});
|
|
1030
|
-
this.
|
|
1031
|
-
this.
|
|
1068
|
+
this.home.onTurn?.();
|
|
1069
|
+
this.home.onSave?.(this.state.entries);
|
|
1032
1070
|
this.askQueued();
|
|
1033
1071
|
}
|
|
1034
1072
|
|
|
@@ -1050,7 +1088,7 @@ export class AskThread {
|
|
|
1050
1088
|
// may save an "always" rule into the checkout's settings.
|
|
1051
1089
|
if (
|
|
1052
1090
|
allowOnce &&
|
|
1053
|
-
(this.state.bypass ||
|
|
1091
|
+
(this.state.permissions === "bypass" ||
|
|
1054
1092
|
(server &&
|
|
1055
1093
|
(READ_ONLY_MCP_SERVERS.has(server) ||
|
|
1056
1094
|
this.mcpServers.some((provided) => provided.name === server))))
|
|
@@ -1302,7 +1340,7 @@ export class AskThread {
|
|
|
1302
1340
|
|
|
1303
1341
|
if (title && title !== this.state.title) {
|
|
1304
1342
|
this.emit({ type: "set", title });
|
|
1305
|
-
this.
|
|
1343
|
+
this.home.onTitle?.(title);
|
|
1306
1344
|
}
|
|
1307
1345
|
|
|
1308
1346
|
return true;
|
package/src/ask/threads.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PROTOCOL_VERSION, client, methods } from "@agentclientprotocol/sdk";
|
|
2
|
-
import { errorMessage } from "@dev.fast/trace-core";
|
|
2
|
+
import { errorMessage, shellQuote } from "@dev.fast/trace-core";
|
|
3
3
|
import {
|
|
4
4
|
type AskAgentLauncher,
|
|
5
5
|
askAgentTakesMcp,
|
|
@@ -139,13 +139,59 @@ async function offeredBy(
|
|
|
139
139
|
export interface AskTools {
|
|
140
140
|
/** Its MCP server, for agents whose model gets it. */
|
|
141
141
|
mcpServers?: AskMcpServers;
|
|
142
|
-
/** A shell command that runs Whiteboard's CLI against this server
|
|
143
|
-
* the agents that don't. */
|
|
144
|
-
cli?: () => string | undefined;
|
|
142
|
+
/** A shell command that runs Whiteboard's CLI against this server for a
|
|
143
|
+
* thread, for the agents that don't. */
|
|
144
|
+
cli?: (threadId: string) => string | undefined;
|
|
145
145
|
/** Whether an agent's model gets the MCP servers it is given. */
|
|
146
146
|
takesMcp?: (agent: AskAgentId) => Promise<boolean>;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** Whiteboard's CLI as the tools, with `selector` in its env so it reaches
|
|
150
|
+
* this server and no other. */
|
|
151
|
+
export function cliAskTools(
|
|
152
|
+
cliPath: () => string | undefined,
|
|
153
|
+
selector: { name: string; value: string },
|
|
154
|
+
): AskTools {
|
|
155
|
+
const env = () => [
|
|
156
|
+
selector,
|
|
157
|
+
...(process.versions.electron
|
|
158
|
+
? [{ name: "ELECTRON_RUN_AS_NODE", value: "1" }]
|
|
159
|
+
: []),
|
|
160
|
+
...(process.env.DEV_REVIEW_HOME
|
|
161
|
+
? [{ name: "DEV_REVIEW_HOME", value: process.env.DEV_REVIEW_HOME }]
|
|
162
|
+
: []),
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
mcpServers: () => {
|
|
167
|
+
const cli = cliPath();
|
|
168
|
+
|
|
169
|
+
return cli
|
|
170
|
+
? [
|
|
171
|
+
{
|
|
172
|
+
name: "whiteboard",
|
|
173
|
+
command: process.execPath,
|
|
174
|
+
args: [cli, "mcp"],
|
|
175
|
+
env: env(),
|
|
176
|
+
},
|
|
177
|
+
]
|
|
178
|
+
: [];
|
|
179
|
+
},
|
|
180
|
+
cli: () => {
|
|
181
|
+
const cli = cliPath();
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
cli &&
|
|
185
|
+
[
|
|
186
|
+
...env().map(({ name, value }) => `${name}=${shellQuote(value)}`),
|
|
187
|
+
shellQuote(process.execPath),
|
|
188
|
+
shellQuote(cli),
|
|
189
|
+
].join(" ")
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
149
195
|
/** How an agent reaches Whiteboard's tools, which its first prompt says. */
|
|
150
196
|
export type AskToolsReach =
|
|
151
197
|
| { kind: "mcp" }
|
|
@@ -183,13 +229,13 @@ export class AskThreads {
|
|
|
183
229
|
|
|
184
230
|
/** How the agent's sessions reach Whiteboard's tools: the MCP tools they
|
|
185
231
|
* are given where its model gets them, else the CLI from its shell. */
|
|
186
|
-
async reach(agent: AskAgentId): Promise<AskToolsReach> {
|
|
232
|
+
async reach(agent: AskAgentId, threadId: string): Promise<AskToolsReach> {
|
|
187
233
|
if (
|
|
188
|
-
this.mcpServers().length &&
|
|
234
|
+
this.mcpServers(threadId).length &&
|
|
189
235
|
(await (this.tools.takesMcp ?? askAgentTakesMcp)(agent))
|
|
190
236
|
)
|
|
191
237
|
return { kind: "mcp" };
|
|
192
|
-
const command = this.tools.cli?.();
|
|
238
|
+
const command = this.tools.cli?.(threadId);
|
|
193
239
|
|
|
194
240
|
return command ? { kind: "cli", command } : undefined;
|
|
195
241
|
}
|
package/src/cli-install.ts
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
REVIEW_REMOTE_WRAPPER_MARK,
|
|
9
10
|
type ReviewCliInstallStamp,
|
|
10
11
|
ReviewCliInstallStampSchema,
|
|
11
12
|
type ReviewCliInstallStatus,
|
|
@@ -58,7 +59,8 @@ function hasManagedShimMarker(source: string): boolean {
|
|
|
58
59
|
return (
|
|
59
60
|
source.includes(SHIM_MARKER) ||
|
|
60
61
|
source.includes("Managed by Review Desktop") ||
|
|
61
|
-
source.includes("Managed by Whiteboard Desktop")
|
|
62
|
+
source.includes("Managed by Whiteboard Desktop") ||
|
|
63
|
+
source.includes(REVIEW_REMOTE_WRAPPER_MARK)
|
|
62
64
|
);
|
|
63
65
|
}
|
|
64
66
|
|