@dev.fast/whiteboard 0.2.4 → 0.2.5-preview.20261013.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-alias-_1_7vROx.js → account-alias-D0hF0sKk.js} +2 -3
- package/dist/{agent-cli-BrFqhnzM.js → agent-cli-BBPxwUOd.js} +6 -6
- package/dist/agent-cli-YSuhE5X8.js +2 -0
- package/dist/{agent-client-B_b1GtUc.js → agent-client-P3J4y8Zn.js} +3 -3
- package/dist/{authoring-tools-DvIO906l.js → authoring-tools-D8WXsm_g.js} +11 -10
- package/dist/build-info.json +1 -1
- package/dist/{cli-gRGxkwc0.js → cli-C0ziP6hg.js} +1 -1
- package/dist/{cli-runner-Cjl8h376.js → cli-runner-C1gY-HAI.js} +16 -17
- package/dist/cli.js +1 -1
- package/dist/{client-BFClfQLO.js → client-DzcSt6pV.js} +63 -13
- package/dist/{desktop-discovery-C-Qz54wO.js → desktop-discovery-Ba3dD5Cn.js} +1 -1
- package/dist/{headless-host-DcWaxx9P.js → headless-host-BHD_Vg5F.js} +4 -4
- package/dist/{local-data-Bm84Xdck.js → local-data-BlERtVm2.js} +281 -75
- package/dist/{mcp-BDy9Aybd.js → mcp-t7VN9R3k.js} +4 -4
- package/dist/{package-paths-B6-zxvIO.js → package-paths-B9PDoAl6.js} +1 -1
- package/dist/{process-error-telemetry-Dzh5a3s_.js → process-error-telemetry-C0swByfp.js} +158 -57
- package/dist/{profile-RDEioF8b.js → profile-BsYDBr7t.js} +2 -2
- package/dist/profile-j97aRTS7.js +2 -0
- package/dist/{review-agent-traces-DNJFYzc8.js → review-home-paths-Y9mYWkCI.js} +2600 -62
- package/dist/{review-telemetry-DBy3eVCo.js → review-telemetry-CmM6tCbj.js} +2 -2
- package/dist/server/desktop-host.js +35 -46
- package/dist/{server-discovery-Dbzn3w6Z.js → server-discovery-ZVi8Ay8J.js} +1 -1
- package/dist/sharing/index.d.ts +37 -49
- package/dist/sharing/index.js +1 -1
- package/dist/{src-CmdiBL20.js → src-DiHP11l3.js} +11 -1
- package/dist/{stored-document-migration-BxZWQQjv.js → stored-document-migration-u4L_1VZa.js} +15 -36
- package/dist/{tool-failure-DJmTieh5.js → tool-failure-CJHBCHMh.js} +2 -2
- package/instructions/scratchpad.md +1 -1
- package/package.json +4 -3
- package/src/ask/agents.ts +6 -0
- package/src/ask/thread-state.ts +17 -1
- package/src/ask/thread.ts +116 -22
- package/src/ask/threads.ts +8 -2
- package/src/error-telemetry.ts +13 -9
- package/src/fixtures/blocks/tutorial.json +3 -40
- package/src/review-api/README.md +10 -7
- package/src/review-api/ask-history.ts +37 -0
- package/src/review-api/authoring-tools.ts +8 -5
- package/src/review-api/blocks/database_lens.ts +5 -0
- package/src/review-api/blocks/tutorial.ts +2 -18
- package/src/review-api/comparison-coverage.ts +16 -9
- package/src/review-api/component-reference.ts +1 -1
- package/src/review-api/document.ts +24 -10
- package/src/review-api/http.ts +267 -189
- package/src/review-api/local-data.ts +126 -37
- package/src/review-api/store-schema.ts +25 -1
- package/src/review-api/store.ts +165 -33
- package/src/review-api/workspaces.ts +14 -14
- package/src/review-api/worktree-events.ts +46 -0
- package/src/review-app.ts +1 -1
- package/src/review-info.ts +1 -1
- package/src/review-stack.ts +2 -2
- package/src/server/review-server-core.ts +25 -1
- package/src/server/tutorial-service.ts +22 -46
- package/src/sharing/export.ts +1 -0
- package/src/sharing/import.ts +111 -26
- package/tutorial/document.json +27 -86
- package/tutorial/lenses.json +10 -0
- package/tutorial/runtime-manifest.json +2 -3
- package/tutorial/sample-service/src/database/drizzle-orm.d.ts +16 -0
- package/tutorial/sample-service/src/database/schema.ts +9 -10
- package/tutorial/sample-service/src/inventory/forklift-mood.ts +4 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +18 -1
- package/tutorial/sample-service/src/inventory/warehouse-lore.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +3 -2
- package/tutorial/sample-service/src/orders/order.ts +27 -5
- package/tutorial/sample-service/src/payments/fraud-vibes.ts +4 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +19 -2
- package/tutorial/sample-service/src/payments/raccoon-ledger.ts +9 -0
- package/dist/agent-cli-BR2j-2n5.js +0 -2
- package/dist/input-error-OLgB_h31.js +0 -11
- package/dist/profile-BQGaX8aX.js +0 -2
- package/dist/review-home-paths-6zZH-1c9.js +0 -2560
- package/dist/tutorial-trace-DrBvkaeL.js +0 -43
- package/src/tutorial-conversation.ts +0 -18
- package/tutorial/software-map.json +0 -139
- package/tutorial/trace.json +0 -20
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { _ as readFileAtCommit, b as resolveRevision, c as diffTrees, f as gitCommonDir, g as parseGitRemote, h as listTrackedFilesAtCommit, i as diff, l as diffWorkingTree, m as listCommitRange, n as createBlobBatchReader, o as diffFileSummariesTrees, p as jjRevisionIsConflicted, r as detectLocalVcs, s as diffFileSummariesWorkingTree, t as LocalVcsToolsMissingError, u as git, v as resolveRepoContext, y as resolveRepoContextSync } from "./src-
|
|
1
|
+
import { C as ReviewInputError, _ as readFileAtCommit, b as resolveRevision, c as diffTrees, f as gitCommonDir, g as parseGitRemote, h as listTrackedFilesAtCommit, i as diff, l as diffWorkingTree, m as listCommitRange, n as createBlobBatchReader, o as diffFileSummariesTrees, p as jjRevisionIsConflicted, r as detectLocalVcs, s as diffFileSummariesWorkingTree, t as LocalVcsToolsMissingError, u as git, v as resolveRepoContext, y as resolveRepoContextSync } from "./src-DiHP11l3.js";
|
|
2
2
|
import { c as jsonString, o as jsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
|
|
3
3
|
import { _ as SCRATCHPAD_REVIEW_ID, b as structuralChangeCounts, g as ReviewSelectedDiffSchema, m as ReviewApiSelectionSourceSchema, y as decodeStructuralDiffEvent } from "./src-WCbabvdS.js";
|
|
4
|
-
import {
|
|
5
|
-
import { t as
|
|
4
|
+
import { s as safeStorageSegment } from "./review-home-paths-Y9mYWkCI.js";
|
|
5
|
+
import { $ as unionIntervals, A as applyLensEdit, C as resourceReferences, D as summarizeEdit, E as sourceReferences, F as checkSourcePath, I as fileLineRangeSchema, K as sourceAnchors, L as requireVisibleSource, N as lensSelections, O as checkReferences, P as SourceRangeError, Q as subtractIntervals, R as sliceSourceRange, S as resourceReference, X as intersectIntervals, Y as emptyCoverage, a as defineSoftwareMap, b as pinsSchema, c as traceSchema, d as applyEdit, et as updateCoverage, f as assignFreshIds, g as explicitPins, h as elements, i as SoftwareModelValidationError, j as lensEditSchema, l as textIncludesQuote, m as editSchema, n as migrateStoredDocument, p as documentSchema, q as coverageSchema, r as migrateStoredLenses, t as liftFileLenses, u as anchorPins, v as isUnit, w as reviewTargetSchema } from "./stored-document-migration-u4L_1VZa.js";
|
|
6
6
|
import { D as processIsAlive, O as withFileLock, P as writePrivateJsonAtomic } from "./s3-config-CAxhO9u_.js";
|
|
7
7
|
import { t as errorMessage } from "./error-message-OtiDonty.js";
|
|
8
|
-
import { s as safeStorageSegment } from "./review-home-paths-6zZH-1c9.js";
|
|
9
8
|
import { r as isMissingFileError } from "./fs-utils-BMPLt0cr.js";
|
|
10
9
|
import { createRequire } from "node:module";
|
|
11
10
|
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
12
11
|
import { z } from "zod";
|
|
13
|
-
import { existsSync, realpathSync, rmSync,
|
|
12
|
+
import { existsSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
14
13
|
import * as fs$1 from "node:fs/promises";
|
|
15
14
|
import { lstat, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
16
15
|
import path, { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
@@ -1015,11 +1014,12 @@ async function comparisonCoverage(data, reviewId, pins, mode, signal, publish) {
|
|
|
1015
1014
|
counted: true
|
|
1016
1015
|
};
|
|
1017
1016
|
if (mode === "textual") return coverage;
|
|
1018
|
-
const
|
|
1017
|
+
const unsupported = /* @__PURE__ */ new Set();
|
|
1018
|
+
const current = () => ({
|
|
1019
1019
|
...coverage,
|
|
1020
|
-
files:
|
|
1020
|
+
files: coverage.files.filter((file) => !unsupported.has(file.path))
|
|
1021
1021
|
});
|
|
1022
|
-
|
|
1022
|
+
publish?.(current());
|
|
1023
1023
|
const baseFiles = new Map(coverage.files.map((file, index) => [file.previousPath ?? file.path, index]));
|
|
1024
1024
|
const headFiles = new Map(coverage.files.map((file, index) => [file.path, index]));
|
|
1025
1025
|
const remaining = /* @__PURE__ */ new Set();
|
|
@@ -1037,8 +1037,11 @@ async function comparisonCoverage(data, reviewId, pins, mode, signal, publish) {
|
|
|
1037
1037
|
if (event.type !== "file") continue;
|
|
1038
1038
|
const path = (event.file.rhs ?? event.file.lhs).path;
|
|
1039
1039
|
if (!remaining.delete(path)) throw new Error(`Unexpected structural result: ${path}`);
|
|
1040
|
+
const baseIndex = event.file.lhs && baseFiles.get(event.file.lhs.path);
|
|
1041
|
+
const headIndex = event.file.rhs && headFiles.get(event.file.rhs.path);
|
|
1040
1042
|
if (event.error) {
|
|
1041
1043
|
if (event.error.code !== "unsupported_file_type" && event.error.code !== "not_utf8") throw new Error(`Cannot count ${path}: ${event.error.message}`);
|
|
1044
|
+
for (const index of [baseIndex, headIndex]) if (index !== void 0) unsupported.add(coverage.files[index].path);
|
|
1042
1045
|
} else if (event.diff.type === "text") {
|
|
1043
1046
|
const diff = event.diff;
|
|
1044
1047
|
const folded = foldedChanges(diff);
|
|
@@ -1056,17 +1059,16 @@ async function comparisonCoverage(data, reviewId, pins, mode, signal, publish) {
|
|
|
1056
1059
|
}
|
|
1057
1060
|
};
|
|
1058
1061
|
}
|
|
1059
|
-
const
|
|
1060
|
-
const index = (event.file.rhs && headFiles.get(event.file.rhs.path)) ?? baseIndex;
|
|
1062
|
+
const index = headIndex ?? baseIndex;
|
|
1061
1063
|
if (index !== void 0) {
|
|
1062
1064
|
if ((coverage.fileSources.get(coverage.files[index].path) ?? []).every((source) => source.file === (source.side === "base" ? event.file.lhs : event.file.rhs)?.path)) coverage.alignments = new Map(coverage.alignments).set(coverage.files[index].path, structuralRows(diff));
|
|
1063
1065
|
}
|
|
1064
|
-
publishCurrent();
|
|
1065
1066
|
}
|
|
1067
|
+
publish?.(current());
|
|
1066
1068
|
if (!remaining.size) break;
|
|
1067
1069
|
}
|
|
1068
1070
|
if (remaining.size) throw new Error("Structural coverage is incomplete.");
|
|
1069
|
-
return
|
|
1071
|
+
return current();
|
|
1070
1072
|
}
|
|
1071
1073
|
/**
|
|
1072
1074
|
* The changed lines diffr folds by default. A file it hides (lockfiles,
|
|
@@ -1831,6 +1833,11 @@ const askUsageSchema = z.object({
|
|
|
1831
1833
|
currency: z.string()
|
|
1832
1834
|
}).optional()
|
|
1833
1835
|
});
|
|
1836
|
+
/** A follow-up waiting for the turn under way to end. */
|
|
1837
|
+
const askQueuedSchema = z.object({
|
|
1838
|
+
id: z.string(),
|
|
1839
|
+
text: z.string()
|
|
1840
|
+
});
|
|
1834
1841
|
const statusSchema = z.enum([
|
|
1835
1842
|
"starting",
|
|
1836
1843
|
"running",
|
|
@@ -1867,7 +1874,10 @@ const askThreadStateSchema = z.object({
|
|
|
1867
1874
|
title: z.string(),
|
|
1868
1875
|
quote: z.string().optional()
|
|
1869
1876
|
}),
|
|
1870
|
-
entries: z.array(askEntrySchema)
|
|
1877
|
+
entries: z.array(askEntrySchema),
|
|
1878
|
+
/** Follow-ups asked while the agent answered, which it could not take
|
|
1879
|
+
* then: each goes as its own question once the turn ends. */
|
|
1880
|
+
queued: z.array(askQueuedSchema).optional()
|
|
1871
1881
|
});
|
|
1872
1882
|
/** One change to a thread. Answer text arrives as `append`, so a streamed
|
|
1873
1883
|
* token costs its own length, not the thread's. */
|
|
@@ -1882,6 +1892,8 @@ const askChangeSchema = z.discriminatedUnion("type", [
|
|
|
1882
1892
|
accepts: askAcceptsSchema.optional(),
|
|
1883
1893
|
usage: askUsageSchema.optional(),
|
|
1884
1894
|
title: z.string().optional(),
|
|
1895
|
+
/** Replaces the queue; empty clears it. */
|
|
1896
|
+
queued: z.array(askQueuedSchema).optional(),
|
|
1885
1897
|
/** `null` clears the error; absent leaves it. */
|
|
1886
1898
|
error: z.string().nullable().optional(),
|
|
1887
1899
|
/** `null` clears it; absent leaves it. */
|
|
@@ -1925,7 +1937,7 @@ z.union([z.object({
|
|
|
1925
1937
|
function applyAskChange(state, change) {
|
|
1926
1938
|
switch (change.type) {
|
|
1927
1939
|
case "set": {
|
|
1928
|
-
const { error: _error, signIn: _signIn, ...rest } = state;
|
|
1940
|
+
const { error: _error, signIn: _signIn, queued: _queued, ...rest } = state;
|
|
1929
1941
|
const next = {
|
|
1930
1942
|
...rest,
|
|
1931
1943
|
status: change.status ?? state.status,
|
|
@@ -1942,6 +1954,8 @@ function applyAskChange(state, change) {
|
|
|
1942
1954
|
if (usage) next.usage = usage;
|
|
1943
1955
|
const title = change.title ?? state.title;
|
|
1944
1956
|
if (title) next.title = title;
|
|
1957
|
+
const queued = change.queued ?? state.queued;
|
|
1958
|
+
if (queued?.length) next.queued = queued;
|
|
1945
1959
|
const error = change.error === void 0 ? state.error : change.error;
|
|
1946
1960
|
if (error) next.error = error;
|
|
1947
1961
|
const signIn = change.signIn === void 0 ? state.signIn : change.signIn;
|
|
@@ -2032,6 +2046,10 @@ const rowSchema = z.object({
|
|
|
2032
2046
|
* `reviews`, so there is no foreign key; deleting a review deletes its rows. */
|
|
2033
2047
|
var AskHistory = class {
|
|
2034
2048
|
db;
|
|
2049
|
+
externalWriteGuard;
|
|
2050
|
+
setExternalWriteGuard(guard) {
|
|
2051
|
+
this.externalWriteGuard = guard;
|
|
2052
|
+
}
|
|
2035
2053
|
constructor(db) {
|
|
2036
2054
|
this.db = db;
|
|
2037
2055
|
db.exec(`CREATE TABLE IF NOT EXISTS ask_conversations(
|
|
@@ -2054,25 +2072,42 @@ var AskHistory = class {
|
|
|
2054
2072
|
db.exec("CREATE TABLE IF NOT EXISTS ask_agent_offers(agent TEXT PRIMARY KEY, offer TEXT NOT NULL)");
|
|
2055
2073
|
db.exec("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))");
|
|
2056
2074
|
}
|
|
2075
|
+
assertWritable(reviewId) {
|
|
2076
|
+
if (reviewId.startsWith("shared-")) this.externalWriteGuard?.(reviewId);
|
|
2077
|
+
const row = this.db.prepare("SELECT deleted_at FROM review_attention WHERE review_id=?").get(reviewId);
|
|
2078
|
+
if (row?.deleted_at) throw new ReviewInputError(`This whiteboard was moved to Trash on ${new Date(String(row.deleted_at)).toLocaleDateString("en-US", {
|
|
2079
|
+
month: "short",
|
|
2080
|
+
day: "numeric"
|
|
2081
|
+
})}. Restore it with session_lifecycle({"sessionId":"${reviewId}","state":"active"}) to continue.`, 409);
|
|
2082
|
+
}
|
|
2083
|
+
assertThreadWritable(id) {
|
|
2084
|
+
const record = this.get(id);
|
|
2085
|
+
if (record) this.assertWritable(record.reviewId);
|
|
2086
|
+
}
|
|
2057
2087
|
save(record) {
|
|
2088
|
+
this.assertWritable(record.reviewId);
|
|
2058
2089
|
this.db.prepare(`INSERT OR REPLACE INTO ask_conversations(id,review_id,agent,session_id,version,head,cwd,selection,title,created_at,updated_at,bypass)
|
|
2059
2090
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(record.id, record.reviewId, record.agent, record.sessionId, record.version, record.head, record.cwd, JSON.stringify(record.selection), record.title, record.createdAt, record.updatedAt, record.bypass ? 1 : 0);
|
|
2060
2091
|
}
|
|
2061
2092
|
/** Whether reopening the conversation bypasses permissions. */
|
|
2062
2093
|
setBypass(id, bypass) {
|
|
2094
|
+
this.assertThreadWritable(id);
|
|
2063
2095
|
this.db.prepare("UPDATE ask_conversations SET bypass=? WHERE id=?").run(bypass ? 1 : 0, id);
|
|
2064
2096
|
}
|
|
2065
2097
|
/** Points a conversation at a new session, when its agent could not
|
|
2066
2098
|
* reopen the one it had. */
|
|
2067
2099
|
updateSession(id, sessionId) {
|
|
2100
|
+
this.assertThreadWritable(id);
|
|
2068
2101
|
this.db.prepare("UPDATE ask_conversations SET session_id=? WHERE id=?").run(sessionId, id);
|
|
2069
2102
|
}
|
|
2070
2103
|
/** Keeps what the panel shows, so a reopen need not wait on the agent. */
|
|
2071
2104
|
saveEntries(id, entries) {
|
|
2105
|
+
this.assertThreadWritable(id);
|
|
2072
2106
|
this.db.prepare("UPDATE ask_conversations SET entries=? WHERE id=?").run(JSON.stringify(entries), id);
|
|
2073
2107
|
}
|
|
2074
2108
|
/** The agent's name for the conversation, which the list shows. */
|
|
2075
2109
|
rename(id, title) {
|
|
2110
|
+
this.assertThreadWritable(id);
|
|
2076
2111
|
this.db.prepare("UPDATE ask_conversations SET title=? WHERE id=?").run(title.slice(0, 200), id);
|
|
2077
2112
|
}
|
|
2078
2113
|
saveOffer(agent, offer) {
|
|
@@ -2092,6 +2127,7 @@ var AskHistory = class {
|
|
|
2092
2127
|
return row && askOfferSchema.safeParse(JSON.parse(row.offer)).data;
|
|
2093
2128
|
}
|
|
2094
2129
|
touch(id, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
2130
|
+
this.assertThreadWritable(id);
|
|
2095
2131
|
this.db.prepare("UPDATE ask_conversations SET updated_at=? WHERE id=?").run(at, id);
|
|
2096
2132
|
}
|
|
2097
2133
|
/** Newest first. */
|
|
@@ -2107,7 +2143,11 @@ var AskHistory = class {
|
|
|
2107
2143
|
const row = this.db.prepare("SELECT * FROM ask_conversations WHERE id=?").get(id);
|
|
2108
2144
|
return row ? rowSchema.parse(row) : void 0;
|
|
2109
2145
|
}
|
|
2146
|
+
removeReview(reviewId) {
|
|
2147
|
+
this.db.prepare("DELETE FROM ask_conversations WHERE review_id=?").run(reviewId);
|
|
2148
|
+
}
|
|
2110
2149
|
delete(id) {
|
|
2150
|
+
this.assertThreadWritable(id);
|
|
2111
2151
|
this.db.prepare("DELETE FROM ask_conversations WHERE id=?").run(id);
|
|
2112
2152
|
}
|
|
2113
2153
|
};
|
|
@@ -2145,7 +2185,7 @@ function initializeReviewStoreSchema(db) {
|
|
|
2145
2185
|
CREATE TABLE IF NOT EXISTS versions(review_id TEXT REFERENCES reviews(id), version INTEGER, snapshot TEXT NOT NULL,
|
|
2146
2186
|
PRIMARY KEY(review_id,version));
|
|
2147
2187
|
DROP TABLE IF EXISTS receipts;
|
|
2148
|
-
CREATE TABLE IF NOT EXISTS review_attention(review_id TEXT PRIMARY KEY REFERENCES reviews(id), viewed_at TEXT,
|
|
2188
|
+
CREATE TABLE IF NOT EXISTS review_attention(review_id TEXT PRIMARY KEY REFERENCES reviews(id), viewed_at TEXT, deleted_at TEXT);
|
|
2149
2189
|
CREATE TABLE IF NOT EXISTS repositories(id TEXT PRIMARY KEY, path TEXT NOT NULL UNIQUE, name TEXT NOT NULL);
|
|
2150
2190
|
CREATE TABLE IF NOT EXISTS resources(id TEXT PRIMARY KEY, repository_id TEXT NOT NULL REFERENCES repositories(id),
|
|
2151
2191
|
kind TEXT NOT NULL, mime_type TEXT NOT NULL, data BLOB NOT NULL);
|
|
@@ -2156,6 +2196,17 @@ function initializeReviewStoreSchema(db) {
|
|
|
2156
2196
|
CREATE TABLE IF NOT EXISTS server_identity(one INTEGER PRIMARY KEY CHECK(one=1), id TEXT NOT NULL);
|
|
2157
2197
|
DROP TABLE IF EXISTS authoring_drafts;
|
|
2158
2198
|
`);
|
|
2199
|
+
db.exec("BEGIN IMMEDIATE");
|
|
2200
|
+
try {
|
|
2201
|
+
if (db.prepare("PRAGMA table_info(review_attention)").all().some((column) => column.name === "dismissed_at")) {
|
|
2202
|
+
db.exec("ALTER TABLE review_attention RENAME COLUMN dismissed_at TO deleted_at");
|
|
2203
|
+
db.prepare("UPDATE review_attention SET deleted_at=? WHERE deleted_at IS NOT NULL").run((/* @__PURE__ */ new Date()).toISOString());
|
|
2204
|
+
}
|
|
2205
|
+
db.exec("COMMIT");
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
db.exec("ROLLBACK");
|
|
2208
|
+
throw error;
|
|
2209
|
+
}
|
|
2159
2210
|
}
|
|
2160
2211
|
//#endregion
|
|
2161
2212
|
//#region src/review-api/store.ts
|
|
@@ -2176,14 +2227,15 @@ const commandSchema = z.strictObject({ operation: z.discriminatedUnion("type", [
|
|
|
2176
2227
|
type: z.literal("delete"),
|
|
2177
2228
|
reviewId
|
|
2178
2229
|
}),
|
|
2230
|
+
z.strictObject({
|
|
2231
|
+
type: z.literal("lifecycle"),
|
|
2232
|
+
reviewId,
|
|
2233
|
+
state: z.enum(["active", "trash"])
|
|
2234
|
+
}),
|
|
2179
2235
|
z.strictObject({
|
|
2180
2236
|
type: z.literal("attention"),
|
|
2181
2237
|
reviewId,
|
|
2182
|
-
action: z.
|
|
2183
|
-
"view",
|
|
2184
|
-
"dismiss",
|
|
2185
|
-
"restore"
|
|
2186
|
-
])
|
|
2238
|
+
action: z.literal("view")
|
|
2187
2239
|
}),
|
|
2188
2240
|
z.strictObject({
|
|
2189
2241
|
type: z.literal("create"),
|
|
@@ -2241,6 +2293,7 @@ var ReviewStore = class {
|
|
|
2241
2293
|
liveSources = /* @__PURE__ */ new Map();
|
|
2242
2294
|
refreshTimer;
|
|
2243
2295
|
refreshSubscribers = 0;
|
|
2296
|
+
purgeTimer;
|
|
2244
2297
|
watchWorktrees() {
|
|
2245
2298
|
this.refreshSubscribers++;
|
|
2246
2299
|
this.refreshTimer ??= setInterval(() => {
|
|
@@ -2284,6 +2337,7 @@ var ReviewStore = class {
|
|
|
2284
2337
|
if (this.refreshPending) return this.refreshPending;
|
|
2285
2338
|
const run = this.pending.then(async () => {
|
|
2286
2339
|
for (const summary of this.list()) {
|
|
2340
|
+
if (summary.trashedAt) continue;
|
|
2287
2341
|
const snapshot = this.read(summary.reviewId, summary.version);
|
|
2288
2342
|
try {
|
|
2289
2343
|
const live = this.liveSources.get(summary.reviewId);
|
|
@@ -2324,6 +2378,7 @@ var ReviewStore = class {
|
|
|
2324
2378
|
externalChanges;
|
|
2325
2379
|
observedDataVersion;
|
|
2326
2380
|
observedVersions = /* @__PURE__ */ new Map();
|
|
2381
|
+
observedDeletions = /* @__PURE__ */ new Map();
|
|
2327
2382
|
subscribeCatalog(listener) {
|
|
2328
2383
|
this.catalogListeners.add(listener);
|
|
2329
2384
|
return () => {
|
|
@@ -2346,10 +2401,14 @@ var ReviewStore = class {
|
|
|
2346
2401
|
this.providers = providers;
|
|
2347
2402
|
this.db = new DatabaseSync(databasePath, { timeout: 5e3 });
|
|
2348
2403
|
initializeReviewStoreSchema(this.db);
|
|
2349
|
-
this.activity = new ReviewActivity(this.db, (id) => this.
|
|
2404
|
+
this.activity = new ReviewActivity(this.db, (id) => this.assertWritable(id));
|
|
2350
2405
|
this.askHistory = new AskHistory(this.db);
|
|
2406
|
+
this.purgeExpired();
|
|
2407
|
+
this.purgeTimer = setInterval(() => this.purgeExpired(), 6e4);
|
|
2408
|
+
this.purgeTimer.unref();
|
|
2351
2409
|
this.observedDataVersion = this.dataVersion();
|
|
2352
2410
|
this.observedVersions = this.currentVersions();
|
|
2411
|
+
this.observedDeletions = this.currentDeletions();
|
|
2353
2412
|
this.externalChanges = setInterval(() => this.refreshExternalChanges(), 250);
|
|
2354
2413
|
this.externalChanges.unref();
|
|
2355
2414
|
}
|
|
@@ -2359,17 +2418,29 @@ var ReviewStore = class {
|
|
|
2359
2418
|
currentVersions() {
|
|
2360
2419
|
return new Map(this.db.prepare("SELECT id,version FROM reviews").all().map((row) => [String(row.id), Number(row.version)]));
|
|
2361
2420
|
}
|
|
2421
|
+
currentDeletions() {
|
|
2422
|
+
return new Map(this.db.prepare("SELECT review_id,deleted_at FROM review_attention WHERE deleted_at IS NOT NULL").all().map((row) => [String(row.review_id), String(row.deleted_at)]));
|
|
2423
|
+
}
|
|
2362
2424
|
refreshExternalChanges() {
|
|
2363
2425
|
const version = this.dataVersion();
|
|
2364
2426
|
if (version === this.observedDataVersion) return;
|
|
2365
2427
|
this.observedDataVersion = version;
|
|
2366
2428
|
const current = this.currentVersions();
|
|
2367
2429
|
const previous = this.observedVersions;
|
|
2430
|
+
const deletions = this.currentDeletions();
|
|
2431
|
+
const previousDeletions = this.observedDeletions;
|
|
2432
|
+
this.observedDeletions = deletions;
|
|
2368
2433
|
this.observedVersions = current;
|
|
2369
2434
|
for (const [reviewId, savedVersion] of current) if (previous.get(reviewId) !== savedVersion) this.notify({
|
|
2370
2435
|
reviewId,
|
|
2371
2436
|
version: savedVersion
|
|
2372
2437
|
});
|
|
2438
|
+
else if (previousDeletions.get(reviewId) !== deletions.get(reviewId)) this.notify({
|
|
2439
|
+
reviewId,
|
|
2440
|
+
version: savedVersion,
|
|
2441
|
+
lifecycle: true,
|
|
2442
|
+
attention: true
|
|
2443
|
+
});
|
|
2373
2444
|
for (const [reviewId, savedVersion] of previous) if (!current.has(reviewId)) {
|
|
2374
2445
|
this.activity.deleted(reviewId);
|
|
2375
2446
|
this.notify({
|
|
@@ -2392,9 +2463,10 @@ var ReviewStore = class {
|
|
|
2392
2463
|
}]));
|
|
2393
2464
|
}
|
|
2394
2465
|
updateViewedCoverage(reviewId, files, viewed) {
|
|
2395
|
-
this.
|
|
2466
|
+
this.assertWritable(reviewId);
|
|
2396
2467
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2397
2468
|
try {
|
|
2469
|
+
this.assertWritable(reviewId);
|
|
2398
2470
|
const current = this.viewedCoverage(reviewId);
|
|
2399
2471
|
for (const file of files) {
|
|
2400
2472
|
const previous = current.get(file.path);
|
|
@@ -2473,6 +2545,7 @@ var ReviewStore = class {
|
|
|
2473
2545
|
this.closing = true;
|
|
2474
2546
|
clearInterval(this.externalChanges);
|
|
2475
2547
|
clearInterval(this.refreshTimer);
|
|
2548
|
+
clearInterval(this.purgeTimer);
|
|
2476
2549
|
await this.pending;
|
|
2477
2550
|
this.listeners.clear();
|
|
2478
2551
|
this.catalogListeners.clear();
|
|
@@ -2498,6 +2571,53 @@ var ReviewStore = class {
|
|
|
2498
2571
|
assertExists(id) {
|
|
2499
2572
|
if (!this.db.prepare("SELECT 1 FROM reviews WHERE id=?").get(id)) throw new ReviewInputError("Review not found. If this is an old Whiteboard review, ask your agent to migrate your old Whiteboard reviews.", 404);
|
|
2500
2573
|
}
|
|
2574
|
+
assertWritable(id) {
|
|
2575
|
+
this.assertExists(id);
|
|
2576
|
+
const trashedAt = this.trashTime(id);
|
|
2577
|
+
if (trashedAt) throw new ReviewInputError(`This whiteboard was moved to Trash on ${new Date(trashedAt).toLocaleDateString("en-US", {
|
|
2578
|
+
month: "short",
|
|
2579
|
+
day: "numeric"
|
|
2580
|
+
})}. Restore it with session_lifecycle({"sessionId":"${id}","state":"active"}) to continue.`, 409);
|
|
2581
|
+
}
|
|
2582
|
+
trashTime(id) {
|
|
2583
|
+
const row = this.db.prepare("SELECT deleted_at FROM review_attention WHERE review_id=?").get(id);
|
|
2584
|
+
return row?.deleted_at ? String(row.deleted_at) : null;
|
|
2585
|
+
}
|
|
2586
|
+
removePermanently(id) {
|
|
2587
|
+
for (const table of [
|
|
2588
|
+
"ask_conversations",
|
|
2589
|
+
"authoring_presences",
|
|
2590
|
+
"review_coverage",
|
|
2591
|
+
"review_attention",
|
|
2592
|
+
"versions"
|
|
2593
|
+
]) this.db.prepare(`DELETE FROM ${table} WHERE review_id=?`).run(id);
|
|
2594
|
+
this.db.prepare("DELETE FROM reviews WHERE id=?").run(id);
|
|
2595
|
+
this.liveSources.delete(id);
|
|
2596
|
+
}
|
|
2597
|
+
purgeExpired() {
|
|
2598
|
+
const cutoff = (/* @__PURE__ */ new Date(Date.now() - 2592e6)).toISOString();
|
|
2599
|
+
const expired = [];
|
|
2600
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2601
|
+
try {
|
|
2602
|
+
for (const row of this.db.prepare("SELECT review_id,version FROM review_attention JOIN reviews ON reviews.id=review_id WHERE deleted_at <= ?").all(cutoff)) {
|
|
2603
|
+
const reviewId = String(row.review_id);
|
|
2604
|
+
expired.push({
|
|
2605
|
+
reviewId,
|
|
2606
|
+
version: Number(row.version),
|
|
2607
|
+
deleted: true
|
|
2608
|
+
});
|
|
2609
|
+
this.removePermanently(reviewId);
|
|
2610
|
+
}
|
|
2611
|
+
this.db.exec("COMMIT");
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
this.db.exec("ROLLBACK");
|
|
2614
|
+
throw error;
|
|
2615
|
+
}
|
|
2616
|
+
for (const result of expired) {
|
|
2617
|
+
this.activity.deleted(result.reviewId);
|
|
2618
|
+
this.notify(result);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2501
2621
|
read(id, version) {
|
|
2502
2622
|
const row = version === void 0 ? this.db.prepare("SELECT snapshot FROM versions JOIN reviews ON reviews.id=review_id AND reviews.version=versions.version WHERE reviews.id=?").get(id) : this.db.prepare("SELECT snapshot FROM versions WHERE review_id=? AND version=?").get(id, version);
|
|
2503
2623
|
if (!row) {
|
|
@@ -2516,7 +2636,11 @@ var ReviewStore = class {
|
|
|
2516
2636
|
head: snapshot.pins.head
|
|
2517
2637
|
};
|
|
2518
2638
|
const live = version === void 0 ? this.liveSources.get(id) : void 0;
|
|
2519
|
-
if (live?.version === snapshot.version) return
|
|
2639
|
+
if (live?.version === snapshot.version) return {
|
|
2640
|
+
...structuredClone(live),
|
|
2641
|
+
trashedAt: this.trashTime(id)
|
|
2642
|
+
};
|
|
2643
|
+
snapshot.trashedAt = this.trashTime(id);
|
|
2520
2644
|
return snapshot;
|
|
2521
2645
|
}
|
|
2522
2646
|
setDiffStats(pins, stats, mode = "structural") {
|
|
@@ -2540,7 +2664,7 @@ var ReviewStore = class {
|
|
|
2540
2664
|
summaries(mode, id) {
|
|
2541
2665
|
const reviews = this.db.prepare(`SELECT json_remove(versions.snapshot,'$.document') AS summary,
|
|
2542
2666
|
(SELECT json_extract(first.snapshot,'$.createdAt') FROM versions AS first WHERE first.review_id=reviews.id ORDER BY first.version LIMIT 1) AS first_created_at,
|
|
2543
|
-
review_attention.viewed_at, review_attention.
|
|
2667
|
+
review_attention.viewed_at, review_attention.deleted_at, repositories.name AS repository_name, repositories.path AS repository_path
|
|
2544
2668
|
FROM reviews
|
|
2545
2669
|
JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version
|
|
2546
2670
|
LEFT JOIN review_attention ON review_attention.review_id=reviews.id
|
|
@@ -2564,7 +2688,7 @@ var ReviewStore = class {
|
|
|
2564
2688
|
repositoryGroup: row.repository_path ? this.repositoryGroup(String(row.repository_path)) : void 0,
|
|
2565
2689
|
repositoryName: row.repository_name ? String(row.repository_name) : summary.pins?.repositoryId ?? "",
|
|
2566
2690
|
viewedAt: row.viewed_at ? String(row.viewed_at) : null,
|
|
2567
|
-
|
|
2691
|
+
trashedAt: row.deleted_at ? String(row.deleted_at) : null,
|
|
2568
2692
|
working: this.activity.isWorking(summary.reviewId)
|
|
2569
2693
|
};
|
|
2570
2694
|
if (summary.kind === "scratchpad") listed.contents = this.scratchpadContents();
|
|
@@ -2621,6 +2745,8 @@ var ReviewStore = class {
|
|
|
2621
2745
|
pullRequest?.catch(() => {});
|
|
2622
2746
|
const run = this.pending.then(async () => {
|
|
2623
2747
|
const op = command.operation;
|
|
2748
|
+
this.purgeExpired();
|
|
2749
|
+
if (op.type !== "create" && op.type !== "delete" && op.type !== "lifecycle") this.assertWritable(op.reviewId);
|
|
2624
2750
|
if (op.type !== "create" && op.type !== "edit" && op.type !== "lens_edit" && op.type !== "restore" && this.read(op.reviewId).kind === "scratchpad") throw new ReviewInputError("The scratchpad has no lifecycle, title or pins of its own.", 409);
|
|
2625
2751
|
if (op.type === "create" && op.kind === "scratchpad") {
|
|
2626
2752
|
if (op.target) throw new ReviewInputError("A scratchpad has no target of its own.");
|
|
@@ -2639,39 +2765,37 @@ var ReviewStore = class {
|
|
|
2639
2765
|
if (found) return this.existingReview(found, others, resolvedTarget.pins);
|
|
2640
2766
|
}
|
|
2641
2767
|
if (op.type === "delete") {
|
|
2768
|
+
this.assertExists(op.reviewId);
|
|
2769
|
+
if (!this.trashTime(op.reviewId)) throw new ReviewInputError("Move this whiteboard to Trash before deleting it.", 409);
|
|
2642
2770
|
const result = {
|
|
2643
2771
|
reviewId: op.reviewId,
|
|
2644
2772
|
version: this.read(op.reviewId).version,
|
|
2645
2773
|
deleted: true
|
|
2646
2774
|
};
|
|
2647
|
-
this.commitCommand(result, () =>
|
|
2648
|
-
for (const table of [
|
|
2649
|
-
"ask_conversations",
|
|
2650
|
-
"authoring_presences",
|
|
2651
|
-
"review_coverage",
|
|
2652
|
-
"review_attention",
|
|
2653
|
-
"versions"
|
|
2654
|
-
]) this.db.prepare(`DELETE FROM ${table} WHERE review_id=?`).run(op.reviewId);
|
|
2655
|
-
this.db.prepare("DELETE FROM reviews WHERE id=?").run(op.reviewId);
|
|
2656
|
-
}, () => this.assertMutation(op.reviewId, result.version));
|
|
2775
|
+
this.commitCommand(result, () => this.removePermanently(op.reviewId), () => this.assertMutation(op.reviewId, result.version));
|
|
2657
2776
|
return result;
|
|
2658
2777
|
}
|
|
2659
|
-
if (op.type === "attention") {
|
|
2778
|
+
if (op.type === "attention" || op.type === "lifecycle") {
|
|
2660
2779
|
const result = {
|
|
2661
2780
|
reviewId: op.reviewId,
|
|
2662
2781
|
version: this.read(op.reviewId).version,
|
|
2663
|
-
attention: true
|
|
2782
|
+
attention: true,
|
|
2783
|
+
...op.type === "lifecycle" && { lifecycle: true }
|
|
2664
2784
|
};
|
|
2665
2785
|
this.commitCommand(result, () => {
|
|
2666
2786
|
this.db.prepare("INSERT OR IGNORE INTO review_attention(review_id) VALUES(?)").run(op.reviewId);
|
|
2667
|
-
if (op.
|
|
2668
|
-
else
|
|
2787
|
+
if (op.type === "attention") this.db.prepare("UPDATE review_attention SET viewed_at=? WHERE review_id=?").run((/* @__PURE__ */ new Date()).toISOString(), op.reviewId);
|
|
2788
|
+
else {
|
|
2789
|
+
const timestamp = op.state === "trash" ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
2790
|
+
this.db.prepare("UPDATE review_attention SET deleted_at=CASE WHEN ? IS NULL THEN NULL ELSE COALESCE(deleted_at, ?) END WHERE review_id=?").run(timestamp, timestamp, op.reviewId);
|
|
2791
|
+
}
|
|
2669
2792
|
});
|
|
2670
2793
|
return result;
|
|
2671
2794
|
}
|
|
2672
2795
|
const id = op.type !== "create" ? op.reviewId : op.kind === "scratchpad" ? SCRATCHPAD_ID : randomUUID();
|
|
2673
2796
|
const previous = op.type === "create" ? void 0 : this.read(id);
|
|
2674
2797
|
let snapshot = op.type === "create" ? createdSnapshot(id, op, resolvedTarget, fromPullRequest?.title) : structuredClone(previous);
|
|
2798
|
+
delete snapshot.trashedAt;
|
|
2675
2799
|
delete snapshot.sourceUnavailable;
|
|
2676
2800
|
delete snapshot.lastEdit;
|
|
2677
2801
|
let nextId = previous ? Number(this.db.prepare("SELECT next_id FROM reviews WHERE id=?").get(id).next_id) : 0;
|
|
@@ -2760,7 +2884,10 @@ var ReviewStore = class {
|
|
|
2760
2884
|
this.commitCommand(result, () => {
|
|
2761
2885
|
this.db.prepare("INSERT INTO reviews(id,version,next_id) VALUES(?,?,?) ON CONFLICT(id) DO UPDATE SET version=excluded.version,next_id=excluded.next_id").run(id, snapshot.version, nextId);
|
|
2762
2886
|
this.db.prepare("INSERT INTO versions(review_id,version,snapshot) VALUES(?,?,?)").run(id, snapshot.version, JSON.stringify(snapshot));
|
|
2763
|
-
}, previous ? () =>
|
|
2887
|
+
}, previous ? () => {
|
|
2888
|
+
this.assertWritable(id);
|
|
2889
|
+
this.assertMutation(id, previous.version);
|
|
2890
|
+
} : void 0, op.type === "edit" || op.type === "lens_edit" ? {
|
|
2764
2891
|
surface: op.type === "edit" ? "document" : "lenses",
|
|
2765
2892
|
activityId: op.activityId,
|
|
2766
2893
|
owned: (owner) => {
|
|
@@ -2778,6 +2905,11 @@ var ReviewStore = class {
|
|
|
2778
2905
|
if (op.type !== "create" || op.kind || op.target || !op.pullRequestUrl) return void 0;
|
|
2779
2906
|
if (!this.providers.resolvePullRequest) return Promise.reject(new ReviewInputError("Pull request targets are unavailable."));
|
|
2780
2907
|
const [existing] = op.reuseExisting === false ? [] : this.reviewsForPullRequest(op.pullRequestUrl);
|
|
2908
|
+
if (existing) try {
|
|
2909
|
+
this.assertReusableReview(existing);
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
return Promise.reject(error);
|
|
2912
|
+
}
|
|
2781
2913
|
return this.providers.resolvePullRequest(op.pullRequestUrl, {
|
|
2782
2914
|
id: op.repositoryId,
|
|
2783
2915
|
preferred: existing && this.read(existing).pins?.repositoryId
|
|
@@ -2791,10 +2923,14 @@ var ReviewStore = class {
|
|
|
2791
2923
|
AND COALESCE(json_extract(versions.snapshot,'$.origin.tutorial'), 0) = 0
|
|
2792
2924
|
ORDER BY json_extract(versions.snapshot,'$.createdAt') DESC, reviews.rowid DESC`).all(pullRequestKey(url)).map((row) => String(row.id));
|
|
2793
2925
|
}
|
|
2926
|
+
assertReusableReview(reviewId) {
|
|
2927
|
+
if (this.trashTime(reviewId)) throw new ReviewInputError(`This PR's whiteboard is in Trash. Restore it to continue:\n\nsession_lifecycle({"sessionId":"${reviewId}","state":"active"})`, 409);
|
|
2928
|
+
}
|
|
2794
2929
|
/** The answer to a create that found its PR's review. Its target stays:
|
|
2795
2930
|
* moving it would silently point existing links at different code. */
|
|
2796
2931
|
existingReview(reviewId, others, requested) {
|
|
2797
2932
|
const snapshot = this.read(reviewId);
|
|
2933
|
+
this.assertReusableReview(reviewId);
|
|
2798
2934
|
const headMoved = snapshot.pins?.repositoryId !== requested.repositoryId || snapshot.pins?.head !== requested.head;
|
|
2799
2935
|
const working = this.activity.read(reviewId).activities ?? [];
|
|
2800
2936
|
return {
|
|
@@ -2840,16 +2976,16 @@ var ReviewStore = class {
|
|
|
2840
2976
|
notify(result) {
|
|
2841
2977
|
if (result.deleted) this.observedVersions.delete(result.reviewId);
|
|
2842
2978
|
else if (!result.attention) this.observedVersions.set(result.reviewId, result.version);
|
|
2843
|
-
if (!result.attention) for (const listener of this.listeners) try {
|
|
2979
|
+
if (!result.attention || result.lifecycle) for (const listener of this.listeners) try {
|
|
2844
2980
|
listener(result);
|
|
2845
2981
|
} catch {}
|
|
2846
2982
|
for (const listener of this.catalogListeners) try {
|
|
2847
2983
|
listener();
|
|
2848
2984
|
} catch {}
|
|
2849
2985
|
}
|
|
2850
|
-
/**
|
|
2851
|
-
|
|
2852
|
-
return this.db.prepare("SELECT review_id FROM review_attention WHERE
|
|
2986
|
+
/** Viewing a review in Trash does not restore it. */
|
|
2987
|
+
trashedIds() {
|
|
2988
|
+
return this.db.prepare("SELECT review_id FROM review_attention WHERE deleted_at IS NOT NULL").all().map((row) => String(row.review_id));
|
|
2853
2989
|
}
|
|
2854
2990
|
has(reviewId) {
|
|
2855
2991
|
return this.db.prepare("SELECT 1 FROM reviews WHERE id=?").get(reviewId) !== void 0;
|
|
@@ -2986,23 +3122,23 @@ var ReviewWorkspaces = class {
|
|
|
2986
3122
|
}
|
|
2987
3123
|
this.stop = store.subscribeCatalog(() => {
|
|
2988
3124
|
this.collect();
|
|
2989
|
-
this.
|
|
3125
|
+
this.releaseTrashed();
|
|
2990
3126
|
});
|
|
2991
3127
|
this.collect();
|
|
2992
|
-
this.
|
|
3128
|
+
this.releaseTrashed();
|
|
2993
3129
|
}
|
|
2994
|
-
|
|
3130
|
+
trashed;
|
|
2995
3131
|
releasing = /* @__PURE__ */ new Map();
|
|
2996
3132
|
/**
|
|
2997
|
-
* Frees a newly
|
|
3133
|
+
* Frees a newly trashed review's checkouts to save disk. The review itself
|
|
2998
3134
|
* stays; reopening it rebuilds them. Checkouts rebuilt by peeking at a
|
|
2999
|
-
*
|
|
3135
|
+
* trashed review are kept until the next startup or move to Trash, since
|
|
3000
3136
|
* the Desktop's canvas tabs and source windows may still show them.
|
|
3001
3137
|
*/
|
|
3002
|
-
|
|
3003
|
-
const
|
|
3004
|
-
const released = [...
|
|
3005
|
-
this.
|
|
3138
|
+
releaseTrashed() {
|
|
3139
|
+
const trashed = new Set(this.store.trashedIds());
|
|
3140
|
+
const released = [...trashed].filter((id) => !this.trashed?.has(id));
|
|
3141
|
+
this.trashed = trashed;
|
|
3006
3142
|
if (!released.length) return;
|
|
3007
3143
|
const repositories = this.repositoryDirs();
|
|
3008
3144
|
repositories.catch(() => void 0);
|
|
@@ -3026,7 +3162,7 @@ var ReviewWorkspaces = class {
|
|
|
3026
3162
|
await Promise.allSettled(requests);
|
|
3027
3163
|
await this.releaseCheckouts(reviewId, await repositories);
|
|
3028
3164
|
}).catch((error) => {
|
|
3029
|
-
console.error(`Could not free the pinned checkouts of
|
|
3165
|
+
console.error(`Could not free the pinned checkouts of trashed review ${reviewId}:`, error);
|
|
3030
3166
|
}).finally(() => {
|
|
3031
3167
|
if (this.releasing.get(reviewId) === done) this.releasing.delete(reviewId);
|
|
3032
3168
|
});
|
|
@@ -3034,7 +3170,7 @@ var ReviewWorkspaces = class {
|
|
|
3034
3170
|
this.cleanup = done;
|
|
3035
3171
|
}
|
|
3036
3172
|
async releaseCheckouts(reviewId, repositories) {
|
|
3037
|
-
if (!this.store.
|
|
3173
|
+
if (!this.store.trashedIds().includes(reviewId)) return;
|
|
3038
3174
|
const environments = this.all().filter((item) => item.reviewId === reviewId);
|
|
3039
3175
|
const checkouts = repositories.filter((repository) => existsSync(reviewManagedCheckoutRoot(repository, reviewId)));
|
|
3040
3176
|
if (!environments.length && !checkouts.length || !this.claim(reviewId)) return;
|
|
@@ -3350,7 +3486,43 @@ function rootEventMatters(name, tracked) {
|
|
|
3350
3486
|
if (file === ".git" || file.startsWith(".git/")) return false;
|
|
3351
3487
|
return !tracked || tracked.has(parent(file));
|
|
3352
3488
|
}
|
|
3489
|
+
/** A directory appearing at the edge of the tracked tree may be newly ignored. */
|
|
3490
|
+
function atTrackedEdge(name, tracked) {
|
|
3491
|
+
const file = normalize(name);
|
|
3492
|
+
return !!tracked && tracked.has(parent(file)) && !tracked.has(file);
|
|
3493
|
+
}
|
|
3494
|
+
/** Wholly ignored directories, so watchers can skip them. */
|
|
3495
|
+
async function ignoredDirectories(rootPath, gitDirectory) {
|
|
3496
|
+
try {
|
|
3497
|
+
const { stdout } = await exec$1("git", [
|
|
3498
|
+
"-C",
|
|
3499
|
+
rootPath,
|
|
3500
|
+
...gitDirectory ? [
|
|
3501
|
+
"--git-dir",
|
|
3502
|
+
gitDirectory,
|
|
3503
|
+
"--work-tree",
|
|
3504
|
+
rootPath
|
|
3505
|
+
] : [],
|
|
3506
|
+
"ls-files",
|
|
3507
|
+
"-z",
|
|
3508
|
+
"--others",
|
|
3509
|
+
"--ignored",
|
|
3510
|
+
"--exclude-standard",
|
|
3511
|
+
"--directory"
|
|
3512
|
+
], { maxBuffer: 33554432 });
|
|
3513
|
+
return stdout.split("\0").filter((entry) => entry.endsWith("/") && entry !== ".jj/").map((entry) => entry.slice(0, -1));
|
|
3514
|
+
} catch {
|
|
3515
|
+
return [];
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3353
3518
|
const CHECKOUT_STATE = /^(?:index|[A-Z_]+|(?:rebase-merge|rebase-apply|sequencer|refs\/(?:bisect|worktree|rewritten))(?:\/.*)?)$/;
|
|
3519
|
+
/** Objects, reflogs, LFS objects and Review's own checkouts; never watched. */
|
|
3520
|
+
const INERT_GIT_PATHS = [
|
|
3521
|
+
"objects",
|
|
3522
|
+
"logs",
|
|
3523
|
+
"lfs",
|
|
3524
|
+
"dev-fast"
|
|
3525
|
+
];
|
|
3354
3526
|
/** Objects, reflogs, locks and other checkouts' state cannot change this one. */
|
|
3355
3527
|
function gitEventMatters(name, prefix) {
|
|
3356
3528
|
const file = normalize(name);
|
|
@@ -3762,7 +3934,7 @@ var LocalReviewData = class {
|
|
|
3762
3934
|
worktrees = /* @__PURE__ */ new Map();
|
|
3763
3935
|
forgetWorktree(repositoryId) {
|
|
3764
3936
|
const entry = this.worktrees.get(repositoryId);
|
|
3765
|
-
if (entry)
|
|
3937
|
+
if (entry) unwatch(entry);
|
|
3766
3938
|
this.worktrees.delete(repositoryId);
|
|
3767
3939
|
}
|
|
3768
3940
|
async worktreeState(repositoryId, vcs) {
|
|
@@ -3771,33 +3943,59 @@ var LocalReviewData = class {
|
|
|
3771
3943
|
entry = {
|
|
3772
3944
|
epoch: 0,
|
|
3773
3945
|
inspectedEpoch: -1,
|
|
3774
|
-
|
|
3946
|
+
subscriptions: /* @__PURE__ */ new Map(),
|
|
3775
3947
|
healthy: true,
|
|
3776
3948
|
forks: /* @__PURE__ */ new Map()
|
|
3777
3949
|
};
|
|
3778
3950
|
this.worktrees.set(repositoryId, entry);
|
|
3779
3951
|
const state = entry;
|
|
3952
|
+
const current = () => this.worktrees.get(repositoryId) === state;
|
|
3780
3953
|
const common = await gitCommonDir(vcs.rootPath);
|
|
3781
3954
|
const prefix = vcs.kind === "git" && common ? await gitDirectoryPrefix(vcs.rootPath, common) : null;
|
|
3782
|
-
const
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3955
|
+
const watch = async (root, ignore, matters) => {
|
|
3956
|
+
try {
|
|
3957
|
+
const subscription = await (this.options.subscribe ?? (await import("@parcel/watcher")).subscribe)(root, (error, events) => {
|
|
3958
|
+
if (error) {
|
|
3959
|
+
state.healthy = false;
|
|
3960
|
+
const failed = state.subscriptions.get(root);
|
|
3961
|
+
if (failed) release(failed);
|
|
3962
|
+
state.subscriptions.delete(root);
|
|
3963
|
+
} else if (events.map((event) => matters(path.relative(root, event.path), event)).includes(true)) state.epoch++;
|
|
3964
|
+
}, { ignore });
|
|
3965
|
+
if (!current()) return void release(subscription);
|
|
3966
|
+
const previous = state.subscriptions.get(root);
|
|
3967
|
+
state.subscriptions.set(root, subscription);
|
|
3968
|
+
if (previous) release(previous);
|
|
3969
|
+
} catch {
|
|
3791
3970
|
state.healthy = false;
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
state.
|
|
3798
|
-
|
|
3971
|
+
}
|
|
3972
|
+
};
|
|
3973
|
+
const rootIgnore = async () => [...common ? [".git"] : [], ...await ignoredDirectories(vcs.rootPath, vcs.kind === "jj" ? common ?? void 0 : void 0)].map((directory) => path.join(vcs.rootPath, directory));
|
|
3974
|
+
let ignored = await rootIgnore();
|
|
3975
|
+
const refreshIgnored = () => {
|
|
3976
|
+
clearTimeout(state.ignoring);
|
|
3977
|
+
state.ignoring = setTimeout(async () => {
|
|
3978
|
+
const next = await rootIgnore();
|
|
3979
|
+
if (!current() || next.join("\0") === ignored.join("\0")) return;
|
|
3980
|
+
ignored = next;
|
|
3981
|
+
const previous = state.subscriptions.get(vcs.rootPath);
|
|
3982
|
+
state.subscriptions.delete(vcs.rootPath);
|
|
3983
|
+
if (previous) await release(previous);
|
|
3984
|
+
await watch(vcs.rootPath, ignored, rootMatters);
|
|
3985
|
+
state.epoch++;
|
|
3986
|
+
}, 1e3);
|
|
3987
|
+
state.ignoring.unref();
|
|
3988
|
+
};
|
|
3989
|
+
const rootMatters = (name, event) => {
|
|
3990
|
+
if (path.basename(name) === ".gitignore" || event.type === "create" && atTrackedEdge(name, state.tracked) && statSync(event.path, { throwIfNoEntry: false })?.isDirectory()) refreshIgnored();
|
|
3991
|
+
return prefix === null || rootEventMatters(name, state.tracked);
|
|
3992
|
+
};
|
|
3993
|
+
await Promise.all([watch(vcs.rootPath, ignored, rootMatters), common && watch(common, INERT_GIT_PATHS.map((directory) => path.join(common, directory)), (name) => {
|
|
3994
|
+
if (name === path.join("info", "exclude")) refreshIgnored();
|
|
3995
|
+
return prefix === null || gitEventMatters(name, prefix);
|
|
3996
|
+
})]);
|
|
3799
3997
|
}
|
|
3800
|
-
if (entry.inspection && entry.inspectedEpoch === entry.epoch && entry.healthy && entry.
|
|
3998
|
+
if (entry.inspection && entry.inspectedEpoch === entry.epoch && entry.healthy && entry.subscriptions.size) return {
|
|
3801
3999
|
...entry.inspection,
|
|
3802
4000
|
forks: entry.forks
|
|
3803
4001
|
};
|
|
@@ -3820,7 +4018,7 @@ var LocalReviewData = class {
|
|
|
3820
4018
|
this.structuralComparisons.close();
|
|
3821
4019
|
this.closed = true;
|
|
3822
4020
|
await this.workspaceManager?.close();
|
|
3823
|
-
|
|
4021
|
+
await Promise.all([...this.worktrees.values()].map(unwatch));
|
|
3824
4022
|
this.worktrees.clear();
|
|
3825
4023
|
const readers = [...this.readers.values()];
|
|
3826
4024
|
this.readers.clear();
|
|
@@ -4445,6 +4643,14 @@ function inputError(run) {
|
|
|
4445
4643
|
throw error;
|
|
4446
4644
|
}
|
|
4447
4645
|
}
|
|
4646
|
+
async function unwatch(entry) {
|
|
4647
|
+
clearTimeout(entry.ignoring);
|
|
4648
|
+
const subscriptions = [...entry.subscriptions.values()];
|
|
4649
|
+
entry.subscriptions.clear();
|
|
4650
|
+
await Promise.all(subscriptions.map(release));
|
|
4651
|
+
}
|
|
4652
|
+
/** Linux fails to unwatch a directory that is already gone. */
|
|
4653
|
+
const release = (subscription) => subscription.unsubscribe().catch(() => {});
|
|
4448
4654
|
function openLocalReviewStore(databasePath, options = {}) {
|
|
4449
4655
|
const store = new ReviewStore(databasePath, {
|
|
4450
4656
|
projectSource: (snapshot, pins) => data.projectSource(snapshot, pins),
|