@dev.fast/whiteboard 0.2.1-preview.20261007.105 → 0.2.1-preview.20261007.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli-50KJgMKi.js +2 -0
- package/dist/{agent-cli-DTY727wD.js → agent-cli-DRRQfHQ-.js} +1 -1
- package/dist/{authoring-tools-ml8wRIRj.js → authoring-tools-F_q8cC7J.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-runner-BApCwwRD.js → cli-runner-Cwg5PeH2.js} +4 -4
- package/dist/cli.js +1 -1
- package/dist/{headless-host-CUyloTAd.js → headless-host-Dd_-zkC4.js} +2 -2
- package/dist/{local-data-B_Kfm3Id.js → local-data-D-EoJfPx.js} +10 -1
- package/dist/{mcp-BF2_RoAR.js → mcp-BAE5rMgB.js} +1 -1
- package/dist/{process-error-telemetry-Gp3cCIu4.js → process-error-telemetry-mzRG988B.js} +124 -31
- package/dist/profile-Cqztt-9-.js +2 -0
- package/dist/{profile-CeT6-ZWV.js → profile-WzZu7Spt.js} +1 -1
- package/dist/server/desktop-host.js +3 -3
- package/package.json +6 -6
- package/src/ask/thread-state.ts +9 -0
- package/src/ask/thread.ts +5 -0
- package/src/ask/threads.ts +61 -1
- package/src/ask/watch.ts +70 -34
- package/src/review-api/http.ts +37 -11
- package/dist/agent-cli-Q_EpjIbV.js +0 -2
- package/dist/profile-BKncMvSE.js +0 -2
|
@@ -23,7 +23,7 @@ async function runReviewAgentCli(input) {
|
|
|
23
23
|
[REVIEW_AGENT_HEADER]: reviewSessionAgent(env)
|
|
24
24
|
};
|
|
25
25
|
if (mode === "mcp") {
|
|
26
|
-
const { serveReviewMcp } = await import("./mcp-
|
|
26
|
+
const { serveReviewMcp } = await import("./mcp-BAE5rMgB.js");
|
|
27
27
|
await serveReviewMcp(async (key, agentKind) => {
|
|
28
28
|
const connected = await connectReviewInstance(key ? {
|
|
29
29
|
...env,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as activityUpdateSchema, h as activityEndSchema, i as commandSchema, m as activityBeginSchema, n as uploadSchema } from "./local-data-
|
|
1
|
+
import { g as activityUpdateSchema, h as activityEndSchema, i as commandSchema, m as activityBeginSchema, n as uploadSchema } from "./local-data-D-EoJfPx.js";
|
|
2
2
|
import { x as publishedEditSchema, y as pathTargetSchema } from "./stored-document-migration-T9Uj5MkN.js";
|
|
3
3
|
import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
|
|
4
4
|
import { t as findReviewPackageRoot } from "./package-paths-B6-zxvIO.js";
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.2.1-preview.20261007.
|
|
1
|
+
{"version":"0.2.1-preview.20261007.106","commit":"e4eca7ac75d6e6ed7dc027eecca4883eb67579f6","dirty":true,"builtAt":"2026-10-07T23:14:48.283Z"}
|
|
@@ -13,7 +13,7 @@ import { t as ReviewTelemetry } from "./review-telemetry-Pp4ZsVrh.js";
|
|
|
13
13
|
import { a as isOwnedShim, f as windowsInstallerCommand, m as connectPrompts, o as pathShimPath, p as scanLegacySkills, t as aliasInstallationToAccount } from "./account-alias-DLE9XZhi.js";
|
|
14
14
|
import { t as fuzzyRank } from "./fuzzy-match-BoAmcyak.js";
|
|
15
15
|
import { n as readReviewServerDiscovery, o as reviewServerStateDir, r as readReviewServerHealth, s as serverNotReady } from "./server-discovery-Dbzn3w6Z.js";
|
|
16
|
-
import { t as reviewAgentCliHelp } from "./agent-cli-
|
|
16
|
+
import { t as reviewAgentCliHelp } from "./agent-cli-DRRQfHQ-.js";
|
|
17
17
|
import { closeSync, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readSync, renameSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { realpath } from "node:fs/promises";
|
|
19
19
|
import path from "node:path";
|
|
@@ -4495,7 +4495,7 @@ async function runReviewCli(input) {
|
|
|
4495
4495
|
process.once("SIGINT", stop);
|
|
4496
4496
|
process.once("SIGTERM", stop);
|
|
4497
4497
|
try {
|
|
4498
|
-
const { runHeadlessServer } = await import("./headless-host-
|
|
4498
|
+
const { runHeadlessServer } = await import("./headless-host-Dd_-zkC4.js");
|
|
4499
4499
|
await runHeadlessServer({
|
|
4500
4500
|
stateDir,
|
|
4501
4501
|
port,
|
|
@@ -4546,7 +4546,7 @@ async function runReviewCli(input) {
|
|
|
4546
4546
|
if (problem) throw new Error(`Cannot tell whether a Whiteboard Desktop is using ${stateDir}: ${problem.message}`);
|
|
4547
4547
|
const inUse = () => /* @__PURE__ */ new Error(`A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`);
|
|
4548
4548
|
if (desktops.instances.some(({ discovery }) => [discovery.appPid, discovery.serverPid].some((pid) => processIsAlive(pid)))) throw inUse();
|
|
4549
|
-
const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-
|
|
4549
|
+
const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-Cqztt-9-.js"), import("./headless-host-Dd_-zkC4.js")]);
|
|
4550
4550
|
const reset = await withHeadlessServerLock(await realpath(stateDir), async () => {
|
|
4551
4551
|
const local = await openReviewProfile(stateDir, { manageWorkspaces: false });
|
|
4552
4552
|
try {
|
|
@@ -4774,7 +4774,7 @@ async function runReviewCli(input) {
|
|
|
4774
4774
|
return commands;
|
|
4775
4775
|
} });
|
|
4776
4776
|
for (const [name, description] of [["api", "Call a JSON review authoring tool on the running server"], ["mcp", "Serve the JSON review authoring tools over stdio MCP"]]) configureOutput(program.command(name).description(description).argument("[args...]", "tool name and JSON input").allowUnknownOption().allowExcessArguments().helpOption(false).passThroughOptions().addHelpText("after", `\n${reviewAgentCliHelp}`), "plain").action(async (args) => {
|
|
4777
|
-
const { runReviewAgentCli } = await import("./agent-cli-
|
|
4777
|
+
const { runReviewAgentCli } = await import("./agent-cli-50KJgMKi.js");
|
|
4778
4778
|
state.exitCode = await runReviewAgentCli({
|
|
4779
4779
|
...input,
|
|
4780
4780
|
env: authoringEnv(),
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { p as isObjectValue } from "./src-X9phtB2j.js";
|
|
2
2
|
import { O as withFileLock, P as writePrivateJsonAtomic } from "./s3-config-CAxhO9u_.js";
|
|
3
3
|
import "./review-home-paths-6zZH-1c9.js";
|
|
4
|
-
import { t as openReviewProfile } from "./profile-
|
|
5
|
-
import { i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, p as GlobalReviewDesktopVerbRelay, t as drainServerCrashReport } from "./process-error-telemetry-
|
|
4
|
+
import { t as openReviewProfile } from "./profile-WzZu7Spt.js";
|
|
5
|
+
import { i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, p as GlobalReviewDesktopVerbRelay, t as drainServerCrashReport } from "./process-error-telemetry-mzRG988B.js";
|
|
6
6
|
import { i as reviewServerDiscoveryPath, t as headlessServerLockPath } from "./server-discovery-Dbzn3w6Z.js";
|
|
7
7
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
import { mkdir, realpath, rm } from "node:fs/promises";
|
|
@@ -1913,13 +1913,22 @@ const askChangeSchema = z.discriminatedUnion("type", [
|
|
|
1913
1913
|
ids: z.array(z.string())
|
|
1914
1914
|
})
|
|
1915
1915
|
]);
|
|
1916
|
-
|
|
1916
|
+
/** A line of the watch stream. A snapshot can come at any point and resets
|
|
1917
|
+
* the watcher; each change carries the next `seq` after the one before it. */
|
|
1918
|
+
const askUpdateSchema = z.union([z.object({
|
|
1917
1919
|
seq: z.number().int().nonnegative(),
|
|
1918
1920
|
snapshot: askThreadStateSchema
|
|
1919
1921
|
}), z.object({
|
|
1920
1922
|
seq: z.number().int().positive(),
|
|
1921
1923
|
change: askChangeSchema
|
|
1922
1924
|
})]);
|
|
1925
|
+
z.union([z.object({
|
|
1926
|
+
threadId: z.string(),
|
|
1927
|
+
update: askUpdateSchema
|
|
1928
|
+
}), z.object({
|
|
1929
|
+
threadId: z.string(),
|
|
1930
|
+
ended: z.literal(true)
|
|
1931
|
+
})]);
|
|
1923
1932
|
/** The server and the panel both advance a thread with this, so they agree. */
|
|
1924
1933
|
function applyAskChange(state, change) {
|
|
1925
1934
|
switch (change.type) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as toolResultText, s as ReviewApiError } from "./agent-client-DoaODH6I.js";
|
|
2
2
|
import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
|
|
3
|
-
import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-
|
|
3
|
+
import { n as REVIEW_STATUS_TOOL, t as authoringTools } from "./authoring-tools-F_q8cC7J.js";
|
|
4
4
|
import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-Djeeax6q.js";
|
|
5
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as jsonString, m as isStringValue, n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
|
|
2
2
|
import { a as sharedReviewId, c as verifyShareRepository, d as StreamLimitError, f as readBoundedStream, l as exportShare, m as shareIdSchema, n as ShareClient, p as parseShareLink, r as SharePreflightError, s as readShareRepository, t as ShareAuthError, u as validateShareSources } from "./client-D01H2KOV.js";
|
|
3
|
-
import { _ as invalidateStructuralComparisons, a as inspectSnapshot, c as askAgentIds, d as askQuestionSchema, f as AgentSelectionSchema, i as commandSchema, l as askChoiceKinds, p as selectionMarkdown, r as SCRATCHPAD_ID, s as applyAskChange, u as askPicksSchema, v as diffrExecutable, y as diffrMissingError } from "./local-data-
|
|
3
|
+
import { _ as invalidateStructuralComparisons, a as inspectSnapshot, c as askAgentIds, d as askQuestionSchema, f as AgentSelectionSchema, i as commandSchema, l as askChoiceKinds, p as selectionMarkdown, r as SCRATCHPAD_ID, s as applyAskChange, u as askPicksSchema, v as diffrExecutable, y as diffrMissingError } from "./local-data-D-EoJfPx.js";
|
|
4
4
|
import { d as REVIEW_CLIENT_REMOTE, i as parseReviewVerbRequest, r as parseReviewDesktopVerbResult, u as REVIEW_CLIENT_HEADER, v as reviewDiffrSummarizerInputSchema, y as decodeStructuralDiffEvent } from "./src-DHiDJQDy.js";
|
|
5
5
|
import { C as TraceStorageDeniedError, G as readStoreAuth, H as DEFAULT_STORE_ORIGIN, U as clearStoreAuth, W as openUrlInBrowser, _ as isTraceStorageConfigured, b as selectTraceStorage, o as listReviewTraceSessions, q as runStoreLogin, s as loadReviewAgentTrace, y as resolveTraceStorage } from "./review-agent-traces-DNJFYzc8.js";
|
|
6
6
|
import { $ as unionIntervals, G as selectionKey, H as comparisonKey, I as fileLineRangeSchema, J as coverageSources, N as lensSelections, Q as subtractIntervals, T as selectionReferences, U as formatAnchor, V as anchorSelection, W as resolveDiffSelection, Y as emptyCoverage, Z as scopedCoverage, h as elements, k as UNCATEGORIZED_LENS_ID, u as anchorPins } from "./stored-document-migration-T9Uj5MkN.js";
|
|
@@ -10,7 +10,7 @@ import { t as errorMessage } from "./error-message-OtiDonty.js";
|
|
|
10
10
|
import { T as traceMachineEnabled } from "./review-home-paths-6zZH-1c9.js";
|
|
11
11
|
import { i as readReviewPackageVersion, n as readBuildCommit } from "./package-paths-B6-zxvIO.js";
|
|
12
12
|
import { a as REVIEW_APP_SESSION_ID_HEADER, c as sanitizeUiTelemetryEvent, i as BUNDLE_FRAME_PATTERN, l as cleanTelemetryText, r as reviewRequestOrigin, s as isReportableCleanedMessage } from "./request-origin-DlHTFCrQ.js";
|
|
13
|
-
import { a as scratchpadAvailable, c as readQuerySchemas, i as renderInstructions, o as inspectQuerySchema, r as instructionsQuerySchema, s as queryAnchor, t as authoringTools } from "./authoring-tools-
|
|
13
|
+
import { a as scratchpadAvailable, c as readQuerySchemas, i as renderInstructions, o as inspectQuerySchema, r as instructionsQuerySchema, s as queryAnchor, t as authoringTools } from "./authoring-tools-F_q8cC7J.js";
|
|
14
14
|
import { t as fuzzyRank } from "./fuzzy-match-BoAmcyak.js";
|
|
15
15
|
import { createRequire } from "node:module";
|
|
16
16
|
import crypto$1, { createHash, randomUUID } from "node:crypto";
|
|
@@ -1322,6 +1322,10 @@ var AskThread = class {
|
|
|
1322
1322
|
this.listeners.add(listener);
|
|
1323
1323
|
return () => this.listeners.delete(listener);
|
|
1324
1324
|
}
|
|
1325
|
+
/** Whether anything follows the thread, as an open panel does. */
|
|
1326
|
+
watched() {
|
|
1327
|
+
return this.listeners.size > 0;
|
|
1328
|
+
}
|
|
1325
1329
|
/** Starts the agent and asks the first question, or loads an earlier
|
|
1326
1330
|
* conversation; failures land in the state. */
|
|
1327
1331
|
async open() {
|
|
@@ -2122,6 +2126,12 @@ const OFFER_TIMEOUT_MS = 3e4;
|
|
|
2122
2126
|
/** How long after its session opens an agent may take to list its
|
|
2123
2127
|
* commands, which it sends on its own rather than in the response. */
|
|
2124
2128
|
const COMMANDS_WAIT_MS = 2e3;
|
|
2129
|
+
/** How long a thread nobody follows may sit idle before it ends: its panel
|
|
2130
|
+
* went with a reload, or its review's tab never came back. The conversation
|
|
2131
|
+
* stays saved, so opening it again starts its agent again. */
|
|
2132
|
+
const UNWATCHED_IDLE_MS = 6e5;
|
|
2133
|
+
/** How often idle threads nobody follows are looked for. */
|
|
2134
|
+
const SWEEP_MS = 6e4;
|
|
2125
2135
|
/** What an agent offers, from a session it starts and leaves without
|
|
2126
2136
|
* asking anything: with the model given, where it offers that model, since
|
|
2127
2137
|
* the efforts on offer depend on it. */
|
|
@@ -2176,20 +2186,26 @@ async function offeredBy(launch, agent, cwd, model) {
|
|
|
2176
2186
|
process.stop();
|
|
2177
2187
|
}
|
|
2178
2188
|
}
|
|
2179
|
-
/** The live Ask threads of one server; they end with it
|
|
2189
|
+
/** The live Ask threads of one server; they end with it, or once idle with
|
|
2190
|
+
* nothing following them. */
|
|
2180
2191
|
var AskThreads = class {
|
|
2181
2192
|
launch;
|
|
2182
2193
|
tools;
|
|
2183
2194
|
limits;
|
|
2195
|
+
unwatchedIdleMs;
|
|
2184
2196
|
threads = /* @__PURE__ */ new Map();
|
|
2197
|
+
/** When each thread was last seen idle with nothing following it. */
|
|
2198
|
+
unwatchedSince = /* @__PURE__ */ new Map();
|
|
2199
|
+
sweeper;
|
|
2185
2200
|
/** One question to each agent at a time about what it offers with a
|
|
2186
2201
|
* model. */
|
|
2187
2202
|
offers = /* @__PURE__ */ new Map();
|
|
2188
2203
|
mcpServers;
|
|
2189
|
-
constructor(launch, tools = {}, limits = askThreadLimits) {
|
|
2204
|
+
constructor(launch, tools = {}, limits = askThreadLimits, unwatchedIdleMs = UNWATCHED_IDLE_MS) {
|
|
2190
2205
|
this.launch = launch;
|
|
2191
2206
|
this.tools = tools;
|
|
2192
2207
|
this.limits = limits;
|
|
2208
|
+
this.unwatchedIdleMs = unwatchedIdleMs;
|
|
2193
2209
|
this.mcpServers = tools.mcpServers ?? (() => []);
|
|
2194
2210
|
}
|
|
2195
2211
|
/** How the agent's sessions reach Whiteboard's tools: the MCP tools they
|
|
@@ -2206,8 +2222,35 @@ var AskThreads = class {
|
|
|
2206
2222
|
const thread = new AskThread(this.launch, start, this.mcpServers, this.limits);
|
|
2207
2223
|
this.threads.set(thread.id, thread);
|
|
2208
2224
|
thread.open();
|
|
2225
|
+
this.sweeper ??= setInterval(() => this.sweep(), SWEEP_MS);
|
|
2226
|
+
this.sweeper.unref?.();
|
|
2209
2227
|
return thread;
|
|
2210
2228
|
}
|
|
2229
|
+
/** The agents answering now, in every review. */
|
|
2230
|
+
working() {
|
|
2231
|
+
return [...this.threads.values()].flatMap((thread) => {
|
|
2232
|
+
const { status, agentName } = thread.read();
|
|
2233
|
+
return status === "running" || status === "waiting" ? [{
|
|
2234
|
+
threadId: thread.id,
|
|
2235
|
+
reviewId: thread.reviewId,
|
|
2236
|
+
agentName
|
|
2237
|
+
}] : [];
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
/** Ends the threads left idle with nothing following them for long
|
|
2241
|
+
* enough. One answering or waiting on a decision runs on. */
|
|
2242
|
+
sweep(now = Date.now()) {
|
|
2243
|
+
for (const [id, thread] of this.threads) {
|
|
2244
|
+
const { status } = thread.read();
|
|
2245
|
+
if (thread.watched() || status === "starting" || status === "running" || status === "waiting") {
|
|
2246
|
+
this.unwatchedSince.delete(id);
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
const since = this.unwatchedSince.get(id) ?? now;
|
|
2250
|
+
this.unwatchedSince.set(id, since);
|
|
2251
|
+
if (now - since >= this.unwatchedIdleMs) this.close(id);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2211
2254
|
get(id) {
|
|
2212
2255
|
return this.threads.get(id);
|
|
2213
2256
|
}
|
|
@@ -2225,9 +2268,12 @@ var AskThreads = class {
|
|
|
2225
2268
|
close(id) {
|
|
2226
2269
|
this.threads.get(id)?.close();
|
|
2227
2270
|
this.threads.delete(id);
|
|
2271
|
+
this.unwatchedSince.delete(id);
|
|
2228
2272
|
}
|
|
2229
2273
|
closeAll() {
|
|
2230
2274
|
for (const id of [...this.threads.keys()]) this.close(id);
|
|
2275
|
+
clearInterval(this.sweeper);
|
|
2276
|
+
this.sweeper = void 0;
|
|
2231
2277
|
}
|
|
2232
2278
|
};
|
|
2233
2279
|
//#endregion
|
|
@@ -2323,45 +2369,82 @@ function resolveFileRefs(root, files, paths, touched = []) {
|
|
|
2323
2369
|
return resolved;
|
|
2324
2370
|
}
|
|
2325
2371
|
/**
|
|
2326
|
-
* NDJSON of
|
|
2327
|
-
*
|
|
2372
|
+
* NDJSON of several threads over one connection: for each, a snapshot, then
|
|
2373
|
+
* each change in `seq` order, then `ended` once it closes. A thread that
|
|
2374
|
+
* isn't running is `ended` at once. Answer tokens arrive as appends, so a
|
|
2375
|
+
* reply costs about what the agent sent.
|
|
2376
|
+
*
|
|
2377
|
+
* One stream for every open Ask of a review: a browser opens only a few
|
|
2378
|
+
* connections to a host, and each stream holds one for as long as it runs.
|
|
2328
2379
|
*/
|
|
2329
|
-
function
|
|
2380
|
+
function watchAskThreads(threads, backlog = 256) {
|
|
2330
2381
|
const encoder = new TextEncoder();
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2382
|
+
const queues = /* @__PURE__ */ new Map();
|
|
2383
|
+
/** Threads still running, which may send more. */
|
|
2384
|
+
const live = /* @__PURE__ */ new Set();
|
|
2385
|
+
const stops = [];
|
|
2335
2386
|
/** The stream ended, so nothing more may be enqueued. */
|
|
2336
2387
|
let finished = false;
|
|
2337
2388
|
const flush = (controller) => {
|
|
2338
|
-
|
|
2339
|
-
|
|
2389
|
+
let sent = true;
|
|
2390
|
+
while (sent && (controller.desiredSize ?? 0) > 0) {
|
|
2391
|
+
sent = false;
|
|
2392
|
+
for (const queue of queues.values()) {
|
|
2393
|
+
const line = queue.shift();
|
|
2394
|
+
if (!line) continue;
|
|
2395
|
+
controller.enqueue(encoder.encode(`${JSON.stringify(line)}\n`));
|
|
2396
|
+
sent = true;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
if (!live.size && [...queues.values()].every((queue) => !queue.length) && !finished) {
|
|
2340
2400
|
finished = true;
|
|
2341
2401
|
controller.close();
|
|
2342
2402
|
}
|
|
2343
2403
|
};
|
|
2344
2404
|
const body = new ReadableStream({
|
|
2345
2405
|
start(controller) {
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2406
|
+
for (const [threadId, thread] of threads) {
|
|
2407
|
+
const queue = [];
|
|
2408
|
+
queues.set(threadId, queue);
|
|
2409
|
+
if (!thread) {
|
|
2410
|
+
queue.push({
|
|
2411
|
+
threadId,
|
|
2412
|
+
ended: true
|
|
2413
|
+
});
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
live.add(threadId);
|
|
2417
|
+
queue.push({
|
|
2418
|
+
threadId,
|
|
2419
|
+
update: thread.snapshot()
|
|
2420
|
+
});
|
|
2421
|
+
const unsubscribe = thread.subscribe((update) => {
|
|
2422
|
+
queue.push({
|
|
2423
|
+
threadId,
|
|
2424
|
+
update
|
|
2425
|
+
});
|
|
2426
|
+
if (queue.length > backlog) queue.splice(0, queue.length, {
|
|
2427
|
+
threadId,
|
|
2428
|
+
update: thread.snapshot()
|
|
2429
|
+
});
|
|
2430
|
+
flush(controller);
|
|
2431
|
+
});
|
|
2432
|
+
const unclose = thread.onClose(() => {
|
|
2433
|
+
live.delete(threadId);
|
|
2434
|
+
unsubscribe();
|
|
2435
|
+
queue.push({
|
|
2436
|
+
threadId,
|
|
2437
|
+
ended: true
|
|
2438
|
+
});
|
|
2439
|
+
flush(controller);
|
|
2440
|
+
});
|
|
2441
|
+
stops.push(unsubscribe, unclose);
|
|
2442
|
+
}
|
|
2360
2443
|
flush(controller);
|
|
2361
2444
|
},
|
|
2362
2445
|
pull: flush,
|
|
2363
2446
|
cancel() {
|
|
2364
|
-
stop();
|
|
2447
|
+
for (const stop of stops) stop();
|
|
2365
2448
|
}
|
|
2366
2449
|
});
|
|
2367
2450
|
return new Response(body, { headers: {
|
|
@@ -3091,6 +3174,9 @@ const askStartSchema = z.strictObject({
|
|
|
3091
3174
|
bypass: z.boolean().optional()
|
|
3092
3175
|
});
|
|
3093
3176
|
const askPermitSchema = z.strictObject({ bypass: z.boolean() });
|
|
3177
|
+
/** The threads one watch follows: those of a review's open Asks. A body,
|
|
3178
|
+
* not the query, so there can be as many as are open. */
|
|
3179
|
+
const askWatchSchema = z.strictObject({ threads: z.array(z.string().min(1)) });
|
|
3094
3180
|
const askOpenSchema = z.strictObject({ picks: askPicksSchema.optional() });
|
|
3095
3181
|
const askChoiceSchema = z.strictObject({
|
|
3096
3182
|
kind: z.enum(askChoiceKinds),
|
|
@@ -3637,6 +3723,7 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3637
3723
|
await selectionContext(reviewId, selection, version)
|
|
3638
3724
|
].join("\n");
|
|
3639
3725
|
};
|
|
3726
|
+
app.get("/ask/working", (context) => context.json({ agents: ask.threads.working() }));
|
|
3640
3727
|
app.get("/:id/ask/agents", async (context) => context.json({ agents: await ask.agents() }));
|
|
3641
3728
|
app.get("/:id/ask/agents/:agent/offer", async (context) => {
|
|
3642
3729
|
const agent = z.enum(askAgentIds).parse(context.req.param("agent"));
|
|
@@ -3710,6 +3797,15 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3710
3797
|
readReview(reviewId);
|
|
3711
3798
|
return context.json({ threads: store.askHistory.list(reviewId) });
|
|
3712
3799
|
});
|
|
3800
|
+
app.post("/:id/ask/watch", async (context) => {
|
|
3801
|
+
const reviewId = context.req.param("id");
|
|
3802
|
+
readReview(reviewId);
|
|
3803
|
+
const { threads } = askWatchSchema.parse(await readBoundedRequestJson(context.req.raw));
|
|
3804
|
+
return watchAskThreads(new Map(threads.map((threadId) => {
|
|
3805
|
+
const thread = ask.threads.get(threadId);
|
|
3806
|
+
return [threadId, thread?.reviewId === reviewId ? thread : void 0];
|
|
3807
|
+
})));
|
|
3808
|
+
});
|
|
3713
3809
|
app.get("/:id/ask/:threadId", (context) => {
|
|
3714
3810
|
const record = store.askHistory.get(context.req.param("threadId"));
|
|
3715
3811
|
if (record?.reviewId !== context.req.param("id")) throw new ReviewInputError("This conversation was not found.", 404);
|
|
@@ -3767,9 +3863,6 @@ function createReviewApi(store, data, open, shared, capabilities = () => ({
|
|
|
3767
3863
|
});
|
|
3768
3864
|
return context.json({ threadId: record.id });
|
|
3769
3865
|
});
|
|
3770
|
-
app.get("/:id/ask/:threadId/watch", (context) => {
|
|
3771
|
-
return watchAskThread(readThread(context.req.param("id"), context.req.param("threadId")));
|
|
3772
|
-
});
|
|
3773
3866
|
app.post("/:id/ask/:threadId/prompt", async (context) => {
|
|
3774
3867
|
const thread = readThread(context.req.param("id"), context.req.param("threadId"));
|
|
3775
3868
|
const { question } = askFollowUpSchema.parse(await readBoundedRequestJson(context.req.raw, ASK_REQUEST_MAX_BYTES));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-
|
|
1
|
+
import { o as initializeReviewStoreSchema, t as openLocalReviewStore } from "./local-data-D-EoJfPx.js";
|
|
2
2
|
import { O as withFileLock } from "./s3-config-CAxhO9u_.js";
|
|
3
3
|
import "./review-home-paths-6zZH-1c9.js";
|
|
4
4
|
import { access, mkdir } from "node:fs/promises";
|
|
@@ -9,12 +9,12 @@ import { A as devReviewHome, P as writePrivateJsonAtomic } from "../s3-config-CA
|
|
|
9
9
|
import { U as shellQuote, i as reviewInstanceDiscoveryPath, o as reviewLegacyDiscoveryPath } from "../review-home-paths-6zZH-1c9.js";
|
|
10
10
|
import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "../package-paths-B6-zxvIO.js";
|
|
11
11
|
import { c as reviewInstanceIdentity, t as REVIEW_INSTANCE_ENV } from "../desktop-discovery-DK2HoBy5.js";
|
|
12
|
-
import { t as openReviewProfile } from "../profile-
|
|
12
|
+
import { t as openReviewProfile } from "../profile-WzZu7Spt.js";
|
|
13
13
|
import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-Pp4ZsVrh.js";
|
|
14
14
|
import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-DlHTFCrQ.js";
|
|
15
15
|
import { c as removeLegacyReviewSkills, d as skipCliInstall, i as finishCliInstallUpdate, l as resetCliInstall, n as applyCliInstall, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-DLE9XZhi.js";
|
|
16
|
-
import { _ as setDiffrConfigValue, a as serverJson, c as recordClientError, d as HttpJsonError, f as ReviewServerError, g as saveDiffrSummarizer, h as readDiffrConfig, i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, o as captureSanitizedUiTelemetry, p as GlobalReviewDesktopVerbRelay, r as serverCrashReportPath, s as clientErrorsForSession, t as drainServerCrashReport, u as readBoundedRequestJson, v as testDiffrSummarizer } from "../process-error-telemetry-
|
|
17
|
-
import { c as readQuerySchemas } from "../authoring-tools-
|
|
16
|
+
import { _ as setDiffrConfigValue, a as serverJson, c as recordClientError, d as HttpJsonError, f as ReviewServerError, g as saveDiffrSummarizer, h as readDiffrConfig, i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, o as captureSanitizedUiTelemetry, p as GlobalReviewDesktopVerbRelay, r as serverCrashReportPath, s as clientErrorsForSession, t as drainServerCrashReport, u as readBoundedRequestJson, v as testDiffrSummarizer } from "../process-error-telemetry-mzRG988B.js";
|
|
17
|
+
import { c as readQuerySchemas } from "../authoring-tools-F_q8cC7J.js";
|
|
18
18
|
import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import fs, { existsSync } from "node:fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev.fast/whiteboard",
|
|
3
|
-
"version": "0.2.1-preview.20261007.
|
|
3
|
+
"version": "0.2.1-preview.20261007.106",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@agentclientprotocol/claude-agent-acp": "0.84.0",
|
|
44
44
|
"@agentclientprotocol/codex-acp": "2.0.0",
|
|
45
45
|
"@agentclientprotocol/sdk": "1.5.1",
|
|
46
|
-
"@dev.fast/diffr": "0.1.
|
|
46
|
+
"@dev.fast/diffr": "0.1.15",
|
|
47
47
|
"@hono/node-server": "2.0.12",
|
|
48
48
|
"@img/sharp-wasm32": "0.35.5",
|
|
49
49
|
"@modelcontextprotocol/sdk": "1.31.0",
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"typescript": "6.0.3",
|
|
73
73
|
"vitest": "4.1.11",
|
|
74
74
|
"@dev.fast/json": "0.0.0",
|
|
75
|
-
"@dev.fast/review-
|
|
76
|
-
"@dev.fast/trace-core": "0.0.0",
|
|
75
|
+
"@dev.fast/review-protocol": "0.0.0",
|
|
77
76
|
"@dev.fast/trace-protocol": "0.5.0",
|
|
78
|
-
"@dev.fast/
|
|
77
|
+
"@dev.fast/trace-core": "0.0.0",
|
|
78
|
+
"@dev.fast/review-share-protocol": "0.1.2"
|
|
79
79
|
},
|
|
80
80
|
"engines": {
|
|
81
81
|
"node": ">=24 <25"
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"repo": "devdotfast/review",
|
|
86
86
|
"docs": "packages/review/instructions/authoring.md"
|
|
87
87
|
},
|
|
88
|
-
"gitHead": "
|
|
88
|
+
"gitHead": "e4eca7ac75d6e6ed7dc027eecca4883eb67579f6",
|
|
89
89
|
"scripts": {
|
|
90
90
|
"prebuild": "pnpm run build:workspace-deps",
|
|
91
91
|
"build": "node --max-old-space-size=8192 node_modules/tsdown/dist/run.mjs",
|
package/src/ask/thread-state.ts
CHANGED
|
@@ -255,6 +255,15 @@ export const askUpdateSchema = z.union([
|
|
|
255
255
|
|
|
256
256
|
export type AskUpdate = z.infer<typeof askUpdateSchema>;
|
|
257
257
|
|
|
258
|
+
/** One line of a watch of several threads: a thread's update, or that it
|
|
259
|
+
* has ended. */
|
|
260
|
+
export const askWatchLineSchema = z.union([
|
|
261
|
+
z.object({ threadId: z.string(), update: askUpdateSchema }),
|
|
262
|
+
z.object({ threadId: z.string(), ended: z.literal(true) }),
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
export type AskWatchLine = z.infer<typeof askWatchLineSchema>;
|
|
266
|
+
|
|
258
267
|
/** The server and the panel both advance a thread with this, so they agree. */
|
|
259
268
|
export function applyAskChange(
|
|
260
269
|
state: AskThreadState,
|
package/src/ask/thread.ts
CHANGED
|
@@ -250,6 +250,11 @@ export class AskThread {
|
|
|
250
250
|
return () => this.listeners.delete(listener);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/** Whether anything follows the thread, as an open panel does. */
|
|
254
|
+
watched() {
|
|
255
|
+
return this.listeners.size > 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
253
258
|
/** Starts the agent and asks the first question, or loads an earlier
|
|
254
259
|
* conversation; failures land in the state. */
|
|
255
260
|
async open() {
|
package/src/ask/threads.ts
CHANGED
|
@@ -26,6 +26,14 @@ const OFFER_TIMEOUT_MS = 30_000;
|
|
|
26
26
|
* commands, which it sends on its own rather than in the response. */
|
|
27
27
|
const COMMANDS_WAIT_MS = 2_000;
|
|
28
28
|
|
|
29
|
+
/** How long a thread nobody follows may sit idle before it ends: its panel
|
|
30
|
+
* went with a reload, or its review's tab never came back. The conversation
|
|
31
|
+
* stays saved, so opening it again starts its agent again. */
|
|
32
|
+
const UNWATCHED_IDLE_MS = 10 * 60_000;
|
|
33
|
+
|
|
34
|
+
/** How often idle threads nobody follows are looked for. */
|
|
35
|
+
const SWEEP_MS = 60_000;
|
|
36
|
+
|
|
29
37
|
/** What an agent offers, from a session it starts and leaves without
|
|
30
38
|
* asking anything: with the model given, where it offers that model, since
|
|
31
39
|
* the efforts on offer depend on it. */
|
|
@@ -144,9 +152,20 @@ export type AskToolsReach =
|
|
|
144
152
|
| { kind: "cli"; command: string }
|
|
145
153
|
| undefined;
|
|
146
154
|
|
|
147
|
-
/**
|
|
155
|
+
/** An agent answering in Ask, which ending the server would stop. */
|
|
156
|
+
export interface AskWorkingAgent {
|
|
157
|
+
threadId: string;
|
|
158
|
+
reviewId: string;
|
|
159
|
+
agentName: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The live Ask threads of one server; they end with it, or once idle with
|
|
163
|
+
* nothing following them. */
|
|
148
164
|
export class AskThreads {
|
|
149
165
|
private readonly threads = new Map<string, AskThread>();
|
|
166
|
+
/** When each thread was last seen idle with nothing following it. */
|
|
167
|
+
private readonly unwatchedSince = new Map<string, number>();
|
|
168
|
+
private sweeper?: ReturnType<typeof setInterval>;
|
|
150
169
|
/** One question to each agent at a time about what it offers with a
|
|
151
170
|
* model. */
|
|
152
171
|
private readonly offers = new Map<string, Promise<AskOffer>>();
|
|
@@ -157,6 +176,7 @@ export class AskThreads {
|
|
|
157
176
|
private readonly launch: AskAgentLauncher,
|
|
158
177
|
private readonly tools: AskTools = {},
|
|
159
178
|
private readonly limits: AskThreadLimits = askThreadLimits,
|
|
179
|
+
private readonly unwatchedIdleMs = UNWATCHED_IDLE_MS,
|
|
160
180
|
) {
|
|
161
181
|
this.mcpServers = tools.mcpServers ?? (() => []);
|
|
162
182
|
}
|
|
@@ -184,10 +204,47 @@ export class AskThreads {
|
|
|
184
204
|
|
|
185
205
|
this.threads.set(thread.id, thread);
|
|
186
206
|
void thread.open();
|
|
207
|
+
this.sweeper ??= setInterval(() => this.sweep(), SWEEP_MS);
|
|
208
|
+
this.sweeper.unref?.();
|
|
187
209
|
|
|
188
210
|
return thread;
|
|
189
211
|
}
|
|
190
212
|
|
|
213
|
+
/** The agents answering now, in every review. */
|
|
214
|
+
working(): AskWorkingAgent[] {
|
|
215
|
+
return [...this.threads.values()].flatMap((thread) => {
|
|
216
|
+
const { status, agentName } = thread.read();
|
|
217
|
+
|
|
218
|
+
return status === "running" || status === "waiting"
|
|
219
|
+
? [{ threadId: thread.id, reviewId: thread.reviewId, agentName }]
|
|
220
|
+
: [];
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Ends the threads left idle with nothing following them for long
|
|
225
|
+
* enough. One answering or waiting on a decision runs on. */
|
|
226
|
+
sweep(now = Date.now()) {
|
|
227
|
+
for (const [id, thread] of this.threads) {
|
|
228
|
+
const { status } = thread.read();
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
thread.watched() ||
|
|
232
|
+
status === "starting" ||
|
|
233
|
+
status === "running" ||
|
|
234
|
+
status === "waiting"
|
|
235
|
+
) {
|
|
236
|
+
this.unwatchedSince.delete(id);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const since = this.unwatchedSince.get(id) ?? now;
|
|
241
|
+
|
|
242
|
+
this.unwatchedSince.set(id, since);
|
|
243
|
+
|
|
244
|
+
if (now - since >= this.unwatchedIdleMs) this.close(id);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
191
248
|
get(id: string) {
|
|
192
249
|
return this.threads.get(id);
|
|
193
250
|
}
|
|
@@ -211,9 +268,12 @@ export class AskThreads {
|
|
|
211
268
|
close(id: string) {
|
|
212
269
|
this.threads.get(id)?.close();
|
|
213
270
|
this.threads.delete(id);
|
|
271
|
+
this.unwatchedSince.delete(id);
|
|
214
272
|
}
|
|
215
273
|
|
|
216
274
|
closeAll() {
|
|
217
275
|
for (const id of [...this.threads.keys()]) this.close(id);
|
|
276
|
+
clearInterval(this.sweeper);
|
|
277
|
+
this.sweeper = undefined;
|
|
218
278
|
}
|
|
219
279
|
}
|
package/src/ask/watch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AskUpdate } from "@review/ask/thread-state.js";
|
|
1
|
+
import type { AskUpdate, AskWatchLine } from "@review/ask/thread-state.js";
|
|
2
2
|
|
|
3
3
|
/** What a watch stream needs from a thread. */
|
|
4
4
|
export interface AskWatchable {
|
|
@@ -7,35 +7,55 @@ export interface AskWatchable {
|
|
|
7
7
|
onClose(closed: () => void): () => void;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
/** Changes a slow reader may fall behind by
|
|
11
|
-
* with one snapshot, which bounds the memory a
|
|
10
|
+
/** Changes a slow reader may fall behind by on one thread before that
|
|
11
|
+
* thread's backlog is replaced with one snapshot, which bounds the memory a
|
|
12
|
+
* stalled panel holds. */
|
|
12
13
|
export const ASK_WATCH_BACKLOG = 256;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* NDJSON of
|
|
16
|
-
*
|
|
16
|
+
* NDJSON of several threads over one connection: for each, a snapshot, then
|
|
17
|
+
* each change in `seq` order, then `ended` once it closes. A thread that
|
|
18
|
+
* isn't running is `ended` at once. Answer tokens arrive as appends, so a
|
|
19
|
+
* reply costs about what the agent sent.
|
|
20
|
+
*
|
|
21
|
+
* One stream for every open Ask of a review: a browser opens only a few
|
|
22
|
+
* connections to a host, and each stream holds one for as long as it runs.
|
|
17
23
|
*/
|
|
18
|
-
export function
|
|
19
|
-
|
|
24
|
+
export function watchAskThreads(
|
|
25
|
+
threads: ReadonlyMap<string, AskWatchable | undefined>,
|
|
20
26
|
backlog = ASK_WATCH_BACKLOG,
|
|
21
27
|
): Response {
|
|
22
28
|
const encoder = new TextEncoder();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let ended = false;
|
|
29
|
+
const queues = new Map<string, AskWatchLine[]>();
|
|
30
|
+
/** Threads still running, which may send more. */
|
|
31
|
+
const live = new Set<string>();
|
|
32
|
+
const stops: (() => void)[] = [];
|
|
28
33
|
/** The stream ended, so nothing more may be enqueued. */
|
|
29
34
|
let finished = false;
|
|
30
35
|
|
|
36
|
+
// A line from each thread in turn, so one busy thread can't hold back
|
|
37
|
+
// the others.
|
|
31
38
|
const flush = (controller: ReadableStreamDefaultController<Uint8Array>) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
let sent = true;
|
|
40
|
+
|
|
41
|
+
while (sent && (controller.desiredSize ?? 0) > 0) {
|
|
42
|
+
sent = false;
|
|
43
|
+
|
|
44
|
+
for (const queue of queues.values()) {
|
|
45
|
+
const line = queue.shift();
|
|
46
|
+
|
|
47
|
+
if (!line) continue;
|
|
48
|
+
controller.enqueue(encoder.encode(`${JSON.stringify(line)}\n`));
|
|
49
|
+
sent = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
36
52
|
|
|
37
|
-
//
|
|
38
|
-
if (
|
|
53
|
+
// Every thread has closed and said so; the stream ends.
|
|
54
|
+
if (
|
|
55
|
+
!live.size &&
|
|
56
|
+
[...queues.values()].every((queue) => !queue.length) &&
|
|
57
|
+
!finished
|
|
58
|
+
) {
|
|
39
59
|
finished = true;
|
|
40
60
|
controller.close();
|
|
41
61
|
}
|
|
@@ -43,30 +63,46 @@ export function watchAskThread(
|
|
|
43
63
|
|
|
44
64
|
const body = new ReadableStream<Uint8Array>({
|
|
45
65
|
start(controller) {
|
|
46
|
-
const
|
|
47
|
-
|
|
66
|
+
for (const [threadId, thread] of threads) {
|
|
67
|
+
const queue: AskWatchLine[] = [];
|
|
68
|
+
|
|
69
|
+
queues.set(threadId, queue);
|
|
70
|
+
|
|
71
|
+
if (!thread) {
|
|
72
|
+
queue.push({ threadId, ended: true });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
live.add(threadId);
|
|
77
|
+
queue.push({ threadId, update: thread.snapshot() });
|
|
78
|
+
|
|
79
|
+
const unsubscribe = thread.subscribe((update) => {
|
|
80
|
+
queue.push({ threadId, update });
|
|
48
81
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
// The snapshot already holds every change it would replay.
|
|
83
|
+
if (queue.length > backlog)
|
|
84
|
+
queue.splice(0, queue.length, {
|
|
85
|
+
threadId,
|
|
86
|
+
update: thread.snapshot(),
|
|
87
|
+
});
|
|
88
|
+
flush(controller);
|
|
89
|
+
});
|
|
53
90
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
const unclose = thread.onClose(() => {
|
|
92
|
+
live.delete(threadId);
|
|
93
|
+
unsubscribe();
|
|
94
|
+
queue.push({ threadId, ended: true });
|
|
95
|
+
flush(controller);
|
|
96
|
+
});
|
|
59
97
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
unclose();
|
|
63
|
-
};
|
|
98
|
+
stops.push(unsubscribe, unclose);
|
|
99
|
+
}
|
|
64
100
|
|
|
65
101
|
flush(controller);
|
|
66
102
|
},
|
|
67
103
|
pull: flush,
|
|
68
104
|
cancel() {
|
|
69
|
-
stop();
|
|
105
|
+
for (const stop of stops) stop();
|
|
70
106
|
},
|
|
71
107
|
});
|
|
72
108
|
|
package/src/review-api/http.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
askQuestionSchema,
|
|
25
25
|
} from "@review/ask/thread-state.js";
|
|
26
26
|
import type { AskThreads } from "@review/ask/threads.js";
|
|
27
|
-
import {
|
|
27
|
+
import { watchAskThreads } from "@review/ask/watch.js";
|
|
28
28
|
import { fuzzyRank } from "@review/fuzzy-match.js";
|
|
29
29
|
import { resolveReviewStackLayers } from "@review/review-stack.js";
|
|
30
30
|
import { readBoundedRequestJson } from "@review/server/hono-http.js";
|
|
@@ -91,6 +91,10 @@ const askStartSchema = z.strictObject({
|
|
|
91
91
|
|
|
92
92
|
const askPermitSchema = z.strictObject({ bypass: z.boolean() });
|
|
93
93
|
|
|
94
|
+
/** The threads one watch follows: those of a review's open Asks. A body,
|
|
95
|
+
* not the query, so there can be as many as are open. */
|
|
96
|
+
const askWatchSchema = z.strictObject({ threads: z.array(z.string().min(1)) });
|
|
97
|
+
|
|
94
98
|
const askOpenSchema = z.strictObject({ picks: askPicksSchema.optional() });
|
|
95
99
|
|
|
96
100
|
const askChoiceSchema = z.strictObject({
|
|
@@ -1171,6 +1175,11 @@ export function createReviewApi(
|
|
|
1171
1175
|
].join("\n");
|
|
1172
1176
|
};
|
|
1173
1177
|
|
|
1178
|
+
// The agents answering in any review, which quitting the app would stop.
|
|
1179
|
+
app.get("/ask/working", (context) =>
|
|
1180
|
+
context.json({ agents: ask.threads.working() }),
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1174
1183
|
// Each agent with the models and efforts it offered last; none until
|
|
1175
1184
|
// it has run.
|
|
1176
1185
|
app.get("/:id/ask/agents", async (context) =>
|
|
@@ -1310,6 +1319,31 @@ export function createReviewApi(
|
|
|
1310
1319
|
return context.json({ threads: store.askHistory.list(reviewId) });
|
|
1311
1320
|
});
|
|
1312
1321
|
|
|
1322
|
+
// The threads of every open Ask in a review, over one connection. Before
|
|
1323
|
+
// the route for one thread, which would read `watch` as its id.
|
|
1324
|
+
app.post("/:id/ask/watch", async (context) => {
|
|
1325
|
+
const reviewId = context.req.param("id");
|
|
1326
|
+
|
|
1327
|
+
readReview(reviewId);
|
|
1328
|
+
|
|
1329
|
+
const { threads } = askWatchSchema.parse(
|
|
1330
|
+
await readBoundedRequestJson(context.req.raw),
|
|
1331
|
+
);
|
|
1332
|
+
|
|
1333
|
+
return watchAskThreads(
|
|
1334
|
+
new Map(
|
|
1335
|
+
threads.map((threadId) => {
|
|
1336
|
+
const thread = ask.threads.get(threadId);
|
|
1337
|
+
|
|
1338
|
+
return [
|
|
1339
|
+
threadId,
|
|
1340
|
+
thread?.reviewId === reviewId ? thread : undefined,
|
|
1341
|
+
];
|
|
1342
|
+
}),
|
|
1343
|
+
),
|
|
1344
|
+
);
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1313
1347
|
// A saved conversation, without starting its agent.
|
|
1314
1348
|
app.get("/:id/ask/:threadId", (context) => {
|
|
1315
1349
|
const record = store.askHistory.get(context.req.param("threadId"));
|
|
@@ -1390,15 +1424,6 @@ export function createReviewApi(
|
|
|
1390
1424
|
return context.json({ threadId: record.id });
|
|
1391
1425
|
});
|
|
1392
1426
|
|
|
1393
|
-
app.get("/:id/ask/:threadId/watch", (context) => {
|
|
1394
|
-
const thread = readThread(
|
|
1395
|
-
context.req.param("id"),
|
|
1396
|
-
context.req.param("threadId"),
|
|
1397
|
-
);
|
|
1398
|
-
|
|
1399
|
-
return watchAskThread(thread);
|
|
1400
|
-
});
|
|
1401
|
-
|
|
1402
1427
|
app.post("/:id/ask/:threadId/prompt", async (context) => {
|
|
1403
1428
|
const thread = readThread(
|
|
1404
1429
|
context.req.param("id"),
|
|
@@ -1547,7 +1572,8 @@ export function createReviewApi(
|
|
|
1547
1572
|
return context.json({ ok: true });
|
|
1548
1573
|
});
|
|
1549
1574
|
|
|
1550
|
-
// Closing
|
|
1575
|
+
// Closing an Ask ends its agent; the conversation stays saved. A tab
|
|
1576
|
+
// switched away from or a reload leaves it running, to come back to.
|
|
1551
1577
|
app.post("/:id/ask/:threadId/close", (context) => {
|
|
1552
1578
|
readThread(context.req.param("id"), context.req.param("threadId"));
|
|
1553
1579
|
ask.threads.close(context.req.param("threadId"));
|
package/dist/profile-BKncMvSE.js
DELETED