@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,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReviewInstanceUnavailableError,
|
|
3
|
+
healthyReviewInstance,
|
|
4
|
+
reviewInstanceUnavailable,
|
|
5
|
+
selectReviewInstance,
|
|
6
|
+
} from "@review/desktop-discovery.js";
|
|
7
|
+
import {
|
|
8
|
+
readReviewServerDiscovery,
|
|
9
|
+
reviewServerIsHealthy,
|
|
10
|
+
reviewServerStateDir,
|
|
11
|
+
serverNotReady,
|
|
12
|
+
} from "@review/server-discovery.js";
|
|
13
|
+
|
|
14
|
+
import { ReviewApiClient, ReviewApiError } from "./client.js";
|
|
15
|
+
import { ReviewInputError } from "./input-error.js";
|
|
16
|
+
|
|
17
|
+
export interface AuthoringTool {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
inputSchema: Tool["inputSchema"];
|
|
21
|
+
method: "GET" | "POST";
|
|
22
|
+
path: string;
|
|
23
|
+
commandType?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TEXT_TOOLS = new Set([
|
|
27
|
+
"review_get",
|
|
28
|
+
"review_get_instructions",
|
|
29
|
+
"session_get",
|
|
30
|
+
"session_get_instructions",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export interface ConnectedReview {
|
|
34
|
+
client: ReviewApiClient;
|
|
35
|
+
/** The Desktop reached; absent for a headless server. */
|
|
36
|
+
instance?: { key: string; appVersion?: string };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function connectReviewApi(
|
|
40
|
+
env = process.env,
|
|
41
|
+
headers: Record<string, string> = {},
|
|
42
|
+
) {
|
|
43
|
+
return (await connectReviewInstance(env, headers)).client;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function connectReviewInstance(
|
|
47
|
+
env = process.env,
|
|
48
|
+
headers: Record<string, string> = {},
|
|
49
|
+
): Promise<ConnectedReview> {
|
|
50
|
+
const request: ConstructorParameters<typeof ReviewApiClient>[1] = (
|
|
51
|
+
url,
|
|
52
|
+
init,
|
|
53
|
+
) => {
|
|
54
|
+
const merged = new Headers(init?.headers);
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of Object.entries(headers)) merged.set(key, value);
|
|
57
|
+
|
|
58
|
+
return fetch(url, { ...init, headers: merged });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (env.DEV_REVIEW_SERVER_DIR?.trim()) {
|
|
62
|
+
const stateDir = reviewServerStateDir(env);
|
|
63
|
+
const server = await readReviewServerDiscovery(stateDir);
|
|
64
|
+
|
|
65
|
+
if (!server || !(await reviewServerIsHealthy(server)))
|
|
66
|
+
throw serverNotReady(stateDir);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
client: new ReviewApiClient(
|
|
70
|
+
{ serverUrl: server.url, token: server.token },
|
|
71
|
+
request,
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const selection = await selectReviewInstance({ env });
|
|
77
|
+
const discovery = healthyReviewInstance(selection);
|
|
78
|
+
|
|
79
|
+
if (!discovery) {
|
|
80
|
+
const unavailable = reviewInstanceUnavailable(selection);
|
|
81
|
+
const message = `${unavailable.message} For headless authoring, select a running server with --state-dir or DEV_REVIEW_SERVER_DIR.`;
|
|
82
|
+
|
|
83
|
+
if (unavailable instanceof ReviewInstanceUnavailableError)
|
|
84
|
+
throw new ReviewInstanceUnavailableError(message);
|
|
85
|
+
|
|
86
|
+
throw unavailable;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
client: new ReviewApiClient(
|
|
91
|
+
{ serverUrl: discovery.url, token: discovery.token },
|
|
92
|
+
request,
|
|
93
|
+
),
|
|
94
|
+
instance: { key: selection.key, appVersion: discovery.appVersion },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Only translate the tool envelope. The host owns validation and persistence. */
|
|
99
|
+
export async function callAuthoringTool(
|
|
100
|
+
client: ReviewApiClient,
|
|
101
|
+
tool: AuthoringTool,
|
|
102
|
+
input: NonNullable<CallToolRequest["params"]["arguments"]>,
|
|
103
|
+
signal?: AbortSignal,
|
|
104
|
+
) {
|
|
105
|
+
if (tool.commandType) {
|
|
106
|
+
try {
|
|
107
|
+
return await client.post<JsonValue>(
|
|
108
|
+
tool.path,
|
|
109
|
+
{ operation: { ...input, type: tool.commandType } },
|
|
110
|
+
signal,
|
|
111
|
+
);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// A host reply (any status) is definite: a failed command rolls back.
|
|
114
|
+
if (error instanceof ReviewApiError || signal?.aborted) throw error;
|
|
115
|
+
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Whiteboard may or may not have applied ${tool.name} (${error instanceof Error ? error.message : String(error)}). Check with review_get before retrying.`,
|
|
118
|
+
{ cause: error },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const fields = { ...input };
|
|
124
|
+
|
|
125
|
+
const route = tool.path.replace(/:reviewId\b/g, () => {
|
|
126
|
+
const value = fields.reviewId;
|
|
127
|
+
|
|
128
|
+
if (!isStringValue(value) || !value)
|
|
129
|
+
throw new ReviewInputError("reviewId is required.");
|
|
130
|
+
delete fields.reviewId;
|
|
131
|
+
|
|
132
|
+
return encodeURIComponent(value);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (tool.method === "POST")
|
|
136
|
+
return client.post<JsonValue>(route, fields, signal);
|
|
137
|
+
const query = new URLSearchParams();
|
|
138
|
+
|
|
139
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
140
|
+
// Hosts often send null for an unused optional field; the route reads absence.
|
|
141
|
+
if (value === null || value === undefined) continue;
|
|
142
|
+
|
|
143
|
+
// Arrays travel as repeated keys, as in paths=a&paths=b.
|
|
144
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
145
|
+
if (!isStringValue(item) && !isNumberValue(item) && !isBooleanValue(item))
|
|
146
|
+
throw new ReviewInputError(
|
|
147
|
+
`${key} must be a string, number, boolean or list of them.`,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
query.append(key, String(item));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const response = await client.response(
|
|
155
|
+
route + (query.size ? `?${query}` : ""),
|
|
156
|
+
{ signal },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return response.headers.get("content-type")?.startsWith("text/plain")
|
|
160
|
+
? new ToolText(await response.text())
|
|
161
|
+
: parseJsonText(await response.text());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A plain-text reply, shown to the agent as-is instead of as a JSON string. */
|
|
165
|
+
export class ToolText {
|
|
166
|
+
constructor(readonly text: string) {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The text an agent sees for a tool result. */
|
|
170
|
+
export function toolResultText(
|
|
171
|
+
tool: Pick<AuthoringTool, "name">,
|
|
172
|
+
result: JsonValue | ToolText,
|
|
173
|
+
) {
|
|
174
|
+
if (result instanceof ToolText) return result.text;
|
|
175
|
+
|
|
176
|
+
return TEXT_TOOLS.has(tool.name) && isStringValue(result)
|
|
177
|
+
? result
|
|
178
|
+
: JSON.stringify(result);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
import {
|
|
182
|
+
isBooleanValue,
|
|
183
|
+
isNumberValue,
|
|
184
|
+
isStringValue,
|
|
185
|
+
type JsonValue,
|
|
186
|
+
parseJsonText,
|
|
187
|
+
} from "@dev.fast/json";
|
|
188
|
+
import type { CallToolRequest, Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type LensSource,
|
|
3
|
+
formatAnchor,
|
|
4
|
+
selectionKey,
|
|
5
|
+
} from "@review/lens-selection.js";
|
|
6
|
+
|
|
7
|
+
import { type Pins, anchorPins, selectionReferences } from "./document.js";
|
|
8
|
+
import type { Snapshot } from "./store.js";
|
|
9
|
+
|
|
10
|
+
export interface AnchorQuote {
|
|
11
|
+
anchor: string;
|
|
12
|
+
first: string;
|
|
13
|
+
last?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const QUOTE_LIMIT = 20;
|
|
17
|
+
|
|
18
|
+
// A blank line quoted as "" reads like a failed quote.
|
|
19
|
+
const clip = (line: string) =>
|
|
20
|
+
line.trim() === ""
|
|
21
|
+
? "(blank line)"
|
|
22
|
+
: line.length > 120
|
|
23
|
+
? `${line.slice(0, 119)}…`
|
|
24
|
+
: line;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The first and last line of each anchor an edit added or changed, so its
|
|
28
|
+
* author can see a range is the one meant without reading the file again.
|
|
29
|
+
* A range that can't be read was already rejected, so it is skipped here.
|
|
30
|
+
* `unquoted` counts the anchors past the limit, so a short list isn't
|
|
31
|
+
* mistaken for a complete one.
|
|
32
|
+
*/
|
|
33
|
+
export async function anchorQuotes(
|
|
34
|
+
before: Snapshot | undefined,
|
|
35
|
+
after: Snapshot,
|
|
36
|
+
read: (pins: Pins, side: "base" | "head", file: string) => Promise<string>,
|
|
37
|
+
): Promise<{ quotes: AnchorQuote[]; unquoted: number }> {
|
|
38
|
+
const key = (id: string, source: LensSource) =>
|
|
39
|
+
`${id}\0${selectionKey(source)}`;
|
|
40
|
+
|
|
41
|
+
const known = new Set(
|
|
42
|
+
before
|
|
43
|
+
? selectionReferences(before.document, { tolerant: true }).map(
|
|
44
|
+
({ id, source }) => key(id, source),
|
|
45
|
+
)
|
|
46
|
+
: [],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const added = selectionReferences(after.document, { tolerant: true }).filter(
|
|
50
|
+
({ id, source }) => !known.has(key(id, source)),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const quotes = await Promise.all(
|
|
54
|
+
added
|
|
55
|
+
.slice(0, QUOTE_LIMIT)
|
|
56
|
+
.map(async ({ source }): Promise<AnchorQuote | undefined> => {
|
|
57
|
+
try {
|
|
58
|
+
const pins = anchorPins(source, after.pins);
|
|
59
|
+
|
|
60
|
+
const line = async (endpoint: LensSource["start"]) =>
|
|
61
|
+
(await read(pins, endpoint.side, source.file)).split("\n")[
|
|
62
|
+
endpoint.line - 1
|
|
63
|
+
] ?? "";
|
|
64
|
+
|
|
65
|
+
const first = clip(await line(source.start));
|
|
66
|
+
|
|
67
|
+
const single =
|
|
68
|
+
source.start.side === source.end.side &&
|
|
69
|
+
source.start.line === source.end.line;
|
|
70
|
+
|
|
71
|
+
return single
|
|
72
|
+
? { anchor: formatAnchor(source), first }
|
|
73
|
+
: {
|
|
74
|
+
anchor: formatAnchor(source),
|
|
75
|
+
first,
|
|
76
|
+
last: clip(await line(source.end)),
|
|
77
|
+
};
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
quotes: quotes.filter((quote) => quote !== undefined),
|
|
86
|
+
unquoted: Math.max(0, added.length - QUOTE_LIMIT),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AskAgentId,
|
|
5
|
+
type AskEntry,
|
|
6
|
+
type AskHistoryEntry,
|
|
7
|
+
type AskOffer,
|
|
8
|
+
askEntrySchema,
|
|
9
|
+
askHistoryEntrySchema,
|
|
10
|
+
askOfferSchema,
|
|
11
|
+
} from "@review/ask/thread-state.js";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
/** A saved Ask conversation: its history entry, and how to reopen it. */
|
|
15
|
+
export const askRecordSchema = askHistoryEntrySchema.extend({
|
|
16
|
+
reviewId: z.string(),
|
|
17
|
+
/** The ACP session the agent keeps the transcript under. */
|
|
18
|
+
sessionId: z.string(),
|
|
19
|
+
/** The review version asked about; its pins recreate the checkout. */
|
|
20
|
+
version: z.number().int(),
|
|
21
|
+
cwd: z.string(),
|
|
22
|
+
/** What the panel showed last; absent until a turn ends or it closes. */
|
|
23
|
+
entries: z.array(askEntrySchema).optional(),
|
|
24
|
+
/** The agent edits and runs commands without asking. */
|
|
25
|
+
bypass: z.boolean().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type AskRecord = z.infer<typeof askRecordSchema>;
|
|
29
|
+
|
|
30
|
+
const rowSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
review_id: z.string(),
|
|
34
|
+
agent: z.string(),
|
|
35
|
+
session_id: z.string(),
|
|
36
|
+
version: z.number(),
|
|
37
|
+
head: z.string(),
|
|
38
|
+
cwd: z.string(),
|
|
39
|
+
selection: z.string(),
|
|
40
|
+
title: z.string(),
|
|
41
|
+
created_at: z.string(),
|
|
42
|
+
updated_at: z.string(),
|
|
43
|
+
entries: z.string().nullable(),
|
|
44
|
+
bypass: z.number(),
|
|
45
|
+
})
|
|
46
|
+
.transform((row) =>
|
|
47
|
+
askRecordSchema.parse({
|
|
48
|
+
id: row.id,
|
|
49
|
+
reviewId: row.review_id,
|
|
50
|
+
agent: row.agent,
|
|
51
|
+
sessionId: row.session_id,
|
|
52
|
+
version: row.version,
|
|
53
|
+
head: row.head,
|
|
54
|
+
cwd: row.cwd,
|
|
55
|
+
selection: JSON.parse(row.selection),
|
|
56
|
+
title: row.title,
|
|
57
|
+
createdAt: row.created_at,
|
|
58
|
+
updatedAt: row.updated_at,
|
|
59
|
+
entries: row.entries === null ? undefined : JSON.parse(row.entries),
|
|
60
|
+
bypass: row.bypass === 1,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/** Saved Ask conversations, per review. Shared reviews are not in
|
|
65
|
+
* `reviews`, so there is no foreign key; deleting a review deletes its rows. */
|
|
66
|
+
export class AskHistory {
|
|
67
|
+
constructor(private readonly db: DatabaseSync) {
|
|
68
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ask_conversations(
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
review_id TEXT NOT NULL,
|
|
71
|
+
agent TEXT NOT NULL,
|
|
72
|
+
session_id TEXT NOT NULL,
|
|
73
|
+
version INTEGER NOT NULL,
|
|
74
|
+
head TEXT NOT NULL,
|
|
75
|
+
cwd TEXT NOT NULL,
|
|
76
|
+
selection TEXT NOT NULL,
|
|
77
|
+
title TEXT NOT NULL,
|
|
78
|
+
created_at TEXT NOT NULL,
|
|
79
|
+
updated_at TEXT NOT NULL,
|
|
80
|
+
entries TEXT,
|
|
81
|
+
bypass INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
UNIQUE(agent, session_id));
|
|
83
|
+
CREATE INDEX IF NOT EXISTS ask_conversations_review ON ask_conversations(review_id, updated_at);`);
|
|
84
|
+
|
|
85
|
+
// Conversations saved before bypassing permissions lack the column.
|
|
86
|
+
if (
|
|
87
|
+
!db
|
|
88
|
+
.prepare("PRAGMA table_info(ask_conversations)")
|
|
89
|
+
.all()
|
|
90
|
+
.some((column) => String(column.name) === "bypass")
|
|
91
|
+
)
|
|
92
|
+
db.exec(
|
|
93
|
+
"ALTER TABLE ask_conversations ADD COLUMN bypass INTEGER NOT NULL DEFAULT 0",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// What each agent offered last, so a new question can pick a model and
|
|
97
|
+
// effort, and a command, before its agent starts.
|
|
98
|
+
db.exec(
|
|
99
|
+
"CREATE TABLE IF NOT EXISTS ask_agent_offers(agent TEXT PRIMARY KEY, offer TEXT NOT NULL)",
|
|
100
|
+
);
|
|
101
|
+
// And with each model it ran, since the efforts on offer depend on it.
|
|
102
|
+
db.exec(
|
|
103
|
+
"CREATE TABLE IF NOT EXISTS ask_agent_model_offers(agent TEXT NOT NULL, model TEXT NOT NULL, offer TEXT NOT NULL, PRIMARY KEY(agent, model))",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
save(record: AskRecord) {
|
|
108
|
+
this.db
|
|
109
|
+
.prepare(
|
|
110
|
+
`INSERT OR REPLACE INTO ask_conversations(id,review_id,agent,session_id,version,head,cwd,selection,title,created_at,updated_at,bypass)
|
|
111
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
112
|
+
)
|
|
113
|
+
.run(
|
|
114
|
+
record.id,
|
|
115
|
+
record.reviewId,
|
|
116
|
+
record.agent,
|
|
117
|
+
record.sessionId,
|
|
118
|
+
record.version,
|
|
119
|
+
record.head,
|
|
120
|
+
record.cwd,
|
|
121
|
+
JSON.stringify(record.selection),
|
|
122
|
+
record.title,
|
|
123
|
+
record.createdAt,
|
|
124
|
+
record.updatedAt,
|
|
125
|
+
record.bypass ? 1 : 0,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Whether reopening the conversation bypasses permissions. */
|
|
130
|
+
setBypass(id: string, bypass: boolean) {
|
|
131
|
+
this.db
|
|
132
|
+
.prepare("UPDATE ask_conversations SET bypass=? WHERE id=?")
|
|
133
|
+
.run(bypass ? 1 : 0, id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Points a conversation at a new session, when its agent could not
|
|
137
|
+
* reopen the one it had. */
|
|
138
|
+
updateSession(id: string, sessionId: string) {
|
|
139
|
+
this.db
|
|
140
|
+
.prepare("UPDATE ask_conversations SET session_id=? WHERE id=?")
|
|
141
|
+
.run(sessionId, id);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Keeps what the panel shows, so a reopen need not wait on the agent. */
|
|
145
|
+
saveEntries(id: string, entries: AskEntry[]) {
|
|
146
|
+
this.db
|
|
147
|
+
.prepare("UPDATE ask_conversations SET entries=? WHERE id=?")
|
|
148
|
+
.run(JSON.stringify(entries), id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The agent's name for the conversation, which the list shows. */
|
|
152
|
+
rename(id: string, title: string) {
|
|
153
|
+
this.db
|
|
154
|
+
.prepare("UPDATE ask_conversations SET title=? WHERE id=?")
|
|
155
|
+
.run(title.slice(0, 200), id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
saveOffer(agent: AskAgentId, offer: AskOffer) {
|
|
159
|
+
this.db
|
|
160
|
+
.prepare(
|
|
161
|
+
"INSERT OR REPLACE INTO ask_agent_offers(agent, offer) VALUES(?, ?)",
|
|
162
|
+
)
|
|
163
|
+
.run(agent, JSON.stringify(offer));
|
|
164
|
+
this.saveModelOffer(agent, offer);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Keeps what the agent offers with the offer's model, without making
|
|
168
|
+
* it what the agent offered last. */
|
|
169
|
+
saveModelOffer(agent: AskAgentId, offer: AskOffer) {
|
|
170
|
+
const model = offer.choices.model?.current;
|
|
171
|
+
|
|
172
|
+
if (!model) return;
|
|
173
|
+
this.db
|
|
174
|
+
.prepare(
|
|
175
|
+
"INSERT OR REPLACE INTO ask_agent_model_offers(agent, model, offer) VALUES(?, ?, ?)",
|
|
176
|
+
)
|
|
177
|
+
.run(agent, model, JSON.stringify(offer));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** What the agent offered last, or last with the model given. */
|
|
181
|
+
offer(agent: AskAgentId, model?: string): AskOffer | undefined {
|
|
182
|
+
const row = z
|
|
183
|
+
.object({ offer: z.string() })
|
|
184
|
+
.safeParse(
|
|
185
|
+
model === undefined
|
|
186
|
+
? this.db
|
|
187
|
+
.prepare("SELECT offer FROM ask_agent_offers WHERE agent=?")
|
|
188
|
+
.get(agent)
|
|
189
|
+
: this.db
|
|
190
|
+
.prepare(
|
|
191
|
+
"SELECT offer FROM ask_agent_model_offers WHERE agent=? AND model=?",
|
|
192
|
+
)
|
|
193
|
+
.get(agent, model),
|
|
194
|
+
).data;
|
|
195
|
+
|
|
196
|
+
return row && askOfferSchema.safeParse(JSON.parse(row.offer)).data;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
touch(id: string, at = new Date().toISOString()) {
|
|
200
|
+
this.db
|
|
201
|
+
.prepare("UPDATE ask_conversations SET updated_at=? WHERE id=?")
|
|
202
|
+
.run(at, id);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Newest first. */
|
|
206
|
+
list(reviewId: string): AskHistoryEntry[] {
|
|
207
|
+
return this.db
|
|
208
|
+
.prepare(
|
|
209
|
+
"SELECT * FROM ask_conversations WHERE review_id=? ORDER BY updated_at DESC",
|
|
210
|
+
)
|
|
211
|
+
.all(reviewId)
|
|
212
|
+
.map((row) => {
|
|
213
|
+
const {
|
|
214
|
+
reviewId: _reviewId,
|
|
215
|
+
sessionId: _sessionId,
|
|
216
|
+
version: _version,
|
|
217
|
+
cwd: _cwd,
|
|
218
|
+
entries,
|
|
219
|
+
bypass: _bypass,
|
|
220
|
+
...entry
|
|
221
|
+
} = rowSchema.parse(row);
|
|
222
|
+
|
|
223
|
+
const question = entries?.find((entry) => entry.kind === "user")?.text;
|
|
224
|
+
|
|
225
|
+
if (question?.trim()) entry.question = question;
|
|
226
|
+
|
|
227
|
+
return entry;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
get(id: string): AskRecord | undefined {
|
|
232
|
+
const row = this.db
|
|
233
|
+
.prepare("SELECT * FROM ask_conversations WHERE id=?")
|
|
234
|
+
.get(id);
|
|
235
|
+
|
|
236
|
+
return row ? rowSchema.parse(row) : undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
delete(id: string) {
|
|
240
|
+
this.db.prepare("DELETE FROM ask_conversations WHERE id=?").run(id);
|
|
241
|
+
}
|
|
242
|
+
}
|