@dev.fast/whiteboard 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AvailableCommand,
|
|
5
|
+
type ContentBlock,
|
|
6
|
+
RequestError,
|
|
7
|
+
type SessionConfigOption,
|
|
8
|
+
type ToolCallUpdate,
|
|
9
|
+
} from "@agentclientprotocol/sdk";
|
|
10
|
+
import {
|
|
11
|
+
type AskAttachment,
|
|
12
|
+
type AskChoiceKind,
|
|
13
|
+
type AskCommand,
|
|
14
|
+
type AskEntry,
|
|
15
|
+
type AskQuestion,
|
|
16
|
+
type AskSelect,
|
|
17
|
+
askChoiceKinds,
|
|
18
|
+
} from "@review/ask/thread-state.js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
/** Claude's adapter names the MCP server behind an `mcp__*` permission here. */
|
|
22
|
+
const claudeMcpMetaSchema = z.object({
|
|
23
|
+
claudeCode: z.object({ mcpServer: z.object({ name: z.string() }) }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** Codex's adapter puts the server in an MCP tool call's input. */
|
|
27
|
+
const mcpInputSchema = z.union([
|
|
28
|
+
z.object({ server: z.string() }),
|
|
29
|
+
z.object({ serverName: z.string() }),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** The MCP server a tool call runs on, if the adapter says. */
|
|
33
|
+
export function mcpServerOf(
|
|
34
|
+
toolCall: Pick<ToolCallUpdate, "_meta" | "rawInput">,
|
|
35
|
+
) {
|
|
36
|
+
const claude = claudeMcpMetaSchema.safeParse(toolCall._meta).data;
|
|
37
|
+
|
|
38
|
+
if (claude) return claude.claudeCode.mcpServer.name;
|
|
39
|
+
const input = mcpInputSchema.safeParse(toolCall.rawInput).data;
|
|
40
|
+
|
|
41
|
+
return input && ("server" in input ? input.server : input.serverName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Wraps the selection context in the first prompt, so a replayed
|
|
45
|
+
* conversation can show the question without it. */
|
|
46
|
+
const CONTEXT_OPEN = "<whiteboard-context>";
|
|
47
|
+
|
|
48
|
+
const CONTEXT_CLOSE = "</whiteboard-context>";
|
|
49
|
+
|
|
50
|
+
/** A conversation closed mid-turn, as it should look when reopened: the
|
|
51
|
+
* agent stopped, so nothing is still running or waiting on the reviewer. */
|
|
52
|
+
export function settled(entries: AskEntry[]): AskEntry[] {
|
|
53
|
+
return entries.map((entry) => {
|
|
54
|
+
if (entry.kind === "permission" && entry.outcome === undefined)
|
|
55
|
+
return { ...entry, outcome: "cancelled" };
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
entry.kind === "tool" &&
|
|
59
|
+
(entry.status === "pending" || entry.status === "in_progress")
|
|
60
|
+
)
|
|
61
|
+
return { ...entry, status: "failed" };
|
|
62
|
+
|
|
63
|
+
return entry;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Enough of a tool's output to see what came back. */
|
|
68
|
+
const OUTPUT_LIMIT = 4_000;
|
|
69
|
+
|
|
70
|
+
const selectOptionSchema = z.object({
|
|
71
|
+
value: z.string(),
|
|
72
|
+
name: z.string(),
|
|
73
|
+
description: z.string().nullish(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** A select's choices, flat or in groups. */
|
|
77
|
+
export const selectOptionsSchema = z.array(
|
|
78
|
+
z.union([
|
|
79
|
+
selectOptionSchema,
|
|
80
|
+
z.object({ group: z.string(), options: z.array(selectOptionSchema) }),
|
|
81
|
+
]),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/** The ACP config option category each choice comes from. */
|
|
85
|
+
const choiceCategories = new Map<AskChoiceKind, string>([
|
|
86
|
+
["model", "model"],
|
|
87
|
+
["effort", "thought_level"],
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// Agents name some efforts by their value.
|
|
91
|
+
const effortNames = new Map([["xhigh", "Extra high"]]);
|
|
92
|
+
|
|
93
|
+
/** What the agent offers of each choice, from its session config options,
|
|
94
|
+
* with the config option that sets it. */
|
|
95
|
+
export function choicesOf(options: SessionConfigOption[] | null | undefined) {
|
|
96
|
+
const found = new Map<
|
|
97
|
+
AskChoiceKind,
|
|
98
|
+
{ configId: string; select: AskSelect }
|
|
99
|
+
>();
|
|
100
|
+
|
|
101
|
+
for (const kind of askChoiceKinds) {
|
|
102
|
+
const option = options?.find(
|
|
103
|
+
(candidate) =>
|
|
104
|
+
candidate.type === "select" &&
|
|
105
|
+
(candidate.category === choiceCategories.get(kind) ||
|
|
106
|
+
candidate.id === kind),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (option?.type !== "select") continue;
|
|
110
|
+
const choices = selectOptionsSchema.safeParse(option.options).data;
|
|
111
|
+
|
|
112
|
+
if (!choices) continue;
|
|
113
|
+
|
|
114
|
+
found.set(kind, {
|
|
115
|
+
configId: option.id,
|
|
116
|
+
select: {
|
|
117
|
+
current: option.currentValue,
|
|
118
|
+
options: choices
|
|
119
|
+
.flatMap((choice) => ("group" in choice ? choice.options : [choice]))
|
|
120
|
+
.map(({ value, name: offered, description }) => {
|
|
121
|
+
const name =
|
|
122
|
+
(kind === "effort" && effortNames.get(value)) || offered;
|
|
123
|
+
|
|
124
|
+
return description ? { value, name, description } : { value, name };
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A slash command as the panel offers it. */
|
|
134
|
+
export function commandOf({
|
|
135
|
+
name,
|
|
136
|
+
description,
|
|
137
|
+
input,
|
|
138
|
+
}: AvailableCommand): AskCommand {
|
|
139
|
+
return input?.hint
|
|
140
|
+
? { name, description, hint: input.hint }
|
|
141
|
+
: { name, description };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** What a question carries, as its entry shows it. */
|
|
145
|
+
export function attachmentsOf({
|
|
146
|
+
mentions = [],
|
|
147
|
+
images = [],
|
|
148
|
+
}: AskQuestion): AskAttachment[] {
|
|
149
|
+
return [
|
|
150
|
+
...mentions.map((mention) => ({ kind: "file" as const, path: mention })),
|
|
151
|
+
...images.map(({ name }) => ({ kind: "image" as const, name })),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const toolInputSchema = z.object({
|
|
156
|
+
command: z.union([z.string(), z.array(z.string())]).optional(),
|
|
157
|
+
description: z.string().optional(),
|
|
158
|
+
file_path: z.string().optional(),
|
|
159
|
+
path: z.string().optional(),
|
|
160
|
+
// What a search looked for: fff's grep and find_files, and multi_grep's
|
|
161
|
+
// alternatives.
|
|
162
|
+
query: z.string().optional(),
|
|
163
|
+
pattern: z.string().optional(),
|
|
164
|
+
patterns: z.array(z.string()).optional(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
type ToolDetails = Pick<
|
|
168
|
+
Extract<AskEntry, { kind: "tool" }>,
|
|
169
|
+
"input" | "summary"
|
|
170
|
+
>;
|
|
171
|
+
|
|
172
|
+
/** What a tool call ran or opened, and why, from what the adapter sent. */
|
|
173
|
+
export function toolDetails(update: ToolCallUpdate) {
|
|
174
|
+
const input = toolInputSchema.safeParse(update.rawInput).data;
|
|
175
|
+
const command = input?.command;
|
|
176
|
+
|
|
177
|
+
const target =
|
|
178
|
+
(Array.isArray(command) ? command.join(" ") : command) ??
|
|
179
|
+
input?.file_path ??
|
|
180
|
+
input?.path ??
|
|
181
|
+
input?.query ??
|
|
182
|
+
input?.pattern ??
|
|
183
|
+
input?.patterns?.join(" | ") ??
|
|
184
|
+
update.locations?.[0]?.path;
|
|
185
|
+
|
|
186
|
+
const details: ToolDetails = {};
|
|
187
|
+
|
|
188
|
+
if (target) details.input = target;
|
|
189
|
+
|
|
190
|
+
if (input?.description) details.summary = input.description;
|
|
191
|
+
|
|
192
|
+
return details;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A finished tool's text output. Claude fences a command's output. */
|
|
196
|
+
export function toolOutput(update: ToolCallUpdate) {
|
|
197
|
+
const text = (update.content ?? [])
|
|
198
|
+
.flatMap((item) =>
|
|
199
|
+
item.type === "content" && item.content.type === "text"
|
|
200
|
+
? [item.content.text]
|
|
201
|
+
: [],
|
|
202
|
+
)
|
|
203
|
+
.join("\n")
|
|
204
|
+
.replace(/^\s*```[^\n]*\n([\s\S]*?)\n?```\s*$/u, "$1")
|
|
205
|
+
.trimEnd();
|
|
206
|
+
|
|
207
|
+
if (!text) return undefined;
|
|
208
|
+
|
|
209
|
+
return text.length > OUTPUT_LIMIT
|
|
210
|
+
? `${text.slice(0, OUTPUT_LIMIT)}\n…`
|
|
211
|
+
: text;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A first question, after the selection it is about. */
|
|
215
|
+
export function withContext(context: string, question: string): ContentBlock[] {
|
|
216
|
+
return [
|
|
217
|
+
{ type: "text", text: `${CONTEXT_OPEN}\n${context}\n${CONTEXT_CLOSE}` },
|
|
218
|
+
{ type: "text", text: question },
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A replayed first message, as the reviewer typed it. */
|
|
223
|
+
export function withoutContext(text: string) {
|
|
224
|
+
const start = text.indexOf(CONTEXT_OPEN);
|
|
225
|
+
|
|
226
|
+
if (start === -1) return text.trim();
|
|
227
|
+
const end = text.indexOf(CONTEXT_CLOSE, start);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
text.slice(0, start) +
|
|
231
|
+
(end === -1 ? "" : text.slice(end + CONTEXT_CLOSE.length))
|
|
232
|
+
).trim();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** ACP's "authentication required": the agent's login lapsed or never was.
|
|
236
|
+
* Some adapters send the same code for other failures, so its message has
|
|
237
|
+
* to say so too. */
|
|
238
|
+
export function signedOut(error: unknown): error is RequestError {
|
|
239
|
+
return (
|
|
240
|
+
error instanceof RequestError &&
|
|
241
|
+
error.code === RequestError.authRequired().code &&
|
|
242
|
+
/auth/i.test(error.message)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { AgentSelectionSchema } from "@review/agent-selection.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// The canvas imports this module, so it stays free of Node APIs.
|
|
5
|
+
export const askAgentIds = [
|
|
6
|
+
"claude",
|
|
7
|
+
"codex",
|
|
8
|
+
"cursor",
|
|
9
|
+
"opencode",
|
|
10
|
+
"pi",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export type AskAgentId = (typeof askAgentIds)[number];
|
|
14
|
+
|
|
15
|
+
const permissionOptionSchema = z.object({
|
|
16
|
+
optionId: z.string(),
|
|
17
|
+
name: z.string(),
|
|
18
|
+
kind: z.enum(["allow_once", "allow_always", "reject_once", "reject_always"]),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** What a question carries besides its text, as the thread shows it. An
|
|
22
|
+
* image shows by name: its bytes go to the agent, not into the thread. */
|
|
23
|
+
const askAttachmentSchema = z.discriminatedUnion("kind", [
|
|
24
|
+
z.object({ kind: z.literal("file"), path: z.string() }),
|
|
25
|
+
z.object({ kind: z.literal("image"), name: z.string() }),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export type AskAttachment = z.infer<typeof askAttachmentSchema>;
|
|
29
|
+
|
|
30
|
+
export const askEntrySchema = z.discriminatedUnion("kind", [
|
|
31
|
+
z.object({
|
|
32
|
+
kind: z.literal("user"),
|
|
33
|
+
id: z.string(),
|
|
34
|
+
text: z.string(),
|
|
35
|
+
/** When it was asked, in epoch milliseconds; unknown for a replayed one. */
|
|
36
|
+
at: z.number().optional(),
|
|
37
|
+
attachments: z.array(askAttachmentSchema).optional(),
|
|
38
|
+
}),
|
|
39
|
+
z.object({ kind: z.literal("agent"), id: z.string(), text: z.string() }),
|
|
40
|
+
z.object({
|
|
41
|
+
kind: z.literal("tool"),
|
|
42
|
+
id: z.string(),
|
|
43
|
+
title: z.string(),
|
|
44
|
+
toolKind: z.string(),
|
|
45
|
+
status: z.enum(["pending", "in_progress", "completed", "failed"]),
|
|
46
|
+
/** The command it ran, or the file it opened. */
|
|
47
|
+
input: z.string().optional(),
|
|
48
|
+
/** The agent's own note on why, when it gives one. */
|
|
49
|
+
summary: z.string().optional(),
|
|
50
|
+
/** What came back, shortened. */
|
|
51
|
+
output: z.string().optional(),
|
|
52
|
+
}),
|
|
53
|
+
z.object({
|
|
54
|
+
kind: z.literal("permission"),
|
|
55
|
+
id: z.string(),
|
|
56
|
+
title: z.string(),
|
|
57
|
+
toolKind: z.string(),
|
|
58
|
+
/** The command it would run, or the file it would open, when the title
|
|
59
|
+
* does not say. */
|
|
60
|
+
input: z.string().optional(),
|
|
61
|
+
options: z.array(permissionOptionSchema),
|
|
62
|
+
/** The chosen option, `cancelled`, or absent while the user decides. */
|
|
63
|
+
outcome: z.string().optional(),
|
|
64
|
+
/** Refused by Whiteboard's read-only policy, not by the user. */
|
|
65
|
+
automatic: z.boolean().optional(),
|
|
66
|
+
}),
|
|
67
|
+
/** An aside apart from the answer, which it is not part of: an agent's
|
|
68
|
+
* warning about its own setup, or where the reviewer stopped it. */
|
|
69
|
+
z.object({
|
|
70
|
+
kind: z.literal("notice"),
|
|
71
|
+
id: z.string(),
|
|
72
|
+
severity: z.string(),
|
|
73
|
+
title: z.string(),
|
|
74
|
+
description: z.string().optional(),
|
|
75
|
+
}),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/** One of the agent's settings: its choices, and the one in use. */
|
|
79
|
+
export const askSelectSchema = z.object({
|
|
80
|
+
current: z.string(),
|
|
81
|
+
options: z.array(
|
|
82
|
+
z.object({
|
|
83
|
+
value: z.string(),
|
|
84
|
+
name: z.string(),
|
|
85
|
+
description: z.string().optional(),
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export type AskSelect = z.infer<typeof askSelectSchema>;
|
|
91
|
+
|
|
92
|
+
/** The settings a reviewer can pick: the model, and how hard it thinks. */
|
|
93
|
+
export const askChoiceKinds = ["model", "effort"] as const;
|
|
94
|
+
|
|
95
|
+
export type AskChoiceKind = (typeof askChoiceKinds)[number];
|
|
96
|
+
|
|
97
|
+
/** What the agent offers of each, when it offers it. */
|
|
98
|
+
export const askChoicesSchema = z.object({
|
|
99
|
+
model: askSelectSchema.optional(),
|
|
100
|
+
effort: askSelectSchema.optional(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export type AskChoices = z.infer<typeof askChoicesSchema>;
|
|
104
|
+
|
|
105
|
+
/** The values picked, before the agent says what it offers. */
|
|
106
|
+
export const askPicksSchema = z.strictObject({
|
|
107
|
+
model: z.string().min(1).max(200).optional(),
|
|
108
|
+
effort: z.string().min(1).max(200).optional(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export type AskPicks = z.infer<typeof askPicksSchema>;
|
|
112
|
+
|
|
113
|
+
/** A slash command the agent takes, its skills among them. */
|
|
114
|
+
export const askCommandSchema = z.object({
|
|
115
|
+
name: z.string(),
|
|
116
|
+
description: z.string(),
|
|
117
|
+
/** What it takes after its name, when it takes anything. */
|
|
118
|
+
hint: z.string().optional(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export type AskCommand = z.infer<typeof askCommandSchema>;
|
|
122
|
+
|
|
123
|
+
/** What a question to the agent may carry besides text and file links. */
|
|
124
|
+
export const askAcceptsSchema = z.object({ image: z.boolean() });
|
|
125
|
+
|
|
126
|
+
export type AskAccepts = z.infer<typeof askAcceptsSchema>;
|
|
127
|
+
|
|
128
|
+
/** What an agent offers before anything is asked of it, as it last said. */
|
|
129
|
+
export const askOfferSchema = z.object({
|
|
130
|
+
choices: askChoicesSchema,
|
|
131
|
+
commands: z.array(askCommandSchema).optional(),
|
|
132
|
+
accepts: askAcceptsSchema.optional(),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export type AskOffer = z.infer<typeof askOfferSchema>;
|
|
136
|
+
|
|
137
|
+
/** The images a question can carry, as agents take them. */
|
|
138
|
+
export const askImageTypes = [
|
|
139
|
+
"image/png",
|
|
140
|
+
"image/jpeg",
|
|
141
|
+
"image/gif",
|
|
142
|
+
"image/webp",
|
|
143
|
+
] as const;
|
|
144
|
+
|
|
145
|
+
/** An image's base64 bytes: up to 5 MB of image. */
|
|
146
|
+
const IMAGE_DATA_MAX = 7_000_000;
|
|
147
|
+
|
|
148
|
+
/** A question as the reviewer asks it: its text, the checkout files it
|
|
149
|
+
* mentions, and any images. */
|
|
150
|
+
export const askQuestionSchema = z.strictObject({
|
|
151
|
+
text: z.string().trim().min(1).max(8_000),
|
|
152
|
+
/** Paths relative to the checkout's root. */
|
|
153
|
+
mentions: z.array(z.string().min(1).max(400)).max(20).optional(),
|
|
154
|
+
images: z
|
|
155
|
+
.array(
|
|
156
|
+
z.strictObject({
|
|
157
|
+
name: z.string().min(1).max(200),
|
|
158
|
+
mimeType: z.enum(askImageTypes),
|
|
159
|
+
data: z.string().min(1).max(IMAGE_DATA_MAX),
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
.max(4)
|
|
163
|
+
.optional(),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export type AskQuestion = z.infer<typeof askQuestionSchema>;
|
|
167
|
+
|
|
168
|
+
/** How full the agent's context window is, and what the session cost. */
|
|
169
|
+
export const askUsageSchema = z.object({
|
|
170
|
+
used: z.number(),
|
|
171
|
+
size: z.number(),
|
|
172
|
+
cost: z.object({ amount: z.number(), currency: z.string() }).optional(),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export type AskUsage = z.infer<typeof askUsageSchema>;
|
|
176
|
+
|
|
177
|
+
const statusSchema = z.enum([
|
|
178
|
+
"starting",
|
|
179
|
+
"running",
|
|
180
|
+
"waiting",
|
|
181
|
+
"idle",
|
|
182
|
+
"failed",
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
/** Everything the Ask panel renders. A watcher gets it whole once, then
|
|
186
|
+
* follows it through `AskChange`s. */
|
|
187
|
+
export const askThreadStateSchema = z.object({
|
|
188
|
+
id: z.string(),
|
|
189
|
+
agent: z.enum(askAgentIds),
|
|
190
|
+
agentName: z.string(),
|
|
191
|
+
status: statusSchema,
|
|
192
|
+
error: z.string().optional(),
|
|
193
|
+
/** The agent's login lapsed: the command that signs in again. */
|
|
194
|
+
signIn: z.string().optional(),
|
|
195
|
+
/** The mode that keeps the agent from changing files was accepted. */
|
|
196
|
+
readOnly: z.boolean(),
|
|
197
|
+
/** The agent edits and runs commands without asking. */
|
|
198
|
+
bypass: z.boolean(),
|
|
199
|
+
head: z.string(),
|
|
200
|
+
/** The checkout the agent works in. */
|
|
201
|
+
cwd: z.string(),
|
|
202
|
+
/** Absent until the agent says, or when it offers no choice. */
|
|
203
|
+
choices: askChoicesSchema.optional(),
|
|
204
|
+
/** Absent until the agent says, or when it takes none. */
|
|
205
|
+
commands: z.array(askCommandSchema).optional(),
|
|
206
|
+
accepts: askAcceptsSchema.optional(),
|
|
207
|
+
usage: askUsageSchema.optional(),
|
|
208
|
+
/** What the agent calls the conversation. */
|
|
209
|
+
title: z.string().optional(),
|
|
210
|
+
selection: z.object({ title: z.string(), quote: z.string().optional() }),
|
|
211
|
+
entries: z.array(askEntrySchema),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export type AskEntry = z.infer<typeof askEntrySchema>;
|
|
215
|
+
|
|
216
|
+
export type AskThreadState = z.infer<typeof askThreadStateSchema>;
|
|
217
|
+
|
|
218
|
+
/** One change to a thread. Answer text arrives as `append`, so a streamed
|
|
219
|
+
* token costs its own length, not the thread's. */
|
|
220
|
+
export const askChangeSchema = z.discriminatedUnion("type", [
|
|
221
|
+
z.object({
|
|
222
|
+
type: z.literal("set"),
|
|
223
|
+
status: statusSchema.optional(),
|
|
224
|
+
readOnly: z.boolean().optional(),
|
|
225
|
+
bypass: z.boolean().optional(),
|
|
226
|
+
choices: askChoicesSchema.optional(),
|
|
227
|
+
commands: z.array(askCommandSchema).optional(),
|
|
228
|
+
accepts: askAcceptsSchema.optional(),
|
|
229
|
+
usage: askUsageSchema.optional(),
|
|
230
|
+
title: z.string().optional(),
|
|
231
|
+
/** `null` clears the error; absent leaves it. */
|
|
232
|
+
error: z.string().nullable().optional(),
|
|
233
|
+
/** `null` clears it; absent leaves it. */
|
|
234
|
+
signIn: z.string().nullable().optional(),
|
|
235
|
+
}),
|
|
236
|
+
z.object({ type: z.literal("add"), entry: askEntrySchema }),
|
|
237
|
+
/** Replaces the entry with the same id and kind. */
|
|
238
|
+
z.object({ type: z.literal("entry"), entry: askEntrySchema }),
|
|
239
|
+
z.object({ type: z.literal("append"), id: z.string(), text: z.string() }),
|
|
240
|
+
/** Drops what a failed turn left, before it is asked again. */
|
|
241
|
+
z.object({ type: z.literal("remove"), ids: z.array(z.string()) }),
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
export type AskChange = z.infer<typeof askChangeSchema>;
|
|
245
|
+
|
|
246
|
+
/** A line of the watch stream. A snapshot can come at any point and resets
|
|
247
|
+
* the watcher; each change carries the next `seq` after the one before it. */
|
|
248
|
+
export const askUpdateSchema = z.union([
|
|
249
|
+
z.object({
|
|
250
|
+
seq: z.number().int().nonnegative(),
|
|
251
|
+
snapshot: askThreadStateSchema,
|
|
252
|
+
}),
|
|
253
|
+
z.object({ seq: z.number().int().positive(), change: askChangeSchema }),
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
export type AskUpdate = z.infer<typeof askUpdateSchema>;
|
|
257
|
+
|
|
258
|
+
/** The server and the panel both advance a thread with this, so they agree. */
|
|
259
|
+
export function applyAskChange(
|
|
260
|
+
state: AskThreadState,
|
|
261
|
+
change: AskChange,
|
|
262
|
+
): AskThreadState {
|
|
263
|
+
switch (change.type) {
|
|
264
|
+
case "set": {
|
|
265
|
+
const { error: _error, signIn: _signIn, ...rest } = state;
|
|
266
|
+
|
|
267
|
+
const next: AskThreadState = {
|
|
268
|
+
...rest,
|
|
269
|
+
status: change.status ?? state.status,
|
|
270
|
+
readOnly: change.readOnly ?? state.readOnly,
|
|
271
|
+
bypass: change.bypass ?? state.bypass,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const choices = change.choices ?? state.choices;
|
|
275
|
+
|
|
276
|
+
if (choices) next.choices = choices;
|
|
277
|
+
const commands = change.commands ?? state.commands;
|
|
278
|
+
|
|
279
|
+
if (commands) next.commands = commands;
|
|
280
|
+
const accepts = change.accepts ?? state.accepts;
|
|
281
|
+
|
|
282
|
+
if (accepts) next.accepts = accepts;
|
|
283
|
+
const usage = change.usage ?? state.usage;
|
|
284
|
+
|
|
285
|
+
if (usage) next.usage = usage;
|
|
286
|
+
const title = change.title ?? state.title;
|
|
287
|
+
|
|
288
|
+
if (title) next.title = title;
|
|
289
|
+
|
|
290
|
+
// Absent keeps the error, null clears it, a message replaces it.
|
|
291
|
+
const error = change.error === undefined ? state.error : change.error;
|
|
292
|
+
|
|
293
|
+
if (error) next.error = error;
|
|
294
|
+
|
|
295
|
+
const signIn = change.signIn === undefined ? state.signIn : change.signIn;
|
|
296
|
+
|
|
297
|
+
if (signIn) next.signIn = signIn;
|
|
298
|
+
|
|
299
|
+
return next;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case "add":
|
|
303
|
+
return { ...state, entries: [...state.entries, change.entry] };
|
|
304
|
+
case "entry":
|
|
305
|
+
return {
|
|
306
|
+
...state,
|
|
307
|
+
entries: state.entries.map((entry) =>
|
|
308
|
+
entry.id === change.entry.id && entry.kind === change.entry.kind
|
|
309
|
+
? change.entry
|
|
310
|
+
: entry,
|
|
311
|
+
),
|
|
312
|
+
};
|
|
313
|
+
case "remove": {
|
|
314
|
+
const ids = new Set(change.ids);
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
...state,
|
|
318
|
+
entries: state.entries.filter((entry) => !ids.has(entry.id)),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case "append":
|
|
323
|
+
return {
|
|
324
|
+
...state,
|
|
325
|
+
entries: state.entries.map((entry) =>
|
|
326
|
+
entry.kind === "agent" && entry.id === change.id
|
|
327
|
+
? { ...entry, text: entry.text + change.text }
|
|
328
|
+
: entry,
|
|
329
|
+
),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** A saved conversation, as the history list shows it. The transcript stays
|
|
335
|
+
* with the agent; Whiteboard keeps what it needs to find and reopen it. */
|
|
336
|
+
export const askHistoryEntrySchema = z.object({
|
|
337
|
+
id: z.string(),
|
|
338
|
+
agent: z.enum(askAgentIds),
|
|
339
|
+
/** What the agent calls it, else the first question. */
|
|
340
|
+
title: z.string(),
|
|
341
|
+
/** The first saved user question, independent of the agent's title. */
|
|
342
|
+
question: z.string().optional(),
|
|
343
|
+
selection: AgentSelectionSchema,
|
|
344
|
+
/** The commit the agent read. */
|
|
345
|
+
head: z.string(),
|
|
346
|
+
createdAt: z.string(),
|
|
347
|
+
updatedAt: z.string(),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
export type AskHistoryEntry = z.infer<typeof askHistoryEntrySchema>;
|